Как разрешить службу и внедрить дополнительные параметры конструктора во время выполнения с помощью внедрения зависимости .NET Core? - PullRequest
0 голосов
/ 29 января 2019

У меня есть сценарий использования, в котором я хочу создать экземпляры репозитория с помощью внедрения зависимостей .NET Core, но мне нужно изменить один из параметров конструктора во время выполнения.Чтобы быть точным, параметр, который должен быть определен во время выполнения, является «соединением с базой данных», которое будет указывать на ту или иную базу данных, выбранную вызывающей стороной.Этот тип, кстати, не зарегистрирован в контейнере DI, но все остальные.

Вызывающая сторона будет использовать тип фабрики хранилища для создания хранилища с желаемым соединением.

Это выглядит примерно так:

class ARepository : IARepository
{
    public ARepository(IService1 svc1, IService2 svc2, IConnection connection) { }

    public IEnumerable<Data> GetData() { }
}

class RepositoryFactory : IRepositoryFactory
{
    public RepositoryFactory(IServiceProvider serviceProvider) =>
        _serviceProvider = serviceProvider;

    public IConnection CreateAlwaysFresh<TRepository>() =>
        this.Create<TRepository>(new FreshButExpensiveConnection());

    public IConnection CreatePossiblyStale<TRepository>() =>
        return this.Create<TRepository>(new PossiblyStaleButCheapConnection());

    private IConnection Create<TRepository>(IConnection conn)
    {
        // Fails because TRepository will be an interface, not the actual type
        // that I want to create (see code of AService below)
        return ActivatorUtilities.CreateInstance<TRepository>(_serviceProvider,conn);

        // Fails because IConnection is not registered, which is normal
        // because I want to use the instance held in parameter conn
        return _serviceProvider.GetService<TRepository>();
    }
}

Были зарегистрированы следующие типы:

services.AddTransient<IARepository, ARepository>();  // Probably not needed
services.AddTransient<IService1, Service1>();
services.AddTransient<IService2, Service2>();
services.AddTransient<IRepositoryFactory, RepositoryFactory>();

И фабрика будет использоваться таким образом:

class AService
{
    public AService(IRepositoryFactory factory)
    {
        _factory = factory;
    }

    public void ExecuteCriticalAction()
    {
        var repo = _factory.CreateAlwaysFresh<IARepository>();

        // Gets the freshest data because repo was created using
        // AlwaysFresh connection
        var data = repo.GetData();

        // Do something critical with data
    }

    public void ExecuteRegularAction()
    {
        var repo = _factory.CreatePossiblyStale<IARepository>();

        // May get slightly stale data because repo was created using 
        // PossiblyStale connection
        var data = repo.GetData();

        // Do something which won't suffer is data is slightly stale
    }
}

Одна из причин, по которой я сохранил весь код на основе интерфейсов, это, конечно, модульное тестирование.Однако, как вы можете видеть из псевдо-реализации RepositoryFactory.Create<TRepository>, это также проблема, потому что я достигаю точки, где мне нужно либо:

  • определить конкретный тип, связанный сIARepository в контейнере DI для передачи его в ActivatorUtilities для создания его экземпляра с использованием требуемого значения IConnection при разрешении других параметров конструктора с помощью IServiceProvider или

  • как-то сказать IServiceProvider использовать конкретный экземпляр IConnection при получении определенной услуги

Возможно ли это вообще с помощью .NET Core DI?

(Бонусный вопрос: должен ли я использовать другой, более простой подход?)

Обновление: Я немного отредактировал пример кода, чтобы, надеюсь, сделать мои намерения более ясными.Идея состоит в том, чтобы позволить одному и тому же хранилищу, точно одному и тому же коду, использовать разные соединения (которые настраиваются во время запуска приложения) в зависимости от конкретных потребностей вызывающей стороны .Подводя итог:

  • ответственность Репозитария состоит в том, чтобы выполнять правильные запросы к Соединению, когда запрашивается действие.
  • Вызывающий объект будет воздействовать на данные, возвращаемые репозиторием
  • однако , вызывающей стороне может потребоваться, чтобы Репозиторий выполнил свои запросы к определенному Соединению (которое в этом примере контролирует свежесть данных)

Несколько обходных путей подошлик проблеме внедрения правильного соединения на фабрике:

  • добавьте изменяемое свойство Connection в репозитории и установите его сразу после создания => что меня больше всего беспокоит это решение, так это то, что оно делает его оченьлегко забыть установить соединение, например, в тестовом коде.Он также оставляет дверь открытой для изменения свойства хранилища, которое должно быть неизменным.
  • не вставляет Соединение в класс, а вместо этого передает его как параметр метода => это делает для менее элегантного API, поскольку каждый метод теперь будет иметь «дополнительный» параметр, который мог бы просто быть предоставлен классу для начала, а дополнительный параметр - всего лишь «деталь реализации»

Ответы [ 2 ]

0 голосов
/ 29 января 2019

Проблема в регистрации и попытке ввести IRepository вообще.По вашей собственной спецификации, репо не может быть создано с помощью внедрения зависимостей, поскольку передаваемое в него соединение будет меняться во время выполнения.Таким образом, вы должны создать фабрику (что вы уже сделали), зарегистрировать и внедрить ее вместо этого.Затем вы передаете соединение вашему методу Create.

public TRepository Create<TRepository>(IConnection conn)
    where TRepository : IRepository, new()
{
    return new TRepository(conn);
}

Вместо этого вы, скорее всего, захотите создать какой-нибудь шаблон локатора экземпляра.Например, вы можете сохранить созданный экземпляр в ключе ConcurrentDictionary, указанном соединением.Тогда вы бы вернулись из словаря.Вероятно, не так уж и много, если репозиторий действительно создается несколько раз в условиях гонки - это должно быть довольно незначительное распределение объектов.Однако вы можете использовать SemaphoreSlim для создания блокировок при доступе к ConcurrentDictionary, чтобы предотвратить это.

Вы не предоставили тонны информации о вашем конкретном случае использования, поэтому я также добавлюпотенциальное альтернативное решение.Чтобы это работало, соединение должно быть определено через config или что-то еще.Если он действительно обеспечен runtime , это не сработает.Вы можете предоставить действие для регистрации услуги, например:

services.AddScoped<IRepository, ARepository>(p => {
    // create your connection here
    // `p` is an instance of `IServiceProvider`, so you can do service lookups
    return new ARepository(conn);
});
0 голосов
/ 29 января 2019

Так как IConnection не будет создан DI, вы можете удалить его из конструктора хранилища и иметь его в качестве свойства, тогда на вашем заводе вы можете назначить его значение после создания:

interface IARepository 
{ 
    IConnection Connection { set; }
}

class ARepository : IARepository
{
    public IConnection Connection { private get; set; }

    public ARepository(IService1 svc1, IService2 svc2)
    { /* ... */ }
}


class RepositoryFactory : IRepositoryFactory
{
    /* ... */
    private IConnection Create<TRepository>(IConnection conn) 
        where TRepository : IARepository
    {
        var svc = _serviceProvider.GetService<TRepository>();
        svc.Connection = conn;
        return svc;
    }
}
...