ASP.NET Core MVC 3.x – AddMvc(), AddMvcCore(), AddControllers() and other bootstrapping approaches

There are several ways of bootstrapping your MVC applications on top of ASP.NET Core 3.x. One thing that you need to do, in order to use the framework, is to initialize it into the state where it can actually discover your controllers, views, pages and expose them as HTTP endpoints.

I've recently had some conversations with folks about that, and it occurred to me that this is not necessarily all that obvious to everyone. That's because there are a few ways of doing that, so let's quickly run through them.

The past and the present

In .NET Core 2.x and earlier, you could register the MVC framework in the ASP.NET Core dependency injection container in two ways:

  • services.AddMvc()
  • services.AddMvcCore()

In ASP.NET Core 3.x, aside from those two approaches, there are three additional ways:

  • services.AddControllers()
  • services.AddControllersWithViews()
  • services.AddRazorPages()

Let's discuss the differences between them, starting from the most “lightweight” to the most “heavyweight”.

AddMvcCore()

AddMvcCore() registers all the core services required for the MVC application to work at all. We do not need to list them all, but pretty much everything related to the controller invocation pipeline gets activated there. These are low(er) level services, that only get customized when you are doing something quite complex or unusual (i.e. building a CMS). Some examples of them are: the controller activation services, the MVC options pipeline, application model provider infrastructure, action constraints, filter pipeline, model binder infrastructure, action result executors and a few more.

At the same time, the initialized framework configuration is completely “bare bones”. It is functional from the perspective of being able to handle an incoming HTTP call, but it is missing several core features. For example, the model validation via data annotations is no activated, same with authorization.

In this set-up, you are in control of (or, if you will, you are responsible for) what is plugged in and used at runtime. In other words, if you need anything beyond the most basic framework feature, you have to add it manually. In fact, in .NET Core 2.x and earlier, not even JSON support was there; this has now changed and the System.Text.Json formatter is actually already included in the call to AddMvcCore().

For example:

This should be the default choice for you if you really like to bootstrap the minimal amount of things at runtime and only activate the individual features you really use.

AddControllers()

AddControllers() was introduced in ASP.NET Core 3.0 as a mechanism that would simplify the manual setup needed together with calling the lightweight AddMvcCore().

What you get with AddControllers() is:

  • everything that AddMvcCore() does
  • authorization services – needed for authorization policies, filters and other authorization components to work
  • API explorer – required if you want to build dynamic API documentation, generate Swagger/OpenAPI files
  • data annotations – needed for model validation with attributes and IValidateableObject to work
  • formatter mappings – needed for content negotiation to work
  • CORS

In other words, what you can expect from AddControllers() is that it would give you the most comfortable setup for API development. None of the view services are registered here so you don't “drag” any of the Razor related baggage with you. What's worth noting is that the name itself – AddControllers() – sort of blurs the line between the ASP.NET Core and the MVC framework, as it doesn't really tell you at first glance that you are activating the MVC framework.

This should be the default choice for you if are developing an API and want to quickly and reliably bootstrap the framework.

AddControllersWithViews()

AddControllersWithViews() is the one you should pick if you are building a “classic” MVC site, just like we have been doing it for years – with controllers and Razor views. It will end up activating:

  • everything that AddControllers() does
  • views functionality – explicitly registers the Razor view engine
  • cache tag helper

This should be the default choice for you if you do not need the new Razor pages functionality – you are either building the MVC website exactly how it was built in old desktop framework MVC and in earlier versions of ASP.NET Core MVC or if you are migrating an older site.

AddRazorPages()

AddRazorPages() is intended to serve as a bootstrapping helper for working with the new Razor Pages feature. Under the hood, it ends up activating the following:

  • all the core Razor pages features
  • everything that AddMvcCore() does – this is a bit surprising at first glance
  • authorization services – needed for authorization policies, filters and other authorization components to work
  • data annotations – needed for model validation to work
  • cache tag helper

The fact that it ends up activating AddMvcCore() is an internal implementation detail, since the Razor Pages pipeline is relying on a lot of the core MVC infrastructure under the hood. As a side effect, it means that when calling AddRazorPages() you are sort of ready to do API endpoints too. This may change in the future, and therefore I wouldn't take strategic decisions based on that. In other words, even though we could now say:

if you ever need to host an API and Razor Pages in same project, I'd rather recommend to make these activations explicit, so that you don't get surprised in the future when something changes internally:

Of course the AddRazorPages() should be your default choice if you plan to work with Razor Pages.

AddMvc()

Finally, we have AddMvc(), which simply registers the entire kitchen sink of all the features. It gives you:

  • everything that AddControllersWithViews() does
  • everything that AddRazorPages() does

While I'd imagine you know what you are trying to build – if you ever have any doubts in which direction your project will evolve, or if you are afraid that some MVC feature would be missing (or in fact, if you already ran into a missing feature), calling AddMvc() would be the safest bet to resolve any of those worries or issues.