Изменить разрешение зависимостей только для определенной области - PullRequest
0 голосов
/ 16 октября 2019

У меня есть одна зарегистрированная зависимость следующим образом:

interface IDependency { }

class DependencyImpl : IDependency { }

Запуск:

services.AddScoped<IDependency, DependencyImpl>();

Это работает так, как я хочу, чтобы использовать один и тот же экземпляр в области действия моего WebЗапросы API.

Однако в одном фоновом сервисе я бы хотел сказать, к какому экземпляру он будет обращаться:

class MyBackgroundService
{
    private readonly IServiceScopeFactory _scopeFactory; // set in ctor

    public void DoStuff()
    {
        var itens = GetItens();

        var dependencyInstance = new DependencyImpl();

        Parallel.ForEach(itens, (item) =>
        {
             using(var scope = _scopeFactory.CreateScope())
             {
                 scope.SwapDependencyForThisScopeOnly<IDependency>( () => dependencyInstance ); // something like this

                 var someOtherService = scope.ServiceProvider.GetRequiredService<ItemService(); // resolve subsequent services with provided dependencyInstance
                 someOtherService.Process(item);
             }
        });
    }

}

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

Можно ли здесь делать то, что я хочу? Имеет ли это смысл?

Я использую dotnet core 2.2 с контейнером IoC по умолчанию для этого.

Редактировать в ответ на @Steven: DependencyImpl содержит конфигурации для того, какпредмет будет обработан. Один из них включает относительно дорогой запрос. DependencyImpl также вводится в график более одного раза. Таким образом, в настоящее время он читает конфигурацию один раз, кэширует ее в частных свойствах и использует кэшированную версию при последующих чтениях. Поскольку я знаю, что здесь я буду повторно использовать одну и ту же конфигурацию для всех типов, я бы хотел избежать повторного чтения конфигурации для каждого параллельного выполнения.

Моя зависимость от реального мира больше похожа на эту:

interface IDependency 
{ 
    Task<Configuration> GetConfigurationAsync();
}

class DependencyImpl : IDependency 
{ 
    private readonly Configuration _configuration;
    private readonly DbContext _dbContext;

    ctor(DbContext dbContext)
    {
        _dbContext = dbContext;
    }

    public async  Task<Configuration> GetConfigurationAsync()
    {
         if(_configuration is null)
         {
              // read configurations
         }

         return _configuration;
    }
}

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

Кроме того, те обработки, которые происходили во время жизненного цикла веб-запроса, и фоновая служба - это новое. Я бы предпочел изменить как можно меньше существующего кода, потому что тестов на месте немного, и, конечно, ограничения по времени зависят от возможностей.

Ответы [ 2 ]

1 голос
/ 16 октября 2019

Как правило, не рекомендуется менять структуру графиков зарегистрированных объектов во время работы приложения. Мало того, что это трудно достичь с большинством контейнеров, оно подвержено сложным проблемам, которые трудно обнаружить. Поэтому я предлагаю небольшое изменение в вашем дизайне, которое обходит проблему, с которой вы сталкиваетесь.

Вместо того, чтобы пытаться изменить зависимость в целом, вместо этого предварительно заполняйте существующую зависимость данными, загруженными в aa. другой поток.

Это можно сделать с помощью следующей пары абстракция / реализация:

public interface IConfigurationProvider
{
    Task<Configuration> GetConfigurationAsync();
}

public sealed class DatabaseConfigurationProvider : IConfigurationProvider
{
    private readonly DbContext _dbContext;

    public DatabaseConfigurationProvider(DbContext dbContext)
    {
        _dbContext = dbContext;
    }

    public Configuration Configuration { get; set; }

    public async Task<Configuration> GetConfigurationAsync()
    {
         if (Configuration is null)
         {
             await // read configurations
         }

         return Configuration;
    }
}

Обратите внимание на общедоступную Configuration в реализации DatabaseConfigurationProvider, которая not на интерфейсе IConfigurationProvider.

Это ядро ​​решения, которое я представляю. Позвольте вашему Composition Root установить значение, не загрязняя абстракции вашего приложения, поскольку коду приложения не нужно перезаписывать объект Configuration;только корень композиции необходим.

С этой парой абстракции / реализации фоновая служба может выглядеть следующим образом:

class MyBackgroundService
{
    private readonly IServiceScopeFactory _scopeFactory; // set in ctor

    public Task DoStuff()
    {
        var itens = GetItens();

        // Create a scope for the root operation.
        using (var scope = _scopeFactory.CreateScope())
        {
            // Resolve the IConfigurationProvider first to load
            // the configuration once eagerly.
            var provider = scope.ServiceProvider
                .GetRequiredService<IConfigurationProvider>();

            var configuration = await provider.GetConfigurationAsync();

            Parallel.ForEach(itens, (item) => Process(configuration, item));
        }
    }

    private void Process(Configuration configuration, Item item)
    {
        // Create a new scope per thread
        using (var scope = _scopeFactory.CreateScope())
        {
            // Request the configuration implementation that allows
            // setting the configuration.
            var provider = scope.ServiceProvider
                .GetRequiredService<DatabaseConfigurationProvider>();

            // Set the configuration object for the duration of the scope
            provider.Configuration = configuration;

            // Resolve an object graph that depends on IConfigurationProvider.
            var service = scope.ServiceProvider.GetRequiredService<ItemService>();

            service.Process(item);
        }    
    }
}

Чтобы выполнить это, вам потребуется следующая конфигурация DI:

services.AddScoped<DatabaseConfigurationProvider>();
services.AddScoped<IConfigurationProvider>(
    p => p.GetRequiredService<DatabaseConfigurationProvider>());

Эта предыдущая конфигурация регистрирует DatabaseConfigurationProvider дважды: один раз для его конкретного типа, один раз для его интерфейса. Регистрация интерфейса перенаправляет вызов и напрямую разрешает конкретный тип. Это особая «хитрость», которую нужно применять при работе с контейнером MS.DI, чтобы предотвратить попадание двух отдельных экземпляров DatabaseConfigurationProvider в одну область видимости. Это полностью нарушило бы правильность этой реализации.

0 голосов
/ 16 октября 2019

Создайте интерфейс, который расширяет IDependency и применяется только к более быстрой реализации, которую вам нужно запросить, например, IFasterDependency. Затем сделайте регистрацию на IFasterDependency. Таким образом, ваш более быстрый класс по-прежнему является объектом IDependency, и вы не нарушите слишком много существующего кода, но теперь вы можете запросить его свободно.

public interface IDependency
{
    // Actual, useful interface definition
}

public interface IFasterDependency : IDependency
{
    // You don't actually have to define anything here
}

public class SlowClass : IDependency
{

}

// FasterClass is now a IDependencyObject, but has its own interface 
// so you can register it in your dependency injection
public class FasterClass : IFasterDependency
{

}
...