Andriy Shyrokoryadov

.Net developer, data scientist

№3 Внедрение зависимостей в Asp.Net Core.

Текст к видео "Внедрение зависимостей в Asp.Net Core в приложении Asp.Net Core MVC / Web API" на канале YouTube

Приветствую Вас на моем канале. Это очередное видео на тему технологии ASP.Net Core. В прошлом видео мы рассматривали тематику внедрения зависимостей, но в более широком контексте – в контексте приложения .Net без привязки к какому-то определенному типу приложений. Сегодня мы рассмотрим внедрение зависимости в контексте ASP.Net Core. Стоит вспомнить, что в приложениях ASP.Net Core встроен изначально фреймворк внедрения зависимостей. И этот фреймворк необходимо рассматривать как очень толстый намёк на тонкое обстоятельство – без внедрения зависимостей хорошее современное приложения написать невозможно. Сегодня мы познакомимся с этим фреймворком. Теория была в прошлом видео, сегодня будут только практические примеры. Что можно еще сказать? Всегда есть люди, которым встроенных базовых возможностей будет не хватать. Для таких людей мы сегодня рассмотрим интеграцию приложения ASP.Net Core с фреймворком внедрения зависимостей Autofac. Этот фреймворк я использовал в предыдущем видео, так что у моих постоянных зрителей уже должно быть некоторое понимание о чём идет речь, а если данное видео вы не смотрели, то ссылочка будет в правом верхнем углу и в описании к данному видео. Кстати, я хочу еще раз напомнить, что практические примера в этом и других видео доступны на моём профиле GitHub. Вы можете их скачать и запустить локально, сделать debug, самостоятельно проверить как всё работает.

Когда речь идет о внедрении зависимостей, то всегда, кроме фреймфорка, должны быть эти самые зависимости (или услуги, или интерфейсы – в данному контексте это всё одно и тоже) и их реализации, то есть конкретные классы, которые реализуют определенные интерфейсы, называемые в данном контексте зависимостями. Чтобы данное видео было вдвойне полезным я не стал выдумывать какой-то абстрактный пример с калькулятором или чем-то подобным. Я решил, что в рамках данного видео я объясню шаблон проектирования «Репозиторий» и классы, написанные с помощью данного шаблона, мы будет внедрять в качестве зависимостей в нашем приложение ASP.Net Core.

Начинаем с объяснения шаблона проектирования «Репозиторий», чтобы далее было понятно, с чем мы будем работать. Давайте представим себе библиотеку. В библиотеке можно получить книги если вы записаны читателем в данной библиотеке. Также руководство библиотеки может добавлять книги в библиотеку, удалять старые и ветхие экземпляры книг или заменять старые издания одной и той же книги на равнозначные им новые издания, то есть определенным образом обновлять книжный фонд. Районная администрация, которая формально финансирует библиотеку, может запросить список всех книг, если есть такая необходимость. То есть по большому счету мы имеем делом с хранилищем чего-либо (в данном случае с библиотекой, которая является хранилищем книг) и мы можем в зависимости от наших полномочий взаимодействовать с данным хранилищем. Изначально латинское слово repositorium использовалось в значении «гробница», но потому к этому значению было добавлено еще одно – «хранилище».

Итого, если в нашем задании мы видим, что у нас будет необходимость создать класс, который сам в себе имеет функции хранилища или будет взаимодействовать с некоторым хранилищем, то возможно имеет смысл использовать шаблон «Репозиторий». Как понять, что мы на 100% имеем дело с хранилищем данных и здесь нам необходим «Репозиторий». Очень просто. Если мы на этом хранилище будем выполнять операции CRUD, то можно сказать, что «это наши клиент». Операции CRUD – это операции создания, чтения, обновления и удаления. Почему CRUD, потому что это аббревиатура от первых букв английских слов: CREATE, READ, UPDATE, DELETE - создание, чтение, обновление и удаление. Возможно, названия этих методов будут немного другие, но, если смысл тот же – смело создавайте класс «Репозиторий». О каких других названиях может идти речь? Ну например вместо CREATE – создания, у вас может быть добавления, а вместо READ – то есть чтения, у вас может быть «получение». Ну тут я думаю понятно – смысл этих операций абсолютно одинаков, независимо от названия.

А как шаблон «Репозиторий» выглядит спросите вы? Я рад что у вас такой вопрос возник. И на этот вопрос я отвечу с использованием практического примера. Посмотрите на проект, который я подготовил для данного видео – здесь есть папка Repository с 3 файлами. Один из этих файлов — это интерфейс репозитория, который, собственно, декларирует операции CRUD. Что мы здесь имеем? Несколько метод, а именно:

  • Add – добавляет или создаёт новый элемент в хранилище
  • Update – обновляет элемент в хранилище
  • Remove – удаляет элемент из хранилища
  • Get – читает / получает элемент по определенному идентификатору
  • GetAll – получает все элементы из коллекции.

