Real World ASP.NET MVC Action Filters

Action filters are probably one the most under-appreciated and underutilized features of ASP.NET MVC. Since version 1.0, the IActionFilter interface served as a way embed code directly into the MVC pipeline, in an aspect oriented programming sort of way.

Implementations of the IActionFilter interface serve as perfect candidates for cross cutting concerns, such as validation, transaction management, and exception handling. Unfortunately, most of this logic either ends up in a custom controller base class or other model classes.

1x6r4w

Here are some examples of IActionFilter implementations I have used in production environments.

Remove Code Duplication

On this action method, I am using model binding to build a CreateViewModel object from the submitted form values.

public IActionResult Create(CreateViewModel model)
{
    ...
}
public class CreateViewModel
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Email { get; set; }
    public string Phone { get; set; }
    public int CurrentUserId { get; set; }
}

When I looked at the controllers in my application, I noticed that they were using the same logic to set the CurrentUserId on view models. How can we use action filters to remove this code duplication?

public interface ICurrentUserRequired
{
    public int CurrentUserId { get; set; }
}

We will use the ICurrentUserRequired interface to identify the view models that require the CurrentUserId. Here is the updated view model:

public class CreateViewModel : ICurrentUserRequired
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Email { get; set; }
    public string Phone { get; set; }
    public int CurrentUserId { get; set; }
}

Here is the action filter that will automatically set the CurrentUserId:

public class CurrentUserRequiredActionFilter : IActionFilter
{
    public void OnActionExecuted(ActionExecutedContext filterContext) { }

    public void OnActionExecuting(ActionExecutingContext filterContext)
    {
        (1)
        var items =
            filterContext.ActionParameters.Values
                .OfType<ICurrentUserRequired>()
                .ToArray();

        if (items.Any())
        {
            (2)
            var identity = 
                filterContext.HttpContext.User.Identity as ClaimsIdentity;
                 
            var userId = identity.GetUserId();
             
            (3)
            foreach(var item in items)
            {
                item.CurrentUserId = userId;
            }
        }
    }
}

This action filter does the following:

  1. Get all action method parameters that implement the ICurrentUserRequired interface.
  2. Retrieve the UserId from our ClaimsIdentity.
  3. Set the CurrentUserId for each action method parameter from step #1.

I have used a similar approach to set client information in multi-tenant scenarios.

public interface IClientRequired
{
    Guid ClientId { get; }
}

In this action filter, I retrieve the ClientId from a identity claim.

public class ClientRequiredActionFilter : IActionFilter
{
    void IActionFilter.OnActionExecuted(ActionExecutedContext filterContext) { }
    void IActionFilter.OnActionExecuting(ActionExecutingContext filterContext)
    {
        var items =
            filterContext.ActionParameters.Values
                .OfType<IClientRequired>()
                .ToArray();
                
        if (items.Any())
        {
            var clientClaim =
                (filterContext.HttpContext.User.Identity as ClaimsIdentity).Claims
                    .FirstOrDefault(t => t.Type == ClaimTypeNames.Client);
                
            Guid clientKey;
            if (clientClaim != null && Guid.TryParse(clientClaim.Value, out clientKey))
            {
                foreach (var item in items)
                {
                    item.ClientKey = clientKey;
                }
            }
        }
    }
}

The last step is to register these action filters in the global filter repository. In ASP.NET MVC 5, we do that in the FilterConfig.cs.

 public class FilterConfig
 {
     public static void RegisterGlobalFilters(GlobalFilterCollection filters)
     {
         ...
         filters.Add(new ClientRequiredActionFilter());
         filters.Add(new CurrentUserRequiredActionFilter());
         ...
     }
 }

In ASP.NET MVC Core, global filter registration is done in the ConfigureServices method of Startup.cs.

 public IServiceProvider ConfigureServices(IServiceCollection services)
 {
     ...
     services
         .AddMvc(options =>
         {
             options.Filters.Add(new ClientRequiredActionFilter());
             options.Filters.Add(new CurrentUserRequiredActionFilter());
         });
     ...
 }

Transaction Management

In the post, Manage Transactions in ASP.NET Core Razor Pages 2.0, I discussed how Razor Pages 2.0 did not support action filters. Here is the original action filter that was converted to an IPageFilter:

public class AppDbContextTransactionFilter : IAsyncActionFilter
{
    private readonly AppDbContext context;
    
    public AppDbContextTransactionFilter(AppDbContext context)
    {
        this.context = context;
    }
    
    async Task IAsyncActionFilter.OnActionExecutionAsync(
        ActionExecutingContext t, 
        ActionExecutionDelegate next)
    {
        try
        {
            context.BeginTransaction();
            await next();
            await context.CommitTransactionAsync();
        }
        catch (Exception)
        {
            context.RollbackTransaction();
            throw;
        }
    }
}

In closing, Steve Smith has an excellent MSDN article on Real-World ASP.NET Core MVC Filters. Although his article targets ASP.NET Core, most of these can be modified to work on ASP.NET MVC 5.

Enjoy!

Show Comments