Nota: Este post ha sido importado de mi blog de geeks.ms. Es posible que algo no se vea del todo "correctamente". En cualquier caso puedes acceder a la versión original aquí
Hoy me he encontrado un controlador MVC6 con la siguiente acción:
[HttpPut]
[Route("{userid:int}/faceprofile")]
public async Task<IActionResult> SetFaceProfileId(int userid, [FromBody] Guid id)
Claramente su autor esperaba que pudieramos poner un Guid en el cuerpo de la petición y eso funcionaría… Pero, ¿como debe mandarse?
Primeras pruebas – Usando binding “a lo WebApi”
La primera prueba es mandar un Guid directo en el cuerpo: curl -X PUT –header “Content-Type: application/json” -d “9B606B9F-9835-4E97-A5BC-C445831591AB” http://localhost:15858/api/Profiles/1/faceprofile
Tiene cierta lógica que no funcione ya que realmente lo que estamos enviando no es un JSON, así que raro sería que eso hubiese funcionado.
¿Y si mandamos un JSON Correcto? curl -X PUT –header ‘Content-Type: application/json’ -d ‘{“id”: “9B606B9F-9835-4E97-A5BC-C445831591AB”}’ ‘http://localhost:15858/api/Profiles/1/faceprofile’
Pues tampoco funciona, porque el input formatter no sabe enlazar un Guid a partir de la cadena indicada.
La tercera alternativa plausible, mandar el GUID directamente en el cuerpo pero con content-type plain/text no funciona tampoco porque no tenemos media formatter para text/plain por lo que recibimos un 415.
Alternativa – Usando binding “a lo MVC”
Con el binding a lo WebApi no vamos a poder enlazar un Guid. Los media formatters que vienen por defecto no entienden de Guids, así que poco haremos. Una alternativa es irnos al binding “a lo MVC” a ver si el model binder si que entiende de Guids.
Para ello eliminamos el [FromBody] del parámetro y veamos que ocurre.
Usar el content-type text/plain con el GUID en el cuerpo que parecía una alternativa plausible no funciona tampoco. El problema está en que MVC6 no sabe que esa única cadena sin más info, se corresponde a parámetro de tipo Guid.
¿Y si simulamos el POST de un formulario HTML? Para ello debemos mandar el content-type application/x-www-form-urlencoded y en el cuerpo mandar el nombre del parámetro (id) un igual y el valor:
curl -X PUT –header “Content-Type: application/x-www-form-urlencoded” -d “id=9B606B9F-9835-4E97-A5BC-C445831591AB” “http://localhost:15858/api/Profiles/1/faceprofile”
Ahora si que funciona correctamente. También funciona si usamos query string por supuesto:
curl -X PUT –header “Content-Type: application/x-www-form-urlencoded” –header “Content-Length: 0” “http://localhost:15858/api/Profiles/1/faceprofile?id=9B606B9F-9835-4E97-A5BC-C445831591AB”
Ojo que, dado que es un PUT, debemos especificar content-length a 0 si no mandamos nada en el cuerpo de la petición.
¿Y todo eso por qué?
El model binding de MVC6 es una mezcla del model binding de MVC5 y WebAPI2. Eso implica que es fácil confundirse si vienes de esos dos frameworks, ya que el uso de algunos atributos funciona distinto y un ejemplo es [FromBody]:
-
En WebApi2 [FromBody] se usa para indicar al framework que un tipo que habitualmente se resuelve via query string o route value (es decir en la URL) debe resolverse con los datos del cuerpo de la petición. En WebApi2 por defecto los tipos simples (value types) se resuelven desde la URL y los tipos complejos (clases) desde el cuerpo de la petición. A diferencia de MVC5 solo un parámetro puede ser resuelto desde el cuerpo. En el caso de tener un parámetro que es una clase este parámetro será el resuelto a través del cuerpo de la petición, aun sin necesidad de usar [FromBody]
- En MVC6 [FromBody] se usa para habilitar el binding “a lo WebApi”. Por defecto se usa un binding “a lo MVC” donde los datos de la petición son recogidos por los value providers y los parámetros enlazados vía los model binders. Por lo tanto. Hay value providers que leen el cuerpo de la petición (aunque no en todos los content types) y es por esto que un mismo parámetro se puede enlazar vía el cuerpo de la petición o via URL (como en nuestro ejemplo). Pero cuando aparece un solo parámetro con [FromBody] (solo puede aparecer uno) se usa un binding basado en media formatters. Solo un media formatter puede leer el cuerpo (cual, depende del content type) y enlazar un solo parámetro. Así [FromBody] en MVC6 no significa “enlaza esto que habitualmente harías vía URL a través del cuerpo] si no “para este parámetro usa el media formatter correspondiente”. Si hubiera más parámetros esos serían enlazados vía los value providers y el model binder (es decir “a lo MVC”).
Por defecto MVC6 trae value providers que leen el cuerpo para el content-type application/x-www-form-urlencoded” y media formatters para los content-type de JSON y XML. Recuerda que MVC5 traía value providers para content-types de JSON, en MVC6 ya no estan, porque su rol lo juega el media formatter. Así, la idea es:
- Si vas a recibir POSTS de formularios HTML no debes decorar nada con [FromBody]. No uses [FromBody] y MVC6 se comportará como lo hace MVC5.
- Si vas a recibir datos en JSON o XML debes decorar el parámetro con [FromBody]. Si no lo haces no recibirás los datos (nota que en WebApi2 si que los recibirías). Solo debes decorar un parámetro via [FromBody] (el que se serializa en el cuerpo). Puedes recibir datos adicionales vía URL.
Probablemente el desarrollador de esta acción del controlador venía de WebApi2 y de ahí este uso de [FromBody] sobre un Guid (un tipo simple, que por defecto enlazaríamos a través de la URL).
**Vale, pero quiero enlazar mi Guid y no quiero usar application/x-www-formurlencoded**
Bueno, pues entonces te toca hacer un _media formatter_ que entienda los _Guids_. Veamos como. El **primer paso es crear un IInputFormatter nuevo**. En MVC6 los _media formatters_ se han dividido en dos (los input formatters que entienden datos de entrada y los output formatters que serializan los datos de salida). En este caso queremos **entender datos en un _Content-Type_ nuevo**, no devolver datos en un _Content-Type_ adicional. De ahí que necesitamos un _IInputFormatter_ adicional.
Lo vamos a asignar a **un _content-type_ nuevo:** el de text/plain. De esta manera aceptaremos peticiones con text/plain y directamente un valor en el cuerpo. El _input formatter_ será capaz de enlazar parámetros de tipo Guid y, ya puestos, string:
<pre style="max-width: 700px; font-family: consolas; background: #1e1e1e; white-space: nowrap; overflow-x: scroll; color: gainsboro"><span style="color: #569cd6">public</span> <span style="color: #569cd6">class</span> <span style="color: #4ec9b0">TextPlainInputFormatter</span> : <span style="color: #b8d7a3">IInputFormatter</span><br />{<br /> <span style="color: #569cd6">private</span> <span style="color: #569cd6">const</span> <span style="color: #569cd6">string</span> TextContentType <span style="color: #b4b4b4">=</span> <span style="color: #d69d85">"text/plain"</span>;<br /> <br /> <span style="color: #569cd6">public</span> <span style="color: #569cd6">bool</span> CanRead(<span style="color: #4ec9b0">InputFormatterContext</span> context)<br /> {<br /> <span style="color: #569cd6">var</span> typeValid <span style="color: #b4b4b4">=</span> context<span style="color: #b4b4b4">.</span>ModelType <span style="color: #b4b4b4">==</span> <span style="color: #569cd6">typeof</span>(<span style="color: #569cd6">string</span>)<br /> <span style="color: #b4b4b4">||</span> context<span style="color: #b4b4b4">.</span>ModelType <span style="color: #b4b4b4">==</span> <span style="color: #569cd6">typeof</span>(<span style="color: #4ec9b0">Guid</span>);<br /> <br /> <span style="color: #569cd6">var</span> contentTypeValid <span style="color: #b4b4b4">=</span><br /> context<span style="color: #b4b4b4">.</span>HttpContext<span style="color: #b4b4b4">.</span>Request<span style="color: #b4b4b4">.</span>ContentType <span style="color: #b4b4b4">==</span> TextContentType;<br /> <br /> <span style="color: #569cd6">return</span> typeValid <span style="color: #b4b4b4">&&</span> contentTypeValid;<br /> } <br /> <br /> <span style="color: #569cd6">public</span> <span style="color: #569cd6">async</span> <span style="color: #4ec9b0">Task</span><<span style="color: #4ec9b0">InputFormatterResult</span>> ReadAsync(<span style="color: #4ec9b0">InputFormatterContext</span> context)<br /> {<br /> <span style="color: #569cd6">var</span> request <span style="color: #b4b4b4">=</span> context<span style="color: #b4b4b4">.</span>HttpContext<span style="color: #b4b4b4">.</span>Request;<br /> <span style="color: #569cd6">var</span> destIsGuid <span style="color: #b4b4b4">=</span> context<span style="color: #b4b4b4">.</span>ModelType <span style="color: #b4b4b4">==</span> <span style="color: #569cd6">typeof</span>(<span style="color: #4ec9b0">Guid</span>);<br /> <span style="color: #569cd6">if</span> (request<span style="color: #b4b4b4">.</span>ContentLength <span style="color: #b4b4b4">==</span> <span style="color: #b5cea8"></span>)<br /> {<br /> <span style="color: #569cd6">if</span> (destIsGuid)<br /> {<br /> <span style="color: #569cd6">return</span> <span style="color: #4ec9b0">InputFormatterResult</span><span style="color: #b4b4b4">.</span>Success(<span style="color: #4ec9b0">Guid</span><span style="color: #b4b4b4">.</span>Empty);<br /> }<br /> <span style="color: #569cd6">else</span><br /> {<br /> <span style="color: #569cd6">return</span> <span style="color: #4ec9b0">InputFormatterResult</span><span style="color: #b4b4b4">.</span>Success(<span style="color: #569cd6">null</span>);<br /> }<br /> }<br /> <br /> <span style="color: #569cd6">using</span> (<span style="color: #569cd6">var</span> reader <span style="color: #b4b4b4">=</span> <span style="color: #569cd6">new</span> <span style="color: #4ec9b0">StreamReader</span>(request<span style="color: #b4b4b4">.</span>Body))<br /> {<br /> <br /> <span style="color: #569cd6">var</span> str <span style="color: #b4b4b4">=</span> <span style="color: #569cd6">await</span> reader<span style="color: #b4b4b4">.</span>ReadToEndAsync();<br /> <span style="color: #569cd6">if</span> (destIsGuid)<br /> {<br /> <span style="color: #569cd6">return</span> ParseGuidResult(str);<br /> }<br /> <span style="color: #569cd6">else</span><br /> {<br /> <span style="color: #569cd6">return</span> <span style="color: #4ec9b0">InputFormatterResult</span><span style="color: #b4b4b4">.</span>Success(str);<br /> }<br /> }<br /> <br /> <br /> }<br /> <br /> <span style="color: #569cd6">private</span> <span style="color: #4ec9b0">InputFormatterResult</span> ParseGuidResult(<span style="color: #569cd6">string</span> str)<br /> {<br /> <span style="color: #4ec9b0">Guid</span> guid;<br /> <span style="color: #569cd6">var</span> ok <span style="color: #b4b4b4">=</span> <span style="color: #4ec9b0">Guid</span><span style="color: #b4b4b4">.</span>TryParse(str, <span style="color: #569cd6">out</span> guid);<br /> <span style="color: #569cd6">if</span> (ok)<br /> {<br /> <span style="color: #569cd6">return</span> <span style="color: #4ec9b0">InputFormatterResult</span><span style="color: #b4b4b4">.</span>Success(guid);<br /> }<br /> <span style="color: #569cd6">else</span><br /> {<br /> <span style="color: #569cd6">return</span> <span style="color: #4ec9b0">InputFormatterResult</span><span style="color: #b4b4b4">.</span>Failure();<br /> }<br /> }<br />}</pre>
El código **no es para nada complicado**. Simplemente implementamos los dos métodos de la interfaz:
* **CanRead**: Que debe devolver _true_ si este media formatter es el que va a procesar el argumento (recuerda, el único argumento [FromBody]). En este caso solo lo procesamos si el tipo es Guid o string y el content-type es text/json.
* **ReadAsync**: Método asíncrono que lee el cuerpo de la petición y devuelve un objeto del tipo correcto (Guid o String). Es importante resaltar que, al igual que WebApi 2, no hay _model binder_ de por medio: el _formatter_ es el encargado de devolver el objeto creado para asignar al parámetro correspondiente.</ul>
Tan solo nos falta **añadir el _formatter_** a MVC6. Para ello, desde la clase Startup:
<pre style="max-width: 700px; font-family: consolas; background: #1e1e1e; white-space: nowrap; overflow-x: scroll; color: gainsboro">services<span style="color: #b4b4b4">.</span>AddMvc()<br /> <span style="color: #b4b4b4">.</span>AddMvcOptions(options <span style="color: #b4b4b4">=></span><br /> {<br /> options<span style="color: #b4b4b4">.</span>InputFormatters<span style="color: #b4b4b4">.</span>Insert(<span style="color: #b5cea8"></span>, <span style="color: #569cd6">new</span> <span style="color: #4ec9b0">TextPlainInputFormatter</span>());<br /> });</pre>
Usamos el método _AddMvcOptions_ para añadir nuestro _formatter_ a la colección de formatters.
¡Y listos! Como ves… ¡tampoco es tan difícil!