Обратите внимание, что в данном конкретном пример мы имеем дело с обобщенном интерфейсом. О обобщенном программировании у меня снято видео на канале – ссылка в правом вернем углу, а также в описании к данному видео. Здесь смысл обобщения в том, чтобы для каждого типа элементов не создавать свой интерфейс, а использовать один и тот же интерфейс, изменяя только тип. Вы также можете заметить, что на тип, который мы можем использовать в нашем хранилище, мы накладываем только одно ограничение – данный тип должен реализовывать интерфейс IIdentifiable. Данный интерфейс это всего лишь одно свойство – Id, для однозначной идентификации объекта. Интерфейс IIdentifiable не является требованием шаблона «Репозиторий» - я сделал это для удобства, но моно было обойтись и без него.

Далее в папке Repository есть еще 2 файла – это CollectionBasedRepo и DataBaseRepo. Оба файла – класса это реализации интерфейс IRepo с типом данных Book. Если мы посмотрим на класс Book, то мы увидим, что это типичный класс – модель. Он содержит только свойства и ничего больше. Никакой бизнес-логики. Только данные. Если вы видите, что в классе – модели реализована какая-то бизнес логика, например валидация или какие-то специфические статически методы, то спешу вас расстроить – вы имеете дело с не совсем качественным кодом. Единственный код, который может еще появиться в классах - моделях – это атрибуты (например атрибуты для той же валидации или атрибуты связанные с данными, например для EntityFramework), реализации стандартных интерфейсов .Net, например IComparable (для сортировки объектов класса - модели), а также переопределение стандартных методов ToString, Equals, GetType, GetHashCode. Всё. Ничего другого в классе модели быть не может. А если есть, то это значит, что кто-то сделал ошибку. Это было небольшое лирическое отступление на тему чистоты классов – моделей. Возвращаемся к объяснению. В классе Book у нас есть свойство типа Author. Класс Author это опять-таки типичный класс – модель. Ничего лишнего. У классов Book и Author есть одна общая черта. Обратите внимание, как реализован конструктор. Имплементация таким образом позволяет гарантировать, что все свойства будут инициализированы. Например, свойство Author класса Book никогда не будет null.

Возвращаемся к классам CollectionBasedRepo и DataBaseRepo. Первый класс реализует хранилище в виде внутренней коллекции, которая называется _repository. Код данного класса достаточно простой, реализованы все методы интерфейса IRepo и данная реализация опирается на функционал типа хранилища, то есть на функционал типа List. Второй класс – чуть сложнее. Он реализует хранилище данных не во внутренней коллекции, а в базе данных. Здесь я явно реализовал только 2 метода Get и GetAll. Для работы с базой данных я не использовал специфические фреймворки, я просто взял и написал всё в старом добром **ADO.NET**. Как видите этот код достаточно громоздкий, и чтобы не загромождать его еще больше я не имплементировал методы Add, Remove и Update. Там будет выброшено исключение – как раз будет возможность посмотреть, как поведет себя наше приложение ASP.Net Core в случае ошибки. Если вы будете запускать пример с этим классом у себя локально, то необходимо выполнить 3 условия:

  • у вас должен быть локальный сервер базы данных MS-SQL
  • используя данный локальный сервер, вы должны подменить значение переменной connectionString на значение, которое соответствует вашему серверу
  • на вашем сервер баз данных вы должны запустить скрипт CREATE_TEST_TABLES из папки Scripts. Этот скрипт создаст в вашей базе данных 2 таблицы и добавит в них соответствующие данные.

Итак, у нас есть интерфейс хранилища и его 2 реализации. Где мы это всё будем использовать? Мы будем это всё хозяйство использовать в контроллере BooksController. Вы можете заметить несколько интересных моментов в этом контроллере:

  1. Первое – это то, что данный контроллер в своём конструкторе принимает зависимость **IRepo**. То есть, чтобы создать объект контроллера нам необходима конкретная реализация IRepo.
  2. Второе – все методы или действия контроллера так или иначе используют функциональность объекта типа **IRepo**.
  3. Третье – над каждым методом или действием контроллера используется атрибут – глагол запроса. Более правильное название для глаголов запросов – HTTP методы. Таких методов около 15, но в нашем конкретном примере используется только несколько. Некоторые атрибуты задекларированы с параметрами, некоторые нет. Это вопрос маршрутизации, и он будет рассмотрен в одном из следующих видео. Единственное, что я могу вам сказать на данный момент – это то, что если мы вышлем 2 одинаковых запроса к нашему приложению, на с разными методами HTTP, то они будут обработаны разными методами контроллера. Например, посмотрите на методы Add и Update – сигнатуры методов одинаковы, но методы HTTP в атрибутах разные.

