5

Trying to do some DI on Web API 2 without third-party tools.
So, from some examples I've got custom dependency resolver (why there's no integrated one? Strange, even Microsoft.Extensions.DependencyInjection provides nothing):

public class DependencyResolver : IDependencyResolver
    {
        protected IServiceProvider _serviceProvider;

        public DependencyResolver(IServiceProvider serviceProvider)
        {
            this._serviceProvider = serviceProvider;
        }

        public IDependencyScope BeginScope()
        {
            return this;
        }

        public void Dispose()
        {

        }

        public object GetService(Type serviceType)
        {
            return this._serviceProvider.GetService(serviceType);
        }

        public IEnumerable<object> GetServices(Type serviceType)
        {
            return this._serviceProvider.GetServices(serviceType);
        }

        public void AddService()
        {

        }
    }

then created this class:

public class ServiceConfig
    {
        public static void Register(HttpConfiguration config)
        {
            var services = new ServiceCollection();
            services.AddScoped<IMyService, MyServiceClient>();

            var resolver = new DependencyResolver(services.BuildServiceProvider());
            config.DependencyResolver = resolver;
        }

    }

and registered it:

protected void Application_Start()
        {
            GlobalConfiguration.Configure(WebApiConfig.Register);
            GlobalConfiguration.Configure(ServiceConfig.Register);
        }

But when I'm trying to use it:

public class TestController : ApiController
    {
        private IMyService _myService = null;

        public TestController(IMyService myService)
        {
            _myService = myService;
        }

        public void Get()
        {
            _myService.DoWork();
        }
}

I'm getting error:

An error occurred when trying to create a controller of type 'TestController'. Make sure that the controller has a parameterless public constructor.

How to cook this one in right way?

Steven
  • 166,672
  • 24
  • 332
  • 435
Troll the Legacy
  • 675
  • 2
  • 7
  • 22

1 Answers1

11

What you see happening is related to this problem. In short, Web API will call its default IHttpControllerActivator implementation to request a new controller instance. That instance will call into your DependencyResolver.GetService method. That method will forward the call to MS.DI's GetService method. However, since you didn't register your controllers into the MS.DI container, it will return null. This will cause the default IHttpControllerActivator to try to create the controller using reflection, but this requires a default constructor. Since the controller doesn't have one, this results in the rather cryptic exception message.

The quick solution, therefore, is to register your controllers, e.g.:

services.AddTransient<TestController>();

This, however, will only partly solve your problem because your IDependencyResolver implementation is broken. It is broken in an ugly way, because it might seem to work at first, but will result in memory leaks, because you always resolve from the root container, instead of resolving from a scope. This will cause your resolved controller instances (and other disposable transient components) to stay referenced for the lifetime of your application.

To fix this, you should change your IDependencyResolver implementation to the following:

public class DependencyResolver : IDependencyResolver
{
    private readonly IServiceProvider provider;
    private readonly IServiceScope scope;

    public DependencyResolver(ServiceProvider provider) => this.provider = provider;

    internal DependencyResolver(IServiceScope scope)
    {
        this.provider = scope.ServiceProvider;
        this.scope = scope;
    }

    public IDependencyScope BeginScope() =>
        new DependencyResolver(provider.CreateScope());

    public object GetService(Type serviceType) => provider.GetService(serviceType);
    public IEnumerable<object> GetServices(Type type) => provider.GetServices(type);
    public void Dispose() => scope?.Dispose();
}

This implementation will ensure a new IServiceScope is created on each web request and services are always resolved from a request; not from the root IServiceProvider.

Although this will fix your problems, another implementation might still be benificial.

The IDependencyResolver contract is problematic, because it is forced to return null when a call to GetService doesn't result in the correct resolution of a registration. This means that you will end up with these annoying "Make sure that the controller has a parameterless public constructor" errors when you forget to register your controllers.

It is, therefore, much easier to create a custom IHttpControllerActivator instead. In that case you can call GetRequiredService which will never return null:

public class MsDiHttpControllerActivator : IHttpControllerActivator
{
    private readonly ServiceProvider provider;

    public MsDiHttpControllerActivator(ServiceProvider provider) =>
        this.provider = provider;

    public IHttpController Create(
        HttpRequestMessage request, HttpControllerDescriptor d, Type controllerType)
    {
        IServiceScope scope = this.provider.CreateScope();
        request.RegisterForDispose(scope); // disposes scope when request ends
        return (IHttpController)scope.ServiceProvider.GetRequiredService(controllerType);
    }
}

This MsDiHttpControllerActivator implementation can be added to the Web API pipeline as follows:

GlobalConfiguration.Configuration.Services
  .Replace(typeof(IHttpControllerActivator),
    new MsDiHttpControllerActivator(services.BuildServiceProvider(true)));

This removes the need to have an IDependencyResolver implementation. You still need to register your controllers, though:

services.AddTransient<TestController>();

Also note that I changed this:

services.BuildServiceProvider()

To this:

services.BuildServiceProvider(true)

This is a really important change; it protects you (for some part) against Captive Dependencies, which are one of the major problems when using DI Containers. For some obscure reason, the BuildServiceProvider() overload defaults to false, which means it will not validate your scopes.

Steven
  • 166,672
  • 24
  • 332
  • 435
  • Thank you for detailed answer. At first I've tried simply to copy-pase your code to project - `MsDiHttpControllerActivator` instead of custom resolver and `GlobalConfiguration.Configuration.Services .Replace...` in my *ServiceConfig.Register*, and while all compiles good, I'm now getting `No service for type 'ManagementApp.Controllers.TestController' has been registered.`. Maybe there's a little misunderstanding? I'm trying to do some DI with WCF services in WebAPI project - maybe that's making sense? `MyService` is a WCF, yes. – Troll the Legacy Apr 04 '19 at 12:46
  • Hmm, but it works when I'm adding `services.AddTransient();`. It's not looks as good and clean solution, so what do you think about that? – Troll the Legacy Apr 04 '19 at 12:49
  • I think you should apply auto-registration (a.k.a. assembly scanning). This prevents you from having to _explicitly_ register each controller. You can do this by reflecting over the assembly to find all controller types and register them in a foreach loop. Or, alternatively, you can pick on of the mature, and feature-rich containers (Autofac, Simple Injector, Castle Windsor, etc), because they _all_ contain features that can do this for you (and much more) out of the box. – Steven Apr 04 '19 at 12:59
  • Yes, I've done in my `ServiceConfig` this https://pastebin.com/jtkdSX0E and call it like `services.RegisterApiControllers()` and all works, at least for now. Also I've replaced `GlobalConfiguration.Configuration` with `HttpConfiguration config` argument, which provided by `GlobalConfiguration.Configure` in *Global.asax*. – Troll the Legacy Apr 04 '19 at 13:41