Как реализовать внедрение зависимостей с помощью MVC5 и MEF2 (на основе соглашений) в n-уровневом приложении?

#dependency-injection #controller #asp.net-mvc-5 #repository-pattern #mef2

#внедрение зависимостей #контроллер #asp.net-mvc-5 #репозиторий-шаблон #mef2

Вопрос:

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

  1. Уровень представления: ASP.Net Интерфейс MVC (представления / контроллеры и т.д.)
  2. Уровень служб (бизнес-уровень, если хотите): интерфейсы и DTO.
  3. Уровень данных: реализации интерфейса и классы Entity Framework.

В моем решении это 3 отдельных проекта. Уровень представления имеет только ссылку на уровень сервисов. Уровень данных также имеет только ссылку на уровень сервисов — так что это в основном соответствует дизайну, управляемому доменом.

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

Часть, с которой я сталкиваюсь, — это внедрение объекта, реализующего интерфейс, из уровня данных в уровень представления, который знает только об интерфейсах на уровне сервисов. Похоже, это именно то, для чего нужен DI, а фреймворки IoC (предположительно!) упрощают это, поэтому я решил попробовать MEF2. Но из десятков статей, вопросов и ответов, которые я прочитал за последние несколько дней, похоже, ничто не решает эту проблему таким образом, который соответствует моей структуре. Почти все они устарели и / или являются примерами простых консольных приложений, которые имеют все интерфейсы и классы в одной сборке, знают все друг о друге и полностью игнорируют точку свободной связи и DI. Я также видел другие, для которых требуется, чтобы DLL уровня данных помещалась в папку bin уровня представления и настраивала другие классы для просмотра там, что снова мешает идее свободной связи.

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

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

Ирония заключается в том, что современные шаблоны проектирования должны абстрагировать сложность и упрощать нашу задачу, но я бы уже наполовину закончил, если бы просто ссылался на DAL из PL и работал над фактической функциональностью приложения. Я был бы очень признателен, если бы кто-нибудь мог сказать: «Да, я понимаю, что вы делаете, но вам не хватает xyz. Что вам нужно сделать, это abc ‘.

Спасибо.

Ответ №1:

Да, я понимаю, что вы делаете (более или менее), но (насколько я могу судить) вам не хватает а) разделения контрактов и типов реализации на их собственные проекты / сборки и б) концепции настройки DI-контейнера, т.Е. Настройки того, какие реализации должны использоваться дляинтерфейсы.

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

a. Всегда должны быть проекты: MyNamespace.Something и MyNamespace.Something.Contracts

В общем, для DI у меня есть две сборки: одна для контрактов, которая содержит только интерфейсы, и одна для реализации этих интерфейсов. В вашем случае у меня, вероятно, было бы пять сборок: Presentation.dll , Services.dll , Services.Contracts.dll , DataAccess.dll и DataAccess.Contracts.dll .

(Другой допустимый вариант — поместить все контракты в одну сборку, давайте назовем это Commons.dll )

Очевидно, DataAccess.dll ссылки DataAccess.Contracts.dll , поскольку классы внутри DataAccess.dll реализуют интерфейсы внутри DataAccess.Contracts.dll . То же самое для Services.dll и Services.Contracts.dll .

Нет, разделяющая часть: ссылки и . Presentation Services.Contracts Data.Contracts Ссылки на службы Data.Contracts . Как вы видите, нет зависимости от конкретных реализаций. Вот в чем суть всего DI. Если вы решите обменять свой уровень доступа к данным, вы можете поменять DataAccess.dll местами, DataAccess.Contracts.dll оставаясь неизменным. Ни одна из ваших других сборок не ссылается на доступ к данным.dll напрямую, поэтому нет неработающих ссылок, конфликтов версий и т. Д. Если это непонятно, попробуйте нарисовать небольшую диаграмму зависимостей. Вы увидите, что нет стрелок, указывающих на какие-либо сборки, которых нет .Contracts в их названии.

Имеет ли это смысл для вас? Пожалуйста, спросите, если есть что-то неясное.

б. Выберите способ настройки контейнера

