Несколько месяцев назад, когда ASP.NET Core был еще в RC1, я делал первые неловкие попытки перевести свой тестовый проект с MVC 5 на ASP.NET Core. На тот момент в проекте уже использовалась IOC библиотека Simple Injector, и по этой причине я хотел продолжать использовать эту библиотеку, благо была поддержка с rc1. Я следил за выходом новых версий этой библиотеки и относительно недавно наткнулся на довольно интересную, на мой взгляд, статью, размещенную в тематическом блоге Simple Injector. Хоть статья и опирается на соответствующую библиотеку, но основная ее ценность в поднятии более общей проблемы — новой DI абстракции в ASP.NET Core.

Статья из блога IOC библиотеки Simple Injector
Автор Steve
Буду рад, если укажете на ошибки и неточности перевода.


Последние несколько лет Microsoft занималась разработкой новой версии платформы. NET: .NET Core. .NET Core — это полный редизайн существующей платформы .NET, нацеленный на настоящую кроссплатформенность и совместимость с облачными технологиями. Мы внимательно следили за развитием .NET Core и выпускали совместимые с платформой версии Simple Injector, начиная с RC1. С выпуском Simple Injector v3.2 мы официально поддерживаем .NET Core.

Как вы могли заметить, Microsoft добавила свою собственную DI библиотеку в качестве одного из основных компонентов фреймворка. Кто-то может воскликнуть «наконец-то!». Отсутствие такого компонента породило множество опенсорсных DI библиотек для .NET. И Simple Injector, очевидно, один из них.

Не поймите меня неправильно, мы благодарны Microsoft за продвижение DI в качестве основной практики в .NET — это, вероятно, приведет к появлению еще большего количества разработчиков, практикующих DI, что в свою очередь положительно скажется на нашей отрасли. Проблема, однако, начинается с абстракции, которую Microsoft определила на вершине своего встроенного DI контейнера. По сравнению с предыдущими Resolve абстракциями (IDependencyResolver и IServiceProvider), новая абстракция добавляет Register API поверх IServiceCollection. Суть этой абстракции для Microsoft в том, что другие (более функционально богатые) DI библиотеки могут подключаться в платформу, в то время как разработчики приложений, сторонних инструментов и фреймворков используют стандартизированную абстракцию для регистрации зависимостей. Это дает разработчикам приложений стандарт для интеграции DI библиотек на их выбор.

На первый взгляд может показаться, что иметь такую абстракцию — хорошая мысль. Вообще говоря, в нашей отрасли программного обеспечения мало проблем, которые не могут быть решены путем добавления (дополнительных) уровней абстракции. Хотя в данном случае рассуждения Microsoft ошибочны. Эксперты DI предупреждали их об этой проблеме с самого начала, но безуспешно. Mark Seemann довольно точно описал проблемы с этим подходом в целом здесь, где, на мой взгляд, Можно выделить следующие ключевые моменты:
  • Такой подход тянет в направлении наименьшего общего знаменателя
  • Такой подход подавляет инновации
  • Такой подход добавляет ад версионирования
  • Становится сложнее работать, не используя DI контейнер
  • Если разработкой адаптеров будут заниматься члены open-source сообщества, у этих адаптеров может быть разный уровень качества и они могут быть несовместимы с последней версией Conforming Container(прим.пер. имеется в виду шаблон, описанный здесь)

Это реальные проблемы, стоящие перед нами сегодня в новой DI абстракции в .NET Core. DI контейнеры часто имеют очень уникальные и несовместимые особенности, когда речь заходит об их registration API. Simple Injector, например, очень тщательно спроектирован в области обнаружения многочисленных ошибок конфигурации. Один из самых ярких примеров (а их гораздо больше) — его диагностические способности. Это одна из особенностей, которые в корне несовместимы с ожиданиями, которые будут у пользователей DI абстракции. А что же будут ожидать пользователи от новой абстракции?

Пользователей DI абстракции можно разделить на три группы: разработчики фреймворков, внешних библиотек и самих приложений; особенно разработчики фреймворков и внешних библиотек, которые сейчас задумываются над добавлением регистрации своих зависимостей через общую абстракцию. Так как для этих двух групп разработчиков практически невозможно проверить их код со всеми доступными адаптерами они будут тестировать свой код с помощью встроенного контейнера. И пока эти разработчики используют встроенный контейнер они будут (и, вероятно, должны) неявно ожидать стандартного поведения от встроенного контейнера — не важно какой адаптер используется. Другими словами, это встроенный контейнер определяет и контракт, и поведение абстракции. Каждый созданный адаптер должен быть точным надмножеством встроенного контейнера. Отклонение от нормы не допускается, так как это нарушило бы работу внешних библиотек, которые зависят от поведения по умолчанию встроенного контейнера.

