Определение реализации для внедрения во время выполнения с помощью внедрения зависимости .NET Core - PullRequest
0 голосов
/ 28 декабря 2018

В моем приложении три типа пользователей, скажем, Type1, Type2 and Type3.Затем я хочу создать одну реализацию сервиса для каждого типа, скажем, у меня есть сервис для получения фотографий, у меня будет три сервиса: Type1PhotosService, Type2PhotosService and Type3PhotosService, каждый из которых реализует IPhotosService.

в веб-интерфейсе., я бы вставил IPhotosService:

IPhotosService _service;

public PhotosController(IPhotosService service){
   _service = service;
} 

Web API использует аутентификацию токена с заявками.Итак, чего я хочу добиться, так это для каждого пользователя, в зависимости от утверждения, которое он имеет: type1 or type2 or type3, правильная реализация службы будет автоматически внедрена, а не внедрять одну службу в файле startup.Чего я хочу избежать, так это иметь один сервис с кучей операторов switch и if для возврата правильных данных в зависимости от типа пользователя и ролей, которые у него есть.

РЕДАКТИРОВАТЬ: некоторые комментарии были интересныв чем смысл трех реализаций, так что здесь есть больше деталей, чтобы придать ему немного больше смысла.Сервис является сервисом поиска работы, и приложение имеет три различных профиля: candidate, employer and administration.Каждый из этих профилей нуждается в правильной реализации.Поэтому вместо того, чтобы иметь три метода GetCandidateJobs, GetEmployerJobs and GetAdministrationJobs внутри одной и той же службы и включать тип пользователя, я предпочел иметь одну реализацию для каждого типа профиля, а затем, в зависимости от типа профиля, использовать правильную реализацию.

Ответы [ 3 ]

0 голосов
/ 29 декабря 2018

Я собираюсь выйти здесь на конечность и сказать, что попытка использовать внедрение зависимостей для этой цели является неоптимальной.Обычно это обрабатывается шаблоном Factory, который создает реализации сервиса, используя страшные операторы if и switch.Простой пример:

public interface IPhotoService { 
     Photo CreatePhoto(params);
}

public class PhotoServiceFactory {
    private readonly IPhotoService _type1;
    private readonly IPhotoService _type2;
    private readonly IPhotoService _type3;
    public PhotoServiceFactory(IDependency1 d1, IDependency2 d2, ...etc) {
        _type1 = new ConcreteServiceA(d1);
        _type2 = new ConcreteServiceB(d2);
        _type3 = new ConcreteServiceC(etc);
    }
    public IPhotoService Create(User user) {
        switch(user.Claim) {
            case ClaimEnum.Type1:
                return _type1;
            case ClaimEnum.Type2:
                return _type2;
            case ClaimEnum.Type3:
                return _type3;
            default:
                throw new NotImplementedException
        }
    }
}

Затем в вашем контроллере:

public class PhotosController {
    IPhotoServiceFactory _factory;
    public PhotosController(IPhotoServiceFactory factory){
       _factory = factory;
    } 
    public IHttpActionResult GetPhoto() {
       var photoServiceToUse = _factory.Create(User);
       var photo = photoServiceToUse.CreatePhoto(params);
       return Ok(photo);
    }
}

Альтернативно просто используйте конкретные классы в качестве аргументов в конструкторе и следуйте логике, аналогичной приведенной выше.

0 голосов
/ 29 декабря 2018

Без использования отдельного контейнера IoC

Этот подход намного проще, чем конфигурировать ваше приложение для использования другого контейнера IoC и затем для настройки этого контейнера.После работы с Windsor это решение кажется намного проще.

Этот подход наиболее прост, если вы можете использовать одноэлементный экземпляр каждой реализации сервиса.

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

public interface ICustomService { }
public class CustomServiceOne : ICustomService { }
public class CustomServiceTwo : ICustomService { }
public class CustomServiceThree : ICustomService { }

public interface ICustomServiceFactory
{
    ICustomService Create(string input);
}

Вот действительно грубая реализация фабрики.(Не использовал строковые константы и не полировал их вообще.)

public class CustomServiceFactory : ICustomServiceFactory
{
    private readonly Dictionary<string, ICustomService> _services 
        = new Dictionary<string, ICustomService>(StringComparer.OrdinalIgnoreCase);

    public CustomServiceFactory(IServiceProvider serviceProvider)
    {
        _services.Add("TypeOne", serviceProvider.GetService<CustomServiceOne>());
        _services.Add("TypeTwo", serviceProvider.GetService<CustomServiceTwo>());
        _services.Add("TypeThree", serviceProvider.GetService<CustomServiceThree>());
    }