Вы можете выбирать между явной конфигурацией (XML и т. Д.), конфигурацией на основе атрибутов и регистрацией на основе соглашений. Хотя первое является проблемой по понятным причинам, я поклонник второго. Я думаю, что он более удобочитаем и прост в отладке, чем конфигурация на основе соглашений, но это дело вкуса.

Конечно, контейнерный тип объединяет все зависимости, которые вы сохранили в своей архитектуре приложения. Чтобы было понятно, что я имею в виду, рассмотрите конфигурацию XML для вашего случая: она будет содержать «ссылки» на все сборки реализации DataAccess.dll, ... . Тем не менее, это не подрывает идею развязки. Понятно, что вам нужно изменить конфигурацию при обмене сборкой реализации.

Однако, работая с конфигурациями, основанными на атрибутах или соглашениях, вы обычно работаете с упомянутыми вами механизмами автоматического обнаружения: «Поиск во всех сборках, расположенных в xyz». Для этого требуется разместить все сборки в каталоге applications bin. В этом нет ничего плохого, так как код должен быть где-то, верно?

Что вы получаете? Представьте, что вы развернули свое приложение и решили поменять уровень доступа к данным. Допустим, вы выбрали конфигурацию вашего контейнера DI на основе соглашений. Что вы можете сделать сейчас, так это открыть новый проект в VS, сослаться на существующий DataAccess.Contracts.dll и реализовать все интерфейсы любым удобным вам способом, если вы следуете соглашениям. Затем вы создаете библиотеку, вызываете ее DataAccess.dll , копируете и вставляете в папку программы вашего исходного приложения, заменяя старую DataAccess.dll . Готово, вы поменяли всю реализацию, даже не заметив ни одной из других сборок.

Я думаю, вы поняли идею. Это действительно компромисс, использующий IoC и DI. Я настоятельно рекомендую быть прагматичным в ваших проектных решениях. Не подключайте все, это просто становится беспорядочным. Решите сами, где DI и IoC действительно имеют смысл, и не поддавайтесь слишком влиянию религиозных дискуссий сообщества. Тем не менее, при разумном использовании IoC и DI действительно, действительно, действительно мощные!

Комментарии:

1. Спасибо за ваш отзыв, Марк. Ваш первый пункт — это то, что я действительно рассмотрел, хотя и немного по-другому для вас. Мои интерфейсы (контракты) находятся в BL, отдельно от реализации в DAL. Единственные зависимости связаны с интерфейсами (контрактами). Однако ваш второй пункт совершенно верен — мне не хватает четкого представления о том, как настроить контейнер DI. Зная, что мне нужно поместить data.dll в корзине приложений мне есть на что опереться. Трудно отфильтровать устаревшие методы (например, фабрики контроллеров) и найти четкие примеры того, что нужно делать. Я буду упорствовать!

Ответ №2:

Ну, я потратил на это еще пару дней (что сейчас составляет около недели в общей сложности) и добился небольшого дальнейшего прогресса. Я совершенно уверен, что у меня был правильно настроен контейнер с моими соглашениями, обнаруживающими правильные части для сопоставления и т. Д., Но я не мог понять, что казалось недостающим звеном для активации контроллера DI — я постоянно получал сообщение об ошибке, в котором говорилось, что я не предоставил конструктор без параметров. Итак, я закончил с этим.

Однако мне удалось продвинуться вперед с моей структурой и намерением использовать DI с IoC. Если кто-то натыкается на ту же стену, что и я, и хочет альтернативное решение: отказаться от MEF 2 и перейти на Unity. В последней версии (3.5 на момент написания) предусмотрено обнаружение по соглашению, и она просто работает как готовая из коробки — в ней даже есть довольно подробное руководство с отработанными примерами. Существуют и другие фреймворки IoC, но я выбрал Unity, поскольку он поддерживается MS и хорошо зарекомендовал себя в тестах производительности. Установите пакет начальной загрузки из NuGet, и большая часть работы будет выполнена за вас. В итоге мне пришлось написать только одну строку кода, чтобы отобразить весь мой DAL (они даже создают заглушку для вас, чтобы вы знали, куда ее вставить):

         container.RegisterTypes(
            AllClasses.FromLoadedAssemblies().Where(t => t.Namespace == "xxx.DAL.Repository"),
            WithMappings.FromMatchingInterface,
            WithName.Default);