Factory Шаблон с открытыми дженериками

В ASP.NET Core одна из вещей, которые вы можете сделать с помощью инфраструктуры внедрения зависимостей Microsoft , - это связать "открытые универсальные типы" (универсальные типы, не привязанные к конкретному типу), например так:

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> с открытым универсальным модулем, я получаю этот ArgumentException с сообщением Open generic service type 'IRepository<T>' requires registering an open generic implementation type. от CLI dotnet. Он даже не доходит до лямбды.

Возможен ли этот тип привязки в среде внедрения зависимостей Microsoft?

Ответы

Ответ 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, ti будет знать, что такое тип T, и правильно создаст коллекцию. Затем мы берем созданную коллекцию, сохраняем ее внутри и делегируем ей все вызовы. (Мы подражаем this=factory.Create(), что запрещено в csharp. :))

Обновить: Как указал Кристиан Хелланг, тот же шаблон используется протоколированием 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

Ответ 2

Я также не понимаю смысл вашего выражения лямбды, поэтому я объясню вам мой способ сделать это.

Я предполагаю, что вы хотите достичь того, что объясняется в статье, которую вы поделили

Это позволило мне проверить входящий запрос перед поставкой зависимости в систему впрыска зависимостей ядра ASP.NET

Мне нужно было проверить пользовательский заголовок в HTTP-запросе, чтобы определить, какой клиент запрашивает мой API. Затем я мог немного позже в конвейере решить, какая реализация моей IDatabaseRepository (Файловая система или Entity Framework связана с базой данных 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);
    }
}

My SettingsProvider - это просто одноэлемент, который предоставляет мне соответствующий объект-клиент.

Чтобы наше промежуточное программное обеспечение получило доступ к этому ContextSettings, сначала необходимо зарегистрировать его в ConfigureServices в Startup.cs

var contextSettings = new ContextSettings();
services.AddSingleton<IContextSettings>(contextSettings);

И в методе Configure мы регистрируем наше промежуточное ПО

app.UseMiddleware<ContextSettingsMiddleware>();

Теперь, когда наш клиент доступен из других источников, напишите наш 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;
        }
    }
}

Чтобы использовать метод serviceProvider.GetService<>(), вам необходимо включить следующее использование в файл CS

using Microsoft.Extensions.DependencyInjection;

Наконец, мы можем использовать наш метод Factory in ConfigureServices

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

Таким образом, каждый HTTP-запрос my DatabaseRepository может отличаться в зависимости от нескольких параметров. Я мог бы использовать файловую систему или базу данных SQL, и я могу получить соответствующую базу данных, соответствующую моему клиенту. (Да, у меня есть несколько баз данных для каждого клиента, не пытайтесь понять, почему)

Я упростил это, насколько возможно, мой код на самом деле более сложный, но вы получаете идею (я надеюсь). Теперь вы можете изменить это в соответствии с вашими потребностями.

Ответ 3

Я также был недоволен существующими решениями.

Вот полное решение с использованием встроенного контейнера, поддерживающего все, что нам нужно:

  • Простые зависимости.
  • Сложные зависимости (требующие разрешения 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 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;
        }
    }
}