    public ICustomService Create(string input)
    {
        return _services.ContainsKey(input) ? _services[input] : _services["TypeOne"];
    }
}

Предполагается, что вы уже зарегистрировали CustomServiceOne, CustomServiceTwo и т. Д. С помощью IServiceCollection.Они не будут зарегистрированы как реализации интерфейса, так как мы не решаем их.Этот класс просто разрешит каждый из них и поместит их в словарь, чтобы вы могли получить их по имени.

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

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

Чтобы зарегистрировать фабрику с IServiceCollection при запуске, мы сделаем это:

services.AddSingleton<ICustomServiceFactory>(provider => 
    new CustomServiceFactory(provider));

IServiceProvider будет введен на заводе после разрешения фабрики, а затем фабрика будет использовать его для разрешения службы.

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

public class Tests
{
    private IServiceProvider _serviceProvider;
    [SetUp]
    public void Setup()
    {
        var services = new ServiceCollection();
        services.AddSingleton<CustomServiceOne>();
        services.AddSingleton<CustomServiceTwo>();
        services.AddSingleton<CustomServiceThree>();
        services.AddSingleton<ICustomServiceFactory>(provider => 
            new CustomServiceFactory(provider));
        _serviceProvider = services.BuildServiceProvider();
    }

    [TestCase("TypeOne", typeof(CustomServiceOne))]
    [TestCase("TypeTwo", typeof(CustomServiceTwo))]
    [TestCase("TYPEThree", typeof(CustomServiceThree))]
    [TestCase("unknown", typeof(CustomServiceOne))]
    public void FactoryReturnsExpectedService(string input, Type expectedType)
    {
        var factory = _serviceProvider.GetService<ICustomServiceFactory>();
        var service = factory.Create(input);
        Assert.IsInstanceOf(expectedType, service);
    }
}

Как и в примере с Виндзор, это написано, чтобы избежать какой-либо ссылки на контейнер за пределами корня композиции.Если класс зависит от ICustomServiceFactory и ICustomService, вы можете переключаться между этой реализацией, реализацией Windsor или любой другой реализацией фабрики.

0 голосов
/ 29 декабря 2018

Использование Windsor

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

Контейнер IoC .NET Coreне очень хорошо подходит для такого сценария.(Они подтверждают это в своей документации.) Вы можете обойти это, добавив еще один контейнер IoC, такой как Windsor.

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

В вашем проекте добавьте Castle.Windsor.MsDependencyInjection Пакет NuGet.

Интерфейсы и реализации для тестирования

Для тестирования я добавил несколько интерфейсов и реализаций:

public interface ICustomService { }
public interface IRegisteredWithServiceCollection { }
public class CustomServiceOne : ICustomService { }
public class CustomServiceTwo : ICustomService { }
public class CustomServiceThree : ICustomService { }
public class RegisteredWithServiceCollection : IRegisteredWithServiceCollection { }

Цель заключается в создании фабрики, которая будет выбирать и возвращать реализацию ICustomService, используянекоторый ввод во время выполнения.

Вот интерфейс, который будет служить фабрикой.Это то, что мы можем внедрить в класс и вызвать во время выполнения, чтобы получить реализацию ICustomService:

public interface ICustomServiceFactory
{
    ICustomService Create(string input);
}

Настройка контейнера Windsor

Далее следует класс, который настроит IWindsorContainer для разрешения зависимостей:

public class WindsorConfiguration : IWindsorInstaller
{
    public void Install(IWindsorContainer container, IConfigurationStore store)
    {
        container.AddFacility<TypedFactoryFacility>();
        container.Register(
            Component.For<ICustomService, CustomServiceOne>().Named("TypeOne"),
            Component.For<ICustomService, CustomServiceTwo>().Named("TypeTwo"),
            Component.For<ICustomService, CustomServiceThree>().Named("TypeThree"),
            Component.For<ICustomService, CustomServiceOne>().IsDefault(),
            Component.For<ICustomServiceFactory>().AsFactory(new CustomServiceSelector())
        );
    }
}

public class CustomServiceSelector : DefaultTypedFactoryComponentSelector
{
    public CustomServiceSelector()
        : base(fallbackToResolveByTypeIfNameNotFound: true) { }

    protected override string GetComponentName(MethodInfo method, object[] arguments)
    {
       return (string) arguments[0];
    }
}

