工厂模式与开放的泛型

Tec*_*ium 29 c# generics dependency-injection asp.net-core

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

public void ConfigureServices(IServiceCollection services) {
    services.AddSingleton(typeof(IRepository<>), typeof(Repository<>))
}
Run Code Online (Sandbox Code Playgroud)

您还可以使用工厂模式来水合依赖项.这是一个人为的例子:

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()
    ); 
}
Run Code Online (Sandbox Code Playgroud)

但是,我还没弄清楚如何将这两个概念结合起来.看起来它会从这样的东西开始,但我需要用于水合实例的具体类型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();
        }           
    ); 
}
Run Code Online (Sandbox Code Playgroud)

如果我尝试使用Func<IServiceProvider, object>带有开放泛型的仿函数,我会使用来自dotnet CLI 的消息来获取ArgumentExceptionOpen generic service type 'IRepository<T>' requires registering an open generic implementation type..它甚至没有达到lambda.

这种类型的绑定是否可以与Microsoft的依赖注入框架一起使用?

noh*_*wnd 13

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
}
Run Code Online (Sandbox Code Playgroud)

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

更新:正如Kristian Hellang所指出的,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);
    }
}
Run Code Online (Sandbox Code Playgroud)

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认同)

Jér*_*VEL 1

我也不明白你的 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);
    }
}
Run Code Online (Sandbox Code Playgroud)

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

为了让我们的中间件访问它,我们首先需要在 Startup.cs 中ContextSettings注册它ConfigureServices

var contextSettings = new ContextSettings();
services.AddSingleton<IContextSettings>(contextSettings);
Run Code Online (Sandbox Code Playgroud)

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

app.UseMiddleware<ContextSettingsMiddleware>();
Run Code Online (Sandbox Code Playgroud)

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

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;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

为了使用serviceProvider.GetService<>()方法,您需要在您的 CS 文件中包含以下内容

using Microsoft.Extensions.DependencyInjection;
Run Code Online (Sandbox Code Playgroud)

最后我们可以在ConfigureServices方法中使用我们的工厂

var databaseRepositoryFactory = new DatabaseRepositoryFactory(_env);
services.AddScoped<IDatabaseRepository>(databaseRepositoryFactory.DatabaseRepository);
Run Code Online (Sandbox Code Playgroud)

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

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