Диагностика и верификация в Simple Injector — одни из многих возможностей, позволяющих вести разработку намного продуктивнее. Они позволяют находить проблемы, которые могли бы быть обнаружены намного позже в процессе разработки, если бы вы использовали другие DI библиотеки. Но выполнение диагностики и приложения и сторонних компонент вызовет проблемы — очень маловероятно, что сторонние компоненты будут автоматически «играть по правилам» с диагностикой Simple Injector. Велика вероятность, что они будут регистрировать зависимости таким образом, при котором Simple Injector будет считать их подозрительными, даже если они (надеюсь) хорошо протестировали регистрацию в особых случаях со стандартным контейнером. Гипотетическому адаптеру для Simple Injector было бы невозможно различить регистрации сторонних зависимостей и зависимостей приложения. Отключение диагностики полностью уберет один из самых важных предохранительных механизмов, в то время как сохранение диагностики приведет к ложным срабатываниям со стороны сторонних компонентов, а эти ложные срабатывания придется подавлять разработчикам приложения. Поскольку регистрация сторонних компонент в большинстве своем скрыта от разработчиков приложений, работа с всеми этими вопросами может оказаться сложной, разочаровывающей и иногда даже невозможной. Можно утверждать — хорошо, что Simple Injector находит проблемы со сторонними инструментами. Но если вы захотите обратиться к разработчикам сторонних библиотек и попытаетесь объяснить им «проблему», то вероятно они переведут стрелки на нас, ведь «очевидно» что мы разработали «несовместимый» адаптер.

Диагностические способности в Simple Injector — одни из многих несовместимостей, с которыми мы столкнулись, когда писали адаптер для .NET Core DI абстракции. Другие несовместимости:

Чтобы сделать полностью совместимый адаптер для Simple Injector потребуется удалить много известных возможностей фреймворка, тем самым изменяя существующее поведение библиотеки и превращая ее во что то, что нарушает принципы, которыми мы руководствовались при разработке. Непривлекательное решение. Мало того, что оно приведет к появлению ломающих совместимость изменений, но так же пропадут возможности и поведение, за которые Simple Injector и любили разработчики. В этом смысле наличие адаптера — это «душение инноваций», как описывал Mark. В Simple Injector мы сделали много инноваций, а адаптер сделает Simple Injector практически бесполезным для его пользователей. Адаптер так же ограничит нас от внесения дальнейших улучшений и новшеств. Кто-то может посчитать философию Simple Injector радикальной, но мы думаем иначе. Мы разработали его таким образом, который, как мы считаем, наилучшим образом подойдет для наших пользователей. И кол-во скачиваний NuGet пакета указывает, что многие разработчики согласны с нами. Соответствие определенному адаптеру будет мешать нам и дальше удовлетворять потребности наших пользователей.

Хотя видение Simple Injector может отклоняться от нормы больше, чем большинство других контейнеров, само определение общей абстракции для будущих DI библиотек — даже более радикальная или инновационная точка зрения, которая «душит инновации» будущих библиотек. Только представьте себе один из других контейнеров, внедряющих такие же проверки, которые обеспечивает Simple Injector? Такая особенность не может быть введена без нарушения контракта DI абстракции. Сам факт наличия такого адаптера может блокировать прогресс в нашей отрасли.

Этим объяснением, я, надеюсь, так же прояснил, что Microsoft DI абстракция даже не «наименьший общий знаменатель», потому что «наименьший общий знаменатель» подразумевает совместимость со всеми DI библиотеками. Как я высказался здесь, есть шанс, что ни одна из существующих DI библиотек не совместима полностью с этой абстракцией. Частично это сводится к тому, что, хотя встроенный контейнер определяет контракт абстракции, проект с тестами этой абстракции испытывает недостаток в солидном количестве тестовых примеров, которые бы полностью определили точное поведение во всех сценариях. До сих пор все реализации адаптера были попыткой угадать и надеяться на лучшее — на то, что реализация адаптера практически синхронизирована с поведением встроенного контейнера. Разработчики Autofac к примеру, только что поняли, что у них есть довольно серьезные проблемы с совместимостью и в итоге пришли к тем же самым выводам что и мы.

