ASP.NET vNext–Model Binding

Bien, en el post anterior comentamos cuatro cosillas sobre el model binding en ASP.NET MVC y WebApi, sus semejanzas y sus diferencias. En ASP.NET vNext ambos frameworks se unifican así que es de esperar que el model binding también lo haga… Veamos como funciona el model binding de vNext.

Nota: Este post está realizado con la versión de ASP.NET vNext que viene con el VS14 CTP2. La mejor manera de probar dicha CTP es usando una VM en Azure creada a partir de una plantilla que ya la contiene instalada. Por supuesto todo lo dicho aquí puede contener cambios en la versión final 🙂

Pruebas de caja negra

Antes que nada he intentado hacer unas pruebas de “caja negra” para ver si el comportamiento era más parecido al de WebApi o al de MVC. He empezado con un proyecto web vNext vacío, y en el project.json he agregado la referencia a Microsoft.AspNet.Mvc. Luego me he creado un controlador como el siguiente:

  1. public class HomeController : Controller
  2. {
  3.     public IActionResult Index(Product product, Customer customer)
  4.     {
  5.         return View();
  6.     }
  7.  
  8.     public bool Post(Product product, Customer customer)
  9.     {
  10.         return true;
  11.     }
  12. }

Finalmente en el Startup.cs he configurado una tabla de rutas que combine MVC y WebApi:

  1. public class Startup
  2. {
  3.     public void Configure(IBuilder app)
  4.     {
  5.         app.UseServices(s => s.AddMvc());
  6.  
  7.         app.UseMvc(r =>
  8.         {
  9.             r.MapRoute(
  10.                 name: "default",
  11.                 template: "{controller}/{action}/{id?}",
  12.                 defaults: new { controller = "Home", action = "Index" });
  13.             r.MapRoute(
  14.                 name: "second",
  15.                 template: "api/{Controller}/{id?}"
  16.                 );
  17.         });
  18.     }
  19. }

Con esa tabla de rutas un POST a /Home/Index debe enrutarme por la primera acción del controlador (al igual que un GET). Mientras que un POST a /api/Home debe enrutarme por la segunda acción del controlador (mientras que un GET a /api/Home debe devolverme un 404). Para más información echa un vistazo a mi post sobre el routing en vNext.

Las clases Customer y Product contienen simplemente propiedades:

  1. public class Customer
  2. {
  3.     public int Id { get; set; }
  4.     public string Name { get; set; }
  5.     public string Gender { get; set; }
  6. }

  1. public class Product
  2. {
  3.     public int Id { get; set; }
  4.     public string Name { get; set; }
  5. }

Luego he usado cURL para realizar unos posts y ver que es lo que tenía:

curl –data "Id=1&Name=eiximenis&Gender=Male" http://localhost:49228/  –header "Content-type:application/x-www-form-urlencoded"

Con esto simulo un post a que contenga los datos Id, Name y Gender y eso es lo que recibo en el controlador (en el método Index):

image

Este comportamiento es el mismo que en ASP.NET MVC. Ahora cambio la petición de cURL para enviar la misma petición pero a /api/Home para que se me enrute al método Post (estilo WebApi). Mi idea era ver si para enrutamiento tipo MVC se usaba un binding parecido a MVC y para enrutamiento tipo WebApi (sin acción y basado en verbo HTTP) se usaba un binding parecido al de WebApi:

curl –data "Id=1&Name=eiximenis&Gender=Male" http://localhost:49228/api/Home  –header "Content-type:application/x-www-form-urlencoded"

El resultado es que se me llama al método Post del controlador pero recibo exactamente los mismos valores que antes. Recordad que en WebApi eso NO era así. Así a simple vista parece que se ha elegido el modelo de model binding de ASP.NET MVC antes que el de web api.

Otra prueba ha sido realizar un POST contra /api/Home/10 (el parámetro 10 se corresponde al route value id) y dado que estamos pasando el id por URL quitarlo del cuerpo de la petición:

curl –data "Name=eiximenis&Gender=Male" http://localhost:49228/api/Home/10  –header "Content-type:application/x-www-form-urlencoded"

El resultado es el mismo que en el caso anterior (y coincide con ASP.NET MVC donde el model binder ni se preocupa de donde vienen los datos).

Por lo tanto estas pruebas parecen sugerir que en vNext el model binding que se sigue es el de ASP.NET MVC.

Claro que cuando uno pruebas de caja negra debe tener presente el máximo número de opciones… Porque resulta que si hago algo parecido a:

curl –data "{‘Name’:’eiximenis’,’Gender’:’Male’}"http://localhost:49228/api/Home  –header "Content-type:application/json"

Entonces resulta que ambos parámetros son null. Parece ser que vNext no enlaza por defecto datos en JSON, solo en www-form-urlencoded. Además mandar datos en JSON hace que los parámetros no se enlacen. Aunque mande datos a través de la URL (p. ej. como route values) esos no se usan.

Por supuesto vNext soporta JSON, pero es que nos falta probar una cosilla…

Atributo [FromBody]

De momento en vNext existe el atributo [FromBody] (pero no existe el [FromUri]). Ni corto ni perezoso he aplicado el FromBody a uno de los parámetros del controlador:

  1. public bool Post(Product product, [FromBody] Customer customer)
  2. {
  3.     return true;
  4. }

