20 abr 2024

.Net minimal API y Auto-Bindings

Github Repo

 Estoy, de a poco, retornando al mundo de .Net, por lo que para ir desempolvando conocimiento al tiempo que pruebo algunos conceptos con los que no estoy familiarizado pensé en realizar un ejercicio sencillo de crear una minimal API y jugar (para entender) con la característica de los auto-bindings que posee .Net 8. Esto basándome en un post en ingles que encontré aquí).

Empecemos pues: Primero escogemos un nuevo proyecto del tipo asp.net core empty, para tener lo mínimo necesario

Le asignamos un nombre y una ubicación y seguidamente escogemos el framework 8.0, con la configuración para HTTPS y habilitamos Docker.


Al final obtendremos una configuración muy básica donde tendremos un archivo program.cs donde podremos agregar nuestros endpoints. El código generado es el siguiente:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();
De hecho yo solo voy a cambiar el texto del "Hello world!" por ""My minimal API is up and running!" y lo voy a ejecutar, al hacerlo se abre una ventana de navegador con el mensaje esperado:
Lo siguiente que voy a hacer es añadir soporte para el estándar OpenAPI, por medio de swagger. Para esto se debe añadir el paquete Swashbuckle.AspNetCore por medio de la Package Manager Console ejecutando la siguiente instrucción en esta consola:
Install-Package Swashbuckle.AspNetCore
Una vez instalado este paquete paquete, modificamos el archivo program.cs para quede de la siguiente manera:
using Microsoft.AspNetCore.Mvc;

#region Configuring

var builder = WebApplication.CreateBuilder(args);

//configuring CORS
builder.Services.AddCors(op => op.AddDefaultPolicy(
    builder =>
    {
        builder.AllowAnyOrigin()
        .AllowAnyMethod()
        .AllowAnyHeader();
    }));

//Adding Swagger
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddMvc(x => x.EnableEndpointRouting = false);

var app = builder.Build();

app.UseCors();

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseMvc();

#endregion Configuring

#region Endpoints

app.MapGet("/", () => "My minimal API is up and running!");

#endregion Endpoints

app.Run();

#region helpers/utilities
#endregion
Esta parte es un poco de "carpintería" estándar en la que añadimos el manejo de CORS para facilitarlos las pruebas, así como la adición del Estándar OpenAPI el que nos da una interfaz más amigable para nuestra api:
Com se puede notar, el código esta divido en tres secciones "configuration", "endpoints" y "helpers/utilities" lo que nos ayudará a organizar e indicar en dónde va el código que vamos a crear. Ahora vamos a añadir nuevos endpoints para probar la característica de Auto-Binding. 

Microsoft .Net permite marcar los parámetros de los endpoints de nuestras APIs (no únicamente de las minimal APIs) con atributos que establecen como éstos se enlazan (binding) automáticamente según como se espera que se envíen al API. Estos atributos en la version 8 de .Net son:
  • [FromQuery]
  • [FromForm]
  • [FromBody]
  • [FromHeader]
  • [FromServices]
  • [FromKeyedServices]

FromQuery

Añadimos un nuevo endpoint Get que obtenga un id del querystring y devuelva un simple objeto con un mensaje indicando que id recibió
app.MapGet("/users", IResult ([FromQuery] int id) =>
{
    return Results.Ok(new { message = $"You send the id {id}" });
});
Esto lo podemos probar atravéz de la interfaz de OpenAPI (Swagger)
Como podemos ver el id es enviado a la API como un parametro querystring (en la url) y es enlazado correctamente, devolviendo una respuesta satisfactoria.

Ahora vamos a crear un endpoint POST que reciba dos parametros por medio de querystring
app.MapPost("/users", IResult ([FromQuery] int id, [FromQuery] string name) =>
{
    return Results.Ok(new { message = $"You send the id {id} with the name {name}" });
});
Aunque es posible y más sencillo probar este nuevo endpoint tal y como lo hicimos con el anterior, usando swagger; Esta vez vamos a usar otra técnica: vamos usar usar javascript. Y ¿Cómo lo hacemos? pues en la ventana del navegador abrimos las herramientas de desarrollador (usualmente con la tecla F12, vamos a la consola y aqui podemos escribir o pegar codigo javascript) por ejemplo:
fetch(`https://localhost:32770/users?id=1&name=foy`,
      {method: 'POST'})
        .then(response => response.json())
        .then(data => console.log(data));
Una vez más obtenemos el resultado esperado.

FromForm

