Removing the MVC Razor dependencies from the Web API template in ASP.NET Core

In this article I'll show how to add the minimal required dependencies to create an ASP.NET Core Web API project, without including the additional MVC/Razor functionality and packages.

Note: I have since created a custom dotnet new template for the code described in this post, plus a few extra features. You can read about it here, or view it on GitHub and NuGet.

MVC vs Web API

In the previous version of ASP.NET, the MVC and Web API stacks were completely separate. Even though there were many similar concepts shared between them, the actual types were distinct. This was generally a little awkward, and often resulted in confusing error messages when you accidentally references the wrong namespace.

In ASP.NET Core, this is no longer an issue - MVC and Web API have been unified under the auspices of ASP.NET Core MVC, in which there is fundamentally no real difference between an MVC controller and a Web API controller. All of your controllers can act both as MVC controllers, serving server-side rendered Razor templates, and as Web API controllers returning formatted (e.g. JSON or XML) data.

This unification is great and definitely reduces the mental overhead required when working with both previously. Even if you are not using both aspects in a single application, the fact the types are all familiar is just a smoother experience.

Having said that, if you only need to use the Web API features (e.g. you're building an API client without any server-side rendering requirements), then you may not want/need the additional MVC capabilities in your app. Currently, the default templates include these by default.

The default templates

When you create a new MVC project from a template in Visual Studio or via the command line, you can choose whether to create an empty ASP.NET Core project, a Web API project or an MVC web app project:

If you create an 'empty' project, then the resulting app really is super-lightweight. It has no dependencies on any MVC constructs, and just produces a very simple 'Hello World' response when run:

At the other end of the scale, the 'MVC web app' gives you a more 'complete' application. Depending on the authentication options you select, this could include ASP.NET Core Identity, EF Core, and SQL server integration, in addition to all the MVC configuration and Razor view templating:

In between these two templates is the Web API template. This includes the necessary MVC dependencies for creating a Web API, and the simplest version just includes a single example ValuesController:

However, while this looks stripped back, it also adds all the necessary packages for creating full MVC applications too, i.e. the server-side Razor packages. This is because it includes the same Microsoft.AspNetCore.Mvc package that the full MVC web app does, and calls AddMvc() in Startup.ConfigureServices.

As described in Steve Gordon's post on the AddMvc function, this adds a bunch of various services to the service collection. Some of these are required to allow you to use Web API, but some of them - the Razor-related services in particular - are unnecessary for a web API.

In most cases, using the Microsoft.AspNetCore.Mvc package is the easiest thing to do, but sometimes you want to trim your dependencies as much as possible, and make your APIs as lightweight as you can. In those cases you may find it useful to specifically add only the MVC packages and services you need for your app.

Adding the package dependencies

We'll start with the 'Empty' web application template, and add the packages necessary for Web API to it.

The exact packages you will need will depend on what features you need in your application. By default, the Empty ASP.NET Core template includes ApplicationInsights and the Microsoft.AspNetCore meta package, so I'll leave those in the project.

On top of those, I'll add the MVC.Core package, the JSON formatter package, and the CORS package:

  • The MVC Core package adds all the essential MVC types such as ControllerBase and RouteAttribute, as well as a host of dependencies such as Microsoft.AspNetCore.Mvc.Abstractions and Microsoft.AspNetCore.Authorization.
  • The JSON formatter package ensures we can actually render our Web API action results
  • The CORS package adds Cross Origin Resource Sharing (CORS) support - a common requirement for web APIs that will be hosted on a different domain to the client calling them.

    The final .csproj file should look something like this:

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>netcoreapp1.1</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.ApplicationInsights.AspNetCore" Version="2.0.0" />
    <PackageReference Include="Microsoft.AspNetCore" Version="1.1.1" />
    <PackageReference Include="Microsoft.AspNetCore.Mvc.Core" Version="1.1.2" />
    <PackageReference Include="Microsoft.AspNetCore.Mvc.Formatters.Json" Version="1.1.2" />
    <PackageReference Include="Microsoft.AspNetCore.Mvc.Cors" Version="1.1.2" />
  </ItemGroup>

</Project>  

Once you've restored the packages, we can update Startup to add our Web API services.

Adding the necessary services to Startup.cs

In most cases, adding the Web API services to a project would be as simple as calling AddMvc() in your ConfigureServices method. However, that method adds a whole load of functionality that I don't currently need. by default, it would add the ApiExplorer, the Razor view engine, Razor views, tag helpers and DataAnnotations - none of which we are using at the moment (We might well want to add the ApiExplorer and DataAnnotations back at a later date, but right now, I don't need them).

