具有开放泛型的工厂模式

本文关键字:工厂 模式 泛型 | 更新日期: 2023-09-27 18:02:57

你可以用微软的依赖注入框架绑定"开放泛型"(未绑定到具体类型的泛型类型),如下所示:

public void ConfigureServices(IServiceCollection services) {
    services.AddSingleton(typeof(IRepository<>), typeof(Repository<>))
}

您还可以使用工厂模式来合并依赖关系。下面是一个做作的例子:

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

然而,我一直不知道如何将这两个概念结合在一起。它似乎会以这样的东西开始,但我需要用于水化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();
        }           
    ); 
}

如果我尝试使用开放泛型的Func<IServiceProvider, object>函子,我将从dotnet CLI获得带有Open generic service type 'IRepository<T>' requires registering an open generic implementation type.消息的ArgumentException。它甚至没有到达lambda

这种类型的绑定是否可能与微软的依赖注入框架?

具有开放泛型的工厂模式

网。Core依赖不允许您在注册开放泛型类型时提供工厂方法,但是您可以通过提供将实现所请求接口的类型来解决这个问题,但在内部它将充当工厂。伪装的工厂:

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
}

当容器解析MongoCollectionFactory时,它将知道T是什么类型,并将正确创建集合。然后我们将创建的集合保存在内部,并将所有调用委托给它。(我们正在模仿this=factory.Create(),这在csharp是不允许的。:))

更新:正如Kristian Hellang所指出的,ASP也使用相同的模式。网络日志

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://twitter.com/khellang/status/839120286222012416

在dotnet(5)运行时git上查看此问题。这将增加通过工厂注册开放泛型的支持。

我对现有的解决方案也不满意。

下面是一个完整的解决方案,使用内置容器,它支持我们需要的一切:

  • 简单依赖。
  • 复杂的依赖项(需要IServiceProvider被解决)。
  • 配置数据(如连接字符串)

我们将注册一个我们真正想要使用的类型的代理。代理简单地继承预期类型,但通过单独注册的Options类型获得"困难"部分(复杂的依赖关系和配置)。

由于Options类型是非泛型的,因此很容易像往常一样自定义。

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

我也不明白你的lambda表达式的意义,所以我将向你解释我的方法。

我想你所希望的是达到你分享的文章中所解释的

这允许我在向ASP提供依赖项之前检查传入请求。Core依赖注入系统

我需要检查HTTP请求中的自定义报头,以确定哪个客户正在请求我的API。然后我可以稍后在管道中决定我的IDatabaseRepository(链接到SQL数据库的文件系统或实体框架)的实现为这个唯一的请求提供。

我先写一个中间件

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

我的SettingsProvider只是一个单例,为我提供了相应的客户对象。

要让我们的中间件访问ContextSettings,我们首先需要在Startup.cs

中注册ConfigureServices
var contextSettings = new ContextSettings();
services.AddSingleton<IContextSettings>(contextSettings);

Configure方法中我们注册了中间件

app.UseMiddleware<ContextSettingsMiddleware>();

现在我们的客户可以从其他地方访问,让我们编写我们的工厂。

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

为了使用serviceProvider.GetService<>()方法,您需要在CS文件

中包含以下使用
using Microsoft.Extensions.DependencyInjection;

最后我们可以在ConfigureServices方法中使用Factory

var databaseRepositoryFactory = new DatabaseRepositoryFactory(_env);
services.AddScoped<IDatabaseRepository>(databaseRepositoryFactory.DatabaseRepository);

所以每一个HTTP请求我的DatabaseRepository将可能是不同的取决于几个参数。我可以使用文件系统或SQL数据库,我可以得到与我的客户对应的适当的数据库。(是的,我每个客户有多个数据库,不要试图理解为什么)

我尽可能地简化了它,我的代码实际上更复杂,但你明白了(我希望)。现在您可以修改它以满足您的需要。

根据软件工程基本定理:"我们可以通过引入额外的间接层次来解决任何问题。"

这个为我工作:

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.
}

如果你有很多开放泛型要注册,虽然这很糟糕,但它解决了你目前不能在IServiceCollection上用工厂实现注册开放泛型的问题。

总的来说,我建议注册为有作用域的或瞬态的,而不是单例的。

services.AddScoped(typeof(IRepository<>), typeof(RepositoryFactory<>));