Modern technology gives us many things.

Dependency Injection контейнеры .NET, допускающие полиморфное поведение

118

Уровень сложности Простой Время на прочтение 6 мин Количество просмотров 5.1K Блог компании RUVDS.com Программирование *.NET *C# *ООП * Туториал

Dependency Injection контейнеры .NET, допускающие полиморфное поведение

Иногда случается так, что при разработке приложения на платформе .NET с внедрением зависимостей и сервисами от контейнера требуется поддержка полиморфного поведения.

Когда, например, у интерфейса есть несколько реализаций, и их нужно грамотно расфасовать по правильным конструкторам так, чтобы всё из коробки работало.

Однако стандартный DI контейнер платформы долгое время не давал этой возможности.

В рамках этой статьи я решил напомнить альтернативы для решения этой задачи на тот случай, если вы ещё не успели переехать на .NET 8 или работаете в каком-нибудь Иннотехе, где в наличии только зеркало NuGet-пакетов, выпущенных до начала 2022 года.

▍ Постановка задачи

Допустим, у нас есть некоторый интерфейс, который имеет несколько реализаций:

public interface IDependency {} public class DependencyImplOne : IDependency {} public class DependencyImplTwo : IDependency {}
И мы хотим, используя стандартный DI контейнер .NET Core, внедрить в определённый сервис конкретную реализацию этого контракта.

То есть существует ряд сервисов, которые будут потреблять различные реализации IDependency.

Например, в некоторый BarService нужно засунуть DependencyImplOne, а в некоторый BazService нужно засунуть DependencyImplTwo:

public class BarService : IBarService { // dependency is DependencyImplOne public BarService(IDependency dependency) { } } public class BazService : IBazService { // dependency is DependencyImplTwo public BazService(IDependency dependency) { } }
К сожалению, стандартный контейнер не предоставляет встроенных возможностей для решения этой задачи.

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

Читать на TechLife:  Представлен Nissan X-Trail, который проедет практически везде. X-Trail Crawler создавали специально для преодоления бездорожья

Однако такая политика Microsoft приводит к тому, что даже для реализации такой элементарной вещи, как паттерн «Декоратор», нужна библиотека.

Scrutor, бесспорно, классный инструмент, но осадок всё же остаётся.

▍ Решение в лоб

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

  1. Создание фабрики

    В этом случае появляется некий дополнительный сервис, скажем, IDependencyProvider, который внедряется туда, где требуется наша зависимость, и на основе какого-либо условия создаётся нужная реализация:

    public class DependencyProvider : IDependencyProvider { public IDependency Create(string key) => key switch { «one» => new DependencyImplOne(), «two» => new DependencyImplTwo(), _ => throw new ArgumentOutOfRangeException(nameof(key)) }; }

  2. Создание Service Delegate

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

    public delegate IDependency DependencyCreator(string key); // … services.AddSingleton<DependencyCreator>(key => …);

  3. Внедрение коллекции зависимостей IEnumerable<IDependency> с её последующим перебором

    Вариант вполне рабочий, но отдаёт ещё большим code smell.

    Напомню, что зарегистрированную зависимость можно получить двумя способами:

    • экземпляром, тогда в наших руках окажется последняя регистрация;
    • коллекцией, тогда в наших руках окажутся все регистрации.

    Во втором случае потребление зависимости будет выглядеть примерно так:

    public class BarService : IBarService { // dependency is DependencyImplOne public BarService(IEnumerable<IDependency> dependencies) { _dependency = dependencies.FirstOrDefault(x => x.GetType() == typeof(DependencyImplOne)); } }

  4. Явная регистрация

    То есть в процессе регистрации сервиса потребителя нужно будет руками описать процесс его инстанциации:

