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, 25 de abril de 2023
ASP.NET Core

Atributos como [FromRoute], [FromForm], [FromQuery] o [FromBody], entre otros, permiten ser muy precisos a la hora de indicar al framework cómo poblar los parámetros de los handlers de nuestros endpoints contruídos con Minimal APIs.

Por ejemplo, en la siguiente API sencilla se puede intuir que el parámetro id del manejador será obtenido de la ruta, mientras que number se obtendrá desde la query string:

app.MapPost("/friends/{id}/phones", ([FromRoute] int id, [FromQuery] string number) =>
{
    // Añadir un número de teléfono al amigo
});

Y otro ejemplo, en el que usamos  [FromBody] para especificar que el parámetro de tipo Friend queremos obtenerlo desde el cuerpo de la petición:

app.MapPut("/friends/{id}", ([FromRoute] int id, [FromBody] Friend friend) =>
{
    // Actualizar amigo
});

Existen también convenciones o comportamientos establecidos por defecto que hacen que esto resulte más sencillo en la mayoría de ocasiones. Por ejemplo, los parámetros de tipo valor siempre intentarán tomarse de la ruta y de la query string (en este orden), o los tipos complejos se obtendrán del cuerpo (siempre que la petición lo soporte, es decir, que sea POST, PUT, etc.) Así pues, los ejemplos anteriores funcionarían exactamente igual si los escribimos de la siguiente manera:

app.MapPost("/friends/{id}/phones", (int id, string number) =>
{
    // Añadir un número de teléfono al amigo
});

app.MapPut("/friends/{id}", (int id, Friend friend) =>
{
    // Actualizar amigo
});

De cualquier forma, ya sea mediante convenciones o bien usando los citados atributos, nos encontraremos un par de limitaciones:

  • Primero, que no podemos influir en la forma en que los datos se transforman antes de introducirlos en los objetos. Por ejemplo, imaginad que en Friend tenemos una propiedad booleana que sabemos que sus datos nos llegarán en forma de enteros 0 y 1.
     
  • Y segundo, que cada parámetro sólo puede proceder de un origen. No tenemos posibilidad de obtener los datos de distintos orígenes (por ejemplo, que una propiedad de Friend salga de la query string y otra de la ruta).

Para estos casos, y otros donde la necesitemos una personalización total en la forma de leer los valores desde los distintos orígenes, es donde BindAsync() toma sentido.

Se trata de un método estático que definiremos en el objeto cuyo binding queremos personalizar, con la siguiente firma, que debemos usar de forma estricta:

public static async ValueTask<T?> BindAsync(HttpContext context)
{
    ...
}

T es el tipo en el que estamos implementando este método; así pues, en el caso concreto de nuestra clase Friend, quedaría más o menos como se muestra a continuación:

public class Friend
{
    public int Id { get; set; }
    public string Name { get; set; }
    public bool IsBestFriend { get; set; }

    public static async ValueTask<Friend?> BindAsync(HttpContext context)
    {
        // Lógica personalizada
    }
}

Una vez introducido ese método, cuando el framework tenga necesidad de bindear un valor de tipo Friend, y siempre que no se hayan usado atributos que indiquen explícitamente qué origen utilizar, se intentará utilizar su método BindAsync(). Observad que a este método llega el contexto HTTP completo, por lo que podemos usarlo a nuestro antojo para personalizar la lógica de creación del objeto a partir de las entradas suministradas.

Veámoslo con un ejemplo que, aunque no muy realista, puede ilustrar un caso complejo . Imaginemos la siguiente llamada:

app.MapPut("/friends/{id}", (Friend f) => 
{
    // Actualizar amigo
});

E imaginad también que las llamadas las haremos de la siguiente forma:

PUT https://localhost:7027/friends/1 HTTP/1.1
Content-Type: application/json

{
    "name": "John Doe",
    "isBestFriend": 1
}

Fijaos en que el campo id no forma parte del JSON, y que isBestFriend recibe 1 para indicar el valor cierto para el campo. Si con todo esto queremos que al handler de la API entre un objeto Friend correcto, podríamos solucionarlo usando BindAsync() como vemos ahora:

public class Friend
{
    public int Id { get; set; }
    public string Name { get; set; }
    public bool IsBestFriend { get; set; }

    public static async ValueTask<Friend?> BindAsync(HttpContext context)
    {
        JsonFriend? jsonFriend;
        try
        {
            jsonFriend = await JsonSerializer.DeserializeAsync<JsonFriend>(
               context.Request.Body
            );
        }
        catch { return null; }

        if (jsonFriend == null 
            || !int.TryParse(context.Request.RouteValues["id"].ToString(), out var id))
            return null;

        return new Friend
        {
            Id = id,
            Name = jsonFriend.name,
            IsBestFriend = jsonFriend.isBestFriend == 1
        };
    }

    record JsonFriend(string name, int isBestFriend);
}    

En este código hemos creado rápidamente un tipo record para deserializar los campos que vienen en el cuerpo de la llamada, obtenemos luego el id desde los parámetros de ruta, y finalmente retornamos el objeto Friend que construimos con los datos que ya tenemos en memoria. Este objeto será el suministrado al handler de la API.

Si algo falla durante el proceso, se retornará null. En estos casos, la infraestructura de Minimal API retornará automáticamente al cliente un error 400 (Bad request) indicando que el parámetro que se intentaba bindear no ha sido proporcionado.

Publicado en Variable not found.

Aún no hay comentarios, ¡sé el primero!