Goodbye Controllers, Hello Minimal APIs
In recent .NET versions, there’s a new way to build JSON-based APIs with ASP.NET Core: Minimal APIs. Inspired by previous attempts in the ASP.NET ecosystem and elements from other communities, the Minimal APIs approach is an attempt to simplify the development of JSON-producing HTTP APIs.
This post explores why it makes sense to use Minimal APIs, the programming model compared to ASP.NET Core MVC, and some drawbacks that might make you consider using it.
Why ASP.NET Core Minimal API?g
Over several versions of .NET Core and .NET, performance has been a central focus of the .NET team. While ASP.NET Core MVC is a robust and production-hardened approach to building Web APIs, the complexity of the MVC pipeline leads to a lot of wasted time and resources when processing incoming HTTP requests.
Steps along a standard ASP.NET Core MVC request include nothing short of routing, controller initialization, action execution with model binding and filters, and result filters. Processing a request is typically a 17-step process, with more steps if you use any view engines. When building JSON-based APIs, you may view these steps as “unnecessary” and reduce your opportunities for greater throughput.
Along with the performance implications, some folks might find the “convention over configuration” approach of ASP.NET Core MVC “too magical”. For example, ASP.NET Core registers controller endpoints by scanning for route attributes or matching user-defined routing patterns. In addition, the ASP.NET Core MVC approach can typically detach the structural definitions of your application from the actual code you write. With global filters, model binders, and middleware, this complexity can lead developers to introduce subtle yet frustrating bugs.
Finally, Minimal APIs fit the programming paradigms developed over the last decade, emphasizing microservices and limiting the functionality that each host exposes. In practice, applications built with Minimal APIs can easily fit into a single file, expressing the functionality in one easy-to-read place. Some developers prefer this explicitness to ASP.NET Core MVC’s sprawl of controllers, models, and views.
Another critical part of ASP.NET Core is the componentization of cross-cutting functionality such as routing, middleware, and dependency injection. The separation of elements from any programming model allows you to mix and match ASP.NET Core MVC, Razor Pages, and Minimal APIs functionality in a single host application. Using Minimal APIs doesn’t mean starting over but reflecting on an existing codebase and an opportunity to optimize.
Your first Minimal API application
You can create a new ASP.NET Core solution using the “Empty” ASP.NET Core project type to get started with Minimal APIs. Once you’ve created your solution, you’ll notice a Program.cs
file with the following code.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run();
First, let’s talk about the WebApplication.CreateBuilder
method call at the top of the file. This method registers commonly used elements of a web application host, such as configuration, logging, host services, routing, and more. Once called, you can augment your host by registering services, reading configuration, logging, and more. Then, once your modifications are complete, you can Build
your application host.
Once you have cemented your application’s configuration, you can modify the request pipeline by registering middleware or adding HTTP endpoints. Take note of the call to MapGet
, which takes both the path and RequestDelegate
to return a string value. Minimal APIs have correlations for all the essential HTTP methods, such as GET
, POST
, PUT
, PATCH
, DELETE
, and others.
The RequestDelegate
is a powerful abstraction, allowing ASP.NET Core a common interface to execute all HTTP requests. While a powerful abstraction, it’s a relatively straightforward delegate
definition with an HttpContext
parameter.
Implement Sample with CQRS pattern and Use ICarter for Use Minimal API
Today I want to show you how to use the CQRS pattern to build fast and scalable applications.
The CQRS pattern separates the writes and reads in the application.
This separation can be logical or physical and has many benefits:
- Complexity management
- Improved performance
- Scalability
- Flexibility
- Security
I’m also going to show you how to implement CQRS in your application using MediatR.
But first, we have to understand what CQRS is.
How Can ICarter Help to use Minimal API?
https://www.nuget.org/packages/Carter
- Create a new empty ASP.NET Core application —
dotnet new web -n MyCarterApp
- Change into the new project location —
cd ./MyCarterApp
- Add Carter package —
dotnet add package carter
- Modify your Program.cs to use Carter
var appPartsAssemblies = ApplicationPartDiscovery.FindAssemblies();
builder.Services.AddMediatR(configuration => configuration.RegisterServicesFromAssemblies(appPartsAssemblies));
builder.Services.AddCarter(new DependencyContextAssemblyCatalog(appPartsAssemblies));
5. Implement ICarterModule
public class GetStockwatchQueryModule : ICarterModule
{
public void AddRoutes(IEndpointRouteBuilder app)
{
app.MapGet(
"api/stockWatch/Info",
async (IMediator mediator, CancellationToken cancellationToken) =>
{
var result = await mediator.Send(new GetStockwatchInfo.Query(), cancellationToken);
return TypedResults.Ok(result);
})
.RequireAuthorization()
.WithOpenApi()
.WithTags("stockwatch")
.Produces<object[]>();
}
}
6. Implement CQRS Sample for GetData
public static class GetStockwatchInfo
{
public class Query : IRequest<object>
{
public string Isin { get; set; }
}
public class Handler : IRequestHandler<Query, object>
{
private readonly IMDPStockwatchService _stockwatchService;
private readonly ILogger<Query> _logger;
public Handler(
IMDPStockwatchService stockwatchService,
ILogger<Query> logger
)
{
_stockwatchService = stockwatchService;
_logger = logger;
}
public async Task<object> Handle(Query request, CancellationToken ct)
{
var data = new object();
try
{
var find = await _stockwatchService.GetStockWatchByIsin(request.Isin,true,ct);
return await Task.FromResult(data);
}
catch (Exception ex)
{
_logger.LogError(ex, ex.Message);
var result = new
{
Data = data,
Message = ex.Message,
Success = false
};
return await Task.FromResult(result);
}
}
}
}