Automatically Register Minimal APIs in ASP.NET Core

Automatically Register Minimal APIs in ASP.NET Core

4 min read ·

Thank you to our sponsors who keep this newsletter free to the reader:

Which Blazor Web App template should you use? Deciding which Blazor Web App template is the best fit depends on the type of application you want to build. Find the answer here.

Become a Postman master with Postman Intergalactic sessions. Test your APIs with ease and collaborate on API development with workspaces. Get Postman for FREE!

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);
}

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);
    }
}

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;
}

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

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

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;
}

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();

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.


Whenever you're ready, there are 4 ways I can help you:

  1. Modular Monolith Architecture (NEW): This in-depth course will transform the way you build modern systems. You will learn the best practices for applying the Modular Monolith architecture in a real-world scenario. Join 2,800+ students 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,800+ students here.
  3. Patreon Community: Join a community of 1,050+ engineers and gain access to the source code I use in my YouTube videos, early access to future videos, and exclusive discounts for my courses. Join 1,050+ engineers here.
  4. Promote yourself to 48,000+ subscribers by sponsoring this newsletter.

Become a Better .NET Software Engineer

Join 48,000+ engineers who are improving their skills every Saturday morning.