Luke T O'Brien
Luke T O'Brien

Reputation: 2845

How to allow for optional services with Microsoft.Extension.DependencyInjection?

I am playing around with ASP.NET Core on my own hobby project, I want to create a framework that will be consumed by a developer, and I want to allow optional service and use defaults if they are not registered.

I am getting the Unable to resolve service for type 'XXX' error, but I would prefer the DI to return null rather then throw an exception. I want to allow for optional services, so if a service is found, use that in the constructor, if not found, pass null into the constructor.

In my implementation I have:

public IServiceManager(IService service, ...)
{
    _service = service ?? new DefaultService();
    ...
}

So as you can see, if the service cannot be found (null) use the default. Perhaps I am misunderstanding how DI works. Perhaps I could use a factory to do this instead? However, in my system I using default services when non is provided will be a common occurrence, so I need a solution that doesn't require the consumer of the API to register a service.

Is there a way to configure ASP.NET Core DI to return null rather then throw an exception?

Upvotes: 28

Views: 18529

Answers (4)

Skrymsli
Skrymsli

Reputation: 5313

I was a little unclear on how the top answer worked, so I made a test. The output is:

Output of this code

As the top answer implies, without = null in the Bob() constructor you get a DI error.

using Microsoft.Extensions.DependencyInjection;

namespace ditest
{
    internal class Program
    {
        static void Main(string[] args)
        {
            var services = new ServiceCollection();
            services.AddScoped<INamed, Bob>();

            var sp = services.BuildServiceProvider();
            var instance = sp.GetRequiredService<INamed>();
            Console.WriteLine(instance.Name);

            services.AddSingleton(new LastName());
            sp = services.BuildServiceProvider();
            instance = sp.GetRequiredService<INamed>();
            Console.WriteLine(instance.Name);
        }
    }

    public interface INamed
    {
        string Name { get; }
    }

    public class Bob : INamed
    {
        private readonly LastName? _lastNameProvider;
        public Bob(LastName? nameProvider = null)
        {
            _lastNameProvider = nameProvider;
        }
        public string Name => $"My name is Bob {_lastNameProvider?.Name}";
    }

    public class LastName
    {
        public string Name => "Dobalina";
    }
}

Upvotes: 0

Tseng
Tseng

Reputation: 64298

By their very nature, constructor injection is always considered as mandatory.

The very first versions of the Microsoft DI (I don't like using the term ASP.NET Core DI, because it does not depend on ASP.NET Core and can be used outside of it) only supported the constructor with the most parameters.

I think this has been changed since then to allow multiple constructors and the IoC container will choose a fitting one. That being said, you'd likely need to define multiple constructors.

public IServiceManager(IService service, IOtherService otherService)
{
}

public IServiceManager(IOtherService otherService)
{
}

Then the second constructor should be called, if IService isn't registered with the IoC container.

But it's still quite a questionable practice at best and makes your code harder to maintain and hold its invariant/loose coupling.

You should never have to instantiate your types inside your services, not even for optional services.

Instead, you should provide registrations which allow a user to override them with their own implementations.

public static IServiceCollection AddMyLibrary(this IServiceCollection services)
{
    services.TryAddTransient<IService, Service>();
    services.TryAddTransient<IOtherService, OtherService>();
}

Then the user override it.

services.AddTransient<IService, CustomService>();
services.AddMyLibrary();

Now CustomService will be injected where IService is requested.

Upvotes: 24

user3636971
user3636971

Reputation: 234

Easiest would be to register the DefaultService component itself for the IService service within your IoC container - I'm using the terminology of Castle Windsor. Most of the containers allow to register multiple components for a service. In case you do not register a custom component for the service (another implementation of IService), DefaultService will be resolved and injected; otherwise your custom component will be resolved for the service, just register the components in proper order (in Castle Windsor, the component registered first will be considered: multiple components for a service)

WindsorContainer container = new WindsorContainer();

container.Register(Component.For<IServiceManager>().ImplementedBy<ServiceManager>());
container.Register(Component.For<IService>().ImplementedBy<CustomService>());
container.Register(Component.For<IService>().ImplementedBy<DefaultService>());

IServiceManager serviceManager = container.Resolve<IServiceManager>();
IService service = ((ServiceManager)serviceManager).Service; // service is of type CustomService

Regarding the comment below from @Tseng:

This beats the idea of having Dependency Injection / IoC container in the firstplace, when you instantiate it inside the constructor

It is not always the case... If you have an optional dependency, first, define it as a property with a public setter, so component can be injected if registered. In case there is no component registered (thus property is not set by the container), I think it can be acceptable to instantiate the default component via the "dangerous" new keyword. Everything is context-dependent - to be clear, I wouldn't instantiate a service manually, but there are always exceptions.

Upvotes: 1

laika
laika

Reputation: 1497

Add default value to that parameter in the constructor.

public IServiceManager(IService service = null, ...)
{
  _service = service ?? new DefaultService();
  ...
}

Upvotes: 53

Related Questions