Automatically Register Minimal APIs in ASP.NET Core

Milan Jovanović - Feb 27 - - Dev Community

In ASP.NET Core applications using Minimal APIs, registering each API endpoint with app.MapGet, app.MapPost, etc., can introduce repetitive code. As projects grow, this manual process becomes increasingly time-consuming and prone to maintenance headaches.

You can try grouping the Minimal API endpoints using extension methods so as not to clutter the Program file. This approach scales well as the project grows. However, it feels like reinventing controllers.

I like to view each Minimal API endpoint as a standalone component.

The vision I have in my mind aligns nicely with the concept of vertical slices.

Today, I'll show you how to register your Minimal APIs automatically with a simple abstraction.

The Endpoint Comes First

Automatically registering Minimal APIs significantly reduces boilerplate, streamlining development. It makes your codebase more concise and improves maintainability by establishing a centralized registration mechanism.

Let's create a simple IEndpoint abstraction to represent a single endpoint.

The MapEndpoint accepts an IEndpointRouteBuilder, which we can use to call MapGet, MapPost, etc.

public interface IEndpoint
{
    void MapEndpoint(IEndpointRouteBuilder app);
}
Enter fullscreen mode Exit fullscreen mode

Each IEndpoint implementation should contain exactly one Minimal API endpoint definition.

Nothing prevents you from registering multiple endpoints in the MapEndpoint method. But you (really) shouldn't.

Additionally, you could implement a code analyzer or architecture test to enforce this rule.

public class GetFollowerStats : IEndpoint
{
    public void MapEndpoint(IEndpointRouteBuilder app)
    {
        app.MapGet("users/{userId}/followers/stats", async (
            Guid userId,
            ISender sender) =>
        {
            var query = new GetFollowerStatsQuery(userId);

            Result<FollowerStatsResponse> result = await sender.Send(query);

            return result.Match(Results.Ok, CustomResults.Problem);
        })
        .WithTags(Tags.Users);
    }
}
Enter fullscreen mode Exit fullscreen mode

Sprinkle Some Reflection Magic

Reflection allows us to dynamically examine code at runtime. For Minimal API registration, we'll use reflection to scan our .NET assemblies and find classes that implement IEndpoint. Then, we will configure them as services with dependency injection.

The Assembly parameter should be the assembly that contains the IEndpoint implementations. If you want to have endpoints in multiple assemblies (projects), you can easily extend this method to accept a collection.

public static IServiceCollection AddEndpoints(
    this IServiceCollection services,
    Assembly assembly)
{
    ServiceDescriptor[] serviceDescriptors = assembly
        .DefinedTypes
        .Where(type => type is { IsAbstract: false, IsInterface: false } &&
                       type.IsAssignableTo(typeof(IEndpoint)))
        .Select(type => ServiceDescriptor.Transient(typeof(IEndpoint), type))
        .ToArray();

    services.TryAddEnumerable(serviceDescriptors);

    return services;
}
Enter fullscreen mode Exit fullscreen mode

We only need to call this method once from the Program file:

builder.Services.AddEndpoints(typeof(Program).Assembly);
Enter fullscreen mode Exit fullscreen mode

Registering Minimal APIs

The final step in our implementation is to register the endpoints automatically. We can create an extension method on the WebApplication, which lets us resolve services using the IServiceProvider.

We're looking for all registrations of the IEndpoint service. These will be the endpoint classes we can now register with the application by calling MapEndpoint.

I'm also adding an option to pass in a RouteGroupBuilder if you want to apply conventions to all endpoints. A great example is adding a route prefix, authentication, or API versioning.

public static IApplicationBuilder MapEndpoints(
    this WebApplication app,
    RouteGroupBuilder? routeGroupBuilder = null)
{
    IEnumerable<IEndpoint> endpoints = app.Services
        .GetRequiredService<IEnumerable<IEndpoint>>();

    IEndpointRouteBuilder builder =
        routeGroupBuilder is null ? app : routeGroupBuilder;

    foreach (IEndpoint endpoint in endpoints)
    {
        endpoint.MapEndpoint(builder);
    }

    return app;
}
Enter fullscreen mode Exit fullscreen mode

Putting It All Together

Here's what the Program file could look like when we put it all together.

We're calling AddEndpoints to register the IEndpoint implementations.

Then, we're calling MapEndpoints to automatically register the Minimal APIs.

I'm also configuring a route prefix and API Versioning for each endpoint using a RouteGroupBuilder.

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

builder.Services.AddEndpoints(typeof(Program).Assembly);

WebApplication app = builder.Build();

ApiVersionSet apiVersionSet = app.NewApiVersionSet()
    .HasApiVersion(new ApiVersion(1))
    .ReportApiVersions()
    .Build();

RouteGroupBuilder versionedGroup = app
    .MapGroup("api/v{version:apiVersion}")
    .WithApiVersionSet(apiVersionSet);

app.MapEndpoints(versionedGroup);

app.Run();
Enter fullscreen mode Exit fullscreen mode

Takeaway

Automatic Minimal API registration with techniques like reflection can significantly improve developer efficiency and project maintainability.

While highly beneficial, it's important to acknowledge the potential performance impact of reflection on application startup.

So, an improvement point could be using source generators for pre-compiled registration logic.

A few alternatives worth exploring:

Hope this was helpful.

See you next week.

P.S. Here's the complete source code for this article.


P.S. Whenever you're ready, there are 3 ways I can help you:

  1. Modular Monolith Architecture: This in-depth course will transform the way you build monolith systems. You will learn the best practices for applying the Modular Monolith architecture in a real-world scenario. Join the wait list here.

  2. Pragmatic Clean Architecture: This comprehensive course will teach you the system I use to ship production-ready applications using Clean Architecture. Learn how to apply the best practices of modern software architecture. Join 2,400+ students here.

  3. Patreon Community: Think like a senior software engineer with access to the source code I use in my YouTube videos and exclusive discounts for my courses. Join 1,050+ engineers here.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .