Reputation: 6158
In ASP.NET Core, one of the things you can do with Microsoft's dependency injection framework is bind "open generics" (generic types unbound to a concrete type) like so:
public void ConfigureServices(IServiceCollection services) {
services.AddSingleton(typeof(IRepository<>), typeof(Repository<>))
}
You can also employ the factory pattern to hydrate dependencies. Here's a contrived example:
public interface IFactory<out T> {
T Provide();
}
public void ConfigureServices(IServiceCollection services) {
services.AddTransient(typeof(IFactory<>), typeof(Factory<>));
services.AddSingleton(
typeof(IRepository<Foo>),
p => p.GetRequiredService<IFactory<IRepository<Foo>>().Provide()
);
}
However, I have not been able to figure out how to combine the two concepts together. It seems like it would start with something like this, but I need the concrete type that is being used to hydrate an instance of IRepository<>
.
public void ConfigureServices(IServiceCollection services) {
services.AddTransient(typeof(IFactory<>), typeof(Factory<>));
services.AddSingleton(
typeof(IRepository<>),
provider => {
// Say the IServiceProvider is trying to hydrate
// IRepository<Foo> when this lambda is invoked.
// In that case, I need access to a System.Type
// object which is IRepository<Foo>.
// i.e.: repositoryType = typeof(IRepository<Foo>);
// If I had that, I could snag the generic argument
// from IRepository<Foo> and hydrate the factory, like so:
var modelType = repositoryType.GetGenericArguments()[0];
var factoryType = typeof(IFactory<IRepository<>>).MakeGenericType(modelType);
var factory = (IFactory<object>)p.GetRequiredService(factoryType);
return factory.Provide();
}
);
}
If I try to use the Func<IServiceProvider, object>
functor with an open generic, I get this ArgumentException
with the message Open generic service type 'IRepository<T>' requires registering an open generic implementation type.
from the dotnet CLI. It doesn't even get to the lambda.
Is this type of binding possible with Microsoft's dependency injection framework?
Upvotes: 65
Views: 22708
Reputation: 444
According to the Fundamental Theorem of Software Engineering: "We can solve any problem by introducing an extra level of indirection."
This worked for me:
internal class RepositoryFactory<T> : IRepository<T>
{
private readonly IRepository<T> _repository;
public RepositoryFactory<T>(IFactory<IRepository<T>> factory)
{
_repository = factory.Provide();
}
// implement IRepository<T> here, with passthroughs to _repository.
}
Although it sucks if you have a lot of open generics to register, it gets around the problem where you can't currently register an open generic with a factory implementation on the IServiceCollection
.
FWIW, I'd recommend doing the registration as either scoped or transient instead of singleton.
services.AddScoped(typeof(IRepository<>), typeof(RepositoryFactory<>));
Upvotes: 0
Reputation: 916
The net.core dependency does not allow you to provide a factory method when registering an open generic type, but you can work around this by providing a type that will implement the requested interface, but internally it will act as a factory. A factory in disguise:
services.AddSingleton(typeof(IMongoCollection<>), typeof(MongoCollectionFactory<>)); //this is the important part
services.AddSingleton(typeof(IRepository<>), typeof(Repository<>))
public class Repository : IRepository {
private readonly IMongoCollection _collection;
public Repository(IMongoCollection collection)
{
_collection = collection;
}
// .. rest of the implementation
}
//and this is important as well
public class MongoCollectionFactory<T> : IMongoCollection<T> {
private readonly _collection;
public RepositoryFactoryAdapter(IMongoDatabase database) {
// do the factory work here
_collection = database.GetCollection<T>(typeof(T).Name.ToLowerInvariant())
}
public T Find(string id)
{
return collection.Find(id);
}
// ... etc. all the remaining members of the IMongoCollection<T>,
// you can generate this easily with ReSharper, by running
// delegate implementation to a new field refactoring
}
When the container resolves the MongoCollectionFactory it will know what type T is and will create the collection correctly. Then we take that created collection save it internally, and delegate all calls to it. ( We are mimicking this=factory.Create()
which is not allowed in csharp. :))
Update: As pointed out by Kristian Hellang the same pattern is used by ASP.NET Logging
public class Logger<T> : ILogger<T>
{
private readonly ILogger _logger;
public Logger(ILoggerFactory factory)
{
_logger = factory.CreateLogger(TypeNameHelper.GetTypeDisplayName(typeof(T)));
}
void ILogger.Log<TState>(...)
{
_logger.Log(logLevel, eventId, state, exception, formatter);
}
}
original discussion here:
https://twitter.com/khellang/status/839120286222012416
Upvotes: 31
Reputation: 71
See this issue on the dotnet (5) runtime git. This will add support to register open generics via a factory.
Upvotes: 5
Reputation: 8680
I was dissatisfied with the existing solutions as well.
Here is a full solution, using the built-in container, that supports everything we need:
IServiceProvider
to be resolved).We will register a proxy of the type that we really want to use. The proxy simply inherits from the intended type, but gets the "difficult" parts (complex dependencies and configuration) through a separately registered Options
type.
Since the Options
type is non-generic, it is easy to customize as usual.
public static class RepositoryExtensions
{
/// <summary>
/// A proxy that injects data based on a registered Options type.
/// As long as we register the Options with exactly what we need, we are good to go.
/// That's easy, since the Options are non-generic!
/// </summary>
private class ProxyRepository<T> : Repository<T>
{
public ProxyRepository(Options options, ISubdependency simpleDependency)
: base(
// A simple dependency is injected to us automatically - we only need to register it
simpleDependency,
// A complex dependency comes through the non-generic, carefully registered Options type
options?.ComplexSubdependency ?? throw new ArgumentNullException(nameof(options)),
// Configuration data comes through the Options type as well
options.ConnectionString)
{
}
}
public static IServiceCollection AddRepositories(this ServiceCollection services, string connectionString)
{
// Register simple subdependencies (to be automatically resolved)
services.AddSingleton<ISubdependency, Subdependency>();
// Put all regular configuration on the Options instance
var optionObject = new Options(services)
{
ConnectionString = connectionString ?? throw new ArgumentNullException(nameof(connectionString))
};
// Register the Options instance
// On resolution, last-minute, add the complex subdependency to the options as well (with access to the service provider)
services.AddSingleton(serviceProvider => optionObject.WithSubdependency(ResolveSubdependency(serviceProvider)));
// Register the open generic type
// All dependencies will be resolved automatically: the simple dependency, and the Options (holding everything else)
services.AddSingleton(typeof(IRepository<>), typeof(ProxyRepository<>));
return services;
// Local function that resolves the subdependency according to complex logic ;-)
ISubdependency ResolveSubdependency(IServiceProvider serviceProvider)
{
return new Subdependency();
}
}
internal sealed class Options
{
internal IServiceCollection Services { get; }
internal ISubdependency ComplexSubdependency { get; set; }
internal string ConnectionString { get; set; }
internal Options(IServiceCollection services)
{
this.Services = services ?? throw new ArgumentNullException(nameof(services));
}
/// <summary>
/// Fluently sets the given subdependency, allowing to options object to be mutated and returned as a single expression.
/// </summary>
internal Options WithSubdependency(ISubdependency subdependency)
{
this.ComplexSubdependency = subdependency ?? throw new ArgumentNullException(nameof(subdependency));
return this;
}
}
}
Upvotes: 3
Reputation: 7812
I also don't understand the point of your lambda expression so I'll explain to you my way of doing it.
I suppose what you wish is to reach what is explained in the article you shared
This allowed me to inspect the incoming request before supplying a dependency into the ASP.NET Core dependency injection system
My need was to inspect a custom header in the HTTP request to determine which customer is requesting my API. I could then a bit later in the pipeline decide which implementation of my IDatabaseRepository
(File System or Entity Framework linked to a SQL Database) to provide for this unique request.
So I start by writing a middleware
public class ContextSettingsMiddleware
{
private readonly RequestDelegate _next;
public ContextSettingsMiddleware(RequestDelegate next, IServiceProvider serviceProvider)
{
_next = next;
}
public async Task Invoke(HttpContext context, IServiceProvider serviceProvider, IHostingEnvironment env, IContextSettings contextSettings)
{
var customerName = context.Request.Headers["customer"];
var customer = SettingsProvider.Instance.Settings.Customers.FirstOrDefault(c => c.Name == customerName);
contextSettings.SetCurrentCustomer(customer);
await _next.Invoke(context);
}
}
My SettingsProvider
is just a singleton that provides me the corresponding customer object.
To let our middleware access this ContextSettings
we first need to register it in ConfigureServices
in Startup.cs
var contextSettings = new ContextSettings();
services.AddSingleton<IContextSettings>(contextSettings);
And in the Configure
method we register our middleware
app.UseMiddleware<ContextSettingsMiddleware>();
Now that our customer is accessible from elsewhere let's write our Factory.
public class DatabaseRepositoryFactory
{
private IHostingEnvironment _env { get; set; }
public Func<IServiceProvider, IDatabaseRepository> DatabaseRepository { get; private set; }
public DatabaseRepositoryFactory(IHostingEnvironment env)
{
_env = env;
DatabaseRepository = GetDatabaseRepository;
}
private IDatabaseRepository GetDatabaseRepository(IServiceProvider serviceProvider)
{
var contextSettings = serviceProvider.GetService<IContextSettings>();
var currentCustomer = contextSettings.GetCurrentCustomer();
if(SOME CHECK)
{
var currentDatabase = currentCustomer.CurrentDatabase as FileSystemDatabase;
var databaseRepository = new FileSystemDatabaseRepository(currentDatabase.Path);
return databaseRepository;
}
else
{
var currentDatabase = currentCustomer.CurrentDatabase as EntityDatabase;
var dbContext = new CustomDbContext(currentDatabase.ConnectionString, _env.EnvironmentName);
var databaseRepository = new EntityFrameworkDatabaseRepository(dbContext);
return databaseRepository;
}
}
}
In order to use serviceProvider.GetService<>()
method you will need to include the following using in your CS file
using Microsoft.Extensions.DependencyInjection;
Finally we can use our Factory in ConfigureServices
method
var databaseRepositoryFactory = new DatabaseRepositoryFactory(_env);
services.AddScoped<IDatabaseRepository>(databaseRepositoryFactory.DatabaseRepository);
So every single HTTP request my DatabaseRepository
will may be different depending of several parameters. I could use a file system or a SQL Database and I can get the proper database corresponding to my customer. (Yes I have multiple databases per customer, don't try to understand why)
I simplified it as possible, my code is in reality more complex but you get the idea (I hope). Now you can modify this to fit your needs.
Upvotes: 1