ASP.NET Core 6: Minimal APIs y Carter
Tags: minimalapis, dotnet, csharp, carter
Published: 2021-10-17 21:42:48

# Introducción

El mundo de .NET 6 está a la vuelta de la esquina (al día de hoy está en RC 2) y no solo yo estoy emocionado con estos cambios que vienen, si no también el equipo de Carter ya se ha encargado de no quedarse atrás y aprovechar lo nuevo que se viene.

[Carter](https://github.com/CarterCommunity/Carter) en versiones anteriores a .NET 6, nos ofrecían esta forma elegante de modularizar nuestras APIs y olvidarnos por siempre de los controladores y tener una forma simple de crear endpoints. Ahora esto ya lo hace las Minimal APIs pero eso significa que Carter esté muerto.

Para conocer más sobre Minimal APIs podemos ver este [gist](https://www.notion.so/ff1addd02d239d2d26f4648a06158727) de David Flow que simplemente es lo que necesitas para entender todo sobre Minimal APIs.

Otra forma de aprender sobre Minimal APIs es este [repositorio](https://github.com/DamianEdwards/MinimalApiPlayground) de Damian Edwards donde explora toda las funcionalidades con las que se cuenta. Si quieren aprender muy bien Minimal APIs les recomiendo esos dos enlaces.

Te recomiendo seguir este post viendo el código en [GitHub](https://github.com/isaacOjeda/DevToPosts/tree/main/MinimalApis) para una mejor comprensión.

# Carter

Lo que exploraremos en este post es la creación de Minimal APIs con carter con un ejemplo sencillo de catálogo de Productos (as usual).

## ¿Por qué usar Carter?

Que usemos Minimal APIs no significa que nuestra aplicación tiene que ser pequeña. Minimal APIs nace para tener una introducción sencilla a .NET y empezar sin tanto **boilerplate** y **ceremony**.

Carter nos da la posibilidad de modularizar nuestra API de una forma efectiva, utilizando una [arquitectura vertical](https://jimmybogard.com/vertical-slice-architecture/) donde todo nuestro código está partido en **Features,** aunque esto es meramente opcional y no será tema de este post.

> Nota 👀: Muchos de los features que exploraremos en este post no son propias de carter sino de Minimal APIs.
> 

También Carter ya nos incluye [FluentValidation](https://github.com/FluentValidation/FluentValidation), que es algo muy importante porque el `ModelState` en Minimal APIs no está disponible. Así que necesitamos alguna forma de validar nuestros modelos. Damian Edwards tiene una [librería](https://github.com/DamianEdwards/MiniValidation) que hace validaciones con Data Annotations, que es un approach válido también, aunque prefiero no decorar los modelos con atributos (y más en este ejemplo que estamos usando directamente los Domain Objects y no DTOs).

## Carter en ASP.NET Core 6

Comenzaremos desde 0, creando un proyecto web vacío con .NET 6 instalado previamente (versión RC 2 para este post).

```bash
dotnet new web -o MinimalApis
```

Y posteriormente instalaremos los siguientes paquetes que incluyen carter y varios de EF Core (revisa mi [GitHub](https://github.com/isaacOjeda/DevToPosts/tree/main/MinimalApis) para ver el ejemplo completo para que no te queden dudas)

```xml
<PackageReference Include="Carter" Version="6.0.0-pre2" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.0-rc.2.21480.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="6.0.0-rc.2.21480.5" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3" />
```

> Nota 👀: Recuerda que estamos usando versiones preview y RC de estos paquetes NuGet. Si no te aparecen, hay que decirle a NuGet que incluya paquetes en **pre-release** y si estás leyendo esto después de noviembre, probablemente ya estarás usando las versiones finales.
> 

### In Memory Database

Para este ejemplo usaremos la siguiente base de datos con Entity Framework Core que mas adelante la configuraremos para que solo sea en memoria (para fines prácticos).

```csharp
namespace MinimalApis.Api.Entities;
public class Product
{
    public Product(string description, double price)
    {
        Description = description;
        Price = price;
    }

    public int ProductId { get; set; }
    public string Description { get; set; }
    public double Price { get; set; }
}
```

```csharp
using Microsoft.EntityFrameworkCore;
using MinimalApis.Api.Entities;

namespace MinimalApis.Api.Persistence;
public class MyDbContext : DbContext
{
    public MyDbContext(DbContextOptions<MyDbContext> options) : base(options) {}
    public DbSet<Product> Products => Set<Product>();
}
```

> Nota 👀: Los nullable reference types han llegado ya por default y es necesario asegurarnos que cualquier tipo de dato por referencia no sea nulo. Esto nos ayudará a que por fin (al menos tratar) eliminemos los `NullReferenceExceptions` que siempre aparecen cuando menos te lo esperas.
> 

`Products` lo he guardado en una carpeta **/Entities** y el contexto en **/Persistence**. Realmente esto puede variar según el estilo de cada quien, así que no es relevante por ahora.

### Products Module

Como mencioné antes, Carter nos pide crear módulos en lugar de Controladores. Por lo que guardaré esté módulo dentro de una carpeta **Features/Products**.

La intención de no poner todo junto es poder separar cada Feature según sus componentes relevantes (como Mappers, Validations, Servicios, etc).

```csharp
namespace MinimalApis.Api.Features.Products;

public class ProductsModule : ICarterModule
{
    public void AddRoutes(IEndpointRouteBuilder app)
    {
       // Los endpoints van aquí
    }
}
```

Para registrar Endpoints en Minimal APIs tenemos disponible todos los verbos HTTP como lo solemos hacer en Web API (Ejem. `[HttpPost]`, `[HttpGet]`, etc).

Ejemplo.

```csharp
app.MapGet("api/products", () => 
{
     // Consultar productos
});
```

Aquí estamos registrando la ruta **api/products** con el verbo `HttpGet`.

### Dependency Injection

Esta es una de las partes un poco extrañas, pero para resolver dependencias (en este caso nuestro `DbContext`) utilizamos los parámetros de la función del Endpoint en lugar del constructor como lo hacíamos antes.

```csharp
app.MapGet("api/products", (MyDbContext context) =>
{
    var products = await context.Products
        .ToListAsync();

    return Results.Ok(products);
});
```

Eso significa que en los parámetros de nuestra función se resuelven dependencias de distintos lados. Como por ejemplo (como ya vimos aquí) del `IServiceCollection` pero también de los datos que se reciben (ejemplo del Body, Headers, Query strings, etc).

Ocurre Model Binding de los datos recibidos por el cliente y también se puede resolver cualquier dependencia desde ahí. Si revisamos los links de Minimal APIs que he puesto más arriba podremos aprender más sobre el tema (ya que para todos es nuevo 😅).

Una versión mejorada y como hice este ejemplo, queda de la siguiente manera.

```csharp
public void AddRoute(IEndpointRouteBuilder app)
{
		app.MapGet("api/products", GetProducts)
		    .Produces<List<Product>>();
}

private static async Task<IResult> GetProducts(MyDbContext context)
{
    var products = await context.Products
        .ToListAsync();

    return Results.Ok(products);
}
```

Aquí ya estamos agregando metadatos al endpoint (con `.Produces<List<Product>>()`) para que librerías como Swagger sepa interpretar y documentar nuestra API.

La clase `Results` cuenta con las posibles respuestas que podemos regresar, así como sucedía con los controladores (que regresábamos un `IActionResult`) aquí regresamos un `IResult` (algo muy similar).

### Problem Details y Validaciones

Una de las cosas que también me gustó de Carter es que ya incluye **FluentValidation**. Y aunque no es algo automático como se podría configurar con MediatR o ASP.NET Web API, es muy sencillo.

Dentro de **Features/Products** puse el siguiente `AbstractValidator`.

```csharp
using FluentValidation;
using MinimalApis.Api.Entities;

namespace MinimalApis.Api.Features.Products
{
    public class ProductValidator : AbstractValidator<Product>
    {
        public ProductValidator()
        {
            RuleFor(q => q.Description).NotEmpty();
            RuleFor(q => q.Price).NotNull();
        }
    }
}
```

Simplemente estamos estableciendo que los campos son obligatorios, pero aquí se pueden configurar muchas validaciones y es demasiado flexible (en comparación a los `DataAnnotations`).

Nuestro endpoint de creación queda así:

```csharp
using Carter;
using Carter.ModelBinding;

// ... código omitido
public void AddRoutes(IEndpointRouteBuilder app)
{
        // ... código omitido
        app.MapPost("api/products", CreateProduct)
            .Produces<Product>(StatusCodes.Status201Created)
            .ProducesValidationProblem()
}

private static async Task<IResult> CreateProduct(HttpRequest req, Product product, MyDbContext context)
{
    var result = req.Validate(product);

    if (!result.IsValid)
    {
        return Results.ValidationProblem(result.ToValidationProblems());
    }

    context.Products.Add(product);

    await context.SaveChangesAsync();

    return Results.Created($"api/products/{product.ProductId}", product);
}
```

Estamos inyectando 3 cosas en esta función:

- `HttpRequest`. El request actual del `HttpContext` del endpoint
- `Product`. Los datos recibidos en el `[FromBody]` del `[HttpPost]` que viene siendo el entity a crear
- `MyDbContext`. DbContext de EF Core

> Nota 👀: Si el método `Validate` no te aparece, probablemente es por namespaces faltantes.
> 

Este endpoint tiene 2 posibles respuestas: un 201 o un 400. Aunque no se está obligado especificarlo, pero con los métodos Produces se especifica eso para que Swagger conozca mejor la API.

Un ejemplo de respuesta de error de validación.

```json
{
    "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
    "title": "One or more validation errors occurred.",
    "status": 400,
    "errors": {
        "Description": [
            "'Description' no debería estar vacío."
        ]
    }
}
```

Este formato es un estandar para APIs llamado [Problem Details](https://datatracker.ietf.org/doc/html/rfc7807), este también se usa en los Api Controllers al usar el atributo `[ApiController]` (por si no sabías 😅).

Ejemplo de respuesta exitosa.

```json
{
    "productId": 5,
    "description": "Product description here.",
    "price": 2499.99
}
```

El método `ToValidationProblems` es una extensión de `ValidationResult` de **FluentValidation** para convertirlo en un diccionario agrupado según las propiedades que validaron.

```csharp
using FluentValidation.Results;

namespace MinimalApis.Api.Extensions;

public static class GeneralExtensions
{
    public static Dictionary<string, string[]> ToValidationProblems(this ValidationResult result) =>
         result.Errors
            .GroupBy(e => e.PropertyName, e => e.ErrorMessage)
            .ToDictionary(failureGroup => failureGroup.Key, failureGroup => failureGroup.ToArray());
}
```

### Products Module completo

Creo que ya expliqué lo relevante en estos 2 endpoints. Así que `ProductsModule` queda de la siguiente manera.

```csharp
using Carter;
using Carter.ModelBinding;
using Microsoft.EntityFrameworkCore;
using MinimalApis.Api.Entities;
using MinimalApis.Api.Extensions;
using MinimalApis.Api.Persistence;

namespace MinimalApis.Api.Features.Products;

public class ProductsModule : ICarterModule
{
    public void AddRoutes(IEndpointRouteBuilder app)
    {
        app.MapGet("api/products", GetProducts)
            .Produces<List<Product>>();

        app.MapGet("api/products/{productId}", GetProduct)
            .Produces<Product>()
            .Produces(StatusCodes.Status404NotFound);

        app.MapPost("api/products", CreateProduct)
            .Produces<Product>(StatusCodes.Status201Created)
            .ProducesValidationProblem();

        app.MapPut("api/products/{productId}", UpdateProduct)
            .Produces(StatusCodes.Status204NoContent)
            .ProducesProblem(StatusCodes.Status404NotFound)
            .ProducesValidationProblem();

        app.MapDelete("api/products/{productId}", DeleteProduct)
            .Produces(StatusCodes.Status204NoContent)
            .ProducesProblem(StatusCodes.Status404NotFound);
    }

    private static async Task<IResult> GetProducts(MyDbContext context)
    {
        var products = await context.Products
            .ToListAsync();

        return Results.Ok(products);
    }

    private static async Task<IResult> GetProduct(int productId, MyDbContext context)
    {
        var product = await context.Products.FindAsync(productId);

        if (product is null)
        {
            return Results.NotFound();
        }

        return Results.Ok(product);
    }

    private static async Task<IResult> CreateProduct(HttpRequest req, Product product, MyDbContext context)
    {
        var result = req.Validate(product);

        if (!result.IsValid)
        {
            return Results.ValidationProblem(result.ToValidationProblems());
        }

        context.Products.Add(product);

        await context.SaveChangesAsync();

        return Results.Created($"api/products/{product.ProductId}", product);
    }

    private static async Task<IResult> UpdateProduct(
        HttpRequest request, MyDbContext context, int productId, Product product)
    {
        var result = request.Validate(product);

        if (!result.IsValid)
        {
            return Results.ValidationProblem(result.ToValidationProblems());
        }

        var exists = await context.Products.AnyAsync(q => q.ProductId == productId);

        if (!exists)
        {
            return Results.Problem(
                detail: $"El producto con ID {productId} no existe",
                statusCode: StatusCodes.Status404NotFound);
        }

        context.Entry(product).State = EntityState.Modified;

        await context.SaveChangesAsync();

        return Results.NoContent();
    }

    private static async Task<IResult> DeleteProduct(int productId, MyDbContext context)
    {
        var product = await context.Products.FirstOrDefaultAsync(q => q.ProductId == productId);

        if (product is null)
        {
            return Results.Problem(
                detail: $"El producto con ID {productId} no existe",
                statusCode: StatusCodes.Status404NotFound);
        }

        context.Remove(product);

        await context.SaveChangesAsync();

        return Results.NoContent();
    }
}
```

## Extensiones (IServiceCollection y WebApplication)

Para dejar un Program.cs más limpio, siempre suelo usar extensiones para agregar la configuración de los middlewares y dependencias.

```csharp
using Microsoft.EntityFrameworkCore;
using Microsoft.OpenApi.Models;
using MinimalApis.Api.Persistence;

namespace MinimalApis.Api.Extensions;

public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddSwagger(this IServiceCollection services)
    {
        services.AddEndpointsApiExplorer();
        services.AddSwaggerGen(c =>
        {
            c.SwaggerDoc("v1", new OpenApiInfo()
            {
                Description = "Minimal API Demo",
                Title = "Minimal API Demo",
                Version = "v1",
                Contact = new OpenApiContact()
                {
                    Name = "Isaac Ojeda",
                    Url = new Uri("https://github.com/isaacOjeda")
                }
            });
        });

        return services;
    }

    public static IServiceCollection AddPersistence(this IServiceCollection services)
    {
        services.AddDbContext<MyDbContext>(options =>
            options.UseInMemoryDatabase(nameof(MyDbContext)));

        return services;
    }
}
```

Aquí simplemente estamos creando 2 métodos de extensión para configurar Swagger y el DbContext (en este caso, en memoria).

```csharp
namespace MinimalApis.Api.Extensions;

public static class WebApplicationExtensions
{
    public static WebApplication MapSwagger(this WebApplication app)
    {
        app.UseSwagger();
        app.UseSwaggerUI(c =>
        {
            c.SwaggerEndpoint("/swagger/v1/swagger.json", "API");
            c.RoutePrefix = "api";
        });

        return app;
    }
}
```

Aquí también configuramos Swagger pero ahora su middleware para que tengamos un endpoint de exploración (el json) y la UI para hacer pruebas.

## Integración de todo en Program

Ya tenemos todo listo para por fin correr esta API hecha con Carter

```csharp
using Carter;
using MinimalApis.Api.Extensions;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSwagger();
builder.Services.AddPersistence();
builder.Services.AddCarter();

var app = builder.Build();

app.MapSwagger();
app.MapCarter();

app.Run();
```

## Probando Swagger

Si ejecutamos `dotnet run` ya podemos explorar la API con Swagger ( o postman si lo prefieres)

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/poo5vgt27z7hryhh6svp.png) 

# Conclusión

Las Minimal APIs es algo que se necesitaba en .NET. Lenguajes como node con express manejaban este concepto desde siempre, pero poderlo hacer con .NET y toda su plataforma, es perfecto.

Carter existe desde versiones anteriores de .NET Core y buscaba simplificar esto, y su antecessor Nancy.Fx lo ha hecho desde siempre. Hay gente que odia los Controllers 😅.

Es cuestión de gustos, tal vez no todos empiecen a utilizar este approach, pero una buena razón es que por ahora es más rápido.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lzl9kjstd7zqkxe9p1ks.png) 

En un futuro tal vez se construyan más capas sobre Minimal APIs (como carter lo hace) y se vuelvan estandard en los templates que el mismo dotnet ofrecerá, no sé, puede pasar.

Espero que te haya gustado 🖖🏽

Code4Fun 🤓