Saltar al contenido

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript... y lo que venga ;)

17 años online

el blog de José M. Aguilar

Inicio El autor Contactar

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web
ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript...

¡Microsoft MVP!
martes, 21 de septiembre de 2021
ASP.NET Core MVC

Si trabajáis con ASP.NET Core MVC, seguro que con frecuencia implementaréis acciones que reciben como argumento objetos complejos, que normalmente vendrán serializados en el cuerpo de la petición como JSON.

Y probablemente, lo habréis hecho esperando que el binder obre su magia y sea capaz de transformar esas secuencias de caracteres procedentes de la red en flamantes instancias de objetos del CLR, listas para ser consumidas desde las aplicaciones. Un ejemplo de acción de este tipo es la siguiente (aunque no funcionaría, luego vemos por qué):

public class FriendsController: Controller
{
    [HttpPost]
    public string Hello(Friend friend)
    {
        return $"Hola {friend.Name}, tienes {friend.Age} años";
    }
}

public class Friend
{
    public int Age { get; set; }
    public string Name { get; set; }
}

Sin embargo, a veces nos encontramos con que, a pesar de que la petición contiene en su body los datos JSON esperados, el objeto que recibimos es nulo o tiene todas sus propiedades sin inicializar.

¿Por qué ocurre esto? En este post vamos a ver distintos motivos que podrían llevar a este comportamiento, y cómo solucionar cada caso.

1. ¿Seguro que estamos enviando bien los datos?

Esta es de cajón, pero a los despistados es algo que nos podría suceder. Quizás estemos enviando una petición sin cuerpo, por lo que difícilmente podrían poblarse los parámetros de nuestra acción.

Lo primero, por tanto, es asegurarnos de que todo lo que comprobaremos después tendrá sentido. Aseguraos que el código cliente de la API esté enviando efectivamente la información o, si usáis herramientas como Fiddler o Postman, comprobad que estáis componiendo la petición correctamente.

2. ¿Hemos olvidado el atributo [FromBody] en el parámetro?

Si, como en el ejemplo que hemos visto más arriba, hemos olvidado incluir el atributo [FromBody] en el parámetro que queremos poblar partiendo de los datos contenidos en el cuerpo de la petición, recibiremos un objeto vacío, es decir, una instancia con todas sus propiedades pobladas con los valores por defecto. La respuesta obtenida en el navegador en este caso sería:

"Hola , tienes 0 años"

La forma correcta de escribir la acción sería la siguiente:

[HttpPost]
public string Hello([FromBody]Friend friend)
{
    return $"Hola {friend.Name}, tienes {friend.Age} años";
}

Aunque ojo, si usamos controladores [ApiController], por convención se aplicará automáticamente el [FromBody] a los parámetros de tipo complejo como Friend, por lo que no será necesario hacerlo explícitamente.

3. ¿El cuerpo de la petición contiene un objeto JSON válido?

Si estáis componiendo la petición manualmente, ya sea usando herramientas como Fiddler o Postman, o desde código serializando manualmente los objetos, es posible que os hayáis equivocado en algo. Detalles simples, como unas comillas de más o de menos o una coma que falte, pueden hacer que recibamos un objeto nulo.

Por ejemplo, las peticiones realizadas con los cuerpos siguientes fallarán:

// Faltan las comillas en el nombre de las propiedades:
{
   age: 45,
   name: "Jon"
}
// Comillas incorrectas en el nombre de las propiedades:
{
   'age': 45,
   'name': "Jon"
}
// Cierre incorrecto de comilla en "age':
{
   "age': 45,
   "name": "Jon"
}
// Falta la coma para separar las propiedades:
{
   "age": 45
   "name": "Jon"
}
// Sobra la coma del final:
{
   "age": 45,
   "name": "Jon",
}

Para solucionarlo sólo tendréis que repasar cuidadosamente el contenido enviado y asegurar que está todo en orden desde el punto de vista sintáctico.

4. ¿El Content-Type es el apropiado?

Otro motivo frecuente para que recibamos objetos nulos en nuestra acción es que no exista el encabezado Content-Type en la petición, o bien su valor no indique correctamente el formato de los datos entrantes.

