Factory Pattern with Open Generics

前端 未结 4 446
南旧
南旧 2020-12-13 18:12

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:<

相关标签:
4条回答
  • 2020-12-13 18:26

    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.

    0 讨论(0)
  • 2020-12-13 18:30

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

    0 讨论(0)
  • 2020-12-13 18:37

    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 ti 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);
        }
    }
    

    https://github.com/aspnet/Logging/blob/dev/src/Microsoft.Extensions.Logging.Abstractions/LoggerOfT.cs#L29

    original discussion here:

    https://twitter.com/khellang/status/839120286222012416

    0 讨论(0)
  • 2020-12-13 18:42

    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;
            }
        }
    }
    
    0 讨论(0)
提交回复
热议问题