ASP.NET MVC6: Model binding

Nota: Este post ha sido realizado con versiones previas de ASP.NET 5 y de Visual Studio 2015 (CTP6), lo aquí escrito puede variar con versiones  finales de la plataforma

ASP.NET MVC5 y WebApi 2 usaban técnicas distintas para realizar el model binding, es decir para enlazar los parámetros de las acciones de los controladores a los valores enviados en la petición web. Ahora que ASP.NET 5 unifica ambos frameworks en el nuevo MVC6 vamos a ver como funciona el model binding en este nuevo framekwork. ¿Se parece más al de MVC 5 o al de WebApi? En este post saldremos de dudas.

Antes de nada aclarar que este post va a ser un poco largo, ya que para entender las diferencias del model binding de MVC6 respecto al de MVC5 y al de WebApi 2 vamos a contar también como funcionaba en estos dos frameworks.

Model Binding en ASP.NET MVC 5

Si ya conoces como funciona el model binding en ASP.NET MVC 5 puedes saltarte este apartado.

En ASP.NET MVC5 el proceso de model binding se realiza a partir de dos elementos: los value providers y los model binders. Los primeros se encargan de analizar la petición HTTP y dejar todos los datos en «un lugar común». Posteriormente los model binders leen de ese «lugar común» y utilizan los datos leídos por los value providers para realizar el enlace. Los value providers son los que entienden de petición HTTP y cada uno se encarga de leer los datos de un determinado sitio. Así un value provider lee los datos de la query string, otro puede leerlos de la URL y otro puede leerlos del cuerpo de la petición. Dado que los datos en el cuerpo de la petición pueden estar en distintos formatos (según el valor de la cabecera «Content-Type») pueden haber varios value providers para leer el cuerpo de la petición según cual sea el valor de esta cabecera (técnicamente nada impediría tener varios value providers que leyesen la querystring pero dado que esta suele tener un solo formato no es algo que tenga mucho sentido).

En una aplicación MVC5 se incluyen los siguientes value providers de serie (realmente lo que muestra la captura son las factorías que crean los value providers):

ValueProviders por defecto en ASP.NET MVC5

  • FormValueProvider: Lee datos del FormData, es decir el cuerpo de la petición cuando «Content-Type» vale application/x-www-form-urlencoded
  • JsonValueProvider: Lee datos del cuerpo de la petición cuando «Content-type» vale application/json y asume que dichos datos están en JSON
  • RouteDataValueProvider: Lee datos de los route values
  • QueryStringValueProvider: Lee datos de la query string
  • HttpFileCollectionValueProvider: Lee datos de los ficheros subidos, es decir cuando hay una llamada HTTP con «Content-Type» multipart/form-data.
  • JQueryFormValueProvider: Lee datos del cuerpo de la petición cuando esos han sido formateados a través de jQuery
  • ChildActionValueProvider: Este es un value provider «interno» que se usa cuando se realizan llamadas a acciones hijas para poder pasarles parámetros.

El framework no da un mecanismo para asignar un value provider a un content-type determinado o a un método HTTP concreto. Simplemente se agregan los value providers (realmente, las factorías ya que los value providers como tal son objetos de un solo uso: se crean y se destruyen en cada petición) en la colección System.Web.Mvc.ValueProviderFactories.Factories y listos. El código para saber si los datos están en el formato esperado va dentro de cada value provider.

Una vez los value providers han leído toda la petición web y han colocado los datos leídos en un «lugar común», entran en juego los model binders. Esos objetos se encargan de leer esos datos y enlazarlos con los parámetros correspondientes de la acción a ejecutar. Los model binders se configuran en un diccionario (System.Web.Mvc.ModelBinders.Binders) donde la clave es un System.Type y el valor el model binder que se debe invocar para enlazar parámetros de este tipo. Adicionalmente existe un model binder por defecto que se usará para enlazar todos los tipos que no se encuentren en el diccionario. Aunque puedes pensar que este diccionario será muy grande para cubrir muchos tipos de parámetros, no es así:

Model Binders MVC5