El parseador de ASP.NET Core debe conocer el formato de los datos entrantes, algo que se especifica con el Content-Type:

  • Si no se indica o es desconocido (es decir, no existe para él un input formatter registrado en el sistema), la acción ni siquiera llegará a ejecutarse, retornando al cliente un error HTTP 415 (Unsupported media type).
  • Si el formato especificado en el Content-Type es conocido pero no coincide con el formato de los datos de entrada, recibiremos un parámetro nulo en la acción.

La solución es sencilla; basta con asegurarse de que la petición incluya en el encabezado el tipo de contenido apropiado:

Content-Type: application/json

5. ¿Los tipos de datos no coinciden?

El binder de ASP.NET Core, o mejor dicho, el deserializador System.Text.Json que usa por debajo, es un poco tiquismiquis a la hora de mapear los datos entrantes a los parámetros requeridos por las acciones, y cualquier problema que se encuentre hace saltar por los aires el proceso. Es decir, si encuentra un problema bindeando una propiedad, no la ignorará y continuará con la siguiente, sino que abortará la deserialización, suministrándonos un objeto nulo.

Por ejemplo, si componemos una petición con el siguiente cuerpo, la acción recibirá un valor nulo:

// La propiedad "age" es int, y le estamos suministrando un string:
{
   "age": "Forty five",
   "name": "Jon"
}

Lo mismo ocurriría si intentamos enviarle un valor decimal:

// La propiedad "age" es int, y la estamos suministrando un decimal:
{
   "age": 45.00,
   "name": "Jon"
}

En este caso, la solución también consiste en revisar los datos que están siendo enviados y asegurar que sus tipos se corresponden con los de las clases donde deben introducirse.

Extra:  Todavía no lo veo, ¿cómo puedo obtener más detalles?

A veces estos errores son difíciles de detectar a simple vista, sobre todo si tratamos con objetos de una cierta complejidad. En estos casos, la forma más sencilla de ver qué está ocurriendo es habilitar el nivel de trazas Debug para los componentes de Microsoft.AspNetCore.Mvc.Formatters, por ejemplo insertando las siguientes líneas en el archivo appsettings.Development.json:

{
  ...
  "Logging": {
    "LogLevel": {
      "Default": "None",
      "Microsoft.AspNetCore.Mvc.Formatters": "Debug",
    }
  }
}

De esta forma, los errores en el proceso de deserialización serán muy visibles en las trazas. Por ejemplo, enviando un contenido como el siguiente:

{
   "age": "Hello", 
   "name": "Jon",
}

Las trazas mostrarán el siguiente mensaje (simplificado para facilitar su lectura. Como podemos observar, las pistas apuntan claramente a un problema con los tipos de datos de la propiedad age del objeto:

dbug: Microsoft.AspNetCore.Mvc.Formatters.SystemTextJsonInputFormatter[1]
      JSON input formatter threw an exception: 
      The JSON value could not be converted to System.Int32. 
      Path: $.age | LineNumber: 1 | BytePositionInLine: 17.
      [...]
       ---> System.FormatException: Either the JSON value is not 
            in a supported format, or is out of bounds for an Int32.
            [...]

También podría ayudarnos habilitar el nivel "Debug" a la categoría Microsoft.AspNetCore.Mvc.ModelBinding, de forma que veríamos claramente errores relativos a los formateadores o el proceso de binding o, incluso, habilitarlo para Microsoft.AspNetCore.Mvc, de forma que los problemas en cualquier componente de MVC pueda quedar al descubierto.

Publicado en Variable not found.

2 Comentarios:

JFM75 dijo...

Buenas, también puede darse otro caso que me ocurrió examinando un problema que tenía un compañero, y era que si por ejemplo la clase Friend en lugar de tener auto-properties, tiene esos campos como campos, es decir, sin get ni set, tampoco se rellenan y vienen vacíos.

José María Aguilar dijo...

Hola,

en efecto, es otra cosa que podría ocurrir, pues por defecto la deserialización opera sobre propiedades. Probablemente podría conseguir usando atributos o configurando el deserializador, pero es algo que habría que probar ;) https://docs.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-how-to?pivots=dotnet-5-0#include-fields

Muchas gracias por la aportación!