loading...
Cover image for Episode 009 - MVC filters - ASP.NET Core: From 0 to overkill

Episode 009 - MVC filters - ASP.NET Core: From 0 to overkill

joaofbantunes profile image João Antunes Originally published at blog.codingmilitia.com on ・7 min read

ASP.NET Core: From 0 to overkill (45 Part Series)

1) ASP.NET Core: From 0 to overkill - Intro 2) Episode 001 - The Reference Project - ASP.NET Core: From 0 to overkill 3 ... 43 3) Episode 002 - Project structure plus first application - ASP.NET Core: From 0 to overkill 4) Episode 003 - First steps with MVC - ASP.NET Core: From 0 to overkill 5) Episode 004 - The Program and Startup classes - ASP.NET Core: From 0 to overkill 6) Episode 005 - Dependency Injection - ASP.NET Core: From 0 to overkill 7) Episode 006 - Configuration - ASP.NET Core: From 0 to overkill 8) Episode 007 - Logging - ASP.NET Core: From 0 to overkill 9) Episode 008 - Middlewares - ASP.NET Core: From 0 to overkill 10) Episode 009 - MVC filters - ASP.NET Core: From 0 to overkill 11) Episode 010 - Async all the things - ASP.NET Core: From 0 to overkill 12) Episode 011 - Data access with Entity Framework Core - ASP.NET Core: From 0 to overkill 13) Episode 012 - Move to a Web API - ASP.NET Core: From 0 to overkill 14) Episode 013 - Starting the frontend with Vue.js - ASP.NET Core: From 0 to overkill 15) Episode 014 - Centralizing frontend state with Vuex - ASP.NET Core: From 0 to overkill 16) Episode 015 - Calling the Web API from the frontend - ASP.NET Core: From 0 to overkill 17) Episode 016 - Authentication with Identity and Razor Pages - ASP.NET Core: From 0 to overkill 18) Episode 017 - More Identity, more Razor Pages - ASP.NET Core: From 0 to overkill 19) Episode 018 - Internationalization - ASP.NET Core: From 0 to overkill 20) Episode 019 - Roles, claims and policies - ASP.NET Core: From 0 to overkill 21) Episode 020 - The backend for frontend and the HttpClient - ASP.NET Core: From 0 to overkill 22) Episode 021 - Integrating IdentityServer4 - Part 1 - Overview - ASP.NET Core: From 0 to overkill 23) Episode 022 - Integrating IdentityServer4 - Part 2 - Auth Service - ASP.NET Core: From 0 to overkill 24) Episode 023 - Integrating IdentityServer4 - Part 3 - API - ASP.NET Core: From 0 to overkill 25) Episode 024 - Integrating IdentityServer4 - Part 4 - Back for Front - ASP.NET Core: From 0 to overkill 26) Episode 025 - Integrating IdentityServer4 - Part 5 - Frontend - ASP.NET Core: From 0 to overkill 27) Episode 026 - Getting started with Docker - ASP.NET Core: From 0 to overkill 28) Episode 027 - Up and running with Docker Compose - ASP.NET Core: From 0 to overkill 29) Episode 028 - Multiple service instances tweaks - ASP.NET Core: From 0 to overkill 30) Episode 029 - Simplifying the BFF with ProxyKit - ASP.NET Core: From 0 to overkill 31) Episode 030 - Analyzing performance with BenchmarkDotNet - ASP.NET Core: From 0 to overkill 32) Episode 031 - Some simple unit tests with xUnit - ASP.NET Core: From 0 to overkill 33) Episode 032 - Upgrading to ASP.NET Core 3.0 - ASP.NET Core: From 0 to overkill 34) E033 - Redesigning the API: Improving the internal architecture - ASPF02O 35) E034 - Segregating use cases with MediatR - ASPF02O 36) E035 - Experimenting with (yet) another approach to data access organization - ASPF02O 37) E036 - Making things more object oriented with rich domain entities - ASPF02O 38) Better use of types - avoiding nulls with an Optional type - ASPF02O|E037 39) More explicit domain error handling and fewer exceptions with Either and Error types [ASPF02O|E038] 40) Event-driven integration - Overview [ASPF02O|E039] 41) Event-driven integration #1 - Intro to the transactional outbox pattern [ASPF02O|E040] 42) Event-driven integration #2 - Inferring events from EF Core changes [ASPF02O|E041] 43) Event-driven integration #3 - Storing events in the outbox table [ASPF02O|E042] 44) Event-driven integration #4 - Outbox publisher (feat. IHostedService & Channels) [ASPF02O|E043] 45) Event-driven integration #5 - Quick intro to Apache Kafka [ASPF02O|E044]

