Minimal API con C# en .NET 7, filtros y Swagger

Actualizado el 08.10.2023 a las 13:37

Publicado el 11.06.2023 a las 22:02

Minimal API con C# en .NET 7, filtros y Swagger

  1. ¿Qué es minimal API?

    • ¿Cuando utilizar minimal API?

  2. Pasos para crear nuestra minimal API con .NET 7

    • Instalar .NET 7

    • Arreglando el problema de NETSDK

    • Creando el proyecto

    • Analizando el proyecto de minimal API creado por defecto con .NET 7

  3. Lo nuevo de minimal API en .NET 7, los filtros

    • ¿Qué es un filtro de minimal API en .NET?

    • Método Get con un parámetro

    • Implementando un filtro antes del endpoint

    • Definiendo un filtro como una función reutilizable

  4. Variables de configuración, el appsettings.json

    • Definiendo variables en el appsettings.json

    • Leyendo variables del appsettings.json

Logo de fjmduran

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.

.NET 8 en preview

¿Qué es minimal API?

.NET 6 ya introdujo el concepto de minimal API y lo conté con algo más de detalle aquí 👇


Imagen del artículo

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 .

.NET 8 en preview

No tiene ninguna ciencia, next, next...

Pantalla de instalación de .NET 7

Para comprobar que se ha instalado correctamente escribe en una pantalla de comandos dotnet y si se ha instalado correctamente verás lo siguiente:

Comprobando la instalación de .NET 7

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.

Error NETSDK al intentar ejectuar el proyecto de .NET 7 con VS 2022

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.

Actualizando VS 2022 a la versión 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

  1. Abrimos Visual Studio 2022 y elegimos crear proyecto Creando un proyecto nuevo con Visual Studio 2022
  2. Escribe web en la caja de búsqueda y elige el tipo de proyecto ASP.NET Core Web API

    Seleccionando el tipo de proyecto
  3. Clic en siguiente y elegimos un nombre
  4. 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.

    Seleccionando el framework

Si toda ha ido bien el proyecto creado tendrá el siguiente aspecto.

Pantalla de bienvenida a minimal API de .NET 7

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();
      
Contenido de program.cs

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

Resultado de ejecutar el proyecto

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 💃

Resultado de ejecutar el proyecto

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:

endpoint saluda

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
  1. Cargaremos el método AddEndpointFilter del endpoint y en su interior especificaremos el filtro.
  2. 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.
  3. 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 
});
      
Resultado del endpoint asíncrono en .NET7 con filtro

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>(); 👈
      
Resultado del endpoint asíncrono en .NET7 con filtro como DRY

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

Fichero appsetting en ráiz de proyecto visual studio

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 🖖