Customizing ASP.NET Core MVC: filters, constraints and conventions

ASP.NET Core – much like the not core predecessor – has many extension possibilities. In this post I’ll cover three of those possibilities that can be useful for application-wide and/or cross-cutting customization:

  • Filters (action filters).
  • Action constraints
  • Application model conventions.

This post is a condensed/revisited version of two posts I did last year about the application model on ASP.NET Core (part I and part II), which by that time was still on beta versions. If you are up to a more in-depth description, take a look at those two posts, as most of them still applies.

Action filters

ASP.NET Core MVC includes multiple types of filters, namely authorization, action and exception filters. All the filters extend IFilterMetadata, which is a marker interface used by the framework to discover filters applied as custom attributes.

The filters are executed within the so called action invocation pipeline or filter pipeline, which runs after MVC selects an action method to handle the request. Different types of filters execute at different stages in the pipeline, hence having different intended usages The official documentation has a decent guide on selecting the appropriate filter type. For this post I’ll stick with action filters.

Actions filters execute immediately before and/or after the action method. They have access to the results of model binding and action parameters (before the action executes) as well as the action result (after the action executes). These filters are represented by the IActionResult interface but the framework includes an utility base class for attribute action filters named ActionFilterAttribute.

A common scenario on action methods (namely Web APIs) is to check if the model state is valid and, if not, return a 400 status code. This is a rather cross-cutting concern and can be easily implemented via an action filter. Using the aforementioned ActionFilterAttribute base class, we can  write a filter that checks the model state validation status before the action method is executed:

public class ValidateModelState : ActionFilterAttribute
{
  public override void OnActionExecuting(ActionExecutingContext context)
  {
    if (!context.ModelState.IsValid)
    {
        context.Result = new BadRequestResult();
    }
  }
}

By setting the Result property during the OnActionExecuting method we prevent the execution of the action method and subsequent action filters. The filter can then be applied to an action method (or a controller class) just like any other custom attribute:

[HttpPost, ValidateModelState]
public IActionResult Test([FromBody] Model model)
{
  // ...
}

Action constraints

An important part of request handling is determining which action method should be executed. This might depend on different factors, such as the request’s HTTP method. Action constraints provide a means of determining if an action is suitable for a given request. They are represented by the IActionConstraint interface, depicted below. The context parameter can be used to access the route data, the HttpContext and the current candidate actions.

public interface IActionConstraint : IActionConstraintMetadata
{
    int Order { get; }
    bool Accept(ActionConstraintContext context);
}

As an example, lets say I want to have an API endpoint that accepts POST requests with two different media types. Instead of if/else on a single action method, we can add an action constraint based on the request’s content type. Here’s how I’d like to write my actions:

[HttpPost, MediaType("application/json")]
public void MyAction(MyModel model) { }

[HttpPost, MediaType("image/png")]
public void MyAction(Stream content) { }

Implementing MediaTypeAttribute  is pretty straight forward: the framework already includes a base class for action constraints implemented as attributes, which we can use in this case:

  public class MediaTypeAttribute : ActionMethodSelectorAttribute
  {
      private readonly string _mediaType;

      public MediaTypeAttribute(string mediaType)
      {
          this._mediaType = mediaType;
      }

      public override bool IsValidForRequest(RouteContext routeContext, ActionDescriptor action)
      {
          return routeContext.HttpContext.Request.ContentType.Equals(_mediaType, StringComparison.OrdinalIgnoreCase);
      }
  }

Application model conventions

If you’re familiar with previous versions of ASP.NET MVC you probably have heard about action descriptors. Action descriptors are discovered by MVC during application startup and represent the HTTP endpoints on the application. As with many other items on MVC, action descriptors are obtained via their own set of providers. While it is possible to add/replace IActionDescriptorProviders, the default implementation includes an intermediate mechanism to enable customization of action descriptors on most scenarios: the ApplicationModel.

The application model is a representation of the application components discovered by the framework, namely controllers and their actions. As illustrated below, this model includes a lot of metadata on the different components. Note that the aforementioned filters and action constraints are also part of the application model.

mvc-appmodel

After the whole model is discovered by the framework, we’re given a chance to customize it before the final action descriptors are created. This customizations are applied using application model conventions:

public interface IApplicationModelConvention
{
    void Apply(ApplicationModel application);
}

On previous versions of Web API, one could prefix action methods with the HTTP method name to make them apply to that method (e.g. GetNNNN, PostNNNN). This convention is not present on ASP.NET Core MVC, but the behavior is easy to mimic using an application model convention.

public class HttpMethodActionPrefixConvention : IApplicationModelConvention
{
    public void Apply(ApplicationModel application)
    {
        var actions = application.Controllers.SelectMany(c => c.Actions);
        foreach (var action in actions)
        {
            if (action.ActionName.StartsWith("get", StringComparison.OrdinalIgnoreCase))
            {
                var selector = action.Selectors.First();
                selector.ActionConstraints.Add(new HttpMethodActionConstraint(new[] { HttpMethod.Get.Method }));
                action.ActionName = action.ActionName.Substring(3);
            }
            // ...
        }
    }
}

In this example, for each action that was collected into the application model I’m checking if its name starts with an HTTP method. If so, a constraint is added to the ActionModel and the final action name is adjusted. This way, if we have a GetData action it will be constrained to HTTP GET and the action name throughout the application (e.g. route matching) will be Data. Finally, to use the convention we need to add it to MVC options in the Startup class:

services.AddMvc(options =>
{
    options.Conventions.Add(new HttpMethodActionPrefixConvention());
});

As a final note, one might ask why the application model was introduced, instead of having the developers directly modify action descriptors. The application model provides a customization model that is closer to how a developer organizes the application (e.g. controllers that contain actions). The final action descriptors are more complex and there’s a lot of plumbing to be done to create them that probably shouldn’t surface to the developer. For instance, a single ActionModel might result on multiple ActionDescriptors, depending on attribute routing. The application model is simpler and easier to modify, leaving all the final hard work to the framework.

This post ended up quite long, but it made sense to me to describe the three mechanisms together. Hope this helps!