Following up on the previous episode on ASP.NET Core middlewares, in this episode we take a look at MVC's filters, an MVC specific way to add behaviors to our request handling pipeline, and how we can use them to implement cross-cutting concerns in our web applications.
For the walk-through you can check the next video, but if you prefer a quick read, skip to the written synthesis.

The playlist for the whole series is here.

Intro

Like introduced, given the previous episode focused on ASP.NET Core's middlewares, taking a look at MVC filters right after it, makes sense to me, as it provides some different options to add behaviors to the request handling pipeline, even if in this case more specific to MVC features.

Going back to the docs for an image that illustrates how the filters fit in the overall picture.

filter pipeline overview

There are 5 types of filters, as seen in the docs:

  • Authorization filters run first and are used to determine whether the current user is authorized for the current request. They can short-circuit the pipeline if a request is unauthorized.
  • Resource filters are the first to handle a request after authorization. They can run code before the rest of the filter pipeline, and after the rest of the pipeline has completed. They're useful to implement caching or otherwise short-circuit the filter pipeline for performance reasons. They run before model binding, so they can influence model binding.
  • Action filters can run code immediately before and after an individual action method is called. They can be used to manipulate the arguments passed into an action and the result returned from the action.
  • Exception filters are used to apply global policies to unhandled exceptions that occur before anything has been written to the response body.
  • Result filters can run code immediately before and after the execution of individual action results. They run only when the action method has executed successfully. They are useful for logic that must surround view or formatter execution.

The following image (from the docs again) shows some more details of how the filters play together.

filter pipeline closeup

In this post we'll only use action and exception filters, as well as the options we have on how to implement and use them. The other filters should be similar in terms of implementation and usage, having of course different reasons to go with them.

Application wide filter

Let's start simple, with an action filter registered to intercept all requests that find their way to an MVC action.

To do this, we start by creating a class that implements IActionFilter (or IAsyncActionFilter if we need to do some async work on there).

DemoActionFilter.cs

public class DemoActionFilter : IActionFilter
{
    private readonly ILogger<DemoActionFilter> _logger;

    public DemoActionFilter(ILogger<DemoActionFilter> logger)
    {
        _logger = logger;
    }

    public void OnActionExecuting(ActionExecutingContext context)
    {
        _logger.LogInformation("Before executing action {action} with arguments \"{@arguments}\" and model state \"{@modelState}\"",
            context.ActionDescriptor.DisplayName,
            context.ActionArguments,
            context.ModelState);
    }

    public void OnActionExecuted(ActionExecutedContext context)
    {
        _logger.LogInformation("After executing action {action}.", context.ActionDescriptor.DisplayName);
    }
}

Rather straightforward stuff. We implement the interface methods OnActionExecuting and OnActionExecuted (which run before and after the action executes respectively).

As usual in my post examples, I'm logging stuff 🤣 The content is not that important I would say, it's just serving as an example of some of the information we have access to in the context of the filter.

We can also see that we can get dependencies injected, as we're getting a logger in the constructor.

To register the filter, so it intercepts all the actions, we head on to the Startup class ConfigureServices method, and register the filter in MVC options as follows:

Startup.cs

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc(options =>
    {
        options.Filters.Add<DemoActionFilter>();
    });

    //...
}

Another alternative (and probably a better idea) would be to use options.Filters.AddService<DemoActionFilter>() to register the filter, but if we do this, we must also register the filter in the DI container.

A sample log output for this filter would be as follows:

19:05:24 CodingMilitia.PlayBall.GroupManagement.Web.Demo.Filters.DemoActionFilter Info Before executing action "CodingMilitia.PlayBall.GroupManagement.Web.Controllers.GroupsController.CreateReally (CodingMilitia.PlayBall.GroupManagement.Web)" with arguments "{"model":{"Id":0, "Name":"Some Group"}}" and model state "[{"Key":"Name", "Value":{"Key":"Name", "SubKey":{"Buffer":"Name", "Offset":0, "Length":4, "Value":"Name", "HasValue":true}, "IsContainerNode":false, "RawValue":"Some Group", "AttemptedValue":"Some Group", "Errors":[], "ValidationState":"Valid"}}]"
19:05:24 CodingMilitia.PlayBall.GroupManagement.Web.Demo.Filters.DemoActionFilter Info After executing action CodingMilitia.PlayBall.GroupManagement.Web.Controllers.GroupsController.CreateReally (CodingMilitia.PlayBall.GroupManagement.Web).

Decorating a controller or action with a filter attribute