Solo esos cuatro tipos tienen un model binder específico. El resto se procesan todos a través del model binder por defecto que es el DefaultModelBinder. El model binder por defecto se guarda en System.Web.Mvc.ModelBinders.Binders.DefaultBinder. Aunque es relativamente habitual crear model binders adicionales y vincularlos a un tipo de datos en concreto, es muy raro cambiar el model binder por defecto (el que existe funciona muy bien para todos los casos generales y es capaz de tratar propiedades simples, complejas y colecciones).

La clave para entender el model binding es saber que dado que los value providers no van por verbo HTTP y que al final todos sus datos «van a un lugar común» da igual de donde provengan realmente los datos que usen los model binders.  Imagina una clase persona como la siguiente:

[snippet id=»225″]

Y una acción de controlador como sigue (en mi caso el controlador home):

[snippet id=»226″]

Ahora podemos comprobar que si le pasamos los parámetros se enlazan provengan de donde provengan. P. ej. podemos navegar via GET a /Home/Test/10?name=eiximenis y verás lo siguiente:

bindingmvc

Observa como el 10 (route value) se usa para enlazar el parámetro id. Observa además que se enlazan los dos parámetros Id (para el model binder que esté dentro de una clase (Person.Id) o como un parámetro directo) es lo mismo) y los dos parámetros name se enlazan a partir de la querystring. Si mandásemos los datos por POST en el cuerpo de la petición, p. ej. en formato JSON se enlazarían igual.

Por lo tanto en MVC mientras los datos estén en la petición HTTP seran enlazados a los parámetros correspondientes con independencia de donde estén esos datos.

Model Binding en WebApi

Si ya conoces como funciona el model binding en Web Api 2 puedes saltarte este apartado.

En WebApi las cosas  no funcionan como en MVC. En WebApi la petición web no se almacena en ningún buffer así que no puede ser leída dos veces (recuerda que en MVC hay varios value providers que leen el cuerpo de la petición). Esto modifica sustancialmente todo el proceso. Básicamente las dos normas fundamentales a recordar son que tan solo un parámetro de la acción puede ser enlazado a partir de los datos del cuerpo de la petición. Y que por defecto los parámetros de tipos simples se enlazan a partir de la URL y los complejos (es decir clases propias) se enlazan a partir del cuerpo de la petición.

Asumamos la misma acción que antes pero ahora en un controlador WebApi:

[snippet id=»227″]

Si ahora navegamos a /api/test/10&name=eiximenis veremos lo siguiente:

bindingwebapi

Se puede observar que el parámetro person no se enlaza porque es un tipo complejo y se enlaza por defecto a partir de los datos del cuerpo.

Además si ahora añadiésemos otro parámetro complejo al controlador recibiríamos un error (Can’t bind multiple parameters to the request’s content), ya que recuerda: el cuerpo de la petición se puede leer una sola vez y dado que los objetos complejos se enlazan a partir del cuerpo, solo se puede enlazar uno. Para forzar a WebApi a enlazar un objeto complejo desde la URL o bien un objeto simple desde el cuerpo de la petición se usan los atributos [FromUri] y [FromBody]. Esos atributos se aplican a los parámetros.

En WebApi los objetos que se encargan de enlazar los datos provenientes del cuerpo de la petición son los MediaTypeFormatters (nos referiremos a ellos simplemente como formatters). De hecho los formatters hacen dos tareas independientes: leer datos del cuerpo de la petición (y enlazar un parámetro de la acción) y escribir datos de la respuesta. Los formatters están vinculados a uno o más content-types (aquí si que el framework discrimina automáticamente) Los formatters que trae de serie una aplicación WebApi son:

formatters

La propiedad Formatters del objeto HttpConfiguration es una colección no un diccionario. La asociación de a que content-type responde cada formatter se hace en el propio código del formatter, agregando en el constructor los media types correspondientes a la propiedad SupportedMediaTypes. Los formatters que trae de serie WebApi le permiten procesar peticiones en JSON, XML y application/x-www-form-urlencoded (submit de formularios).