Vamos a crear un nuevo endpoint POST que espera que los parametros sean enviados atravez de un formulario (form)
app.MapPost("/users2", IResult ([FromForm] int id, [FromForm] string name) =>
{
    return Results.Ok(new { message = $"You send the id {id} with the name {name}" });
});
Este código compila y parece correcto, de hecho lo es. Pero si intentamos probarlo, por ejemplo con Swagger, vamos a obtener el siguiente mensaje
El problema es que apartir de la versión 8 de .Net el envío de formularios requieren, por defecto, validar el token de anti-forgery usando un nuevo middleware. Para los efectos de esta publicación únicamente voy a omitir esta validación ajustando ligeramente el código añadiendo el llamado a DisableAntiforgery al final del endpoint quedado el código de esta manera (esto para nada es una recomendación):
app.MapPost("/users2", IResult ([FromForm] int id, [FromForm] string name) =>
{
    return Results.Ok(new { message = $"You send the id {id} with the name {name}" });
}).DisableAntiforgery();
Para comprobar que ahora funciona correctamente podemos usar este código de javasript o bien Swagger:
var fdata = new FormData();
fdata.append("id", 1);
fdata.append("name","foy");
fetch(`https://localhost:32770/users2`,{
     method:'POST',
     body:fdata,
})
.then(response => response.json())
.then(data => console.log(data));
Adicionalmente podemos usar una clase o un record como parámetro cuando usamos el atributo [FromForm]. Por ejemplo vamos a crear un record en nuestra region de helper/utilities de esta manera:
record User
{
    public int id { get; set; }
    public string name { get; set; }
}
Ahora podemos añadir un nuevo endpoint que utilice este record como el tipo de parámetro
app.MapPost("/users3", IResult ([FromForm] User user) =>
{
    return Results.Ok(new { message = $"You send a model with the id {user.id} and name {user.name} " });
}).DisableAntiforgery();
Se puede usar exactamente el mismo código javascript que utilizamos para probar el último endpoint solo cambiando users3 en lugar de users2 en la URL y obtendremos una respuesta satisfactoria (o también podemos usar el swagger).

FromBody

El attribute [FromBody] es el que usamos para obtener los datos cuando se nos envía un objeto JSON. Y ese concepto del "objeto" es donde esta la clave para entender su comportamiento, Intentemos un enfoque muy simple, creamos un endpoint donde recibimos un único parámetro. 
app.MapPost("/users4", IResult ([FromBody] int id) =>
{
    return Results.Ok(new { message = $"You send the id {id}" });
});
A primera vista este codigo parece intuitivo y correcto, pero tiene un problema. Si probamos el código, usando el swagger por ejemplo, obtenemos esta respuesta:
El mensaje nos indica que no pudo leer el parámetro int id. La pista, como dijimos antes, es que estamos enviando el parámetro id dentro de un objeto JSON, por lo que debemos tener un objeto para recibir este parámetro. Para lograr esto vamos a crear una clase con una única propiedad llamada id en le región de helpers/utilities:
internal class UserId
{
    public int id { get; set; }
}
Es solo un contenedor de la propiedad, un modelo muy simple podríamos decir. Ahora podemos modificar el endpoint users4 para que use esta clase como tipo del parámetro, quedando el código de esta manera:
app.MapPost("/users4", IResult ([FromBody] UserId userId) =>
{
    return Results.Ok(new { message = $"You send the id {userId.id}" });
});

Ya lo podemos probar usando Swagger y obtendremos una respuesta satisfactoria

Por supuesto que el tipo usado como parámetro puede ser mas complejo, de hecho podemos usar el record creado anteriormente. Vamos a crear un nuevo endpoint para comprobarlo
app.MapPost("/users5", IResult ([FromBody] User user) =>
{
    return Results.Ok(new { message = $"You send a model with the id {user.id} and name {user.name} " });
});

Esta vez para probarlo usemos el siguiente código javascript (el resultado es el mismo que con swagger):
var jdata = {"id":1, "name":"Foy"};
fetch(`https://localhost:32770/users5`,{
     method:'POST',
     body:JSON.stringify(jdata), 
  headers: {
           'Content-type':'application/json; charset=UTF-8',
       }  
})
.then(response => response.json())
.then(data => console.log(data));
Como podemos comprobar el resultado es satisfactorio.

FromHeader

El atributo [FromHeader] es muy simple y similar a [FromQuery] sólo que en este caso en lugar de buscar el parámetro en el querystring lo buscaremos en el header del request. Definimos un endpoint que reciba un único parámetro:
app.MapPost("/users6", IResult ([FromHeader] int id) =>
{
    return Results.Ok(new { message = $"You send the id {id} in the header" });
});
Una vez más podemos probar el endoint usando swagger o el siguiente código javascript que envia en el header el parametro id
fetch(`https://localhost:32770/users6`,{
    method:'POST', 
    headers: {
           'Content-type':'application/json; charset=UTF-8',
            "id": 1
       },
})
.then(response => response.json())
.then(data => console.log(data));
Y obtenemos la respuesta es la esperada.

Inyección de dependencias

Ahora vamos a pasar a los últimos dos atributos, pero estos tienen una aplicación diferente a los que hemos visto hasta ahora. [FromQuery], [FromForm], [FromBody] y [FromHeader] nos sirven para determinar la fuente del enlace esperado para los parámetros que definimos, pero esta fuente viene dada por el consumidor de nuestro servicio, es decir, lo que nos envía quien consume nuestra API.

