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.Resolve<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.Resolve<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.Resolve<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

Global pre/post processors that operate on multiple endpoints can be created implementing the IGlobalPreProcessor & IGlobalPostProcessor interfaces.

TenantIDChecker.cs
public class TenantIDChecker : IGlobalPreProcessor
{
    public async Task PreProcessAsync(object req, HttpContext ctx, List<ValidationFailure> failures, CancellationToken ct)
    {
        if (req is MyRequest r) //can work on specific dto types if desired
        {
            var tID = ctx.Request.Headers["x-tenant-id"];
            if (tID.Count > 0)
            {
                r.TenantID = tID[0];
            }
            else
            {
                failures.Add(new("TenandID", "Unable to retrieve tenant id from header!"));
                if (!ctx.Response.HasStarted)
                    await ctx.Response.SendErrorsAsync(failures);
            }
        }
    }
}

To attach the above pre-processor to all endpoints, add it in the global endpoint configurator function like so:

Program.cs
app.UseFastEndpoints(c =>
{
    c.Endpoints.Configurator = ep =>
    {
        ep.PreProcessors(Order.Before, new TenantIDChecker());
    };
});

The Order enum specifies whether to run the global processors before or after the endpoint level processors if there's also endpoint level processors configured.

Dependency Injection

Processors are singletons for performance reasons. I.e. there will only ever be one instance of a processor. You should not maintain state in them. Dependencies can be resolved as shown here.


© FastEndpoints 2022