ASP.NET Core MVC Application Model – Part II

EDIT 03/08/2016: this post was originally written for ASP.NET MVC 6 beta versions. While most of the post and its general idea still applies to ASP:NET Core MVC, some details may not be valid anymore and links might be broken.


On the previous post of this series I introduced the ApplicationModel, its main types and how it can be used to customize the final action descriptors. On this post I’ll go into a couple code samples to make everything clearer.

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 MVC Core, 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"))
            {
                action.HttpMethods.Add("GET");
                action.ActionName = action.ActionName.Substring(3);
            }
            // …
        }
    }
}

As I mentioned on the previous post, a convention is a type that implements IApplicationModelConvention. In the example above, I’m iterating all the actions that were collected into the application model and checking if their names start with an HTTP method. If so, that HTTP method is added to the ActionModel HTTP methods 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, the convention is added to MVC options on the Startup class:

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvcCore(options =>
    {
        options.Conventions.Add(new HttpMethodActionPrefixConvention());
    });
}

Besides conventions, one might also use the different types that are considered by the framework when building the application model (e.g. IActionConstraintMetadata, IFilterMetadata). For this post I’ll stick with IActionConstraintMetadata which is a means of determining if an action is suitable for a given HTTP request.

Recalling the previous post, the default application model provider searches whether attributes applied to action methods implement specific types, namely IActionConstraintMetadata. However, this is a marker interface, meaning there must be something more to it. Looking into ActionDescriptor we can see that it still has a collection of IActionConstraintMetadata. If we keep going up we’ll end-up on action selection. At this point, the IActionConstraintMetadatas are processed to obtain IActionConstraints, which actually determine if an action is suitable for a request, as illustrated below. The context provides access to things such as the HttpContext, the candidate ActionDescriptors for the current request and the ActionDescriptor being evaluated.

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

Taking another look at the DefaultActionSelector one can see that IActionConstraintMetadatas are processed by – guess what? – another set of providers. As with other providers so far, there’s a default implementation that should suite all our needs. Here an excerpt from its source code:

var constraint = item.Metadata as IActionConstraint;
if (constraint != null)
{
    item.Constraint = constraint;
    return;
}

var factory = item.Metadata as IActionConstraintFactory;
if (factory != null)
{
    item.Constraint = factory.CreateInstance(services);
    return;
}

This means that, by default, for a given IActionConstraintMetadata there are two options:

  1. It is an IActionConstraint – The instance can be used directly. This is good enough for most action constraints, since the Accept method gets contextual information.
  2. It is an IActionConstraintFactory – The factory is invoked passing the services locator. Note that a IActionConstraintMetadata is typically a custom attribute, which means it has limited flexibility on construction. If the constraint you’re implementing depends on external services, you can use the factory approach to create it.

At this point, all that’s left is an example. Lets say we 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 attributes that are action constraints, 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 == this.mediaType;
    }
}

And that’s it! On these two posts I covered a lot about application model, how it’s built and how we can use it to customize the final action descriptors on our MVC 6 applications. Hope this helps!

Advertisements

One thought on “ASP.NET Core MVC Application Model – Part II

  1. Pingback: Customizing ASP.NET Core MVC: filters, constraints and conventions | Luís Gonçalves

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s