Some time ago I wrote a blog about how ASP.NET MVC 6 discovers controllers. While a lot has change since then, including the name - now the framework being called ASP.NET Core MVC, the post is still valid and the processes described there haven’t really changed.
That said, there is one extra thing that should be added to it, and that is how external dependency injection containers relate to the process of controller discovery and instantiation, as there is a subtle difference between ASP.NET Core MVC and the “classic” frameworks - MVC 5 or Web API 2. This post is really sparked by the conversation on Twitter with Jeremy and Kristian.
Who creates instances of the controllers? π
So in the “classic” frameworks - MVC 5 and Web API 2, you’d plug in an external container by registering a custom IDependencyResolver, which is an adapter between the framework and a 3rd party DI container. Once that’s registered, the framework’s default controller activator would always try to create an instance of the controller from the container, and only fallback to creating the instance manually (using a type activator service).
That default behavior is not preserved in ASP.NET Core MVC and can lead to some confusion. Consider the following snippet of code, which wires in Autofac (using Autofac.Extensions.DependencyInjection package from NuGet) into an ASP.NET Core application:
public IServiceProvider ConfigureServices(IServiceCollection services)
{
services.AddMvc();
var builder = new ContainerBuilder();
// register your services against Autofac here
builder.RegisterType<FooService>().As<IFooService>();
builder.Populate(services);
var container = builder.Build();
return container.Resolve<IServiceProvider>();
}
In the above listing, we are configuring IServiceProvider to be resolved from Autofac container (effectively setting up an Autofac service adapter). It is then used through the framework to resolve dependencies for controllers and is also exposed on the HttpContext for any other use cases as (ugh) a service locator.
So we can now do this:
[Route("api/[controller]")]
public class ValuesController : Controller
{
private readonly IFooService _service;
public ValuesController(IFooService service)
{
_service = service;
}
[HttpGet]
public IActionResult Get()
{
// use _service here
}
}
When the framework (via a service called DefaultControllerActivator) will create an instance of a controller, it will resolve all of its constructor dependencies from the IServiceProvider - which in our case will be an Autofac specific one. However, the subtle difference between this behavior and what we are used to from Web API 2 and MVC 5, is that the controller itself will not be attempted to be resolved from the container, only its constructor parameters.
Why is this important? Let’s go back to the Autofac set up code, and make it a bit more interesting - and leverage some of the more advanced features of Autofac such as property injection (forget the religious discussion whether property injection is good or bad, it’s just an example of trying to use a container-specific feature).
So our set up code now looks like this:
public IServiceProvider ConfigureServices(IServiceCollection services)
{
services.AddMvc();
var builder = new ContainerBuilder();
// register your services against Autofac here
builder.RegisterType<FooService>().As<IFooService>();
// find and register all controllers from the current assembly
// and autowire their properties
builder.RegisterAssemblyTypes(typeof (ValuesController).GetTypeInfo().Assembly)
.Where(
t =>
typeof (Controller).IsAssignableFrom(t) &&
t.Name.EndsWith("Controller", StringComparison.Ordinal)).PropertiesAutowired();
builder.Populate(services);
var container = builder.Build();
return container.Resolve<IServiceProvider>();
}
As a result, since we now support properties injection, our controller can be modified to look like this:
[Route("api/[controller]")]
public class ValuesController : Controller
{
public IFooService FooService { get; set; }
[HttpGet]
public IActionResult Get()
{
// use FooService here
}
}
Makes sense, right? Unfortunately, the problem is that this code is not going to work here. When you run this API, the request is gonna hit the action, but the FooService will be null - even though we explicitly instructed Autofac to use property injection.
The reason for this is something that we already explained. While controller’s constructor dependencies would be resolved by MVC from the IServiceProvider (so in our case an Autofac adapter), the instance of the controller itself (and its disposal too) is created and owned by the framework, not by the container.
Making MVC resolve controllers from the container π
So in order to fix this, you can do two things. You can use an extension method to force MVC to treat “controllers as services”, meaning that MVC will now attempt to resolve controllers from the IServiceProvider, and also let the container managed their lifetime scope.
This can be done by changing our Autofac set up to (key change is to use AddControllersAsServices method) the following:
public IServiceProvider ConfigureServices(IServiceCollection services)
{
services.AddMvc().AddControllersAsServices(new[] {typeof(ValuesController).GetTypeInfo().Assembly });
var builder = new ContainerBuilder();
builder.RegisterType<FooService>().As<IFooService>();
builder.Populate(services);
var container = builder.Build();
return container.Resolve<IServiceProvider>();
}
It is kind of gonna work, but only partially. After this change, the controllers are indeed going to be resolved from Autofac, but as you probably noticed, we lost the possibility to configure any Autofac specific features against them, so we lost the property injection again. Our above change relies on calling AddControllersAsServices which internally does 3 things:
- sets up a fixed-set StaticControllerTypeProvider based on what we have passed into AddControllersAsServices
- replaces the DefaultControllerActivator with ServiceBasedControllerActivator (meaning the framework will now attempt to resolve controller instances from IServiceProvider)
- registers every single discovered controller instance into services collection (so into Autofac container in our case)
So we are almost there - but if we want to use some specific DI container features against our controllers, you can rewrite the code into this:
public IServiceProvider ConfigureServices(IServiceCollection services)
{
var assemblyProvider = new StaticAssemblyProvider();
assemblyProvider.CandidateAssemblies.Add(typeof (ValuesController).GetTypeInfo().Assembly);
var controllerTypeProvider = new DefaultControllerTypeProvider(assemblyProvider);
var controllerTypes = controllerTypeProvider.ControllerTypes.Select(t => Type.GetType(t.FullName)).ToArray();
services.Replace(ServiceDescriptor.Transient<IControllerActivator, ServiceBasedControllerActivator>());
services.Replace(ServiceDescriptor.Singleton<IControllerTypeProvider>(provider => controllerTypeProvider));
services.Replace(ServiceDescriptor.Singleton<IAssemblyProvider>(provider => assemblyProvider));
services.AddMvc();
var builder = new ContainerBuilder();
builder.RegisterType<FooService>().As<IFooService>();
builder.RegisterTypes(controllerTypes).PropertiesAutowired();
builder.Populate(services);
var container = builder.Build();
return container.Resolve<IServiceProvider>();
}
So let’s recap what we did here - we manually created a StaticAssemblyProvider and pointed it to the assembly from which we wanna discover controllers. We then used DefaultControllerTypeProvider to discover them for us (to avoid hardcoding any controller discovery logic). Once we have these two services, we register them as singleton services, in place of the default ones. Then we replace the the DefaultControllerActivator with ServiceBasedControllerActivator so that MVC will start resolving the controllers from Autofac.
Finally, we use the controller types discovered by DefaultControllerTypeProvider and register them in Autofac manually - additionally enabling property injection (or, at this point, any other advanced feature of a DI container we’d like to use).
And that’s it, we now have controllers from a DI container, leveraging any container specific feature, and we have not broken any MVC internals in the process.