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.
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>();
}
});
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());
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.