WebApi también tiene el concepto de model binders y value providers igual que MVC pero en este caso aplica solo a lo que no gestionan los formatters, es decir solo a los parámetros enlazados por la URL (Recuerda que se pueden enlazar parámetros complejos a través de la URL si se decoran con [FromUri]). Es decir cuando enlazamos a través de la URL da igual si un valor proviene de los route values o de la querystring, ya que los value providers habrán leído toda la URL y la han colocado en un «lugar común» que es de donde leen los model binders para enlazar los parámetros.

Model Binding en ASP.NET MVC6

El model binding en MVC6 es, como no podía ser de otra manera, una mezcla de ambos modelos. El modelo por defecto es el de ASP.NET MVC, es decir da igual de donde provenga un valor (URL o cuerpo de la petición) , si existe se usará. Imagina la siguiente acción de un controlador MVC6:

[snippet id=»228″]

Y ahora lanzamos un POST vacío (sin datos en el cuerpo) a la URL /api/test/10?name=eiximenis y recibiremos lo siguiente:

bindingmvc6

Se puede observar como es el mismo comportamiento que MVC 5. Ahora modificamos la acción para indicar que el parámetro p se recibe desde el cuerpo de la petición (usando [FromBody]).

Ahora hacemos una petición POST a /api/test/10 con los datos:

Content-Type: application/x-www-form-urlencoded

id=42&name=eiximenis&nif=12345678A

Eso es equivalente a hacer submit de un formulario con los campos id (a 42)  name (con valor eiximenis) y nif (con valor 12345678A). El resultado obtenido ahora es:

bindingmvc6_2

El resultado puede sorprender un poco. Fíjate que en la URL no hay mención alguna al parámetro name pero en cambio este se enlaza a partir de los datos del cuerpo. Sorprendentemente el parámetro p (decorado con [FromBody]) no se enlaza a pesar de haber datos en el cuerpo de la petición. La razón es que los parámetros quie no están decorados con [FromBody] se enlazan «a lo MVC», por lo tanto dado que en el cuerpo de la petición existe un campo «name» este parámetro se enlaza. Por otra parte si observamos con atención podemos ver que en la petición HTTP el valor «id» está repetido (en la URL con valor 10) y en el cuerpo (con valor 42). En este caso el parámetro toma el valor de la URL (en MVC 5 ocurre lo mismo, por cierto).

Nos queda responder porque p no se ha enlazado. La respuesta es que al usar [FromBody] hemos «cambiado» de un binding «a lo MVC» a un binding a lo WebApi, es decir a un binding basado en formatters. Y los formatters estñan asociados a los content-types y no hay ningún formatter asociado al content-type application/x-www-form-urlencoded. Es por eso que el parámetro p no se enlaza.

Ahora bien, si cambiamos el content-type para que sea application/json (y modificamos el cuerpo de la petición para que tenga un JSON claro):

POST /api/test/10 HTTP/1.1
Content-Type: application/json

{"id":42, "name": "eiximenis", "nif" : "12345678A"}

Lo que ahora obtenemos es lo siguiente:

bindingmvc6_3

Los parámetros id y name se enlazan a lo MVC, de ahí que id coja el valor de 10 y name sea null (la URL ahora contiene un JSON que por defecto no se sabe leer de ahí que name sea null). Por su parte p se enlaza a través del formatter de JSON que tiene MVC6 y de ahí que el valor de la propiedad Id de p sea 42 (el que está en el cuerpo) y no 10 (el de la URL) y las propiedades Name y Nif estén rellenadas con los valores del JSON del cuerpo de la petición.

Este modelo dual te puede parecer lioso e innecesario, pero realmente no es una mala solución. Generalmente cuando creemos vistas que reciben POSTs de formularios nos interesará más un binding a lo MVC (a partir de datos en application/x-www-form-urlencoded) ya que eso es lo que se suele enviar desde HTML. Por otro lado recibir POSTs con contenido JSON o XML es más usual de apis (desde HTML sin JS no se puede enviar datos en JSON o XML). De ahí que application/x-www-form-urlencoded vaya por un lado y JSON y XML por el otro.

Espero que este post haya ayudado a clarificar como va a funcionar el model binding de ASP.NET MVC6.

Saludos!

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *