Exploring the ApiControllerAttribute and its features for ASP.NET Core MVC 2.1

Β· 993 words Β· 5 minutes to read

ASP.NET Core MVC 2.1 will ship with a nice little feature aimed specifically at people building HTTP APIs - ApiControllerAttribute. While the stable release of 2.1 is not yet here, we can already have a look behind the scenes - what is this feature doing, and how can it help you write Web APIs.

ApiControllerAttribute : ControllerAttribute πŸ”—

ASP.NET Core MVC already had an attribute called ControllerAttribute. It was used to annotate a given type as a controller candidate, so that the framework would use the type’s methods to discover potential endpoints (the framework has other ways of controller discovery too so the attribute was not mandatory).

ApiControllerAttribute is in fact a subclass of ControllerAttribute, so using it on a type forces the type to participate in the controller discovery process the same way.

However, it also brings in a couple of extra features with it, and that’s due to a fact that the ApiControllerAttribute additionally implements an interface called IApiBehaviorMetadata. For each type annotated with IApiBehaviorMetadata, the framework will provide some additional, HTTP API-centric features. Those are discussed here next.

Automatic model state validation πŸ”—

This is a big one. The framework will automatically validate model state for you. This is something that has been a problem in various ASP.NET-based frameworks from Microsoft since, well, forever. You always had to manually, and if you have been reading this blog I have blogged about that in the past.

What the framework is going to automatically do for you in this case, it would register a ModelStateInvalidFilter for you, which runs during OnActionExecuting event (so right before you action runs, but after model binding). It will internally check if the ModelState is valid, and produce an immediate 400 Bad Request response (short circuit) to the caller.

The way the response is constructed is the model state is simply stuffed into the response, and the content type assigned to it is application/problem+json. You have a chance to customize it though, because you could register your own validation delagate (more on this later).

Overall, the feature means that the following code:

[Route("[controller]")]
public class BookController : Controller
{
    [HttpPost("")]
    public IActionResult PostBook([FromBody]Book book)
    {
        if (ModelState.IsValid) 
        {
            return BadRequest(ModelState);
        }
        
        // omitted for brevity
    }
}

Can now be simplified as this snippet has the exact same behavior (automatic validation of model state):

[ApiController]
[Route("[controller]")]
public class BookController : Controller
{
    [HttpPost("")]
    public IActionResult PostBook(Book book)
    {
        // omitted from brevity
    }
}

One thing worth adding is that ModelStateInvalidFilter is a public class and could be freely used in your MVC configuration (for example inserted as global filter against MvcOptions) even without opting in to use ApiControllerAttribute.

Automatic inferring of parameter binding strategies πŸ”—

Another very useful feature for Web API development is that various parameters of your action will automatically infer their binding model.

One of the biggest annoyances of ASP.NET Core MVC was that you had to manually annotate complex parameters of your actions with [FromBody] attribute, to support deserializing them from request bodies - so for example JSON payloads. As a result, various third party solutions have been created to address this. This is now handled automatically.

In addition to this, if the parameter exists in the route, it will be automatically inferred as being bound from the path, otherwise it will be assumed to be a query string parameter. IFormFile parameters will be automatically bound from form.

So this is no longer needed:

[Route("[controller]")]
public class BookController : Controller
{
    [HttpPost("")]
    public IActionResult PostBook([FromBody]Book book)
    {
        // omitted from brevity
    }
}

Because this (finally!) works out of the box:

[ApiController]
[Route("[controller]")]
public class BookController : Controller
{
    [HttpPost("")]
    public IActionResult PostBook(Book book)
    {
        // omitted from brevity
    }
}

Consuming multipart/form-data requests πŸ”—

If a parameter on your action is annotated with [FormFile] attribute (typically done for file uploads), the framework will now automatically assume that the endpoint consumes multipart/form-data requests. This saves you the trouble of having to do it manually.

This can be opted out from if you define your own [Consumes(…)] attribute on the action. In such cases, your value takes precedence.

Other πŸ”—

Few other small notes:

  1. ApiExplorer visibility - each controller is implicitly available in ApiExplorer. This means it will automatically participate in all features built on top of ApiExplorer, such as for example Swagger generation.
  2. Attribute routing only - centralized routing mechanism does not apply to API controllers. The framework requires you to only use attribute routing.

Customizing the behavior πŸ”—

As with most things in the MVC framework, the out of the box behavior of the ApiControllerAttribute is highly customizable. First of all, pretty much all of the above described main features can be toggled on/off.

This is done at application startup, through ApiBehaviorOptions. The options type looks as follows:

    public class ApiBehaviorOptions
    {
        public Func<ActionContext, IActionResult> InvalidModelStateResponseFactory { get; set; }

        public bool SuppressModelStateInvalidFilter { get; set; }

        public bool SuppressInferBindingSourcesForParameters { get; set; }

        public bool SuppressConsumesConstraintForFormFileParameters { get; set; }
    }

All of the boolean flags are by default set to false. You can then configure those as you wish, for example:

services.Configure<ApiBehaviorOptions>(options =>
{
    options.SuppressModelStateInvalidFilter = true;
    options.SuppressConsumesConstraintForFormFileParameters = true;
});

Note that the options configuration also allows you to inject your own model validation delegate, where you are given the current ActionContext and are supposed to produce your own IActionResult. An example of that is shown below, where we still rely on the ModelState, but project onto our own custom response type:

services.Configure<ApiBehaviorOptions>(options =>
{
    options.InvalidModelStateResponseFactory = actionContext => 
    {
        var errors = actionContext.ModelState
            .Where(e => e.Value.Errors.Count > 0)
            .Select(e => new Error
            {
            Name = e.Key,
            Message = e.Value.Errors.First().ErrorMessage
            }).ToArray();

        return new BadRequestObjectResult(errors);
    }
});

class Error
{
    public string Name { get; set; }

    public string Message { get; set; }
}

Availability of ASP.NET Core 2.1 πŸ”—

According to the team, the preview builds should start coming this month, and the final stable release will happen in the first half of 2018.

About


Hi! I'm Filip W., a cloud architect from ZΓΌrich πŸ‡¨πŸ‡­. I like Toronto Maple Leafs πŸ‡¨πŸ‡¦, Rancid and quantum computing. Oh, and I love the Lowlands 🏴󠁧󠁒󠁳󠁣󠁴󠁿.

You can find me on Github, on Mastodon and on Bluesky.

My Introduction to Quantum Computing with Q# and QDK book
Microsoft MVP