Mastering ASP.NET Core with Minimal APIs & CQRS -Part 04 — Deep Dive & Organize Minimal APIs
GitHub repo: https://github.com/SasangaME/MinimalApiDemo/tree/feature/organize-code
Organize Minimal Endpoints
In the previous post, we consolidated all the code into the Program.cs
file. While this approach may work for small applications, it is not considered best practice for larger projects due to several reasons:
- Maintainability: A single, monolithic file becomes cumbersome to navigate and update as the application grows. Separating code into distinct files and classes makes it easier to locate and modify specific functionality.
- Readability: Organized code enhances readability, allowing developers to understand the application’s structure and flow more efficiently. This is particularly important in team environments where multiple developers collaborate on the same codebase.
- Reusability: By dividing code into modular components, you promote reusability across different parts of the application or even in other projects. This modularity aligns with the DRY (Don’t Repeat Yourself) principle, reducing code duplication and potential errors.
Vertical slicing comes into the picture here. Let’s create PetsEndpoints class to add our newly created endpoints in the Features/Pet directory.
PetEndpoints.cs
using MinimalApiDemo.Common.Enums;
namespace MinimalApiDemo.Features.Pet;
public static class PetEndpoints
{
public static void MapPetEndpoints(this WebApplication app)
{
app.MapGet("/api/pets", () => Results.Ok(GetPets()));
app.MapGet("/api/pets/{id:int}", (int id) =>
{
var pet = GetPetById(id);
return pet is not null ? Results.Ok(pet) : Results.NotFound();
});
app.MapPost("/api/pets", (PetDto pet) =>
{
var newPet = CreatePet(pet);
return Results.Created($"/api/pets/{newPet.Id}", newPet);
});
}
private static IEnumerable<PetDto> GetPets()
{
return new List<PetDto>
{
new PetDto { Id = 1, Name = "Pippa", Gender = Gender.Female },
new PetDto { Id = 2, Name = "Ollie", Gender = Gender.Male }
};
}
private static PetDto? GetPetById(int id)
{
return GetPets().FirstOrDefault(p => p.Id == id);
}
private static PetDto CreatePet(PetDto pet)
{
pet.Id = GetPets().Max(p => p.Id) + 1;
return pet;
}
}
PetEndpoints is a static class designed to group related endpoint mappings for better organization. MapEndpoints method is an extension method for the WebApplication
class, allowing the registration of pet-related endpoints. By invoking app.MapPetEndpoints()
, these endpoints become part of the application's request pipeline.
In ASP.NET Core Minimal APIs, parameters in route handler delegates are automatically bound from various parts of an HTTP request based on conventions and, when necessary, explicit attributes.
The route "/api/pets/{id:int}"
defines a path parameter named id
with an integer constraint (:int
).
The delegate (int id)
declares a parameter id
of type int
. ASP.NET Core matches this parameter to the {id}
segment in the route. When a request is made to this endpoint, the framework parses the corresponding segment of the URL, converts it to an integer (enforcing the :int
constraint), and assigns it to the id
parameter. This process is known as model binding from route data.
The delegate (PetDto pet)
declares a parameter pet
of type PetDto
. For HTTP methods like POST, PUT, and PATCH, ASP.NET Core by convention binds complex types from the request body. In this case, the framework expects the request body to contain JSON that can be deserialized into a PetDto
object. This is achieved through model binding from the request body.
This automatic binding simplifies the process of handling HTTP requests, allowing developers to focus on implementing the core logic of their APIs without manually extracting data from the request.
Endpoints Grouping
In ASP.NET Core Minimal APIs, you can group related endpoints using the MapGroup
method, which helps organize your code and apply common configurations to multiple endpoints. Here's how you can refactor your existing code to group the pet-related endpoints:
PetEndpoints.cs
public static void MapPetEndpoints(this WebApplication app)
{
var petGroup = app.MapGroup("/api/pets")
.WithTags("Pets");
petGroup.MapGet("/", () => Results.Ok(GetPets()));
petGroup.MapGet("/{id:int}", (int id) =>
{
var pet = GetPetById(id);
return pet is not null ? Results.Ok(pet) : Results.NotFound();
});
petGroup.MapPost("/", (PetDto pet) =>
{
var newPet = CreatePet(pet);
return Results.Created($"/api/pets/{newPet.Id}", newPet);
});
}
app.MapGroup("/api/pets")
:
- Purpose: Creates a route group with the common prefix
/api/pets
. - Functionality: All endpoints added to this group will inherit the
/api/pets
prefix, streamlining route definitions and enhancing code organization.
.WithTags("Pets")
:
- Purpose: Assigns the tag
"Pets"
to all endpoints within this group. - Functionality: Tags are beneficial for documentation and tooling purposes, such as OpenAPI/Swagger, where they help categorize and display related endpoints together, improving API discoverability and readability.
Benefits:
- Organized Routing: Grouping endpoints under a common prefix reduces redundancy and potential errors in route definitions.
- Shared Configuration: Applying configurations, such as middleware, authorization, or metadata, to the entire group becomes more efficient.
- Enhanced Documentation: Tags facilitate better organization in API documentation tools, making it easier for developers to navigate and understand the API structure.
Endpoints Mapping
After relocating these endpoint definitions, it’s essential to map them back into the application’s request pipeline. This is typically achieved by defining extension methods that encapsulate the endpoint mappings and then invoking these methods within Program.cs
.
Let’s create a class to map our endpoints.
EndpointsMapper.cs
using MinimalApiDemo.Features.Pet;
namespace MinimalApiDemo.Config;
public static class EndpointsMapper
{
public static void MapEndpoints(this WebApplication app)
{
app.MapPetEndpoints();
}
}
A static class is used to hold extension methods that enhance the WebApplication
class. public static void MapEndpoints(this WebApplication app)
: defines an extension method for WebApplication
, enabling the addition of custom endpoint mappings. app.MapPetEndpoints();
: Invokes the MapPetEndpoints
method from the MinimalApiDemo.Features.Pet
namespace, registering all pet-related endpoints to the application's request pipeline.
Benefits of This Approach:
- Modularity: Separating endpoint mappings into distinct methods and classes enhances code readability and maintainability.
- Scalability: As the application grows, additional endpoint groups can be mapped by extending the
MapEndpoints
method, centralizing the configuration. - Organization: Grouping related endpoints and configurations within specific namespaces and classes promotes a clean and understandable project structure.
Now, let’s clean up the Program.cs file and add the endpoint mapping there:
using MinimalApiDemo.Config;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddOpenApi();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
}
var buildInfo = new BuildDto();
builder.Configuration.GetSection("Build").Bind(buildInfo);
app.MapGet("/", () => Results.Json(buildInfo));
app.MapEndpoints();
app.UseHttpsRedirection();
app.Run();
internal record BuildDto
{
public string Title { get; set; } = string.Empty;
public string Version { get; set; } = string.Empty;
}
At the end of this session, your folder structure should look like this:
Let’s learn how to use Dependency Injection in Minimal APIs in our next blog post. Happy coding!