Instead, I'm left with just the following services:

public void ConfigureServices(IServiceCollection services)  
{
    var builder = services.AddMvcCore();
    builder.AddAuthorization();
    builder.AddFormatterMappings();
    builder.AddJsonFormatters();
    builder.AddCors();
}

That's all the services we need for now - next stop, middleware.

Adding the MvcMiddleware

Adding the MvcMiddleware to the pipeline is simple. I just replace the "Hello World" run call with UseMvc(). Note that I'm using the unparameterised version of the method, which does not add any conventional routes to the application. As this is a web API, I will just be using attribute routing, so there's no need to setup the conventional routes.

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)  
{
    loggerFactory.AddConsole();

    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    app.UseMvc();
}

That's all the MVC configuration we need - the final step is to add a controller to show off our new API.

Adding an MVC Controller

There's one important caveat to be aware of when creating a web API in this way - you must use the ControllerBase class, not Controller. The latter is defined in the Microsoft.AspNetCore.Mvc package, which we haven't added. Luckily, it mostly contains methods related to rendering Razor, so it's not a problem for us here. The ControllerBase class includes all the various StatusCodeResult helper methods you will likely use, such as Ok used below.

[Route("api/[controller]")]
public class ValuesController : ControllerBase  
{
    // GET api/values
    [HttpGet]
    public IActionResult Get()
    {
        return Ok(new string[] { "value1", "value2" });
    }
}

And if we take it for a spin:

Voila! A stripped down web API controller, with minimal dependencies.

Bonus: AddWebApi extension method

As a final little piece of tidying up - our ConfigureServices call looks a bit messy now. Personally I'm a fan of the "Clean Startup.cs" approach espoused by K. Scott Allen, in which you reduce the clutter in your Startup.cs class by creating wrapper extension methods for your configuration.

We can do the same with our simplified web API project by adding an extension method called AddWebApi(). I've even created a parameterised overload that takes an Action<MvcOptions>, synonymous with the AddMvc() equivalent that you are likely already using.

using System;  
using Microsoft.AspNetCore.Mvc;  
using Microsoft.AspNetCore.Mvc.Internal;

// ReSharper disable once CheckNamespace
namespace Microsoft.Extensions.DependencyInjection  
{
    public static class WebApiServiceCollectionExtensions
    {
        /// <summary>
        /// Adds MVC services to the specified <see cref="IServiceCollection" /> for Web API.
        /// This is a slimmed down version of <see cref="MvcServiceCollectionExtensions.AddMvc"/>
        /// </summary>
        /// <param name="services">The <see cref="IServiceCollection" /> to add services to.</param>
        /// <returns>An <see cref="IMvcBuilder"/> that can be used to further configure the MVC services.</returns>
        public static IMvcBuilder AddWebApi(this IServiceCollection services)
        {
            if (services == null) throw new ArgumentNullException(nameof(services));

            var builder = services.AddMvcCore();
            builder.AddAuthorization();

            builder.AddFormatterMappings();

            // +10 order
            builder.AddJsonFormatters();

            builder.AddCors();

            return new MvcBuilder(builder.Services, builder.PartManager);
        }

        /// <summary>
        /// Adds MVC services to the specified <see cref="IServiceCollection" /> for Web API.
        /// This is a slimmed down version of <see cref="MvcServiceCollectionExtensions.AddMvc"/>
        /// </summary>
        /// <param name="services">The <see cref="IServiceCollection" /> to add services to.</param>
        /// <param name="setupAction">An <see cref="Action{MvcOptions}"/> to configure the provided <see cref="MvcOptions"/>.</param>
        /// <returns>An <see cref="IMvcBuilder"/> that can be used to further configure the MVC services.</returns>
        public static IMvcBuilder AddWebApi(this IServiceCollection services, Action<MvcOptions> setupAction)
        {
            if (services == null) throw new ArgumentNullException(nameof(services));
            if (setupAction == null) throw new ArgumentNullException(nameof(setupAction));

            var builder = services.AddWebApi();
            builder.Services.Configure(setupAction);

            return builder;
        }

    }
}

Finally, we can use this extension method to tidy up our ConfigureServices method:

public void ConfigureServices(IServiceCollection services)  
{
    services.AddWebApi();
}

Much better!

Summary

This post showed how you could trim the Razor dependencies from your application, when you know you are not going to need them. This represents pretty much the most bare-bones web API template you might use in your application. Obviously mileage may vary, but luckily adding extra capabilities (validation, ApiExplorer for example) is easy!