Boost your .NET minimal APIs
Introduction
Minimal APIs in .NET offer a wide range of benefits for streamlining the development process compared to traditional controller based api development model. The minimal approach let's you focus on the business logic rather than worrying too much about the boilerplate setup.
Minimal APIs have evolved greatly since their launch and in .NET 8 contains features that can further boost your development process and security and we're going to look at some of those which I've experienced with lately.
I'm assuming you're already familiar with minimal APIs basic consepts so we're diving straight to the more advanced scenarios.
Request and response logging
Request and response logging can be useful in local environment for debugging purposes or if there's a specific requirement to log incoming requests for an application on a live environment.
Request and response logging can be enabled globally or for a specific endpoint.
Enabling the logging globally can be done in Program.cs file with extensiong methods AddHttpLogging
and UseHttpLogging
.
using Microsoft.AspNetCore.HttpLogging;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttpLogging(logging =>
{
logging.LoggingFields = HttpLoggingFields.All;
logging.RequestHeaders.Add("sec-ch-ua");
logging.MediaTypeOptions.AddText("application/javascript");
logging.RequestBodyLogLimit = 4096;
logging.ResponseBodyLogLimit = 4096;
logging.CombineLogs = true;
});
var app = builder.Build();
app.UseHttpLogging();
app.UseHttpsRedirection();
MediaTypeOptions
can be used for logging specific content, such as application/javascript
or multipart/form-data
.
CombineLogs
flag is used for combining request and response as a single log entry.
Http logging needs to be enabled also for the LogLevel
configuration in appsettings.json
or the environment-specific equivalent. The required configuration element is Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware
.
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware": "Information"
}
}
More detailed logging capabilities can be implemented by creating a custom logging interceptor with IHttpLoggingInterceptor
interface.
Route groups
Route groups can be used to organize endpoints with a common prefix to avoid repetitive code for generic endpoint functionality.
First, we define a route group using the RouteGroupBuilder
class. The actual endpoints are specified inside the route group.
public static class DeviceRouteExtension
{
public static RouteGroupBuilder MapDeviceEndpoints(this RouteGroupBuilder group)
{
group.MapGet(string.Empty, async (IMediator mediator, [FromQuery] bool? active) =>
{
return await mediator.Send(new GetDevicesQuery { Active = active });
})
.Produces((int)HttpStatusCode.Unauthorized);
group.MapGet("/{id}", async (IMediator mediator, int id) =>
{
return await mediator.Send(new GetDeviceQuery { Id = id });
})
.Produces((int)HttpStatusCode.NotFound)
.Produces((int)HttpStatusCode.Unauthorized);
return group;
}
}
We can now create the group and map the above constructed grouped routes to the group and add common functionality. We'll specify an authorization policy requirement and OpenApi support for the device endpoints within a static extension method.
private static void MapEndpoints(this WebApplication app)
{
app.MapGroup("/api/devices")
.MapDeviceEndpoints()
.RequireAuthorization(AuthorizationPolicies.AuthorizedUser)
.WithOpenApi();
}
Now each api request with a "/api/devices" prefix requires that the authorization policy requirement is met and all endpoints are published with OpenApi support.
Filters
Filters allow us to execute code before and after the request handler. Filters can be applied to endpoint or route group level. Below endpoint filter performs request validation and adds a custom header.
group.MapGet("/{id}", async (IMediator mediator, int id) =>
{
return await mediator.Send(new GetDeviceQuery { Id = id });
})
.AddEndpointFilter(async (context, next) =>
{
var dic = new Dictionary();
var id = context.GetArgument(1);
context.HttpContext.Request.Headers.Append("myCustomHeader", "myHeaderValue");
if (id >= 10000)
{
dic.Add("id", ["Id must be less than 10000"]);
return Results.ValidationProblem(dic);
}
return await next(context);
})
.Produces((int)HttpStatusCode.NotFound)
.Produces((int)HttpStatusCode.Unauthorized);
Filters are executed before the request handler. Multiple filters can be chained and filter logics called before next
delegate are called in First In, First Out(FIFO) order. Place the desired code after the delegate to execute it after the request handler.
Filters can also be created by implementing the IEndpointFilter
interface.
public class CustomEndpointFilter : IEndpointFilter
{
public readonly ILogger Logger;
public CustomEndpointFilter(ILoggerFactory loggerFactory)
{
Logger = loggerFactory.CreateLogger();
}
public virtual async ValueTask
group.MapGet(string.Empty, async (IMediator mediator, [FromQuery] bool? active) =>
{
return await mediator.Send(new GetDevicesQuery { Active = active });
})
.AddEndpointFilter()
.Produces((int)HttpStatusCode.Unauthorized);
Form value binding
Parameter binding from form values is something that was missing from minimal APIs until recently. Form value binding can simply be achieved by using the FromForm
attribute.
group.MapPost(string.Empty, async (IMediator mediator, [FromForm] string name) =>
{
return await mediator.Send(new PostDeviceCommand { Name = name });
})
.Produces((int)HttpStatusCode.Unauthorized);
What's even better, we can use the AsParameters
attribute with a record to make the api definition cleaner.
group.MapPost(string.Empty, async (IMediator mediator, [AsParameters] CreateDeviceRequest request) =>
{
return await mediator.Send(new PostDeviceCommand { Name = request.Name });
})
.Produces((int)HttpStatusCode.Unauthorized);
public record struct CreateDeviceRequest([FromForm] string Name, IFormFile? Attachment);
Antiforgery with Minimal APIs
Cross-site request forgery(XSRF or CSRF) is an attack where a malicious application is intervening a trusted communication between a client browser and a web application. Html forms are a typical antiforgery attack method.
Mitigating antiforgery attacks in minimal APIs is achieved by enabling the antiforgery middleware. Middleware is utilized by calling AddAntiforgery
and UseAntiforgery
extension methods. The middleware adds automatic antiforgery token validation to the request pipeline. The middlware needs to be placed after authentication and authorization middlewares to prevent unauthorized form data access.
Minimal APIs require antiforgery token validation for form data endpoints by default.
builder.Services.AddAntiforgery();
var app = builder.Build();
app.UseAntiforgery();
Calling the endpoint containing form data from a previous example in this blog post without an antiforgery token would throw an exception. The antiforgery token requirement can also be disabled for an endpoint using the DisableAntiforgery
extension method.
group.MapPost(string.Empty, async (IMediator mediator, [FromForm] string name) =>
{
return await mediator.Send(new PostDeviceCommand { Name = name });
})
.Produces((int)HttpStatusCode.Unauthorized)
.DisableAntiforgery();
Summary
Minimal APIs have evolded a lot since their launch back in 2022 and new features were added also in .NET 8. We went through some of the more recent or otherwise useful features of minimal APIs that could improve your productivity, debugging and security. I hope that you find some of them useful for your ongoing or upcoming projects.