Это не было бы так плохо, если бы библиотека DI Microsoft была богата возможностями и содержала такие функции, как верификация и диагностика из Simple Injector. Тогда мы все могли бы использовать одну и ту же полнофункциональную DI библиотеку. К сожалению, реализация далеко не так функционально богата, а сама Microsoft описала их реализацию как

Минималистичный DI контейнер, полезный в тех случаях, когда вам не нужны какие-либо дополнительные возможности для инъекций

Что еще хуже, с тех пор как встроенный контейнер определяет контракт абстракции, добавление новых возможностей во встроенный контейнер сломает все существующие адаптеры! Любой сторонний разработчик, использующий абстракцию, будет тестировать (свою библиотеку) только с помощью встроенной библиотеки (.NET Core's DI). А когда библиотека стороннего разработчика начинает зависеть от какой-то функции, добавленной во встроенный контейнер, и который при этом еще не поддерживается адаптером, то что-то сломается и пострадает разработчик приложения. Это один из аспектов ада версионирования, который Mark Seemann обсуждает в своем блоге. Будем надеяться, что, по крайней мере, Microsoft будет увеличивать основной номер версии (major version number) каждый раз, когда они будут вносить изменения. Мало того, что их текущая реализация «минималистична», она никогда не сможет развиваться в полностью пригодный многофункциональный DI контейнер, потому что они загнали себя в угол: каждое будущее изменение — это изменение, ломающие совместимость, от которого всем будет плохо.

Лучшее решение — избегать использование абстракции и ее адаптеров полностью. Как Mark Seemann довольно точно объяснил здесь и здесь, библиотекам и фреймворкам, возможно, не нужно использовать DI контейнер вообще. К сожалению, сам факт определения абстракции намного усложняет попытку избежать ее использования. Определяя абстракцию и активно продвигая ее использование, Microsoft приводит тысячи сторонних разработчиков библиотек и фреймворков к тому, чтобы они перестать думать об определении правильной абстракции для библиотеки и фреймворка (статьи Mark Seemann ясно описывают это). Разработчики больше не думают об этом, потому что Microsoft заставляет их верить, что весь мир нуждается в одной общей абстракции. Мы уже видели, как новые фабричные интерфейсы для MVC вступали в игру очень поздно (например, как IViewComponentActivator абстракции до начала RC2). И если мы видим, что команда MVC доводит такие ошибки до столь позднего этапа цикла разработки, то что мы можем ожидать от всех тех разработчиков, которые начинают разрабатывать на новой платформе .NET?

Заключение


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

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

Будьте на связи.
Поделиться с друзьями
-->

Комментарии (37)


  1. Flaksirus
    19.07.2016 23:30
    -5

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


    1. lair
      19.07.2016 23:37
      +2

      Только это не стандарт, а реализация. И что делать, если она не покрывает мои потребности?


      1. Flaksirus
        19.07.2016 23:40
        +2

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


        1. lair
          19.07.2016 23:43
          +1

          Угу, и будет реализация, имеющая объединение всех фич — иными словами, зоопарк.


          1. Flaksirus
            19.07.2016 23:48

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


            1. lair
              19.07.2016 23:49
              +1

              Кто будет определять "действительно нужные фичи"?


              Куча реализаций одного и того же — это не зоопарк, а свобода выбора.


              1. DrPass
                20.07.2016 10:29

                Свобода выбора — это когда вы можете выбрать что вы хотите или что вам нужно. А когда вы видите массу по своей сути одинаковых решений, которые в целом равнозначны, делают одно и то же, но… слегка по-разному, это уже зоопарк. Я не могу сказать, чем, например, NInject лучше или хуже Windsor. Я сам для себя, конечно, могу выбрать какой-то любимый IoC-контейнер методом научного тыка, но бывает, приходится поддерживать и чужие проекты. И лично я бы предпочел, чтобы фича, которая продвигается как «мастхэв» процесса разработки на платформе, была реализована стандартными средствами платформы, а не кучей сторонних компонент.


                1. CHROMIGO
                  20.07.2016 19:40

                  Кстати насчет научного тыка и чем они отличаются. Есть неплохая статья в блоге Daniel Palm с небольшим сравнением производительности и доступных фич среди множества IoC контейнеров, довольно часто обновляется.


                1. lair
                  20.07.2016 22:51
                  +1

                  А когда вы видите массу по своей сути одинаковых решений, которые в целом равнозначны, делают одно и то же, но… слегка по-разному, это уже зоопарк

                  Нет, это все еще свобода выбора.


                  И лично я бы предпочел, чтобы фича, которая продвигается как «мастхэв» процесса разработки на платформе

                  Проблема в том, что когда DI-контейнер становится musthave для платформы — это не очень хорошо.


  1. indestructable
    20.07.2016 00:36
    +2

    Стандартный контейнер определяет необходимый для .NET Core минимум способов регистрации зависимости: transient, singleton, scoped. Их реализует любой контейнер. Нужно ли было усложнить интерфейс добавления зависимостей и сделать его более богатым и совместимым со всеми фичами Simple Injector, Ninject, Castle Winsdor, Autofac и т.д?


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


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


    В кои-то веки Майкрософт предпочла простоту универсальности.


    1. lair
      20.07.2016 01:59

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


      1. pakrym
        20.07.2016 08:42

        Какой бы вы хотели видеть возможность подключения сторонних контейнеров в ASP.NET?


        1. AndreyRubankov
          20.07.2016 11:07

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

          Одна из удачных реализаций: помечать мета-данными конструкторы / сеттеры / поля, А конфигурация всех зависимостей остается на плечах имплементации.
          В результате все контейнеры смогут выполнять свою функци и не будут ограничены в том, как организовывать свою конфигурацию, а значит и возможностей для развития у них будет больше.


          1. dotnetdonik
            20.07.2016 11:49

            Одна из удачных реализаций: помечать мета-данными конструкторы / сеттеры / поля, А конфигурация всех зависимостей остается на плечах имплементации.


            Сейчас так и есть, конфигурация специфичная для каждого контейнера как была так и осталась, со всем фичами характерными для каждого контейнера.
            http://docs.autofac.org/en/latest/integration/aspnetcore.html

            Проблема, которая возникла у вендоров состоит совсем в другом — необходимость обеспечения совместимости для интерфейсов, которые МС использует для регистрации внутренних компонентов ASP.NET Core.


          1. indestructable
            20.07.2016 20:25

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


        1. lair
          20.07.2016 22:49

          Я предпочел бы мыслить об этом не в терминах "как подключить сторонний контейнер" (потому что я могу вообще не хотеть пользоваться контейнерами), а "откуда asp.net берет сервисы", и в этом случае паттерны в WebAPI были, мне кажется, весьма неплохими.


      1. dotnetdonik
        20.07.2016 09:07

        А как без него? Раньше тоже был такой интерфейс —
        http://www.asp.net/web-api/overview/advanced/configuring-aspnet-web-api#services

        Все что спасало сторонних вендоров от текущих проблем, что не было одного DI зашаренного между компонентами фреймворка и зависимостями определяемыми разработчиками.
        Был Resolver который где-то мог использоваться фреймворком где-то нет при содании клиентских типов. И набор сервис локатор для фреймворка.

        В свою очередь это порождало другую боль для разработчиков:
        1. Необходимость для вендоров, а иногда и самих разработчиков переопределять стандартные фабрики и провайдеры своими и прогонять обьект через сторонний контейнер.
        2. Непонятный жизненный цикл компонентов — когда надо помнить, что вот у меня эти фильтры живут как синглтоны, а эти transient, а эти Handlers опять singltones.

        Мне не совсем понятен отсыл к предыдущим реализациям 'DI' в Mvc и Web Api даже на фоне текущих проблем он выглядит мягко говоря странным.


        1. lair
          20.07.2016 22:54

          А как без него? Раньше тоже был такой интерфейс —
          http://www.asp.net/web-api/overview/advanced/configuring-aspnet-web-api#services

          Это не "такой же" интерфейс. Это специфичный сервис-контейнер для WebAPI, а не обобщенный DI-контейнер для любого случая. Что более важно, он делегирует свои запросы на resolve в контейнер (если такой есть), но не делегирует туда register.


          Непонятный жизненный цикл компонентов — когда надо помнить, что вот у меня эти фильтры живут как синглтоны, а эти transient, а эти Handlers опять singltones.

          Эта проблема не решается унифицированным DI, она решается унифицированным Resolve.


          1. dotnetdonik
            20.07.2016 23:08

            Что более важно, он делегирует свои запросы на resolve в контейнер (если такой есть), но не делегирует туда register.


            В таком случае, что вам мешает делать как прежде? Использовать контейнер от стороннего вендора вне Asp.Net Core фрейморка и написать UnityGlobalFilterProvider, AutofacFilterProvider, MyControllerFactory и подключить их в IServiceCollection не мешая два контейнера?
            Хотите использовать два контейнера — используйте.


            1. lair
              20.07.2016 23:10

              Да собственно ничего не мешает, кроме того, что я буду идти поперек ожидаемой практики — насколько я ее понимаю.


      1. indestructable
        20.07.2016 20:22
        +1

        Новый дотнет модульный, тот же асп нет конфигурируется путем регистрации и использования зависимостей. Какая этому альтернатива?


        1. lair
          20.07.2016 22:55

          Можно использовать специфичный интерфейс регистрации, а не обобщенный контейнер.


          1. pakrym
            22.07.2016 18:49

            То есть пользователь должен будет в правильном порядке зарегистрировать все сервисы необходимые MVC и EF? https://github.com/aspnet/Mvc/blob/52a7c112e8c0369b4b1626d750fb3d5b43db65bd/src/Microsoft.AspNetCore.Mvc.Core/DependencyInjection/MvcCoreServiceCollectionExtensions.cs#L121


            1. lair
              22.07.2016 19:57

              Конечно, нет. Никто не отменял дефолтные регистрации.


              Кстати, вопрос: "These are consumed only when creating action descriptors, then they can be de-allocated". Описанное поведение — оно определено фреймворком?


              1. pakrym
                22.07.2016 20:41

                Тогда я не очень понимаю, какой интерфейс регистрации должен использовать MVC?

                Этот комментарий объясняет почему они зарегистрированы как transient, он относится к особенностям работы MVC, а не DI


                1. lair
                  22.07.2016 21:39

                  Тогда я не очень понимаю, какой интерфейс регистрации должен использовать MVC?

                  А ему точно нужен интерфейс регистрации? Разве нельзя просто резолвить дефолтные сервисы без всякой регистрации?


                  Проблема в том, что регистрация — это не часть DI как паттерна, это часть конкретной контейнерной реализации DI-шаблона. Но кто-то ведь может делать DI иначе, вплоть до того, что просто иметь написанный руками composition root с конструкторами.


                  Этот комментарий объясняет почему они зарегистрированы как transient, он относится к особенностям работы MVC, а не DI

                  Да, я понимаю. И именно это и расстраивает: в итоге мы все равно должны знать, как именно работает потребляющий фреймворк (и мы не можем повлиять на то, как он работает), когда определяем жизненный цикл. Так зачем нам вообще здесь определение жизненного цикла компонента, на что оно повлияет?


                  1. pakrym
                    22.07.2016 21:53

                    Если MVC будет использовать сервисы по умолчанию вы теряете возможность менять их поведение.

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


                    1. lair
                      22.07.2016 21:56

                      Если MVC будет использовать сервисы по умолчанию вы теряете возможность менять их поведение.

                      Конечно же, нет. По умолчанию — это когда есть способ переопределения. Как, собственно, и в MVC, и в WebAPI было сделано.


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

                      Вы хотите сказать, что раньше нельзя было переопределить нужные сервисы? Или просто что теперь для переопределения доступно больше сервисов?


                      1. pakrym
                        22.07.2016 22:42

                        Можно было переопределить только те для которых команда MVC решила добавить вызов GetService.


                        1. lair
                          22.07.2016 22:59

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


                          1. pakrym
                            23.07.2016 00:04

                            А что бы вы хотели переопределить из того что получается не из контейнера?


                            1. lair
                              23.07.2016 09:29

                              Дело не в том, что я хотел бы переопределить. Дело в том, что я бы хотел иметь возможность обойтись без DI-контейнера.


  1. pakrym
    20.07.2016 05:38
    +1

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


    1. Ununtrium
      22.07.2016 10:57

      А вы это кто, простите? Команда ASP.NET в MS?


      1. pakrym
        22.07.2016 18:46

        Да


  1. Gentlee
    20.07.2016 13:59
    +1

    Я правильно понял что они сделали слишком толстый, негибкий интерфейс? Например добавил туда scope, еще и нерасширяемого типа enum? Тогда почему нельзя было об этом так и написать, снабдив кодом?

    Или суть в том что этот интерфейс не нужен в принципе? Почему тогда не привести примеры кода почему это плохо, а не писать непонятные фразы типа «Такой подход тянет в направлении наименьшего общего знаменателя»?


    1. CHROMIGO
      20.07.2016 16:28

      Меня тоже немного смутила эта фраза. Во многих местах статьи автор ссылается на другие записи в блогах, в т.ч. Mark Seemann Conforming Container , в которой он более подробно описывает эти недостатки(кода там правда не много). Признаюсь, я не глубоко не вникал в стаью Mark-a, но судя под этим «наименьшим общем знаменателем» он имел в виду пересечение или объединение всех фич разных IOC контенеров.