Меня кто-то попросил в комментариях снять видео на тему жизненного цикла контроллера – возможно такое видео будет создано. На данный момент я могу сказать, что контроллер создается отдельно для каждого запроса. То есть в приложении ASP.Net Core не существуют изначально созданные контроллеры, которые ожидает запросы. Наоборот, когда запрос приходит, то в зависимости от адреса, на который он пришел, создается определенный контроллер. Опять-таки здесь мы цепляемся за тему маршрутизации, а это не тема нашего видео.

С контроллером на данный момент мы разобрались. Что у нас есть еще? У нас есть стандартный класс Programm. где создаются 2 хоста – один по умолчанию и второй для использования с фреймворком Autofac. Если мы посмотрим на код создания этих 2 хостов, то заметим, что они отличаются 2 элементами:

  • используются разные классы Startup – о разницах в этих классах мы еще поговорим сегодня.
  • в хосте с Autofac добавлена строчка .UseServiceProviderFactory(new AutofacServiceProviderFactory()). Данная строчка определяет нового поставщика зависимостей – в этом случае это фреймворк Autofac

Рассмотрим первый пример, в котором мы используем стандартный фреймворк внедрения зависимостей ASP.Net Core. То есть познакомимся с классом Startup. Данный класс, как мы уже знаем, имеет стандартную структуру. Обратите внимание на метод ConfigureServices – здесь появилось что-то новенькое. Есть вызов метода AddSingleton. Данный метод имеет несколько перегрузок, но я использовал перегрузку с указанием типа услуги - **typeof(IRepo)** и её имплементации - **typeof(CollectionBasedRepo)**. Также обратите внимание на название метода – добавь синглтон. Что такое синглтон было рассказано в одном из моих видео на эту тему – ссылка будет в правом верхнем углу и в описании к фильму. Вкратце это класс, у которого доступен единственный экземпляр в системе. На человеческий язык эту строчку кода можно перевести следующим образом – если что-то в моем коде будет требовать интерфейс IRepo, то создай экземпляр класса CollectionBasedRepo если он еще не был создан, а если он уже был создан, то верни существующий экземпляр. Как мы знаем код, который требует IRepo

  • это наш контроллер. То есть в данной конфигурации при создании контроллера всегда будет использовать один и тот же экземпляр класса CollectionBasedRepo.

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

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

Теперь давайте откомментируем строчку services.AddTransient и попытаемся разобраться какое отличие этой строчки от строчки с кодом AddSingleton. Сразу заметим, что аргументы этих двух методов одинаковы. Данная строчка services.AddTransient на человеческом языке обозначает следующее: если что-то в моем коде будет требовать интерфейс IRepo, то создай новый экземпляр класса CollectionBasedRepo. Всегда. У нас всегда будет новый экземпляр класса CollectionBasedRepo. Кстати, стоит отметить, что слово “transient" в переводе с английского обозначает «кратковременный, мимолётный, недолговечный, преходящий, скоротечный». Давайте запустим наш пример с этой конфигурации и проанализируем, что происходит.

Мы заметили, что наше приложение работает неправильно. Наши изменения в репозитории не сохраняются. Всё, потому что при каждом новом запросе мы получаем новый экземпляр класса CollectionBasedRepo в котором не сохраняется измененное состояние. Данное состояние хранится в поле типа List. Получается мы должны вернуться к предыдущему коду с методом AddSingleton. Какой можно сделать вывод из всей этой ситуации? Классы, которые имеет внутреннее состояние, например такие как класс CollectionBasedRepo с внутренним состоянием в поле типа List, должны быть синглтонами и регистрироваться с помощью метода AddSingleton. Соответственно классы без внутреннего состояния, например класс Калькулятор из предыдущих видео, который только выполняет расчеты и ничего не хранит внутри себя могут регистрироваться с помощью метода AddTransient.

Теперь давайте представим такую ситуацию – поступило новое задание изменить имплементацию репозитория на такую, которая бы использовала базу данных. К счастью, наш код написан достаточно хорошо и такое изменение мы можем сделать очень быстро. Почему наш код можно назвать хорошим:

  • наш код зависит от абстракций, а именно от интерфейса IRepo
  • наш код использует внедрение зависимостей

