Peter

Un side-project con un nombre con gancho, eso es Peter, un conjunto de utilidades para facilitar el desarrollo de Minimal APIs.

Ya se sabe que en programación hay 2 grandes problemas, invalidar cachés y acertar con el nombrado. Sin embargo, en nuestro caso buscar un nombre a la librería fue fácil, “Esto no lo va a usar ni Peter”, problema resuelto. Doy todo el crédito a Sergio Navarro que acertó de pleno.

En líneas generales, Peter pretende resolver 2 problemas:

  • No usar excepciones como control de flujo.
  • Simplificar al máximo los endpoints de Minimal APIs.

Este post está pensado como una introducción a Peter. En el repositorio hay un buen (en mi opinión) README y tampoco es plan de repetirse.

Excepciones

Hasta hace 2 días parecía claro que un comando tenía que lanzar una excepción de negocio y que un middleware tenía que atraparla y devolver un resultado personalizado. Si siempre se ha hecho así ¿Por qué cambiar?

Sin embargo, hay otra línea de pensamiento que dice “No uses excepciones para controlar el flujo de tu aplicación”. Sin entrar en debates sobre si es o no lo correcto, lo cierto es que para sólo usar excepciones para situaciones excepcionales (valga la redundancia), hacía falta una abstracción que facilitará este nuevo acercamiento (o approach, que queda mejor).

Y es ahí donde descubrimos ardalis/Result. Porque, admitámoslo, al comienzo Peter tomó una descarada inspiración de este proyecto, aunque ahora (y sin ningún atisbo de duda) cualquier parecido con la realidad es pura coincidencia.

Lo que se quiere evitar es esto:

public async Task<CustomerDto> Handle(GetCustomerRequest request, CancellationToken cancellationToken)
{
    var customer = await _context.Customers.FindAsync(new object?[] { request.Id }, cancellationToken: cancellationToken);
    if (customer is null)
    {
        // This exception will be handled in a middleware
        throw new NotFoundException();
    }
    // ...
}

¿Y cómo lo evitamos? Pues devolviendo un resultado desde el comando que pueda representar, no sólo el éxito de la operación, sino también un error. Usando Peter quedaría así:

public async Task<Result<CustomerDto>> Handle(GetCustomerRequest request, CancellationToken cancellationToken)
{
    var customer = await _context.Customers
        .FindAsync(new object?[] { request.Id }, cancellationToken: cancellationToken);
    if (customer is null)
    {
        return new NotFoundResult<CustomerDto>();
    }
    
    return new OkResult<CustomerDto>(new CustomerDto
    {
        Id = customer.Id,
        Name = customer.Name
    });
}

Como cualquier librería que se precie, hay ejemplos en la documentación sobre como extenderla con nuevos tipos de resultado.

Minimal APIs

Con el reto conseguido de no usar excepciones a diestro y siniestro, ¿cómo mapear ahora los distintos resultados devueltos por un comando a respuestas HTTP (básicamente IResult)?.

Pues aquí viene la otra gran pata de Peter (que es totalmente opcional y se puede usar o no en el lado de la API). La de intentar que el código de nuestros endpoints sean mínimos. Si antes nos gustaban los thin controllers ahora nos gustan los minimal endpoints, ¡claro que sí!.

En el mundo pre-Peter, el código de nuestro endpoint podría ser algo así:

app.MapGet("customers/{id:int}", async (IMediator mediator, [AsParameters] GetCustomerRequest request) =>
    {
        var customer =  await mediator.Send(request);
        if (customer is not null)
        {
            return Results.NotFound();
        }

        return Results.Ok(customer);
    });

Pero con el advenimiento de Peter, nuestro endpoint es ¡realmente mínimo!.

app.MapGet("customers/{id:int}", async (IMediator mediator, [AsParameters] GetCustomerRequest request) =>
        (await mediator.Send(request)).ToMinimalApi());

Y es así porque Peter (a través del método de extensión ToMinimalApi) se va a encargar de mapear un OkResult<T> a un 200, un NotFoundResult<T> a un 404 y así sucesivamente.

Bonus

AzuriteFixture

¿Trabajas con Azure Storage y no quieres usar (o no has hecho) la abstracción necesaria para poder usar un doble de test? Usa AzuriteFixture con Testcontainers y al menos no tendrás que ir directamente a Azure cuando ejecutes tus tests. Justo de eso se habló en el post anterior, pero ahora está en Peter listo para ser usado.

AuthenticateApiFixture

Porque en los tests vamos a querer simular ser un usuario autenticado o no, tener exactamente (o no tener) ciertos claims, etc. ¡Peter al rescate!… tomando inspiración, eso sí, de otro gran proyecto como Xabaril/Acheve.TestHost.

Un saludo!


Ver también