In this article, we will explore how we can improve our Minimal API endpoints in .NET with automatic registration.

To download the source code for this article, you can visit our GitHub repository.

Let’s dive in!

Minimal API Setup

In this article, we’ll use a Minimal API that represents a basic school system, focusing on two key endpoints:

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!
app.MapGet("/students", async (IStudentService service) =>
{
    var student = await service.GetAllAsync();

    return Results.Ok(student);
})
.WithOpenApi();

app.MapGet("/students/{id:guid}", async (Guid id, IStudentService service) =>
{
    var student = await service.GetByIdAsync(id);

    return Results.Ok(student);
})
.WithOpenApi();

app.MapPost("/students", async (StudentForCreationDto dto, IStudentService service) =>
{
    var student = await service.CreateAsync(dto);

    return Results.Created($"/students/{student.Id}", student);
})
.WithOpenApi();

//Other CRUD endpoints PUT, DELETE

First, we have the students endpoints with all the basic CRUD operations for creating, reading, updating, and deleting.

Then we have our second endpoint:

app.MapGet("/teachers", async (ITeacherService service) =>
{
    var teacher = await service.GetAllAsync();

    return Results.Ok(teacher);
})
.WithOpenApi();

app.MapGet("/teachers/{id:guid}", async (Guid id, ITeacherService service) =>
{
    var teacher = await service.GetByIdAsync(id);

    return Results.Ok(teacher);
})
.WithOpenApi();

//Other CRUD endpoints POST, PUT, DELETE

The teachers endpoints have the same CRUD actions as we have with the students endpoints.

We can already feel that our Program class is getting longer and harder to navigate. And these are just two endpoints, what happens when we add more? We also have service registrations, alongside all the other settings we need to configure our application properly. Things can easily get out of hand.

Let’s see how we can make our Minimal API endpoints more manageable!

Registration of Minimal API Endpoints Using Extension Methods

Extension methods are a neat tool in .NET that we can utilize for better management of our endpoints:

public static class StudentEndpoint
{
    public static void RegisterStudentEndpoint(this IEndpointRouteBuilder routeBuilder)
    {
        routeBuilder.MapGet("/students", async (IStudentService service) =>
        {
            var student = await service.GetAllAsync();

            return Results.Ok(student);
        })
        .WithOpenApi();

        // omitted for brevity
    }
}

We start by creating the StudentEndpoint class, which we mark as static because we won’t need to create instances of this type.

The next thing we do is to create the RegisterStudentEndpoint() method that extends the IEndpointRouteBuilder interface, which is the reason why we extend the IEndpointRouteBuilder interface is that our WebApplication instance implements it and also uses it to map the endpoints and routes in our application.

Inside our RegisterStudentEndpoint() method, we use the already familiar Map() methods we get from IEndpointRouteBuilder interface to register all routes for our students endpoint.

There is one final thing we need to do:

app.RegisterStudentEndpoint();

In our Program class, we replace all of our students endpoint route registrations by calling the RegisterStudentEndpoint() method we just defined. By doing this, we register all routed for our endpoint and make our codebase much easier to read and maintain.

Registration of Minimal API Endpoints Using Reflection

Reflection is one of the most powerful features in .NET. There is no surprise that we can use it to register our endpoints. So, let’s see how we can do just that.

Define the Endpoints and Their Abstraction

First, we need an abstraction that will represent our endpoints:

public interface IMinimalEndpoint
{
    void MapRoutes(IEndpointRouteBuilder routeBuilder);
}

Here, we define the IMinimalEndpoint interface, with a single method MapRoutes(), which takes an instance of the already familiar to us IEndpointRouteBuilder interface as a parameter.

After this is done, we continue with the next step:

public class TeacherEndpoint : IMinimalEndpoint
{
    public void MapRoutes(IEndpointRouteBuilder routeBuilder)
    {
        routeBuilder.MapGet("/teachers", async (ITeacherService service) =>
        {
            var teacher = await service.GetAllAsync();

            return Results.Ok(teacher);
        })
        .WithOpenApi();

        // omitted for brevity
    }
}