Для реализации задания нам необходимо сделать 2 операции – написать новую имплементацию интерфейса **IRepo** для базы данных. Напомню, что такая имплементация у нас уже есть – это класс **DataBaseRepo**. А затем зарегистрировать класс DataBaseRepo вместо класса CollectionBasedRepo как реализацию зависимости IRepo. Изменения действительно не очень много, а как я уже говорил – меньше изменений меньше ошибок. Вот только возникает вопрос – а как регистрировать данный класс, с помощью метода **AddTransient** или **AddSingleton**. Давайте попробуем сначала зарегистрировать с помощью одного метода, а потом второго и посмотрим есть ли разница.

Как мы видим разницы нет. Это происходит, потому что состояние нашего хранилища находится в базе данных, а не в классе. Поэтому то ли это новый класс, или существующий разницы особой нет. Состояние берется из базы. В таких ситуациях, когда нет разницы какой метод использовать лучше использовать AddTransient.

Мы познакомились со встроенным механизмом внедрения зависимостей, однако, как уже было сказано, некоторым данного механизма может быть недостаточно. В таком случае с приложением ASP.Net Core можно использовать сторонние фреймворки внедрения зависимости. Сегодня я покажу вам как использовать фреймворк Autofac с ASP.Net Core. На это у меня 2 причины. Этот фреймворк я достаточно хорошо знаю и этот фреймворк имеет достаточно хорошую документацию на тему интеграции с ASP.Net Core. Хотя справедливости ради стоит сказать, что не всё описано в этой документации и мне пришлось пораскинуть мозгами чтобы это всё заработало вместе. О неточностях в документации я скажу пару слов чуть позже.

Итак, давайте посмотрим на класс StartupWithAutofac. Это стандартный класс, ничего особенного. Я его скопировал из примера в документации и удалил из него то, что нам не будет надо. Также для тех, кто будет самостоятельно запускать данный пример на своем компьютере я перевел некоторые комментарии с английского на русский язык. По сравнению со стандартным классом Startup у нас появился метод ConfigureContainer – здесь мы регистрируем наши зависимости. В этом методе используется стандартный синтаксис фреймворка Autofac, с помощью которого мы регистрируем класс CollectionBasedRepo в качестве реализации зависимости IRepo. Дополнительный метод SingleInstance означает, что будет использоваться синглтон. В случае класса CollectionBasedRepo это желаемое состояние, так как мы уже знаем из предыдущего примера, что наш код работает неправильно если CollectionBasedRepo не явялется синглтоном.

Также давайте обратим внимание на метод ConfigureServices. Я здесь добавил 1 строчку services.AddAuthorization();. Без этой строчки у меня в приложении возникала ошибка в методе Configure на методе app.UseAuthorization. Так происходило, потому что контейнер Autofac не содержал в себе услуги для авторизации. При вызове метода app.UseAuthorization приложение ASP.Net Core пыталось найти регистрации для этой услуги в контейнере зависимостей Autofac, но безрезультатно. В итоге чтобы исправить эту ошибку я должен был указать явно эту услугу в методе ConfigureServices. Эта информация отсутствует в документации, так что всегда следует иметь в виду, что любая документация не идеальна и может содержать ошибки.

Давайте запустим эти 2 примера и посмотрим, как это работает. [комментарий к примеру]

Подведем итог сегодняшнего видео:

  • мы познакомились с шаблоном проектирования «Репозиторий»
  • мы научились работать со встроенным фреймворком внедрения зависимостей в ASP.Net Core
  • мы научились интегрировать приложение ASP.Net Core со сторонним фреймворком внедрения зависимостей Autofac

Надеюсь, что это видео не вышло очень нудным, а может даже и интересным. По крайней мере я рад, что мы уже начали что-то делать, что похоже на реальное приложение, а не разговариваем о каких-то внутренних механизмах приложений ASP.Net Core. Фактом остается то, что внутренне устройство приложений также надо знать – это может пригодиться на собеседовании, но все-таки интереснее писать код, результат работы которого можно сразу же проверить.

Для закрепления данного материала я рекомендую скачать пример кода с моего профиля GitHub и попытаться самостоятельно запустить данное приложении и проанализировать его работу. Все необходимы ссылки в описании к данному видео и на моей веб-странице с текстами к видео на моем канале. Ну а пока у меня всё по данной теме. Благодарю за внимание и до новых встреч. Я также буду вам благодарен за лайк или положительный комментарий к данному видео – это помогает в продвижении канала. Спасибо!

Пример кода из видео на GitHub

Для открытия файла проекта необходимо Visual Studio 2019.