【问题标题】:Factory Pattern with Open Generics具有开放泛型的工厂模式
【发布时间】:2016-12-26 01:01:02
【问题描述】:

在 ASP.NET Core 中,您可以使用 Microsoft 的依赖注入框架 is bind "open generics"(未绑定到具体类型的泛型类型)执行以下操作:

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

您也可以使用the factory pattern to hydrate dependencies。这是一个人为的例子:

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&lt;&gt; 实例的具体类型。

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&lt;IServiceProvider, object&gt; 函子与开放泛型一起使用,我会从 dotnet CLI 获得带有消息 Open generic service type 'IRepository&lt;T&gt;' requires registering an open generic implementation type.this ArgumentException。它甚至没有到达 lambda。

微软的依赖注入框架可以实现这种类型的绑定吗?

【问题讨论】:

  • 注册一个解析所需服务的工厂的 lambda 有什么好处?
  • 好问题。它改变了条件水合的复杂性。您不需要显式工厂,因为 lambda 充当一个工厂(它的变量甚至称为“implementationFactory”),但是如果您需要多个服务来决定要水合的实例,您将拥有一个复杂且难以测试的 lambda。我上面链接的博客文章有一个很好的例子:dotnetliberty.com/index.php/2016/05/09/…
  • 你有没有找到一个好的答案?我有同样的问题,但这里的答案似乎都不是解决问题的好方法
  • 我们通过在服务注册前关闭泛型“解决”了这个问题。我把它写在这个 GitHub 问题上。 github.com/aspnet/DependencyInjection/issues/…
  • 恕我直言,真正的解决方案是不使用微软的 DI 容器。他们已经声明他们不会在这个 GitHub 线程中解决这个问题。 github.com/aspnet/DependencyInjection/issues/…

标签: c# generics dependency-injection asp.net-core


【解决方案1】:

net.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.NET 日志记录使用相同的模式

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

这里的原始讨论:

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

【讨论】:

  • 我认为MongoCollectionFactory 更合适的名称是CollectionProxy=)) 因为它没有实现任何CreateCollection 之类的方法。使用新名称将更容易理解此类的真正目的))
【解决方案2】:

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

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

  • 简单的依赖关系。
  • 复杂的依赖关系(需要解析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;
        }
    }
}

【讨论】:

    【解决方案3】:

    看到这个issue on the dotnet (5) runtime git。 这将增加对通过工厂注册开放泛型的支持。

    【讨论】:

      【解决方案4】:

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

      我想你希望达到你分享的文章中解释的内容

      这允许我在向 ASP.NET 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&lt;&gt;() 方法,您需要在您的 CS 文件中包含以下使用

      using Microsoft.Extensions.DependencyInjection;
      

      终于可以在ConfigureServices方法中使用我们的Factory了

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

      因此,我的DatabaseRepository 的每个 HTTP 请求都可能会根据几个参数而有所不同。我可以使用文件系统或 SQL 数据库,并且可以获得与我的客户对应的正确数据库。 (是的,我每个客户都有多个数据库,不要试图理解为什么)

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

      【讨论】:

      • 这似乎与我的问题有点不同,尽管它是一个很好的例子,说明如何在每个请求的基础上延迟 IServiceCollection 的定义。您正在推迟决定使用哪种已知类型 (IDatabaseRepository) 的实现。在我的情况下,IRepository&lt;&gt; 是打开/未知的,因为我没有关于如何在 implementationFactory lambda for ServiceProvider 中关闭它的必要类型信息。
      • 只是为了澄清一些事情。你总是可以摆脱 lambda 表达式。它们只是为了简化开发人员的生活并让我们更快地编写代码。但在那种情况下,我不喜欢在我的 Startup.cs 文件中包含这么大的代码。这就是为什么我选择摆脱它并在DatabaseRepositoryFactory 中实现我的逻辑。如果您愿意,您也可以这样做。
      • 我的数据库存储库实际上实现了IDatabaseRepository&lt;IDatabase&gt;,实际上继承自IRepository&lt;T&gt;的接口。所以我真正的依赖注入是services.AddScoped&lt;IDatabaseRepository&lt;IDatabase&gt;&gt;(databaseRepositoryFactory.DatabaseRepository)。我认为在我的示例中简单地说是一个好主意,但也许您需要完整的示例......我不选择我使用的IDatabase 的实现并不重要。我的存储库只需要尊重它的合同并接受或返回一些IDatabase
      • 为什么是中间件?在创建服务时检查标头。我建议您跳过 Settings.Instance 服务定位器模式的混合来为 DI 提供信息。
      猜你喜欢
      • 1970-01-01
      • 2020-09-17
      • 1970-01-01
      • 1970-01-01
      • 2021-11-19
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多