π οΈ Structuring Minimal APIs in .NET 8: A Modern Approach with Dependency Injection, Handlers, and Endpoint Filters π οΈ
By Gary Butler 8 min read
π© Introduction
Minimal APIs; first introduced in .NET 6 have become a powerful tool for building lightweight, efficient, and clean web applications. Unlike traditional MVC endpoints, Minimal APIs focus on simplicity, allowing developers to achieve more with less boilerplate while maintaining flexibility and testability. In this blog, I will show how
to structure a Minimal API project using:
- π’ Fluent-style Dependency Injection (DI)
- π¦ Handlers with associated services/repositories
- π‘ Endpoint filters for reusable logic; available from .NET 7 onwards
Finally, weβll compare this approach to popular alternatives like FastEndpoints and traditional MVC endpoints to understand its unique advantages.
π Hello World
Using top-level commands and minimal APIs a Hello World
example can be created using only a few lines of code:
var app = WebApplication
.CreateBuilder(args)
.Build();
app.MapGet("/", () => "Hello World!");
app.Run();
π§ Feature Based Example
Letβs explore how this works in a real-world scenario. We need to create an API for managing various features, focusing on the Products feature. The requirements are:
- π Endpoints
- A
GetProducts
endpoint that returns aList<Product>
.
- A
- π‘οΈ Security
- All requests must include a valid API key.
- ποΈ Data Source
- The data will be retrieved from a
ProductRepository
. - The
ProductRepository
can source data from multiple backends, such as:- A database.
- An external API.
- The data will be retrieved from a
The endpoints for a feature will be split into a number of distinct layers to allow for a clear separation of concerns and enhancing maintainability. These layers are:
- π Handler
- Acts as the entry point for HTTP requests.
- Validates inputs and maps request data to models.
- Delegates business logic to the Service layer.
- Returns appropriate HTTP responses with status codes.
- βοΈ Service
- Contains core business logic and applies rules/validations.
- Orchestrates data retrieval and transformation processes.
- Ensures modularity by interacting with repositories instead of data sources directly.
- ποΈ Repository
- Manages data access and communication with databases or external APIs.
- Encapsulates complex queries to abstract data layer details.
- Provides a clean interface for the Service layer.
Step 1οΈβ£: Feature-Based Handlers
Each feature will have a handler that maps endpoints for its specific domain. This keeps routing logic modular and feature-focused.
ProductFeature/Handlers/ProductsHandler.cs
public static class ProductsHandler
{
private static async Task<IResult> GetAllProducts(IProductService productService)
{
var products = await productService.GetAllProductsAsync();
return Results.Ok(products);
}
public static IEndpointRouteBuilder MapProductEndpoints(this IEndpointRouteBuilder endpoints)
{
endpoints.MapGet("/products", () => GetAllProducts)
.AddEndpointFilter<ProductsEndpointFilter>()
.Produces<List<Product>>()
.Produces(StatusCodes.Status400BadRequest)
.WithName("GetProducts")
.WithOpenApi();
return endpoints;
}
}
Step 2οΈβ£: Feature Service
Services encapsulate business logic with each feature maintaining its own service for a clear separation of responsibilities.
ProductFeature/Services/IProductService.cs
public interface IProductService
{
Task<IEnumerable<Product>> GetAllProductsAsync();
}
ProductFeature/Service/ProductService.cs
public class ProductService(IProductRepository repository) : IProductService
{
public async Task<IEnumerable<Product>> GetAllProductsAsync() =>
await repository.GetAllProductsAsync();
}
Step 3οΈβ£: Feature Repository
Repositories handle data access. Each feature maintains its own repository for a clear separation of responsibilities. The repository will also adapt the underlying data to its own model so as to not leak the underlying data structures.
ProductFeature/Repository/IProductRepository.cs
public interface IProductRepository
{
Task<IEnumerable<Product>> GetAllProductsAsync();
}
ProductFeature/Repository/IProductRepository.cs
public interface IProductRepository
{
Task<IEnumerable<Product>> GetAllProductsAsync();
}
ProductFeature/Repository/ProductRepository.cs
public class ProductRepository : IProductRepository
{
public async Task<IEnumerable<Product>> GetAllProductsAsync()
{
// Mocked data return
var products = new List<Product>
{
new() { Id = 1, Name = "Laptop", Price = 1200.24m },
new() { Id = 2, Name = "Smartphone", Price = 800.56m }
};
return await Task.FromResult(products);
}
}
ProductFeature/Repository/Models/Product.cs
public record Product
{
public required int Id { get; init; }
public required string Name { get; init; }
public required decimal Price { get; set; }
}
Step 4οΈβ£: Using Endpoint Filters
Endpoint filters enable reusable logic for Minimal APIs, such as validation or authentication.
EndpointFilters/ValidationFilter.cs
public class ProductsEndpointFilter : IEndpointFilter
{
public async ValueTask<object?> InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next) =>
context.HttpContext.Request.Query.ContainsKey("apiKey")
? await next(context) // Proceed to the next filter or endpoint
: Results.BadRequest("API Key is missing");
}
Step 5οΈβ£: Fluent Endpoint Registration
Register each featureβs handler in Program.cs for a clean, modular setup.
Program.cs
var app = WebApplication
.CreateBuilder(args)
.AddCoreConfiguration()
.AddUserSecretsIfNeeded()
.AddProductConfiguration()
.Build()
.UseMiddleware()
.MapEndpoints();
app.Run();
Step 6οΈβ£: Fluent Dependency Injection
Create an extension for each features dependency injection configuration.
Configuration/ProductConfigurationExtensions.cs
public static WebApplicationBuilder AddProductConfiguration(this WebApplicationBuilder builder)
{
builder.Services.AddScoped<IProductRepository>(
_ => new ProductRepository()
);
builder.Services.AddScoped<IProductService>(
x => new ProductService(
x.GetRequiredService<IProductRepository>())
);
return builder;
}
ποΈ Project Structure
Hereβs how a feature-based folder structure might look:
src/
βββ π Configuration/
β βββ π CoreConfigurationExtensions.cs
β βββ π EndPoints.cs
β βββ π UserSecretsExtensions.cs
β βββ π WebApplicationBuilderExtensions.cs
β
βββ π Features/
β βββ π Configuration/
β βββ π FeaturesConfiguration.cs
β
βββ π HealthCheckFeature/
β βββ π Handlers/
β βββ π HealthCheckHandler.cs
β
βββ π ProductFeature/
β βββ π Configuration/
β βββ π ProductsConfiguration.cs
β βββ π EndpointFilters/
β βββ π ProductsEndpointFilter.cs
β βββ π Handlers/
β βββ π ProductsHandler.cs
β βββ π Services/
β βββ π IProductService.cs
β βββ π ProductService.cs
β βββ π Repository/
β βββ π Models/
β βββ π Product.cs
β βββ π IProductRepository.cs
β βββ π ProductRepository.cs
β
βββ π Program.cs
π Benefits of This Approach
1οΈβ£ β Feature-Based Modularity
Each feature (e.g., Product, Order) is self-contained with its own handlers, services, and repositories. This makes it easier to:
- Navigate the codebase.
- Add new features without affecting others.
- Scale the application as it grows.
2οΈβ£ π§ͺ Testability
- Handlers, services, and repositories are decoupled and easily testable.
- Dependency Injection ensures mock implementations can be injected during unit testing.
3οΈβ£ π§© Flexibility with EndpointFilters
EndpointFilters, such as ProductsEndpointFilter
, allows separation of middleware between endpoints.
4οΈβ£ π§Ή Cleaner Program.cs
Using fluent-style configuration keeps the Program.cs
file clean and focused on application-level setup. Feature-specific logic is delegated to handlers.
5οΈβ£ βοΈ Comparison
Below is a comparison of three popular approaches for building APIs in .NETβββMVC, Minimal APIs, and FastEndpoints β each offering distinct patterns and capabilities.
β¨ Feature | π· MVC | π’ Minimal APIs | π FastEndpoints |
---|---|---|---|
π― Use Case | Full-featured web applications (Views, Controllers, Models) | Lightweight, small to medium APIs | Lightweight APIs with structured approach |
π§© Framework | ASP.NET Core MVC | ASP.NET Core Minimal APIs | ASP.NET Core with FastEndpoints library |
π£οΈ Routing | Attribute-based (e.g., [HttpGet] ) or conventional |
Fluent, inline routing or via Extensions | Declarative, endpoint-based with attributes |
π Boilerplate Code | Moderate to high, requires controllers and models | Minimal, customizable for separation of concerns | Minimal, but enforces endpoint structures |
π Performance | Moderate (slightly more overhead) | Extremely high, possibly faster than FastEndpoints | Very high, close to Minimal APIs |
β‘ Async Support | Fully supports async |
Fully supports async |
Fully supports async |
π Type Safety | Strong, compile-time type safety | Strong, compile-time type safety | Strong, compile-time type safety |
π§© Dependency Injection | Built-in, well-integrated | Built-in, well-integrated | Built-in, well-integrated |
β Validation | Manual or FluentValidation |
Manual or FluentValidation |
Built-in request and response validation |
π OpenAPI Documentation | Requires Swashbuckle/NSwag integration | Requires Swashbuckle/NSwag integration | Built-in OpenAPI documentation |
π Separation of Concerns | Enforced via Controllers, Models, Views | Fully achievable through custom structure | Encouraged via structured endpoints |
π Scalability | High, suitable for large, complex applications | Suitable for small to medium-sized applications | Suitable for small to medium-sized applications |
π Community | Large, long-established ASP.NET MVC community | Growing popularity with Minimal APIs users | Smaller but active and focused community |
π Learning Curve | Moderate to high, especially for beginners | Low to moderate, customizable for use cases | Low to moderate, requires understanding FastEndpoints |
π Key Differentiators | Separation of concerns with Views, Controllers, Models | Inline, fast, customizable for large-scale APIs | Adds validation and structure to Minimal APIs |
π Key Features of Minimal APIβs
1οΈβ£ Separation of Concerns in Minimal APIs:
Minimal APIs allow separation of concerns by moving logic into services, handlers, or dedicated classes, mimicking MVC-like architecture while remaining lightweight.
2οΈβ£ Performance:
Minimal APIs are extremely fast because of their lightweight architecture and reduced middleware pipeline, often faster than frameworks like FastEndpoints.
3οΈβ£ Use Cases:
Minimal APIs can scale to more complex applications with proper organization (e.g., using service classes, extensions for modularity).
π― Conclusion
By structuring Minimal APIs with a feature-based organization, DI, and reusable endpoint filters, you create a clean and scalable architecture that is easy to maintain and test. This approach leverages the strengths of .NET 8, providing a modern, performant alternative to MVC and competing frameworks like FastEndpoints.
Adopting this modular design ensures your Minimal APIs remain efficient and maintainable as your application grows.