Reviewing middleware real quick like

Remember that middleware are the things that handle HTTP requests and responses in ASP.NET Core.

Middleware pipeline Source: ASP.NET Docs

And remember how we added our own middleware to the pipeline to show how they behaved?

app.Use(async (HttpContext context, RequestDelegate next) =>
{
    context.Response.Headers.Append("Content-Type", "text/html");
    await context.Response.WriteAsync("<h1>Welcome to Ahoy!</h1>");
});

Try moving that around to different places and see how it affects your requests. For instance, if you move them after your UseControllers() call, if you make a valid call to a controller endpoint, the middleware we added will not be executed.

These middleware are executed in the order they are added to the pipeline, which makes order extremely important.

Take for instance the middleware chain typically used in ASP.NET Core web apps (documented here):


app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthentication();
app.UseAuthorization();

app.MapRazorPages();
app.MapDefaultControllerRoute();

Does Spencer ever write his own middleware?

Not often, but I have definitely done it - mainly to handle weird things like multi-tenant SaaS products where each tenant is marked with a different subdomain. It would be useful to have a middleware that can read that subdomain and set the tenant for the remainder of the request for easy access:

app.Use(async (HttpContext context, RequestDelegate next) =>
{
    var tenant = context.Request.Host.Host.Split('.')[0];
    var tenantId = await _tenantService.GetTenantIdBySubdomain(tenant);
    context.Items["TenantId"] = tenantId;
    await next(context);
});

For the purposes of this course, it's important to know what middleware is and what it's used for, but we'll mostly be working with the default ones provided by ASP.NET Core.

Filters

Filters are a way to modify the behavior of requests in ASP.NET Core, but only after the request has been routed to a controller method (controller methods are also referred to commonly as actions).

This has one big advantage: filters engage after the route has been matched and model binding has occured.

Think of it as a micro middleware that's invoked after all of the main middleware run (and the route has been matched to something that can actually use the filter).

You can apply a filter globally, or on a more specific set of controllers or actions.

There are several different types of filters:

Filter Type When it's executed Where it's defined
Authorization filters Before the action is invoked Global or per controller
Resource filters Before and after the action is invoked Global, per controller, or per action
Action filters Before and after the action is invoked Global, per controller, or per action
Exception filters When an exception is thrown Global or per controller
Result filters After the action is invoked Global, per controller, or per action

Some example of common filters built into ASP.NET Core:

  • [Authorize] - Checks for valid auth tokens
  • [EnableCors] - Enables CORS for a specific controller or action
  • [ResponseCache] - Adds response caching to a controller or action
  • [RequireHttps] - Redirects to HTTPS if the request is not HTTPS
  • [ValidateAntiForgeryToken] - Validates anti-forgery tokens for POST requests

Let's write our own filter

While we're at it, let's do ourselves a favor and add a custom filter that addresses this piece of boilerplate:

var validationResults = await ValidateAsync(employeeRequest);
if (!validationResults.IsValid)
{
    return ValidationProblem(validationResults.ToModelStateDictionary());
}

You'd think this is built into the framework, but it's not... not exactly anyways. Remember that ApiController attribute we've been using? It actually is supposed to trigger validation behavior, but that doesn't work well with FluentValidation for one major reason: async validation, which isn't supported by ASP.NET Core. (See the GitHub issue here for more information. Fun fact, this issue was actually filed by the creator of FluentValidation!)

The validator pipeline only supports synchronous validation, and because we live in an async world with validators that can be async, we need to validate the request model in a custom filter.

Let's do that now.

First, let's create a new filter attribute:

public class FluentValidationFilter : IAsyncActionFilter
{
    private readonly IServiceProvider _serviceProvider;
    private readonly ProblemDetailsFactory _problemDetailsFactory;

    public FluentValidationFilter(IServiceProvider serviceProvider, ProblemDetailsFactory problemDetailsFactory)
    {
        _serviceProvider = serviceProvider;
        _problemDetailsFactory = problemDetailsFactory;
    }

    public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
    {
        foreach (var parameter in context.ActionDescriptor.Parameters)
        {
            if (context.ActionArguments.TryGetValue(parameter.Name, out var argumentValue) && argumentValue != null)
            {
                var argumentType = argumentValue.GetType();

                var validatorType = typeof(IValidator<>).MakeGenericType(argumentType);

                var validator = _serviceProvider.GetService(validatorType) as IValidator;

                if (validator != null)
                {
                    // Validate the argument
                    ValidationResult validationResult = await validator.ValidateAsync(new ValidationContext<object>(argumentValue));

                    if (!validationResult.IsValid)
                    {
                        validationResult.AddToModelState(context.ModelState);
                        var problemDetails =
                            _problemDetailsFactory.CreateValidationProblemDetails(context.HttpContext,
                                context.ModelState);
                        context.Result = new BadRequestObjectResult(problemDetails);

                        return;
                    }
                }
            }
        }

        await next();
    }