First, we create the TeacherEndpoint class and implement the IMinimalEndpoint interface. Then, inside the MapRoutes() method, we place all the routes for the teachers endpoint that we had in the Program class.

With this, we have a streamlined way of defining endpoints and routes in our application.

Defining Extension Methods for Minimal API Endpoint Registration

For all of this to work, we need some extension methods. So, let’s define our first one:

public static class MinimalEndpointExtensions
{
    public static IServiceCollection AddMinimalEndpoints(this IServiceCollection services)
    {
        var assembly = typeof(Program).Assembly;

        var serviceDescriptors = assembly
            .DefinedTypes
            .Where(type => !type.IsAbstract &&
                           !type.IsInterface &&
                           type.IsAssignableTo(typeof(IMinimalEndpoint)))
            .Select(type => ServiceDescriptor.Transient(typeof(IMinimalEndpoint), type));

        services.TryAddEnumerable(serviceDescriptors);

        return services;
    }
}

First, we create the MinimalEndpointExtensions class that will hold all the extension methods we need. Then, we create the AddMinimalEndpoints() method that extends the IServiceCollection interface.

Next, we retrieve the Assembly in which the Program class is located, filtering its DefinedTypes property (which holds information for all types in the assembly) to find all types that are neither abstract nor are interfaces. Furthermore, we add a final condition that additionally filters the types in the assembly – using the IsAssignableTo() method, we match all types that implement the IMinimalEndpoint interface.

Then, for each matching type, we use the Select() method to create a ServiceDescriptor for a Transient service of IMinimalEndpoint type and the concrete type.

If you want to know more about what Transient means, check out our article Dependency Injection Lifetimes in ASP.NET Core.

Finally, we use the TryAddEnumerable() method to add all of our ServiceDescriptor instances to the IServiceCollection. The TryAddEnumerable() method will register each descriptor only if it has not already been registered.

For this to work, all endpoints in our application must implement the IMinimalEndpoint interface.

Now, let’s define our second extension method:

public static IApplicationBuilder RegisterMinimalEndpoints(this WebApplication app)
{
    var endpoints = app.Services
        .GetRequiredService<IEnumerable<IMinimalEndpoint>>();

    foreach (var endpoint in endpoints)
    {
        endpoint.MapRoutes(app);
    }

    return app;
}

Here, we create the RegisterMinimalEndpoints() method that extends the WebApplication class. Within this method, we use the GetRequiredService() method to get all services that implement our IMinimalEndpoint interface. Then, we loop through all matching services and call their MapRoutes() method.

Now, we need to put the final touches:

builder.Services.AddMinimalEndpoints();

First, in our Program class, we call the AddMinimalEndpoints() extension method on our service collection to register all implementations of the IMinimalEndpoint interface.

Then, we add one final method call after we build our application:

app.RegisterMinimalEndpoints();

Again, in the Program class, we use the RegisterMinimalEndpoints() extension method on our WebApplication instance – this way we will register all routes for each endpoint we have.

Choosing the Right Approach to Register Minimal API Endpoints

Both extension methods and reflection grant us the ability to automatically register Minimal API endpoints.

However, there are several things we must consider before we make our pick.

Project size

For small projects with a pre-determined number of endpoints, using extension methods ensures simplicity and clarity. On the other hand, for larger or ever-growing codebases registration based on reflections offers us more flexibility and scalability.

Maintenance and Clarity

When we use extension methods, we get a great separation of concerns as well as explicit registration. This makes our code easy to maintain and understand.

Reflection-based registration, on the other hand, gives us dynamic behavior. But we have to keep in mind that it can make our code less transparent and harder to understand. 

Conventions and Flexibility

Reflection allows us to dynamically register endpoints based on enforced conventions or runtime conditions, which makes it suitable for complex scenarios.

In contrast, when we use extension methods we get a more deterministic approach, but we have to take extra care to maintain consistency across endpoints.

Conclusion

In this article, we explored two different approaches to the automatic registration of Minimal API endpoints. By either utilizing extension methods or reflection, we can enhance the structure and readability of our application’s code. These techniques enable us to navigate and expand our projects more easily, laying the foundation for efficient and scalable API development in .NET.

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!