Mastering ASP.NET Core with Minimal APIs & CQRS -Part 04 — Deep Dive & Organize Minimal APIs

Sasanga Edirisinghe
5 min readJan 5, 2025

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!

--

--

Sasanga Edirisinghe
Sasanga Edirisinghe

No responses yet