    services.AddTransient<IBazService>(_ => new BazService(new DependencyImplTwo()));

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

Всё говорит о том, что необходимо посмотреть в сторону альтернативных инструментов.

▍ Simple Injector. Условная регистрация

Словосочетание «условная регистрация» означает, что зарегистрированная реализация будет внедрена в потребителей сервиса, удовлетворяющих определённому условию.

В этом контейнере такая возможность внедрять конкретную реализацию зависимости, согласно определённому контексту, реализована с помощью метода RegisterConditional:

container.RegisterConditional<ILogger, NullLogger>( c => c.Consumer.ImplementationType == typeof(HomeController)); container.RegisterConditional<ILogger, FileLogger>( c => c.Consumer.ImplementationType == typeof(UsersController)); container.RegisterConditional<ILogger, DatabaseLogger>(c => !c.Handled);
Из приведённого примера видно, что условная регистрация позволяет настроить поставку зависимости на основе определения типа потребителя.

То есть HomeController получит NullLogger, UsersController получит FileLogger, а все остальные потребители ILogger получат DatabaseLogger.

▍ Castle Windsor. Явное указание зависимостей

Возвращаясь к нашему примеру с логгером, допустим, что у сервиса ILogger есть две реализации: некий стандартный Logger и безопасный SecureLogger, который требуется использовать в некотором сервисе TransactionProcessingEngine.

В контейнере Castle Windsor это можно настроить, используя метод Dependency.OnComponent.

В нём указывается конкретная зависимость, которую требуется внедрить.

Перегрузок метода много, соответственно, вариантов это сделать несколько: от именованных зависимостей до явного указания типов.

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

container.Register( Component.For<ITransactionProcessingEngine>() .ImplementedBy<TransactionProcessingEngine>() .DependsOn(Dependency.OnComponent<ILogger, SecureLogger>()) );

▍ Autofac. Именованные сервисы

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

Например, у нас есть сервис IDisplay, отображающий какие-то произведения искусства IArtwork.

Чтобы указать, что мы хотим внедрить конкретную реализацию MyPainting, можно использовать атрибут KeyFilterAttribute.

По указанному ключу он проведёт фильтрацию и выберет нужную зависимость.

Пример:

public class ArtDisplay : IDisplay { public ArtDisplay([KeyFilter(«MyPainting»)] IArtwork art) { … } } // … var builder = new ContainerBuilder(); builder.RegisterType<MyPainting>() .Keyed<IArtwork>(«MyPainting»); builder.RegisterType<ArtDisplay>() .As<IDisplay>().WithAttributeFiltering(); // … var container = builder.Build();

▍ StructureMap. Настройка конструктора

Контейнер StructureMap позволяет решить задачу, используя настройку конструктора сервиса-потребителя.

Подход похож на то, что предлагает Autofac, но связывание происходит по имени параметра конструктора в потребителе контракта.

Например, у нас есть сервис для отправки сообщений IMessageService, который реализуют, соответственно, SmsService и EmailService. И есть некоторые сценарии, в которых нужно использовать разные имплементации. Тогда конфигурация будет выглядеть примерно следующим образом:

var container = new Container(x => { x.For<FooScenario>().Use<FooScenario>() .Ctor<IMessageService>(«messageService») .Is<SmsService>(); x.For<BarScenario>().Use<BarScenario>() .Ctor<IMessageService>(«messageService») .Is<EmailService>(); }); // … public class FooScenario { // sms public FooScenario(IMessageService messageService) } // … public class BarScenario { // email public BarScenario(IMessageService messageService) }

▍ А что там в .NET 8?

Ну а если вы планируете переезд, то у меня для вас хорошая новость: ASP.NET 8 наконец-то добавит многообразие зависимостей!

Реализовано это будет через механизм с ключами, похожий на Autofac.

Согласно контракту атрибута [FromKeyedServices], ключ имеет тип object, то есть можно использовать строки, енамки и другие варианты.

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

Возвращаясь к старому примеру, он преобразится следующим образом:

public interface IDependency {} public class DependencyImplOne : IDependency {} public class DependencyImplTwo : IDependency {} builder.Services.AddKeyedSingleton<IDependency, DependencyImplOne>(«one»); builder.Services.AddKeyedSingleton<IDependency, DependencyImplTwo>(«two»); // Далее использовать вот так, с помощью атрибута [FromKeyedServices]: public class BarService : IBarService { // DependencyImplOne public BarService([FromKeyedServices(«one»)] IDependency dependency) { } } public class BazService : IBazService { // DependencyImplTwo public BazService([FromKeyedServices(«two»)] IDependency dependency) { } }
Для меня как поклонника ООП это знаковая веха в развитии платформы, поэтому считаю, что ради этой киллер фичи можно смело планировать переезд на новый LTS-релиз!

▍ Заключение

Задачу обеспечения полиморфного поведения в контейнере внедрения зависимостей можно решить красиво и хорошо.

Особенно инструментами, которые реализуют различные способы регистрации и доставки сервиса, у которого существует множество реализаций. Среди них мне больше всего импонируют SimpleInjector и Castle Windsor.

С недавнего времени этот функционал завезли в платформу .NET. Однако, если вы не сможете переехать на восьмёрку в ближайшем будущем, не стоит отчаиваться — выход есть, как показано в статье.

Ещё я веду Telegram-канал StepOne, куда выкладываю много интересного контента про коммерческую разработку, C# и мир IT глазами эксперта.

Помоги спутнику бороться с космическим мусором в нашей новой игре! 🛸

Dependency Injection контейнеры .NET, допускающие полиморфное поведение

Теги:

  • ruvds_статьи
  • backend
  • csharp
  • dotnet
  • oop
  • di
  • polymorphism
  • inheritance
  • interface
  • class
  • implementation

Хабы:

  • Блог компании RUVDS.com
  • Программирование
  • .NET
  • C#
  • ООП

Если эта публикация вас вдохновила и вы хотите поддержать автора — не стесняйтесь нажать на кнопку Задонатить
Источник

Читать на TechLife:  Разъём питания 12VHPWR снова оказался причиной проблем. CableMod отзывает свои адаптеры 12VHPWR

Оставьте ответ

Ваш электронный адрес не будет опубликован.

©Купоно-Мания.ру