minimal-apis.net-9.0fast-endpoints

Is there a way to use the same handler for multiple endpoints using Fast Endpoints?


I'm using fast endpoints in a .NET 9.0 web api project and I'm finding myself writing a lot of boiler plate to get the basic functions up and running. I'm writing just my basic database CRUD and I'm wondering if there's a way to reuse handlers instead of copying and pasting code between endpoints?

I currently have this class for getting a Contact object using my database service and Automapper:

public class GetContact(PostgresD db, IMapper mapper, ILogger<GetContact> logger) : EndpointWithoutRequest<ContactDto>
{
    public override void Configure()
    {
        Get("/contact/{id:Guid}");
    }

    public override async Task HandleAsync(CancellationToken ct)
    {
        var contact = await db.FindAsync<Contact>(Route<Guid>("id"));
        if (contact is null)
        {
            await SendNotFoundAsync(ct);
        }
        else
        {
            Response = mapper.Map<ContactDto>(contact);
        }
    }
}

Now I need to write my endpoints for other basic objects and it seems like I need to literally copy and paste this class, just changing the type of the Endpoint. Is there a way I could reuse this handler code instead of copypasta?

I've been trying to figure out some kind of route builder class but I keep getting hung up on how to define the handlers as delegates that I pass in, they need access to protected fields inside of the endpoint itself. I'm also really not sure that Fast Endpoints will recognize endpoint classes generated by a builder but that's a future me problem.

Ideally everything inside the HandleAsync() call would be encapsulated in a delegate, but I can't figure out a way to write the delegate so that it will have access to protected class fields, and is also accessible when I instantiate the class.


Solution

  • I suggest you to create base abstract controller that derives from EndpointWithoutRequest and overrides only the HandleAsync method:

    public abstract class BaseEndpointWithoutRequest<TEntity, TResponse>(PostgresD db, IMapper mapper, ILogger<GetContact> logger)
        : EndpointWithoutRequest<TResponse>
    {
        public override async Task HandleAsync(CancellationToken ct)
        {
            var entity = await db.FindAsync<TEntity>(Route<Guid>("id"));
            if (entity is null)
            {
                await SendNotFoundAsync(ct);
            }
            else
            {
                Response = mapper.Map<TResponse>(entity);
            }
        }
    }
    
    public class GetContact(PostgresD db, IMapper mapper, ILogger<GetContact> logger)
        : BaseEndpointWithoutRequest<Contact, ContactDto>(db, mapper, logger)
    {
        public override void Configure()
        {
            Get("/contact/{id:Guid}");
        }
    }