    public void OnActionExecuted(ActionExecutedContext context) { }
}

Holy heck, that thing is BEEFY. What's it doing?

  • It implements IAsyncActionFilter, which is the interface for async action filters. (There is a sync one as well, but we need async so we're using the async one.)
  • It gets each of the parameters of the controller action from context.ActionDescriptor.Parameters. (That's why we're using filters for this, not middleware - middleware is NOT aware of the controller action or the parameters.)
  • It uses DI - IServiceProvider - to get the IValidator for the current action's parameter type.
  • It runs validation and adds the results to the ModelState if the validation fails.
  • It creates a ValidationProblemDetails object and sets the ModelState to it if the validation fails.
  • It sets the Result to a BadRequestObjectResult with the ValidationProblemDetails object if the validation fails.

The weirdest part for me is the ProblemDetailsFactory tbh - for some reason that is a separately registered component in ASP.NET Core.

(If you're really curious about this filter and what the heck it's doing, I'd set a breakpoint and step through the code with the debugger to see what's happening.)

We can apply this filter globally by adding it to the FilterCollection like so (in our Program.cs file, replace the AddControllers() call with this):

builder.Services.AddControllers(options =>
{
    options.Filters.Add<FluentValidationFilter>();
});

We can now delete the ValidateAsync call from our controller actions (and delete the method from BaseController too. I know, BaseController feels pretty lonely right now, but we'll use him again soon!)

Run your unit tests and see everything still works.

🌶️🌶️🌶️ While you're free to use the above class for your own code, I wouldn't recommend it as it's pretty simplistic. There is a NuGet package called FluentValidation.AutoValidation that does the same thing except it's much more comprehensive: https://github.com/SharpGrip/FluentValidation.AutoValidation

Refactoring our PUT endpoint

Now that we have our filter, refactoring and adding a test for our PUT endpoint is pretty easy - all we have to do is define a validator and add a test, and we're good to go!

Let's show a slightly more advanced validator that doesn't allow us to null out the Address field if one is already defined in the database.

There's actually a few things we need to do here:

  1. We need a way to pass our ID from the route to the validator.
  2. We need to make sure that our validator can access the database to check for the existence of an address.

To do this, we can use the ActionExecutingContext to get the route values and pass them to our validator.

Here's what that looks like:

public class UpdateEmployeeRequestValidator : AbstractValidator<UpdateEmployeeRequest>
{
    private readonly HttpContext _httpContext;
    private readonly IRepository<Employee> _repository;

    public UpdateEmployeeRequestValidator(IHttpContextAccessor httpContextAccessor, IRepository<Employee> repository)
    {
        this._httpContext = httpContextAccessor.HttpContext!;
        this._repository = repository;

        RuleFor(x => x.Address1).MustAsync(NotBeEmptyIfItIsSetOnEmployeeAlreadyAsync).WithMessage("Address1 must not be empty.");
    }

    private async Task<bool> NotBeEmptyIfItIsSetOnEmployeeAlreadyAsync(string? address, CancellationToken token)
    {
        await Task.CompletedTask;   //again, we'll not make this async for now!

        var id = Convert.ToInt32(_httpContext.Request.RouteValues["id"]);
        var employee = _repository.GetById(id);

        if (employee!.Address1 != null && string.IsNullOrWhiteSpace(address))
        {
            return false;
        }

        return true;
    }
}

What the heck is all this stuff?

  • IHttpContextAccessor is used to get the current HttpContext, which contains the route value of the employee we're updating.
  • IRepository<Employee> is our generic repository, which we can use to get the employee from the database.
  • Convert.ToInt32(_httpContext.Request.RouteValues["id"]) gets the id from the route.
  • The MustAsync method is used to run our validation logic asynchronously. It's a way to run some custom logic outside of the normal "value is empty" stuff that we normally do.

This class is automatically registered by FluentValidation, so we don't need to do anything special to wire it up, except to add HttpContextAccessor to our Program.cs file:

builder.Services.AddHttpContextAccessor();

Finally, we'll update our tests to pass:

[Fact]
public async Task UpdateEmployee_ReturnsOkResult()
{
    var client = _factory.CreateClient();
    var response = await client.PutAsJsonAsync("/employees/1", new Employee { FirstName = "John", LastName = "Doe", Address1 = "123 Main St" });

    response.EnsureSuccessStatusCode();
}

[Fact]
public async Task UpdateEmployee_ReturnsBadRequestWhenAddress()
{
    // Arrange
    var client = _factory.CreateClient();
    var invalidEmployee = new UpdateEmployeeRequest(); // Empty object to trigger validation errors

    // Act
    var response = await client.PutAsJsonAsync($"/employees/{_employeeId}", invalidEmployee);

    // Assert
    Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);

    var problemDetails = await response.Content.ReadFromJsonAsync<ValidationProblemDetails>();
    Assert.NotNull(problemDetails);
    Assert.Contains("Address1", problemDetails.Errors.Keys);
}