Search code examples
c#.net-coredependency-injection

How to dynamically inject a service based on the output of another service


I have 2 possible implementations of an interface, and in my Program.cs I want to dynamically inject the right one. However, the logic of which one to inject depends on another service being used. Previously in I was doing this:

services.AddScoped<IOtherService, OtherService>();
var serviceProvider = services.BuildServiceProvider();
var otherService = serviceProvider.GetRequiredService<IOtherService>();
if (otherService.SomeValue == "Value1")
{
    services.AddScoped<IDynamicService, DynamicService1>();
}
else if (otherService.SomeValue == "Value2")
{
    services.AddScoped<IDynamicService, DynamicService2>();
}

This gives a warning that BuildServiceProvider() shouldn't be called like this because it could result in an additional copy of singleton services being created. I found several answers on the Internet (example) saying that the solution is to use the Options Pattern and AddOptions().Configure(), but I can't quite understand how that works in my case.

Using this example:

services.AddOptions<JwtBearerOptions>(JwtBearerDefaults.AuthenticationScheme)
.Configure<IManageJwtAuthentication>((opts,jwtAuthManager)=>{
    opts.TokenValidationParameters = new TokenValidationParameters
    {
        AudienceValidator = jwtAuthManager.AudienceValidator,
        // More code here...
    };
});

This seems to sets up some configuration details on the injected IManageJwtAuthentication. But I don't need to just configure some property on my injected service, I need to change what service is being injected. I don't see how I can do something like the example above unless this Configure() method could also be passed my IServiceCollection so that I can call services.AddScoped() from within that context. In a few different attempts at similar things, I wasn't ever seeing my .Configure() method called at all.

Am I right that Options Pattern is the way to go for something like this? I don't quite understand the relationship between Options Pattern and accessing other services from within my Program.cs; what I've read suggests that Options Pattern is a way to load and inject subsets of Configuration/settings.


Solution

  • How about using implementation factory? Use overload for service registration method that supply IServiceProvider over func:

    services.AddScoped<DynamicService1>();
    services.AddScoped<DynamicService2>();
    services.AddScoped<IDynamicService>(sp =>
    {
        var evaluationDependency = sp.GetRequiredService<IDependencyEvaluationService>();
        var value = evaluationDependency.GetEvaluationValue();
    
        if (value == "Value1")
        {
            return sp.GetRequiredService<DynamicService1>();
        }
        else
        {
            return sp.GetRequiredService<DynamicService2>();
        }
    });
    

    Caching the evaluation result

    Quite a downside to approach above is that evaluation is performed every time IDynamicService is requested. So, if evaluation is a time costly process, say some IO operation, we can always cache the result. At that point, it's probably better to use a dedicated factory anyway:

    public interface IDynamicServiceFactory
    {
        IDynamicService CreateDynamicService();
    }
    
    public class DynamicServiceFactory : IDynamicServiceFactory
    {
        private readonly IDependencyEvaluationService dependencyEvaluationService;
        private readonly IDependency1Service dependency1Service;
        private readonly IDependency2Service dependency2Service;
    
        public DynamicServiceFactory(
            IDependencyEvaluationService dependencyEvaluationService,
            IDependency1Service dependency1Service,
            IDependency2Service dependency2Service)
        {
            this.dependencyEvaluationService = dependencyEvaluationService;
            this.dependency1Service = dependency1Service;
            this.dependency2Service = dependency2Service;
        }
    
        public IDynamicService CreateDynamicService()
        {
            var value = dependencyEvaluationService.GetEvaluationValue();
            if (value == "Value1")
            {
                return new DynamicService1(dependency1Service);
            }
            else
            {
                return new DynamicService2(dependency2Service);
            }
        }
    }
    

    service getting the value (presumably long-running operation):

    public interface IDependencyEvaluationService
    {
        string GetEvaluationValue();
    }
    
    public class DependencyEvaluationService : IDependencyEvaluationService
    {
        private readonly IDependencyEvaluationStore dependencyEvaluationStore;
    
        public DependencyEvaluationService(IDependencyEvaluationStore dependencyEvaluationStore)
        {
            this.dependencyEvaluationStore = dependencyEvaluationStore;
        }
    
        public string GetEvaluationValue()
        {
            if (dependencyEvaluationStore.TryGetValue(out var value))
            {
                return value;
            }
    
            // Some long operation.
            value = "value2";
    
            dependencyEvaluationStore.SetValue(value);
    
            return value;
        }
    }
    

    and store which holds the result value:

    public interface IDependencyEvaluationStore
    {
        bool TryGetValue(out string value);
    
        void SetValue(string value);
    }
    
    public class DependencyEvaluationInMemoryStore : IDependencyEvaluationStore
    {
        private static readonly object _lock = new ();
    
        private string _value;
        private bool _hasValueSet;
    
        public bool TryGetValue(out string value)
        {
            if (_hasValueSet)
            {
                value = _value;
                return true;
            }
            else
            {
                value = null;
                return false;
            }
        }
    
        public void SetValue(string value)
        {
            lock (_lock)
            {
                _value = value;
                _hasValueSet = true;
            }
        }
    }
    

    More services overall, but simplified registration:

    services.AddScoped<IDependencyEvaluationService, DependencyEvaluationService>();
    services.AddTransient<IDynamicServiceFactory, DynamicServiceFactory>();
    services.AddSingleton<IDependencyEvaluationStore, DependencyEvaluationInMemoryStore>();
    services.AddScoped(sp => sp.GetRequiredService<IDynamicServiceFactory>().CreateDynamicService());
    

    Final words

    Both of these approaches have a weakness that it becomes a pain to maintain correct dependencies needed to create service implementations. While it is not that big of a hassle for simple scenarios, it can become burdensome for more complex services. I'd advise straight away injecting factory and resolving correct implementation at the caller site, if possible, of course.