Minimal API con C# en .NET 7, filtros y Swagger
Tengo que implementar una API para un nuevo servicio esta semana y antes de elegir la tecnología a utilizar he querido probar más a fondo la minimal API de .NET, temas de middleware, filtros, documentación para el frontend...
A fecha de este artículo, la versión 8 de .NET está disponible pero en preview y no lo quiero utilizar para un proyecto en producción aún.
¿Qué es minimal API?
.NET 6 ya introdujo el concepto de minimal API y lo conté con algo más de detalle aquí 👇
Minimal API con C# en .NET 6
Te explico cómo crear una aplicación API REST con el menor código posible con C# en .NET 6
¿Cuando utilizar minimal API?
Haremos uso de este tipo de aplicaciones cuando, por ejemplo, queramos desplegar una API con pocos endpoints.
Lo que te quiero decir es que no tienes necesidad de crear proyectos complejos con arquitecturas por ejemplo MVC de ASP.
El concepto de minimal API se adapta perfectamente a los microservicios 🧐
Pasos para crear nuestra minimal API con .NET 7
Instalar .NET 7
Lo primero será instalar el SDK de .NET 7 si no lo tienes ya instalado.
Lo puedes descargar desde su página oficial .
No tiene ninguna ciencia, next, next...
Para comprobar que se ha instalado correctamente escribe en una pantalla de comandos dotnet y si se ha instalado correctamente verás lo siguiente:
Arreglando el problema de NETSDK
Cuando creé mi proyecto de prueba con .NET 7, me encontré con el siguiente error
🚩NETSDK1 El SDK de .NET actual no admite el destino .NET7.0. Use el destino .NET6.0 u otro inferior, o bien una versión del SDK de .NET que admita .NET 7.0.
Después de estar varias horas leyendo la documentación, el problema era que mi Visual Studio 2022 no estaba actualizado 🙈
En mi caso tenía la versión 17.3.1 y la actualicé a la última disponible, la 17.6.2.
Para actualizar tu Visual Studio 2022 en Windows, ve a Agregar o quitar programas, eliges Visual Studio 2022 y actualizar.
Creando el proyecto
- Abrimos Visual Studio 2022 y elegimos crear proyecto
Escribe web en la caja de búsqueda y elige el tipo de proyecto ASP.NET Core Web API
- Clic en siguiente y elegimos un nombre
Nos aseguramos en la pantalla de selección del Framework usar .NET 7.0, deshabilitar el checkbox de Usar controladores, ya que si lo dejas activado no te creará una plantilla de minimal API.
Ya por último te sugiero que dejes activado el checkbox de Habilitar compatibilidad con OpenAPI para que trabajemos con Swager y poder probar nuestra API sin necesidad de usar ningún tipo de cliente HTTP tipo Postman.
Si toda ha ido bien el proyecto creado tendrá el siguiente aspecto.
Analizando el proyecto de minimal API creado por defecto con .NET 7
Si te fijas, en el proyecto creado por defecto tan sólo tenemos un fichero de clase llamado Program.cs
Modifica el contenido de dicha clase y deja lo siguiente 👇
var builder = WebApplication.CreateBuilder(args); // Add services to the container. // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); var app = builder.Build(); // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseHttpsRedirection(); app.MapGet("/test", () => { return "Hola mundo"; }); app.Run();
En la primera línea de código vemos una llamada al método CreateBuilder por el objeto builder para inicializar una API.
De hecho, en la línea 4 ya estamos creando un endpoint, que será el endpoint por defecto, que devuelve el texto Hola mundo.
Si ejecutas el proyecto y dejaste activada el checkbox de OpenAPI al crear el proyecto verás que se abre tu navegador con la siguiente pantalla de Swagger
Si haces clic en el endpoint que creamos de test veremos la especificación en endpoint perfectamente documentada para que los desarrolladores del frontend sepan cómo usarla.
Incluso podremos probar dicho endpoint pulsando en el botón de Try out 😱
Sin necesidad de usar ningún cliente HTTP 💃
Lo nuevo de minimal API en .NET 7, los filtros
¿Qué es un filtro de minimal API en .NET?
Un filtro es una función o método que se podrá ejecutar antes o después de un endpoint.
Vamos a verlo mejor con un ejemplo.
Método Get con un parámetro
Vamos a crear un endpoint al que llamaremos dameProductos y le pasaremos como parámetro el número de productos que queremos que nos devuelva, como si fuera para una tabla paginada.
El código sería:
var builder = WebApplication.CreateBuilder(args); // Add services to the container. // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); var app = builder.Build(); // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseHttpsRedirection(); var productos = new Producto[] { new Producto("Victus 16-e0101ns AMD Ryzen 7 5800H/16GB/512GB SSD/RTX 3060/16.1\"", "HP"), new Producto("15S-FQ5028NS Intel Core i5-1235U/16GB/512GB SSD/15.6\"", "HP"), new Producto("ASUS VivoBook F1500EA-EJ2384W Intel Core i3-1115G4/8GB/256GB SSD/15.6\"", "Asus"), new Producto("Lenovo IdeaPad 3 15ITL6 Intel Core i7-1165G7/16 GB/512GB SSD/15.6\"", "Lenovo") }; app.MapGet("/test", () => { return "Hola mundo"; }); app.MapGet("/dameProductos/{ cantidad}", (int cantidad) => { return productos.Take(cantidad); }); app.Run(); internal record Producto(string Nombre, String Marca);
Si corremos nuestra aplicación y probamos el nuevo endpoint pasándole como parámetro la cantidad, obtendremos:
Implementando un filtro antes del endpoint
Como hemos dicho anteriormente un filtro en una función o método que se ejecturá antes o después del endpoint.
Vamos a usar un filtro para validar el número de productos que vamos a devolver, porque imagina que nos pasan un número negativo como parámetro de nuestro campo cantidad, no tendría sentido.
Vamos 👇
- Para crear un filtro
- Cargaremos el método AddEndpointFilter del endpoint y en su interior especificaremos el filtro.
- Le pasaremos el filtro como una función de flecha asíncrona que hará la funcionalidad y tomará dos parámetros, el contexto de la solicitud al que llamaremos context y a dónde se dirigirá si todo va bien que llamaremos next.
- En la función de flecha programaremos la funcionalidad y evaluaremos el valor del número de productos que nos solicitan.
Para obtener el parámetro del endpoint usaremos el método GetArgument del context, te recomiendo que lo tipes fuertemente para evitar problemas del tipo injección SQL...
app.MapGet("/dameProductos/{cantidad}", (int cantidad) => { return productos.Take(cantidad); }).AddEndpointFilter(async (context, next) => { //obtenemos el parámetro cantidad //lo haremos con el método GetArgument del context y le //pediremos el primer parámetro que será el de la posición 0 int cantidad = context.GetArgument<int>(0); if(cantidad <= 0) { return Results.Problem("Debe proporcionar un número de productos mayor o igual a 0"); } return await next(context); //continúa al endpoint });
Definiendo un filtro como una función reutilizable en .NET 7
Para poder implementar DRY (Do not Reply Yourself) y no tener que estar copiando un filtro en cada uno de los endpoint, imagina en la validación del token del frontend en los endpoint que requieran autenticación podemos definirlo implementando la interfaz IEndpointFilter:
public class FiltroCantidadGrande : IEndpointFilter 👈 { public async ValueTask<object?> InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next) { int cantidad = context.GetArgument<int>(0); if (cantidad > 4) { return Results.Problem("Debe proporcionar un número de productos mayor o igual a 0"); } return await next(context); //continúa al endpoint } }
Y para añadir este filtro a nuestro endpoint tan fácil como cargar el método AddEndpointFilter pasándole como tipo nuestro nuevo filtro
app.MapGet("/dameProductos/{cantidad}", (int cantidad) => { return productos.Take(cantidad); }).AddEndpointFilter(async (context, next) => { int cantidad = context.GetArgument<int>(0); if (cantidad <= 0) { return Results.Problem($"Debe proporcionar un número de productos menor a {cantidad}"); } return await next(context); //continúa al endpoint }).AddEndpointFilter<FiltroCantidadGrande>(); 👈
A continuación te dejo todo el código junto del fichero Program.cs para que pruebes.
var builder = WebApplication.CreateBuilder(args); // Add services to the container. // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); var app = builder.Build(); // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseHttpsRedirection(); var productos = new Producto[] { new Producto("Victus 16-e0101ns AMD Ryzen 7 5800H/16GB/512GB SSD/RTX 3060/16.1\"", "HP"), new Producto("15S-FQ5028NS Intel Core i5-1235U/16GB/512GB SSD/15.6\"", "HP"), new Producto("ASUS VivoBook F1500EA-EJ2384W Intel Core i3-1115G4/8GB/256GB SSD/15.6\"", "Asus"), new Producto("Lenovo IdeaPad 3 15ITL6 Intel Core i7-1165G7/16 GB/512GB SSD/15.6\"", "Lenovo") }; app.MapGet("/test", () => { return "Hola mundo"; }); app.MapGet("/dameProductos/{cantidad}", (int cantidad) => { return productos.Take(cantidad); }).AddEndpointFilter(async (context, next) => { int cantidad = context.GetArgument<int>(0); if (cantidad <= 0) { return Results.Problem("Debe proporcionar un número de productos mayor o igual a 0"); } return await next(context); //continúa al endpoint }).AddEndpointFilter<FiltroCantidadGrande>(); app.Run(); internal record Producto(string Nombre, String Marca); public class FiltroCantidadGrande : IEndpointFilter { public async ValueTask<object?> InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next) { int cantidad = context.GetArgument<int>(0); if (cantidad > 4) { return Results.Problem($"Debe proporcionar un número de productos menor a {cantidad}"); } return await next(context); //continúa al endpoint } }
¿Cómo se definen y se leen variables de configuración en una minimal API de .NET7?
Cuando Visual Studio nos crea el proyecto, nos añade un archivo JSON en la raíz del mismo llamado appsettings.json
Ese archivo es el archivo que nos recomienda la documentación de Microsoft para almacenar variables en nuestra aplicación, te explico.
Los desarrolladores no debemos tener acceso a información importante de la aplicación desplegada, por ejemplo, a las credenciales de acceso a la base de datos de producción.
En el caso de que tengas acceso que sepas, en mi opinión, que es un gran error 🚩
Los desarrolladores deberíamos trabajar contra una réplica de la base de datos de producción, obviamente con credenciales distintas, de forma que dichas credenciales de producción las maneje solo el DevOps o el manager del proyecto.
Sé lo que me vas a decir, que tienes esas credenciales porque te ha tocado arreglar algún HotFix 🔥, pero eso no es excusa, de hecho, estoy en contra de hacer HotFix.., no obstante, donde manda patrón no manda marinero.
Definiendo variables en el appsettings.json
Sea como sea, a lo que voy es que por ejemplo las credenciales para conectarnos a la base de datos debería ser parte de la configuración de nuestro Sistema y será en el appsettings.json
donde las definamos.
Te muestro un appsettings.json
de una de mis aplicaciones:
{ "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } }, "ApplicationInsights": { "bdd_ip_server": "172.19.2.1", "bdd_port_server": "5432", "bdd_user_server": "debug_user", "bdd_password_server": "Q2fgGzXc34rkñl*&", }, "AllowedHosts": "*" }
Como puedes ver, ese fichero no sólo lo puedes usar para las credenciales para tu base de datos, si no que también lo puedes usar para tu whitelist de dominios autorizados, para definir el nivel de detalles de registros de tu log...
Leyendo variables del appsettings.json
Para poder leer el appsettings.json
al iniciar la aplicación tan fácil como después de crear la apliación añadir dicho fichero a la aplicación y leer sus campos.
Por ejemplo, para leer los campos de la IP del servidor de base de datos y el puerto sería:
var builder = WebApplication.CreateBuilder(args); // Agrega configuración desde appsettings.json builder.Configuration.AddJsonFile("appsettings.json"); var bdd_ip_server = builder.Configuration.GetValue<string>("ApplicationInsights:bdd_ip_server") ?? "localhost"; var bdd_port_server = builder.Configuration.GetValue<string>("ApplicationInsights:bdd_port_server") ?? "5432";
Uso el operador de coalescencia nula (??) para asignar valores por defecto en el caso de que no se encuentra dicho parámetro de configuración en el appsettings.json
Hasta luego 🖖