Ahora bien, [FromServices] y [FromKeyedServices] tienen otro propósito, nos ayudan a enlazar nuestros parámetros con servicios internos de la API, es decir a inyectar dependencias directamente en los métodos que en este caso representan endpoints. Esto es nuevo en .Net 8, en las versiones anteriores la inyección de dependencias la establecíamos en el constructor de la clase, ahora estos nuevos atributos nos brindar flexibilidad adicional en este sentido.

FromServices

Supongamos que tenemos un servicio de caché que implementa la interfaz ICache que hemos definido previamente. En nuestra minimal API (o en una web API "normal") queremos usar este servicio pero únicamente en uno de nuestros endpoints. Anteriormente lo que hubiésemos hecho seria inyectar esta dependencia en el constructor del controlador correspondiente, sin importar la cantidad endpoint que este contenga, aunque solo lo necesitamos en uno.

Ahora con el atributo [FromServices] podemos ser mas flexibles y precisos de dónde queremos esta inyección. Empecemos por añadir en la región de Helpers/Utilities la interfaz ICache y la clase que la implementa que llamaremos SmallCache:
public interface ICache
{
    object Get(string key);
}

public class SmallCache : ICache
{
    public object Get(string key) => $"Resolving {key} from small cache.";
}
Como podemos deducir del código este servicio tiene un único método "Get" que recibe una "key" y, para los propósitos de este código, únicamente devolvemos un mensaje (Si fuera una implementación real devolveríamos el objeto asociado a este key si estuviese almacenado en nuestro caché). Ahora necesitamos registrar este servicio en el builder de la minimal API para que el mecanismo de inyección de dependencias nos permita utilizarlo, para esto en la region de configuration antes de la instrucción var app = builder.Build(); añadimos la siguiente instrucción:
builder.Services.AddSingleton();
Con esto añadimos una instancia singleton de SmallCache que será inyectada cada vez que solicitemos un objeto que implemente la interfaz ICache. Con esta implementación vamos a crear un nuevo endpoint GET que reciba un id en el querystring y al que se le inyectará el servicio SmallCache:

app.MapGet("/users7", IResult ([FromQuery] int id, [FromServices] ICache cacheService) =>
{
    string cacheMessage = (string)cacheService.Get($"MyKey{id}");
    return Results.Ok(new { message = $"You send the id {id}.", cache = cacheMessage });
});

Podemos probarlo usando Swagger para constatar que el servicio small cache es inyectado correctamente y devuelve el resultado esperado.

FromKeyedServices

El atributo [FromKeyedServices] funciona de manera muy similar a [FromServices] con la diferencia de que el caso de que existan varios servicios que implementan la misma interfaz podemos especificar cual queremos que se instancie e inyecte en nuestro método. Para ejemplificar esto supongamos que ahora tenemos otro servicio que implementa la interfaz ICache, la clase que lo implementa se llama BigCache, de manera que en nuestra región Helpers/Utilities añadimos el siguiente código:
public class BigCache : ICache
{
    public object Get(string key) => $"Resolving {key} from big cache.";
}
Ahora necesitamos registrar este nuevo servicio. Vamos a hacerlo en la region configuration antes de la linea var app = builder.Build(); con esta nueva instrucción.
builder.Services.AddKeyedSingleton("big");
Como podemos deducir, estamos registrando un nuevo servicio que implementa la interfaz ICache pero le estamos asignado la llame (key) "big" por lo que ahora podemos crear un nuevo endpoint y por medio de [FromKeyedServices] especificar que queremos esta implementación y no la por defecto por medio de este código:
app.MapGet("/users8", IResult ([FromQuery] int id, [FromKeyedServices("big")] ICache cacheService) =>
{
    string cacheMessage = (string)cacheService.Get($"MyKey{id}");
    return Results.Ok(new { message = $"You send the id {id}.", cache = cacheMessage });
})
El atributo especificamos la "key" del servicio que queremos se inyecte y la inyección de dependencias de .net hace el resto. Al probarlo con Swagger constatamos que funciona correctamente.

También es posible registrar ambos servicios BigCache y SmallCache usando el método AddKeyedSingleton para ambos escogiendo por ejemplo la llave "small" para el segundo, lo que nos obligaría a usar el atributo [FromKeyedServices] en el endpoint users7 si así se desea.

Con esto termino este post que creo que se me extendió más de lo que pretendía. 

Como un recurso adicional se encuentra esta página de la documentación de Microsoft respecto a los bindings disponibles. Eso es todo por hoy.

Roy {aka. Foy}

Autor & Editor

Desarrallador y líder técnico, con experiencia en tecnologías Microsoft desde los tiempos del VB6 y el asp clásico hasta el .Net Core, pasando por COM+, javascript, angularjs, Ionic, xaml, cordova, MVC, Web Api, Sql Server, Oracle... . Ávido lector, apasionado programador.