Last active
April 11, 2020 22:57
-
-
Save chwarr/e53e2f370b91e9e03f2ae445c6300882 to your computer and use it in GitHub Desktop.
Demonstrates how to select a concrete implementation at runtime based on option type values using Microsoft.Extensions.DependencyInjection. Also show automatic registration helper. Answer for https://stackoverflow.com/questions/61156955/ms-di-how-to-configure-services-using-information-known-only-at-runtime
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<Project Sdk="Microsoft.NET.Sdk"> | |
<!-- | |
Licensed under CC BY-SA 4.0. | |
Attribution is required. | |
https://creativecommons.org/licenses/by-sa/4.0/ | |
Answer for | |
https://stackoverflow.com/questions/61156955/ms-di-how-to-configure-services-using-information-known-only-at-runtime | |
The following 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. | |
--> | |
<PropertyGroup> | |
<OutputType>Exe</OutputType> | |
<TargetFramework>netcoreapp2.1</TargetFramework> | |
<LangVersion>latest</LangVersion> | |
<Nullable>enable</Nullable> | |
</PropertyGroup> | |
<ItemGroup> | |
<PackageReference Include="Microsoft.Extensions.Configuration" Version="2.1.1" /> | |
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="2.1.1" /> | |
<PackageReference Include="Microsoft.Extensions.Hosting" Version="2.1.1" /> | |
<PackageReference Include="Microsoft.Extensions.Options" Version="2.1.1" /> | |
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="2.1.1" /> | |
</ItemGroup> | |
</Project> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Copyright 2020, G. Christopher Warrington | |
// | |
// Licensed under CC BY-SA 4.0. | |
// Attribution is required. | |
// https://creativecommons.org/licenses/by-sa/4.0/ | |
// | |
// Answer for | |
// https://stackoverflow.com/questions/61156955/ms-di-how-to-configure-services-using-information-known-only-at-runtime | |
// | |
// The following 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. | |
using System; | |
using System.Collections.Generic; | |
using System.Diagnostics; | |
using System.Linq; | |
using System.Reflection; | |
using Microsoft.Extensions.Configuration; | |
using Microsoft.Extensions.DependencyInjection; | |
using Microsoft.Extensions.Hosting; | |
using Microsoft.Extensions.Options; | |
#nullable enable | |
class Program | |
{ | |
static void Main() | |
{ | |
var hostBuilder = new HostBuilder() | |
.ConfigureAppConfiguration(cfgBuilder => | |
{ | |
// Bind to your configuration as you see fit. | |
cfgBuilder.AddInMemoryCollection(new[] | |
{ | |
KeyValuePair.Create("ImplementationTypeName", nameof(FooSecond)), | |
KeyValuePair.Create("Foo1:ValuePrefix", "PrefixFromConfig_"), | |
KeyValuePair.Create("Foo2:ValueSuffix", "_SuffixFromConfig"), | |
}); | |
}) | |
.ConfigureServices((hostContext, services) => | |
{ | |
services | |
// Finds and registers config & the type for all types | |
// with [AutoRegister] | |
.AddAutoRegisterTypes(hostContext.Configuration) | |
// Adds a factory that can provide IFoo instances | |
.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.Value == "Second_SuffixFromConfig"); | |
} | |
} | |
// 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; } = nameof(FooFirst); | |
} | |
// 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]; | |
} | |
} | |
// 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 }); | |
}; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment