1. Pre / Post Processors

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>:

MyRequestLogger.cs
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:

CreateOrderEndpoint.cs
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:

SalesRequestLogger.cs
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:

SecurityProcessor.cs
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.

NOTE

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.

AbstractPublicEndpoint.cs
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();
    }
}
MyEndpoint.cs
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.


© FastEndpoints 2022