Y he repetido la última petición (el POST a /api/Home/10). Y el resultado ha sido… un error:

System.InvalidOperationException: 415: Unsupported content type Microsoft.AspNet.Mvc.ModelBinding.ContentTypeHeaderValue

He modificado la petición cURL para usar JSON en lugar de form-urlencoded:

curl –data "{‘Name’:’eiximenis’,’Gender’:’Male’}" http://localhost:49228/api/Home/10  –header "Content-type:application/json"

Y el resultado ha sido muy interesante:

image

El parámetro customer se ha enlazado a partir de los datos en JSON del cuerpo (el Id está a 0 porque es un route value y no está en el cuerpo de la petición) pero el parámetro product está a null. Por lo tanto el uso de [FromBody] modifica el model binding a un modelo más parecido al de WebApi.

WebApi solo permite un solo parámetro enlazado desde el cuerpo de la petición. Mi duda ahora era si vNext tiene la misma restricción. Mirando el código fuente de la clase JsonInputFormatter intuía que sí… y efectivamente. Aunque a diferencia de WebApi no da error si no que tan solo enlaza el primer parámetro. Así si tengo el método:

  1. public bool Post([FromBody] Product product, [FromBody] Customer customer)

Y repito la llamada cURL anterior, los datos recibidos son:

image

El parámetro product (el primero) se ha enlazado a partir del cuerpo de la petición y el segundo vale null.

¿Y como funciona todo (más o menos)?

Recordad que ASP.NET vNext es open source y que nos podemos bajar libremente el código de su repositorio de GitHub. Con este vistazo al código he visto algunas cosillas.

El método interesante es el método GetActionArguments de la clase ReflectedActionInvoker. Dicho método es el encargado de obtener los argumentos de la acción (por tanto de todo el proceso de model binding). Dicho método hace lo siguiente:

  • Obtiene el BindingContext. El BindingContext es un objeto que tiene varias propiedades, entre ellas 3 que nos interesan:
    1. El InputFormatterProvider a usar
    2. El ModelBinder a usar
    3. Los Value providers a usar
  • Obtiene los parámetros de la acción. Cada parametro viene representado por un objeto ParameterDescriptor. Si el controlador acepta dos parámtetros (customer y product) existen dos objetos ParameterDescriptor, uno representando a cada parámetro de la acción. Dicha clase tiene una propiedad llamada BodyParameterInfo. Si el valor de dicha propiedad es null se usa un binding más tipo MVC (basado en value providers y model binders). Si el valor no es null se usa un binding más tipo WebApi (basado en InputFormatters).

Por defecto vNext viene con los siguientes Value Providers:

  1. Uno para query string (se crea siempre)
  2. Uno para form data (se crea solo si el content type es application/x-www-form-urlencoded
  3. Otro para route values (se crea siempre)

La clave está en el uso del atributo [FromBody] cuando tenemos un parámetro enlazado mediante este atributo entonces no se usan los value providers si no los InputFormatters. Pueden haber dado de alta varios InputFormatters pero solo se aplicará uno (basado en el content-type). Por defecto vNext incluye un solo InputFormatter para application/json.

Ahora bien… qué pasa si tengo un controlador como el siguiente:

  1. public IActionResult Index([FromBody] Customer customer, Product product)
  2. {
  3.     return View();
  4. }

Y hago la siguiente petición?

C:UsersetomasDesktopcurl>curl –data "{‘Name’:’eiximenis’,’Gender’:’Male’}" http://localhost:38820/Home/Index/100?Name=pepe –header "Content-type:application/json"

Pues el valor de los parámetros será como sigue:

image

Se puede ver como el parámetro enlazado con el [FromBody] se enlaza con los parámetros del cuerpo (en JSON) mientras que el parámetro enlazado sin [FromBody] se enlaza con el resto de parámetros (de la URL, routevalues y querystring). En vNext el [FromUri] no es necesario: si hay un [FromBody] el resto de elementos deben ser enlazados desde la URL. Si no hay [FromBody] los elementos serán enlazados desde cualquier parte de la request.

Bueno… en este post hemos visto un poco el funcionamiento de ASP.NET vNext en cuanto a model binding. El resumen es que estamos ante un modelo mixto del de ASP.NET MVC y WebApi.

En futuros posts veremos como podemos añadir InputFormatters y ValueProviders para configurar el sistema de model binding de vNext.

Saludos!

6 comentarios sobre “ASP.NET vNext–Model Binding”

  1. Hola, interesante el artículo, estoy viendo que aparecen bastantes cosas de la nueva versión de asp.net pero también todos aclaran que es una versión muy preliminar.

    Mi pregunta, que fecha de salida (aproximada) tiene la versión definitiva? Estuve buscando por internet y no encuentro nada al respecto.

    Gracias

  2. @supercordobes
    Pues la verdad es que no lo se. Acaba de salir la CTP2 (es decir una alfa) así que seguro que le quedan todavía algunos meses.
    Por eso se debe tener presente que todo lo dicho en esos posts puede cambiar, pero no está de más empezar a mirar como funciona 🙂

    Saludos!

  3. En realidad no es por aprender o no (siempre es bueno hacerlo), sino que es para planificar para algunos proyectos que están por empezar … mi dilema es «esperar» la nueva versión o empezarlos con lo que esta vigente actualmente.

    Gracias Eduard por tu respuesta, un abrazo!

Deja un comentario

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