simpleinjector / SimpleInjector

An easy, flexible, and fast Dependency Injection library that promotes best practice to steer developers towards the pit of success.
https://simpleinjector.org
MIT License
1.22k stars 152 forks source link

How to conditionally register a service based on existence of another service #908

Closed LadislavBohm closed 3 years ago

LadislavBohm commented 3 years ago

I am trying to register an implementation based on the fact that another specific implementation already exists in a container. If not I want to register another implementation.


public class ServiceAOptions {}
public class ServiceBOptions {}

public interface IService {}

public class ServiceA {
    public ServiceA(ServiceAOptions options) {}
}

public class ServiceB {
    public ServiceB(ServiceBOptions options) {}
}

So basically the question is how to register ServiceA only if ServiceAOptions is registered within the container and register ServiceB only if ServiceBOptions are registered.

Real-life use case is that I have a code that conditionally registers IOptions<OptionsA/B> based on their existence in appsettings.json. With a few helpers that I wrote I can have a code like this:


services
        .ConfigureIfExists<AwsS3Options>(Configuration.GetSection("AwsS3"))
        .OtherwiseIfExists<AzureBlobStorageOptions>(Configuration.GetSection("AzureBlobStorage"))
        .OtherwiseThrow("Could not find any file storage configuration");

My registration using AutoFac would look like this:

builder.RegisterType<AwsS3FileStorage>()
        .OnlyIf(registryBuilder => registryBuilder.IsRegistered(new TypedService(typeof(AwsS3Options))))
        .As<IFileStorage>()
        .InstancePerLifetimeScope();
builder.RegisterType<AzureBlobStorage>()
        .OnlyIf(registryBuilder => registryBuilder.IsRegistered(new TypedService(typeof(AzureBlobStorageOptions))))
        .As<IFileStorage>()
        .InstancePerLifetimeScope();

As you can see I can just change appsettings and my app works with either of the two services. I don't seem to be able to achieve the same result with RegisterConditional or any of the overloads.

dotnetjunkie commented 3 years ago

I'm not sure I fully understand the underlying problem. Can you help me with this by answering my following questions?

I am trying to register an implementation based on the fact that another specific implementation already exists in a container.

What prevents you from making all registrations, even if they aren't used? For instance:

var awsSection = Configuration.GetSection("AwsS3");
var blobSection = Configuration.GetSection("AzureBlobStorage");
container.RegisterInstance(new AwsS3Options(...));
container.RegisterInstance(new AzureBlobStorageOptions(...));

Type storageImplementation = awsSection != null
    ? typeof(AwsS3FileStorage)
    : (blobSection != null ? typeof(AzureBlobStorage) : throw new Exception( "No storage config"));

container.Register(typeof(IFileStorage), storageImplementation, Lifestyle.Scoped);

Would a construct such as follows work for you? And if not, can you explain why it wouldn't work?

var awsSection = Configuration.GetSection("AwsS3");
var blobSection = Configuration.GetSection("AzureBlobStorage");

if (awsSection != null)
{
    container.RegisterInstance(new AwsS3Options(...));
    container.Register<IFileStorage, AwsS3FileStorage>(Lifestyle.Scoped);
}
else if (blobSection != null)
{
    container.RegisterInstance(new AzureBlobStorageOptions(...));
    container.Register<IFileStorage, AzureBlobStorage>(Lifestyle.Scoped);
}
else
{
    throw new ConfigurationErrorsException(
        "Could not find any file storage configuration");
}

By answering these questions you'll give me a better understanding of your problem, which hopefully allows me to give you the answer that best suits your needs.

LadislavBohm commented 3 years ago

Thank you for a great answer.

After looking at your code the only reason why I am not able to use it as you suggest is that services (AwsS3FileStorage, AzureBlobStorage) get registered in a different assembly (specifically in SI Package). While options (AwsS3Options, AzureBlobStorageOptions) get registered in a client type of project/assembly (like an API).

Although after reading what I just wrote above I think that what I am doing is probably not correct. The assembly that contains configurations (appsettings) should decide what gets registered and thus I should probably move the service registration there.

dotnetjunkie commented 3 years ago

The assembly that contains configurations (appsettings) should decide what gets registered and thus I should probably move the service registration there.

That certainly sounds like a good idea. You typically want to keep all registrations inside the Composition Root.

If, however, you need to probe the container for existing registrations, you can do so by calling Container.GetRegistration(Type, false).

LadislavBohm commented 3 years ago

Thank you again, I modified my code and used Container.GetRegistration. I am now able to get exactly what I initially wanted and with a bit cleaner approach.