Вот что здесь происходит:

  • TypedFactoryFacility позволит нам использовать типизированные фабрики Виндзора.Это создаст реализацию нашего фабричного интерфейса для нас.
  • Мы регистрируем три реализации ICustomService.Поскольку мы регистрируем более одной реализации, каждая из них должна иметь имя.Когда мы разрешаем ICustomService, мы можем указать имя, и оно разрешит тип в соответствии с этой строкой.
  • Для иллюстрации я зарегистрировал другую реализацию ICustomService без имени.Это позволит нам разрешить реализацию по умолчанию, если мы попытаемся решить, используя нераспознанное имя.(Некоторые альтернативы просто генерируют исключение, или возвращают «нулевой» экземпляр ICustomService, или создают класс, такой как UnknownCustomService, который генерирует исключение.)
  • Component.For<ICustomServiceFactory>().AsFactory(new CustomServiceSelector()) говорит контейнеру создать проксикласс для реализации ICustomServiceFactory.(Подробнее об этом в их документации .)
  • CustomServiceSelector - это то, что принимает аргумент, переданный фабричному методу Create, и возвращает имя компонента (TypeOne, TypeTwo и т. Д.)это будет использоваться для выбора компонента.В этом случае мы ожидаем, что аргумент, переданный фабрике, будет таким же, как используемое нами регистрационное имя.Но мы могли бы заменить это другой логикой.Наша фабрика может даже принимать аргументы других типов, которые мы можем проверять и определять, какую строку возвращать.

Настройка приложения для использования контейнера Windsor

Теперь в StartUp измените ConfigureServices для возврата IServiceProvider вместо void и создания IServiceProvider, который объединяет службы, зарегистрированные непосредственно в IServiceCollection, с услугами, зарегистрированными в контейнере Windsor:

public IServiceProvider ConfigureServices(IServiceCollection services)
{
    services.AddMvc();
    var container = new WindsorContainer();
    container.Install(new WindsorConfiguration());
    return WindsorRegistrationHelper.CreateServiceProvider(container, services);
}

container.Install(new WindsorConfiguration()) позволяет WindsorConfiguration для настройки нашего контейнера.Мы могли бы просто сконфигурировать контейнер прямо в этом методе, но это хороший способ сохранить наши конфигурации контейнера организованными.Мы можем создать множество реализаций IWindsorInstaller или наших собственных пользовательских классов для настройки контейнера Windsor.
WindsorRegistrationHelper.CreateServiceProvider(container, services) создает IServiceProvider, который использует container и services.

Это работает?

Я бы не стал публиковать все это, не узнав сначала.Вот несколько тестов NUnit.(Я обычно пишу некоторые базовые тесты для конфигурации DI.)

При установке создается IServiceProvider, аналогично тому, что происходит при запуске приложения.Создает контейнер и применяет WindsorConfiguration.Я также регистрирую сервис напрямую в ServiceCollection, чтобы убедиться, что они хорошо играют вместе.Затем я объединяю их в IServiceProvider.

Затем я определяю ICustomerServiceFactory из IServiceProvider и проверяю, что он возвращает правильную реализацию ICustomService для каждой входной строки, включая запасной вариант, когда строка не является распознанным именем зависимости.Я также проверяю, разрешен ли сервис, зарегистрированный непосредственно с помощью ServiceCollection.

public class Tests
{
    private IServiceProvider _serviceProvider;
    [SetUp]
    public void Setup()
    {
        var services = new ServiceCollection();
        services.AddSingleton<IRegisteredWithServiceCollection, RegisteredWithServiceCollection>();
        var container = new WindsorContainer();
        container.Install(new WindsorConfiguration());
        _serviceProvider = WindsorRegistrationHelper.CreateServiceProvider(container, services);
    }

    [TestCase("TypeOne", typeof(CustomServiceOne))]
    [TestCase("TypeTwo", typeof(CustomServiceTwo))]
    [TestCase("TYPEThree", typeof(CustomServiceThree))]
    [TestCase("unknown", typeof(CustomServiceOne))]
    public void FactoryReturnsExpectedService(string input, Type expectedType)
    {
        var factory = _serviceProvider.GetService<ICustomServiceFactory>();
        var service = factory.Create(input);
        Assert.IsInstanceOf(expectedType, service);
    }

    [Test]
    public void ServiceProviderReturnsServiceRegisteredWithServiceCollection()
    {
        var service = _serviceProvider.GetService<IRegisteredWithServiceCollection>();
        Assert.IsInstanceOf<RegisteredWithServiceCollection>(service);
    }
}

Это все стоит?

Теперь, когда я понял это, я 'Я бы, вероятно, использовал его, если бы мне действительно понадобился такой вид функциональности.Это выглядит хуже, если вы пытаетесь ассимилировать оба с использованием Windsor с .NET Core и , впервые увидев его абстрактную фабричную реализацию. Вот еще одна статья с дополнительной информацией об абстрактной фабрике Виндзора без всякого шума о .NET Core.

...