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.
P.S. Whenever you’re ready, there are 2 ways I can help you:
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.
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.