.Net developer, data scientist
Приветствую Вас на моем канале. Это очередное видео на тему технологии 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. Что мы здесь имеем? Несколько метод, а именно:
Обратите внимание, что в данном конкретном пример мы имеем дело с обобщенном интерфейсом. О обобщенном программировании у меня снято видео на канале – ссылка в правом вернем углу, а также в описании к данному видео. Здесь смысл обобщения в том, чтобы для каждого типа элементов не создавать свой интерфейс, а использовать один и тот же интерфейс, изменяя только тип. Вы также можете заметить, что на тип, который мы можем использовать в нашем хранилище, мы накладываем только одно ограничение – данный тип должен реализовывать интерфейс 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
Итак, у нас есть интерфейс хранилища и его 2 реализации. Где мы это всё будем использовать? Мы будем это всё хозяйство использовать в контроллере BooksController. Вы можете заметить несколько интересных моментов в этом контроллере:
Меня кто-то попросил в комментариях снять видео на тему жизненного цикла контроллера – возможно такое видео будет создано. На данный момент я могу сказать, что контроллер создается отдельно для каждого запроса. То есть в приложении ASP.Net Core не существуют изначально созданные контроллеры, которые ожидает запросы. Наоборот, когда запрос приходит, то в зависимости от адреса, на который он пришел, создается определенный контроллер. Опять-таки здесь мы цепляемся за тему маршрутизации, а это не тема нашего видео.
С контроллером на данный момент мы разобрались. Что у нас есть еще? У нас есть стандартный класс Programm. где создаются 2 хоста – один по умолчанию и второй для использования с фреймворком Autofac. Если мы посмотрим на код создания этих 2 хостов, то заметим, что они отличаются 2 элементами:
Рассмотрим первый пример, в котором мы используем стандартный фреймворк внедрения зависимостей ASP.Net Core. То есть познакомимся с классом Startup. Данный
класс, как мы уже знаем, имеет стандартную структуру. Обратите внимание на метод ConfigureServices – здесь появилось что-то новенькое. Есть вызов метода
AddSingleton. Данный метод имеет несколько перегрузок, но я использовал перегрузку с указанием типа услуги - **typeof(IRepo
Давайте запустим наше приложение и посмотрим, как это работает. Перед запуском я расставлю точки остановки или брейк поинты, чтобы мы могли проанализировать, что конкретно у нас тут происходит.
Кроме того, для работы с нашим приложение я создал коллекцию запросов в приложении Postman. Эта коллекция будет доступна по ссылке в описании – вы сможете, используя данную ссылку, импортировать данные запросы в приложение Postman и повторить мои действия самостоятельно у себя на компьютере. Также я добавлю ссылку на приложение Postman – я использую его в моей работе достаточно часто, и я доволен его функционалом. Сразу оговорюсь, что это не была реклама приложения Postman.
Теперь давайте откомментируем строчку services.AddTransient и попытаемся разобраться какое отличие этой строчки от строчки с кодом AddSingleton. Сразу
заметим, что аргументы этих двух методов одинаковы. Данная строчка services.AddTransient на человеческом языке обозначает следующее: если что-то в моем
коде будет требовать интерфейс IRepo
Мы заметили, что наше приложение работает неправильно. Наши изменения в репозитории не сохраняются. Всё, потому что при каждом новом запросе мы получаем
новый экземпляр класса CollectionBasedRepo в котором не сохраняется измененное состояние. Данное состояние хранится в поле типа List
Теперь давайте представим такую ситуацию – поступило новое задание изменить имплементацию репозитория на такую, которая бы использовала базу данных. К счастью, наш код написан достаточно хорошо и такое изменение мы можем сделать очень быстро. Почему наш код можно назвать хорошим:
Для реализации задания нам необходимо сделать 2 операции – написать новую имплементацию интерфейса **IRepo
Как мы видим разницы нет. Это происходит, потому что состояние нашего хранилища находится в базе данных, а не в классе. Поэтому то ли это новый класс, или существующий разницы особой нет. Состояние берется из базы. В таких ситуациях, когда нет разницы какой метод использовать лучше использовать AddTransient.
Мы познакомились со встроенным механизмом внедрения зависимостей, однако, как уже было сказано, некоторым данного механизма может быть недостаточно. В таком случае с приложением ASP.Net Core можно использовать сторонние фреймворки внедрения зависимости. Сегодня я покажу вам как использовать фреймворк Autofac с ASP.Net Core. На это у меня 2 причины. Этот фреймворк я достаточно хорошо знаю и этот фреймворк имеет достаточно хорошую документацию на тему интеграции с ASP.Net Core. Хотя справедливости ради стоит сказать, что не всё описано в этой документации и мне пришлось пораскинуть мозгами чтобы это всё заработало вместе. О неточностях в документации я скажу пару слов чуть позже.
Итак, давайте посмотрим на класс StartupWithAutofac. Это стандартный класс, ничего особенного. Я его скопировал из примера в документации и удалил из
него то, что нам не будет надо. Также для тех, кто будет самостоятельно запускать данный пример на своем компьютере я перевел некоторые комментарии с
английского на русский язык. По сравнению со стандартным классом Startup у нас появился метод ConfigureContainer – здесь мы регистрируем наши зависимости.
В этом методе используется стандартный синтаксис фреймворка Autofac, с помощью которого мы регистрируем класс CollectionBasedRepo в качестве реализации
зависимости IRepo
Также давайте обратим внимание на метод ConfigureServices. Я здесь добавил 1 строчку services.AddAuthorization();. Без этой строчки у меня в приложении возникала ошибка в методе Configure на методе app.UseAuthorization. Так происходило, потому что контейнер Autofac не содержал в себе услуги для авторизации. При вызове метода app.UseAuthorization приложение ASP.Net Core пыталось найти регистрации для этой услуги в контейнере зависимостей Autofac, но безрезультатно. В итоге чтобы исправить эту ошибку я должен был указать явно эту услугу в методе ConfigureServices. Эта информация отсутствует в документации, так что всегда следует иметь в виду, что любая документация не идеальна и может содержать ошибки.
Давайте запустим эти 2 примера и посмотрим, как это работает. [комментарий к примеру]
Подведем итог сегодняшнего видео:
Надеюсь, что это видео не вышло очень нудным, а может даже и интересным. По крайней мере я рад, что мы уже начали что-то делать, что похоже на реальное приложение, а не разговариваем о каких-то внутренних механизмах приложений ASP.Net Core. Фактом остается то, что внутренне устройство приложений также надо знать – это может пригодиться на собеседовании, но все-таки интереснее писать код, результат работы которого можно сразу же проверить.
Для закрепления данного материала я рекомендую скачать пример кода с моего профиля GitHub и попытаться самостоятельно запустить данное приложении и проанализировать его работу. Все необходимы ссылки в описании к данному видео и на моей веб-странице с текстами к видео на моем канале. Ну а пока у меня всё по данной теме. Благодарю за внимание и до новых встреч. Я также буду вам благодарен за лайк или положительный комментарий к данному видео – это помогает в продвижении канала. Спасибо!
Пример кода из видео на GitHub
Для открытия файла проекта необходимо Visual Studio 2019.