Technetium
Technetium

Reputation: 6158

Factory Pattern with Open Generics

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

Answers (5)

aethercowboy
aethercowboy

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

nohwnd
nohwnd

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

Wesley
Wesley

Reputation: 71

See this issue on the dotnet (5) runtime git. This will add support to register open generics via a factory.

Upvotes: 5

Timo
Timo

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:

  • Simple dependencies.
  • Complex dependencies (requiring the IServiceProvider to be resolved).
  • Configuration data (such as connection strings).

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

J&#233;r&#244;me MEVEL
J&#233;r&#244;me MEVEL

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

Related Questions