In this article, we will explore how we can improve our Minimal API endpoints in .NET with automatic registration.
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:
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.
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.