Pre / Post Processors
Rather than writing a common piece of logic repeatedly that must be executed either before or after each request to your system, you can write it as a processor and attach it to endpoints that need them.
There are two types of processors:
- Pre Processors
- Post Processors
Pre Processors
Let's say for example that you'd like to log every request before being executed by your endpoint handlers.
You can simply write a pre-processor like below by implementing the interface IPreProcessor<TRequest>:
public class MyRequestLogger<TRequest> : IPreProcessor<TRequest>
{
public Task PreProcessAsync(TRequest req, HttpContext ctx, List<ValidationFailure> failures, CancellationToken ct)
{
var logger = ctx.RequestServices.GetRequiredService<ILogger<TRequest>>();
logger.LogInformation($"request:{req?.GetType().FullName} path: {ctx.Request.Path}");
return Task.CompletedTask;
}
}
And then attach it to the endpoints you need like so:
public class CreateOrderEndpoint : Endpoint<CreateOrderRequest>
{
public override void Configure()
{
Post("/sales/orders/create");
PreProcessors(new MyRequestLogger<CreateOrderRequest>()); // add this
}
}
You can even write a request DTO specific processor like so:
public class SalesRequestLogger : IPreProcessor<CreateSaleRequest>
{
public Task PreProcessAsync(CreateSaleRequest req, HttpContext ctx, List<ValidationFailure> failures, CancellationToken ct)
{
var logger = ctx.RequestServices.GetRequiredService<ILogger<CreateSaleRequest>>();
logger.LogInformation($"sale value:{req.SaleValue}");
return Task.CompletedTask;
}
}
Short-Circuiting Execution
It is possible to end processing the request by returning a response from within a pre-processor like so:
public class SecurityProcessor<TRequest> : IPreProcessor<TRequest>
{
public Task PreProcessAsync(TRequest req, HttpContext ctx, List<ValidationFailure> failures, CancellationToken ct)
{
var tenantID = ctx.Request.Headers["tenant-id"].FirstOrDefault();
if (tenantID == null)
{
failures.Add(new("MissingHeaders", "The [tenant-id] header needs to be set!"));
return ctx.Response.SendErrorsAsync(failures); //sending response here
}
if (tenantID != "qwerty")
return ctx.Response.SendForbiddenAsync(); //sending response here
return Task.CompletedTask;
}
}
All the Send* methods supported by endpoint handlers are available. The send methods are accessed from the ctx.Response property as shown above. When a response is sent from a pre-processor, the handler method is not executed.
If there are multiple pre-processors configured, they will be executed. if another pre-processor also wants to send a response, they must check if it's possible to do so by checking the property ctx.Response.HasStarted to see if a previously executed pre-processor has already sent a response to the client.
Post Processors
Post-processors are executed after your endpoint handler has completed it's work. They can be created similarly by implementing the interface IPostProcessor<TRequest, TResponse>:
public class MyResponseLogger<TRequest, TResponse> : IPostProcessor<TRequest, TResponse>
{
public Task PostProcessAsync(TRequest req, TResponse res, HttpContext ctx, IReadOnlyCollection<ValidationFailure> failures, CancellationToken ct)
{
var logger = ctx.RequestServices.GetRequiredService<ILogger<TResponse>>();
if (res is CreateSaleResponse response)
{
logger.LogWarning($"sale complete: {response.OrderID}");
}
return Task.CompletedTask;
}
}
And then attach it to endpoints like so:
public class CreateOrderEndpoint : Endpoint<CreateSaleRequest, CreateSaleResponse>
{
public override void Configure()
{
Post("/sales/orders/create");
PostProcessors(new MyResponseLogger<CreateSaleRequest, CreateSaleResponse>());
}
}
Multiple Processors
You can attach multiple processors with both PreProcessors() and PostProcessors() methods.
The processors are executed in the order they are supplied to the methods.
Global Processors/ Filters
The recommended approach for global filters/ processors is to write that logic as a middleware and register it in the ASP.NET pipeline like so:
app.UseMiddleware<MyMiddleware>();
As an alternative to that, you can write a base endpoint like below which includes a processor and derive your endpoint classes from that.
public abstract class PublicEndpoint<TRequest, TResponse> : Endpoint<TRequest, TResponse>
where TRequest : class, new()
where TResponse : notnull, new()
{
public override void Configure()
{
PreProcessors(new MyRequestLogger<TRequest>());
AllowAnonymous();
}
}
public class MyEndpoint : PublicEndpoint<EmptyRequest, EmptyResponse>
{
public override void Configure()
{
Get("test/global-preprocessor");
base.Configure();
}
public override Task HandleAsync(EmptyRequest req, CancellationToken ct)
{
return SendOkAsync();
}
}
This approach is also helpful if you'd like to configure several endpoints with the same base configuration.