Having a filter applied globally is nice, and may suffice for a great amount of cases, but sometimes we really need more control over when the filter should in fact execute. A good way to achieve this is by using attributes, with which we can decorate a controller or an action where we want the filter to be used.

Let's make another silly sample action filter to check this out 🙂

DemoActionFilterAttribute.cs

public class DemoActionFilterAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext context)
    {
        if (context.ActionArguments.TryGetValue("model", out var model)
            && model is GroupViewModel group
            && group.Id == 1)
        {
            group.Name += $" (Added on {nameof(DemoActionFilterAttribute)})";
        }
    }
}

In this example, we're checking if there is an action argument named model and then if it is of type GroupViewModel with id 1. If it is a match, we alter the contents of the object, just to show we can 🙂

You might notice we don't have any dependencies being injected, and that's because by rolling an attribute like this, we can't have them, because we would need to pass them when applying the attribute, which isn't really doable (but we'll see in a bit how we can have a filter applied using an attribute that is able to have dependencies injected).

To apply the attribute, we can go into our GroupsController and apply it to the class directly, or to any method - but to see it working we need to apply it to the Edit method, the others will not make use of it.

GroupsController.cs

[DemoActionFilter]
[HttpPost]
[Route("{id}")]
[ValidateAntiForgeryToken]
public IActionResult Edit(long id, GroupViewModel model)
{
    var group = _groupsService.Update(model.ToServiceModel());

    if (group == null)
    {
        return NotFound();
    }

    return RedirectToAction("Index");
}

To see the end result, we can create a group, then edit it, and we'll see the added text.

Filter attribute with dependencies

Applying a filter using an attribute is nice, but as mentioned, doing it like shown in the previous section doesn't allow us to do much, as we can't get any dependencies in the filter class.

Let's look at some options to have the cake and eat it too. Let's start with creating a sample exception filter.

DemoExceptionFilter.cs

public class DemoExceptionFilter : IExceptionFilter
{
    private readonly ILogger<DemoExceptionFilter> _logger;

    public DemoExceptionFilter(ILogger<DemoExceptionFilter> logger)
    {
        _logger = logger;
    }

    public void OnException(ExceptionContext context)
    {
        if (context.Exception is ArgumentException)
        {
            _logger.LogError("Transforming ArgumentException in 400");
            context.Result = new BadRequestResult();
        }
    }
}

Simple stuff, any time an exception is thrown (and not caught) in an action, it'll end up in the DemoExceptionFilter, and if it's an ArgumentException we respond with a 400. Now let's use the filter.

Using ServiceFilterAttribute

The first option we have is to use the ServiceFilterAttribute. Instead of applying a filter as attribute directly, we apply ServiceFilterAttribute with the type of filter we want as an argument.

GroupsController.cs

[ServiceFilter(typeof(DemoExceptionFilter))]
[Route("groups")]
public class GroupsController : Controller
{
    //...
}

Besides adding the attribute, we also need to register the DemoExceptionFilter in DI, so the ServiceFilterAttribute can fetch it.

Startup.cs

public void ConfigureServices(IServiceCollection services)
{
    //...

    services.AddTransient<DemoExceptionFilter>();

    //...
}

Using a custom filter factory

An alternative to ServiceFilterAttribute, if we require more control over things, is to create an attribute that implements IFilterFactory.

DemoExceptionFilterFactoryAttribute.cs

public class DemoExceptionFilterFactoryAttribute : Attribute, IFilterFactory
{
    public IFilterMetadata CreateInstance(IServiceProvider serviceProvider)
    {
        var filter = serviceProvider.GetRequiredService<DemoExceptionFilter>();
        return filter;
    }

    public bool IsReusable { get; } = false;
}

When implementing IFilterFactory.CreateInstance we get an IServiceProvider instance as argument, so we can fetch anything we need from the dependency injection container. In this case we're simply getting a filter instance from DI, but we could also complicate things if needed.

The IsReusable property is usd to tell the runtime if the filter instances returned by the factory can be reused across requests. If it was a singleton filter, sure, but when it's not the case, IsReusable should be false.

To use it, we can simply go into GroupsController and replace the ServiceFilterAttribute with this one.

GroupsController.cs

[DemoExceptionFilterFactory]
[Route("groups")]
public class GroupsController : Controller
{
    //...
}

Outro

Like I said in the beginning, this is just a quick look at some of the stuff we can do it MVC filters, just so we are aware of our options when developing an application, maybe we recognize that some pattern would map perfectly to an MVC filter (or maybe a middleware like we saw in the previous post).

As always, the docs are a great place to learn more about all these topics, with lots of info I'm not able to cram into these quick posts.

The source code for this post is here.

Please send any feedback so I can improve and adjust the next episodes.

Thanks for stopping by, cyaz!

ASP.NET Core: From 0 to overkill (45 Part Series)

1) ASP.NET Core: From 0 to overkill - Intro 2) Episode 001 - The Reference Project - ASP.NET Core: From 0 to overkill 3 ... 43 3) Episode 002 - Project structure plus first application - ASP.NET Core: From 0 to overkill 4) Episode 003 - First steps with MVC - ASP.NET Core: From 0 to overkill 5) Episode 004 - The Program and Startup classes - ASP.NET Core: From 0 to overkill 6) Episode 005 - Dependency Injection - ASP.NET Core: From 0 to overkill 7) Episode 006 - Configuration - ASP.NET Core: From 0 to overkill 8) Episode 007 - Logging - ASP.NET Core: From 0 to overkill 9) Episode 008 - Middlewares - ASP.NET Core: From 0 to overkill 10) Episode 009 - MVC filters - ASP.NET Core: From 0 to overkill 11) Episode 010 - Async all the things - ASP.NET Core: From 0 to overkill 12) Episode 011 - Data access with Entity Framework Core - ASP.NET Core: From 0 to overkill 13) Episode 012 - Move to a Web API - ASP.NET Core: From 0 to overkill 14) Episode 013 - Starting the frontend with Vue.js - ASP.NET Core: From 0 to overkill 15) Episode 014 - Centralizing frontend state with Vuex - ASP.NET Core: From 0 to overkill 16) Episode 015 - Calling the Web API from the frontend - ASP.NET Core: From 0 to overkill 17) Episode 016 - Authentication with Identity and Razor Pages - ASP.NET Core: From 0 to overkill 18) Episode 017 - More Identity, more Razor Pages - ASP.NET Core: From 0 to overkill 19) Episode 018 - Internationalization - ASP.NET Core: From 0 to overkill 20) Episode 019 - Roles, claims and policies - ASP.NET Core: From 0 to overkill 21) Episode 020 - The backend for frontend and the HttpClient - ASP.NET Core: From 0 to overkill 22) Episode 021 - Integrating IdentityServer4 - Part 1 - Overview - ASP.NET Core: From 0 to overkill 23) Episode 022 - Integrating IdentityServer4 - Part 2 - Auth Service - ASP.NET Core: From 0 to overkill 24) Episode 023 - Integrating IdentityServer4 - Part 3 - API - ASP.NET Core: From 0 to overkill 25) Episode 024 - Integrating IdentityServer4 - Part 4 - Back for Front - ASP.NET Core: From 0 to overkill 26) Episode 025 - Integrating IdentityServer4 - Part 5 - Frontend - ASP.NET Core: From 0 to overkill 27) Episode 026 - Getting started with Docker - ASP.NET Core: From 0 to overkill 28) Episode 027 - Up and running with Docker Compose - ASP.NET Core: From 0 to overkill 29) Episode 028 - Multiple service instances tweaks - ASP.NET Core: From 0 to overkill 30) Episode 029 - Simplifying the BFF with ProxyKit - ASP.NET Core: From 0 to overkill 31) Episode 030 - Analyzing performance with BenchmarkDotNet - ASP.NET Core: From 0 to overkill 32) Episode 031 - Some simple unit tests with xUnit - ASP.NET Core: From 0 to overkill 33) Episode 032 - Upgrading to ASP.NET Core 3.0 - ASP.NET Core: From 0 to overkill 34) E033 - Redesigning the API: Improving the internal architecture - ASPF02O 35) E034 - Segregating use cases with MediatR - ASPF02O 36) E035 - Experimenting with (yet) another approach to data access organization - ASPF02O 37) E036 - Making things more object oriented with rich domain entities - ASPF02O 38) Better use of types - avoiding nulls with an Optional type - ASPF02O|E037 39) More explicit domain error handling and fewer exceptions with Either and Error types [ASPF02O|E038] 40) Event-driven integration - Overview [ASPF02O|E039] 41) Event-driven integration #1 - Intro to the transactional outbox pattern [ASPF02O|E040] 42) Event-driven integration #2 - Inferring events from EF Core changes [ASPF02O|E041] 43) Event-driven integration #3 - Storing events in the outbox table [ASPF02O|E042] 44) Event-driven integration #4 - Outbox publisher (feat. IHostedService & Channels) [ASPF02O|E043] 45) Event-driven integration #5 - Quick intro to Apache Kafka [ASPF02O|E044]

Posted on by:

Discussion

markdown guide