Skip to content

Instantly share code, notes, and snippets.

@chwarr
Last active April 11, 2020 22:57
Show Gist options
  • Save chwarr/e53e2f370b91e9e03f2ae445c6300882 to your computer and use it in GitHub Desktop.
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
<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>
// 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