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 enteros0
y1
.
- 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!
Enviar un nuevo comentario