Reputation: 124766
I'm using Microsoft.Extensions.DependencyInjection 2.1.1 and have services that use the Options pattern to get their configuration. I want to be able to select a concrete implementation of a service using information known only at runtime (e.g. read from configuration).
If I know all the possible concrete service implementations and their options at compile-time, I can do something like the following to select and configure an implementation:
if (useImplementation1)
{
services.Configure<MyServiceImplementation1Options>(config.GetSection("MyServiceImplementation1"));
services.AddSingleton<IMyService, MyServiceImplementation1>();
}
else
{
services.Configure<MyServiceImplementation2Options>(config.GetSection("MyServiceImplementation2"));
services.AddSingleton<IMyService, MyServiceImplementation2>();
}
Is there a way to configure this service and its options using only information known at runtime, e.g.:
Type myServiceOptionsType = ... from configuration, e.g. typeof(MyServiceImplementation1Options)
Type myServiceImplementationType = ... from configuration, e.g. typeof(MyServiceImplementation1)
string myServiceConfigSection = ... from configuration, e.g. "MyServiceImplementation1"
??? what do I do next?
UPDATE
Which I hope will clarify what I'm looking for. Here are sample classes: assume Implementation1 gets data from an XML file, Implementation2 gets data from an SQL database.
Implementation1 code (in assembly MyAssembly):
public class MyServiceImplementation1Options
{
public Uri MyXmlUrl {get; set;}
}
public class MyServiceImplementation1 : IMyService
{
public MyServiceImplementation1(IOptions<MyServiceImplementation1Options> options)
{
...
}
... Implement IMyService ...
}
Implementation2 code (in assembly OtherAssembly):
public class MyServiceImplementation2Options
{
public string ConnectionString {get; set;}
public string ProviderName {get; set;}
}
public class MyServiceImplementation2 : IMyService
{
public MyServiceImplementation2(IOptions<MyServiceImplementation2Options> options)
{
...
}
... Implement IMyService ...
}
Now I'd like to choose between these two implementations without necessarily having compile-time access to the assemblies (MyAssembly and OtherAssembly) that contain the implementations. At runtime I'd read data from a configuration file, that might look something like (in the following, think of Keys and Values as a Dictionary passed to a MemoryConfigurationProvider
- hierarchical configuration is represented using colon separators. It could also be configured using appsettings.json
with hierarchy represented using nesting):
Implementation1 configuration:
Key="MyServiceConcreteType" Value="MyServiceImplementation1,MyAssembly"
Key="MyServiceOptionsConcreteType" Value="MyServiceImplementation1Options,MyAssembly"
Key="MyServiceOptionsConfigSection" Value="MyServiceImplementation1"
Key="MyServiceImplementation1:MyXmlUrl" Value="c:\MyPath\File.xml"
Implementation2 configuration:
Key="MyServiceConcreteType" Value="MyServiceImplementation2,OtherAssembly"
Key="MyServiceOptionsConcreteType" Value="MyServiceImplementation2Options,OtherAssembly"
Key="MyServiceOptionsConfigSection" Value="MyServiceImplementation2"
Key="MyServiceImplementation2:ConnectionString" Value="Server=...etc..."
Key="MyServiceImplementation2:ProviderName" Value="System.Data.SqlClient"
Upvotes: 1
Views: 1609
Reputation: 172826
As @weichch mentioned, the main pain point here is the lack of the existence of a non-generic Configure
overload. I think this is can be seen as an omission on the part of Microsoft's (but creating a feature request for this would be a good idea).
Besides, weichch's solution, you can also make use of reflection to call the Configure<T>
method of your choosing. This would look like this:
// Load configuration
var appSettings = this.Configuration.GetSection("AppSettings");
Type serviceType =
Type.GetType(appSettings.GetValue<string>("MyServiceConcreteType"), true);
Type optionsType =
Type.GetType(appSettings.GetValue<string>("MyServiceOptionsConcreteType"), true);
string section = appSettings.GetValue<string>("MyServiceOptionsConfigSection");
// Register late-bound component
services.AddSingleton(typeof(IMyService), serviceType);
// Configure the component.
// Gets a Confige<{optionsType}>(IServiceCollection, IConfiguration) method and invoke it.
var configureMethod =
typeof(OptionsConfigurationServiceCollectionExtensions).GetMethods()
.Single(m => m.GetParameters().Length == 2)
.MakeGenericMethod(typeof(string));
configureMethod.Invoke(
null, new object[] { services, this.Configuration.GetSection("AppSettings") });
Where the configuration might look as follows:
{
"AppSettings": {
"MyServiceConcreteType": "MyServiceImplementation1,MyAssembly",
"MyServiceOptionsConcreteType": "MyServiceImplementation1Options,MyAssembly",
"MyServiceOptionsConfigSection": "MyServiceImplementation1",
},
"MyServiceImplementation1": {
"SomeConfigValue": false
}
}
Upvotes: 1
Reputation: 10055
OK, now I see where the confusion is. Because the Configure
method doesn't have non-generic versions, so you didn't know how to pass type known at runtime to the method?
In this case, I would use ConfigureOptions
method which allows you to pass a configurator type as parameter. The type must implement IConfigureOptions<T>
which defines Configure(T)
method to configure an options of T.
For example, this type configures MyServiceImplementation1Options
using IConfiguration
:
class ConfigureMyServiceImplementation1 :
IConfigureOptions<MyServiceImplementation1Options>
{
public ConfigureMyServiceImplementation1(IConfiguration config)
{
}
public void Configure(MyServiceImplementation1Options options)
{
// Configure MyServiceImplementation1Options as per configuration section
}
}
MyServiceImplementation1Options.Configure
method is invoked while resolving IOptions<MyServiceImplementation1Options>
, and you could inject IConfiguration
to the type to read configuration from specified section.
And you could use the type like this in Startup:
// Assume you read this from configuration
var optionsType = typeof(MyServiceImplementation1Options);
// Assume you read this type from configuration
// Or somehow could find this type by options type, via reflection etc
var configureOptionsType = typeof(ConfigureMyServiceImplementation1);
// Assume you read this type from configuration
var implementationType = typeof(MyServiceImplementation1);
// Configure options using ConfigureOptions instead of Configure
// By doing this, options is configure by calling
// e.g. ConfigureMyServiceImplementation1.Configure
services.ConfigureOptions(configureOptionsType);
In terms of service registration, there are non-generic versions of Add*
methods. For example, the code below registers type known at runtime as IMyService
:
// Register service
services.AddSingleton(typeof(IMyService), implementationType);
Upvotes: 2
Reputation: 7202
You can select a concrete implementation of an interface based on an options type by using the AddSingleton(implementationFactory)
overload. By using this overload, you can delay resolving which concrete type to use until after you can access an IOptionsSnapshot<>
. Depending on your requirements and the implementations of the interface you're using, you could switch the type returned dynamically by using an IOptionsMonitor<>
instead.
If it helps, you can think of the factory pattern as a way to curry object creation when using the DI container.
class Program
{
static void Main()
{
// WebHostBuilder should work similarly.
var hostBuilder = new HostBuilder()
.ConfigureAppConfiguration(cfgBuilder =>
{
// Bind to your configuration as you see fit.
cfgBuilder.AddInMemoryCollection(new[]
{
KeyValuePair.Create("ImplementationTypeName", "SomeFooImplementation"),
});
})
.ConfigureServices((hostContext, services) =>
{
// Register IFoo implementation however you see fit.
// See below for automatic registration helper.
services
.AddSingleton<FooFactory>()
// Registers options type for FooFactory
.Configure<FooConfiguration>(hostContext.Configuration)
// Adds an IFoo provider that uses FooFactory.
// Notice that we pass the IServiceProvider to FooFactory.Get
.AddSingleton<IFoo>(
sp => sp.GetRequiredService<FooFactory>().Get(sp));
});
IHost host = hostBuilder.Build();
IFoo foo = host.Services.GetRequiredService<IFoo>();
Debug.Assert(foo is SomeFooImplementation);
}
}
// The interface that we want to pick different concrete
// implementations based on a value in an options type.
public interface IFoo
{
public string Value { get; }
}
// The configuration of which type to use.
public class FooConfiguration
{
public string ImplementationTypeName { get; set; } = string.Empty;
}
// Factory for IFoo instances. Used to delay resolving which concrete
// IFoo implementation is used until after all services have been
// registered, including configuring option types.
public class FooFactory
{
// The type of the concrete implementation of IFoo
private readonly Type _implementationType;
public FooFactory(IOptionsSnapshot<FooConfiguration> options)
{
_implementationType = ResolveTypeNameToType(
options.Value.ImplementationTypeName);
}
// Gets the requested implementation type from the provided service
// provider.
public IFoo Get(IServiceProvider sp)
{
return (IFoo)sp.GetRequiredService(_implementationType);
}
private static Type ResolveTypeNameToType(string typeFullName)
{
IEnumerable<Type> loadedTypes = Enumerable.SelectMany(
AppDomain.CurrentDomain.GetAssemblies(),
assembly => assembly.GetTypes());
List<Type> matchingTypes = loadedTypes
.Where(type => type.FullName == typeFullName)
.ToList();
if (matchingTypes.Count == 0)
{
throw new Exception($"Cannot find any type with full name {typeFullName}.");
}
else if (matchingTypes.Count > 1)
{
throw new Exception($"Multiple types matched full name {typeFullName}.");
}
// TODO: add check that requested type implements IFoo
return matchingTypes[0];
}
}
You also asked how to resolve the concrete implementation type based on options.
When using the Microsoft.Extensions.DependencyInjection
container, you have to add all the types to it before building it. However, you cannot access options until after building the container. These two conflict with each other, and I've not found a suitable work around.
One work around that has caused problems for me: build the service provider while populating the service collection. Which objects live in which service provider gets confusing here, and the timing of when the service provider is built heavily changes the results. This approach causes hard to debug problems. I'd avoid it.
If you can make a simplifying assumption that all the possible concrete implementation types are in assemblies that are already loaded, then you could consider an "auto-registration" scheme. This means that you don't have to add a AddSingleton<>
&c. for each new type.
// when configuring services, add a call to AddAutoRegisterTypes()
// ...
.ConfigureServices((hostContext, services) =>
{
services
// Finds and registers config & the type for all types with [AutoRegister]
.AddAutoRegisterTypes(hostContext.Configuration)
// ...
});
// ...
// The first concrete implementation. See below for how AutoRegister
// is used & implemented.
[AutoRegister(optionsType: typeof(FooFirstOptions), configSection: "Foo1")]
public class FooFirst : IFoo
{
public FooFirst(IOptionsSnapshot<FooFirstOptions> options)
{
Value = $"{options.Value.ValuePrefix}First";
}
public string Value { get; }
}
public class FooFirstOptions
{
public string ValuePrefix { get; set; } = string.Empty;
}
// The second concrete implementation. See below for how AutoRegister
// is used & implemented.
[AutoRegister(optionsType: typeof(FooSecondOptions), configSection: "Foo2")]
public class FooSecond : IFoo
{
public FooSecond(IOptionsSnapshot<FooSecondOptions> options)
{
Value = $"Second{options.Value.ValueSuffix}";
}
public string Value { get; }
}
public class FooSecondOptions
{
public string ValueSuffix { get; set; } = string.Empty;
}
// Attribute used to annotate a type that should be:
// 1. automatically added to a service collection and
// 2. have its corresponding options type configured to bind against
// the specificed config section.
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
public class AutoRegisterAttribute : Attribute
{
public AutoRegisterAttribute(Type optionsType, string configSection)
{
OptionsType = optionsType;
ConfigSection = configSection;
}
public Type OptionsType { get; }
public string ConfigSection { get; }
}
public static class AutoRegisterServiceCollectionExtensions
{
// Helper to call Configure<T> given a Type argument. See below for more details.
private static readonly Action<Type, IServiceCollection, IConfiguration> s_configureType
= MakeConfigureOfTypeConfig();
// Automatically finds all types with [AutoRegister] and adds
// them to the service collection and configures their options
// type against the provided config.
public static IServiceCollection AddAutoRegisterTypes(
this IServiceCollection services,
IConfiguration config)
{
foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies())
{
foreach (Type type in assembly.GetTypes())
{
var autoRegAttribute = (AutoRegisterAttribute?)Attribute
.GetCustomAttributes(type)
.SingleOrDefault(attr => attr is AutoRegisterAttribute);
if (autoRegAttribute != null)
{
IConfiguration configForType = config.GetSection(
autoRegAttribute.ConfigSection);
s_configureType(
autoRegAttribute.OptionsType,
services,
configForType);
services.AddSingleton(type);
}
}
}
return services;
}
// There is no non-generic analog to
// OptionsConfigurationServiceCollectionExtensions.Configure<T>(IServiceCollection, IConfiguration)
//
// Therefore, this finds the generic method via reflection and
// creates a wrapper that invokes it given a Type parameter.
private static Action<Type, IServiceCollection, IConfiguration> MakeConfigureOfTypeConfig()
{
const string FullMethodName = nameof(OptionsConfigurationServiceCollectionExtensions) + "." + nameof(OptionsConfigurationServiceCollectionExtensions.Configure);
MethodInfo? configureMethodInfo = typeof(OptionsConfigurationServiceCollectionExtensions)
.GetMethod(
nameof(OptionsConfigurationServiceCollectionExtensions.Configure),
new[] { typeof(IServiceCollection), typeof(IConfiguration) });
if (configureMethodInfo == null)
{
var msg = $"Cannot find expected {FullMethodName} overload. Has the contract changed?";
throw new Exception(msg);
}
if ( !configureMethodInfo.IsGenericMethod
|| configureMethodInfo.GetGenericArguments().Length != 1)
{
var msg = $"{FullMethodName} does not have the expected generic arguments.";
throw new Exception(msg);
}
return (Type typeToConfigure, IServiceCollection services, IConfiguration configuration) =>
{
configureMethodInfo
.MakeGenericMethod(typeToConfigure)
.Invoke(null, new object[] { services, configuration });
};
}
}
If you really need to populate the DI container at runtime with dynamic assembly loading, you can
ConfigureServices
, Bind
to your option type,I don't have an example for this. My experience has been that the automatic registration approach is enough.
This code was written and tested with .NET SDK 3.1.101 targeting netcoreapp2.1
and using version 2.1.1 of the "Microsoft.Extensions.Configuration.*" packages. I've posted a complete working copy as a GitHub Gist.
The use of reflection in FooFactory
and AddAutoRegisterTypes
assumes that these aren't called frequently. If you're just using these once at start up for a long-lived service it should be fine.
The assembly searching in AddAutoRegisterTypes
can slow down as a program gets large. There are some ways to speed it up, like only checking assemblies with a known naming pattern.
While all of this works, I'd love to know if there's a more elegant way to do something like this. The auto-registration scheme can be a bit too magical, but the config scheme, I feel, duplicates a lot of what the options system provides.
Upvotes: 1