Search
K
  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 pre or post 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. Simply write a pre-processor 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 result of ctx.ResponseStarted() to see if a previously executed pre-processor has already sent a response to the client. See example here.

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.ResponseStarted())
                    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.

Sharing State

In order to share state among pre/post processors and the endpoint, you have to first create a class to act as the state holder such as the following:

public class MyStateBag
{
    private readonly Stopwatch _sw = new();

    public bool IsValidAge { get; set; }
    public string Status { get; set; }
    public long DurationMillis => _sw.ElapsedMilliseconds;

    public MyStateBag() => _sw.Start();
}

Then create processors implementing the following abstract classes instead of the interfaces mentioned above. They will have the state bag passed in to the process method like so:

public class AgeChecker : PreProcessor<MyRequest, MyStateBag>
{
    public override Task PreProcessAsync(MyRequest req, MyStateBag state, HttpContext _, List<ValidationFailure> _, CancellationToken _)
    {
        if (req.Age >= 18)
            state.IsValidAge = true;
        state.Status = $"age checked by pre-processor at {state.DurationMillis} ms.";
        return Task.CompletedTask;
    }
}
public class DurationLogger : PostProcessor<MyRequest, MyStateBag, object>
{
    public override Task PostProcessAsync(MyRequest _, MyStateBag state, object _, HttpContext ctx, IReadOnlyCollection<ValidationFailure> _, CancellationToken _)
    {
        ctx.Resolve<ILogger<DurationLogger>>()
           .LogInformation("request took {@duration} ms.", state.DurationMillis);
        return Task.CompletedTask;
    }
}

The endpoint is able to access the same shared/common state by calling the ProcessorState<MyStateBag>() method like so:

public class MyEndpoint : Endpoint<MyRequest>
{
    public override void Configure()
    {
        ...
        PreProcessors(new AgeChecker());
        PostProcessors(new DurationLogger());
    }

    public override async Task HandleAsync(MyRequest r, CancellationToken c)
    {
        var state = ProcessorState<MyStateBag>();
        Logger.LogInformation("endpoint executed at {@duration} ms.", state.DurationMillis);
        await Task.Delay(100);
        await SendAsync(new
        {
            r.Age,
            state.Status
        });
    }
}

It's also possible to access the common state when using the processor interfaces, but it has to be done via the http context like so:

public class MyPreProcessor : IPreProcessor<Request>
{
    public Task PreProcessAsync(Request _, HttpContext ctx, List<ValidationFailure> _, CancellationToken _)
    {
        var state = ctx.ProcessorState<Thingy>();
    }
}

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.


TIP

As an alternative to pre/post processors, you have the option of using endpoint filters with FastEndpoints that was introduced in .NET 7.0 as shown here.


© FastEndpoints 2023