Впровадження залежностей (dependency injection) - документація core 1

ASP.NET Core підтримує впровадження залежностей. Додатки ASP.NET Core використовують вбудовані сервіси фреймворка, які впроваджені в методи класу Startup, а також сервіси додатки можуть бути налаштовані на DI. Контейнер сервісів за замовчуванням надає мінімальний набір функцій і не замінює інші контейнери.

Впровадження залежностей (DI) - це технологія, яка використовується для того, щоб по максимуму розділити об'єкти і залежності. Замість прямої установки компонентів або використання статичних посилань, об'єкт, який потрібен класу для виконання деяких дій, надається цього класу в іншій манері. Найбільш часто класи заявляють про залежності через конструктор, дозволяючи їм діяти за принципом явних залежностей. Такий підхід відомий як "впровадження конструктора".

Якщо класи використовують DI, їх компоненти не залежать один від одного напряму. Це називається Принципом інверсії залежностей. який говорить, що "модулі високого рівня не повинні залежати від модулів низького рівня - і ті, і інші повинні залежати від абстракцій." Замість використання конкретних реалізацій, класи запитують абстракції (зазвичай interface), які надаються їм після створення. Витяг залежностей в інтерфейси і передача реалізацій цих інтерфейсів в якості параметрів також є прикладом шаблону проектування Strategy.

Якщо система використовує DI, коли багато класів використовують залежно через конструктор (або властивості), то корисно, щоб певні класи були пов'язані з потрібними залежностями. Ці класи називаються контейнерами. також вони називаються контейнерами інверсії управління (IoC) або контейнерами DI. Контейнер - це, в принципі, фабрика, яка надає екземпляри необхідних типів. Якщо у даного типу є залежності, а також існує контейнер для надання цих залежностей, залежно будуть створені як частина запитуваної примірника. Таким способом складні залежності надаються класу без жорсткого кодування об'єкта. Крім того, контейнери зазвичай керують життєвим циклом об'єкта всередині програми.

ASP.NET Core включає в себе простий вбудований контейнер (представлений інтерфейсом IServiceProvider), який за замовчуванням підтримує впровадження конструктора, і ASP.NET надає через DI певні сервіси. Контейнер ASP.NET працює з типами як з сервісами. Далі в цій статті сервісами ми будемо називати типи, які управляються IoC контейнером ASP.NET Core. Ви можете налаштовувати вбудовані сервіси контейнера за допомогою методу ConfigureServices класу Startup.

У цій статті ми розглядаємо, як впровадження залежностей застосовується у всіх ASP.NET додатках. Впровадження залежностей в MVC розглядається в статті Впровадження залежностей і контролери `.

Метод ConfigureServices класу Startup визначає сервіси, які будуть використані в додатку, включаючи функції таких платформ, як Entity Framework Core і ASP.NET Core MVC. Спочатку у IServiceCollection. який передається ConfigureServices. є всього парочка сервісів. Ось приклад того, як додавати в контейнер додаткові сервіси за допомогою таких методів розширення, як AddDbContext. AddIdentity і AddMvc.

Функції та сполучна ПО ASP.NET використовують один метод розширення AddService. щоб реєструвати всі сервіси, які потрібні даної функції.

Ви можете запитувати окремі сервіси за допомогою методів Startup. Див. Запуск програми.

Ви можете налаштувати додаток таким чином, щоб воно використовувало різні функції фреймворка, а також ви можете працювати з ConfigureServices. щоб налаштувати власні сервіси.

Ви можете реєструвати власні сервіси. Перший дженерик тип являє тип (зазвичай інтерфейс), який буде запитуватися з контейнера. Другий дженерик тип являє конкретний тип, екземпляр якого створить контейнер і який буде використовуватися для виконання таких запитів.

Кожен виклик services.Add додає сервіси. Наприклад, services.AddMvc () додає сервіс, який потрібен MVC.

Метод AddTransient пов'язує абстрактні типи з конкретними сервісами, екземпляр яких створюється окремо для кожного об'єкта, який їх запрошувати. Це відомо як життєвий цикл сервісу. Важливо вибрати відповідний життєвий цикл для сервісу, який ви реєструєте. Чи повинен новий екземпляр сервісу надаватися кожному класу, який його запитує? Чи повинен новий екземпляр використовуватися протягом усього запиту? Чи повинен один екземпляр використовуватися під час всього життєвого циклу програми?

У прикладі для цієї статті існує один контролер CharacterController. Його метод Index відображає поточний список персонажів, і ініціалізує колекцію, якщо такий ще немає. Хоча в цьому додатку використовується Entity Framework і клас ApplicationDbContext. вони не видно в контролері. Замість цього механізм доступу до конкретних даних "ховається" за інтерфейсом ICharacterRepository. який слід паттерну Repository. Примірник ICharacterRepository запитується через конструктор і присвоюється закритому полю, яке потім використовується для доступу до потрібного персонажу.

ICharacterRepository визначає два методи, які потрібні контролера для роботи з екземплярами Character.

Інтерфейс ж реалізується за допомогою конкретного типу CharacterRepository.

Спосіб, яким використовується DI з класом CharacterRepository є спільною моделлю, і ви можете слідувати їй для роботи з усіма сервісами - не тільки в "репозиторіях" або класах доступу до даних.

Зверніть увагу, що CharacterRepository запитує в свій конструктор ApplicationDbContext. Часто впровадження залежностей використовується по ланцюжку, тобто, кожна запитувана залежність може запитувати свої власні залежності. Контейнер відповідає за використання всіх залежностей в дереві і повертає повноцінний сервіс.

Створення запитуваної об'єкта і всіх об'єктів, які йому потрібні, часто називається графом об'єктів. Так само і набір пов'язаних залежностей називається деревом залежностей або графом залежностей.

В даному випадку ICharacterRepository і ApplicationDbContext повинні бути зареєстровані за допомогою контейнера сервісів в ConfigureServices в Startup. ApplicationDbContext налаштовується за допомогою методу розширення AddDbContext. Далі показана реєстрація типу CharacterRepository:

Контекст Entity Framework повинен бути доданий в контейнер сервісів за допомогою Scoped. Це відбувається автоматично, якщо ви використовуєте допоміжні методи, як показано вище. Репозиторії, які використовуються з Entity Framework, повинні мати такий же життєвий цикл.

Будьте обережні при використанні сервісу Scoped з Singleton. Якщо це трапиться, у сервісів буде некоректне стан при обробці наступних запитів.

ASP.NET сервіси можуть бути налаштовані з наступними життєвими циклами:

Transient Transient-сервіси створюються кожен раз, коли вони запитуються. Такий життєвий цикл найкраще підходить не особливо значущим, "легким" сервісів. Scoped Scoped-сервіси створюються при кожному запиті. Singleton Singleton-сервіси створюються один раз після запиту, а потім кожний наступний запит використовує той же екземпляр. Якщо додатку потрібно singleton, коли контейнер сервісів управляє життєвим циклом сервісу, то вам варто реалізувати патерн singleton і управляти життєвим циклом об'єкта в самому класі.

Сервіси можуть бути зареєстровані за допомогою контейнера різними шляхами. Ми вже бачили, як зареєструвати сервіс даного типу, вказавши конкретний тип, який буде використовуватися. Крім того, ви можете використовувати фабрику, яка потім створить потрібний екземпляр. Третій підхід полягає в тому, що ви безпосередньо вказуєте екземпляр потрібного типу, і тоді контейнер ніколи не спробує створити екземпляр.

Щоб показати вам різницю між цими життєвими циклами і опціями реєстрації, ми будемо використовувати простий інтерфейс, який представляє одну або кілька завдань в якості операції з унікальним ідентифікатором, OperationId. Залежно від того, як ми налаштовуємо життєвий цикл даного сервісу, контейнер передасть ті ж самі або різні екземпляри цього сервісу потрібного класу. Щоб вам було зрозуміло, який життєвий цикл запитується, ми будемо створювати один тип для кожного життєвого циклу:

Ми також реалізуємо всі ці інтерфейси за допомогою класу Operation. який приймає в свій конструктор Guid або створює новий Guid. якщо такого ще немає.

Далі, в ConfigureServices кожен тип додається в конструктор в залежності від відповідного жізненнгого циклу:

Зверніть увагу, що IOperationSingletonInstance використовує екземпляр з ID Guid.Empty. так що при використанні цього типу він буде порожнім. Також ми зареєстрували OperationService. який залежить від інших типів Operation. так що він буде порожнім всередині запиту, якщо сервіс отримує той же примірник, що і контролер, або ж отримує інший для різного типу операцій.

Щоб показати вам життєві цикли об'єкта всередині і між різними запитами в додатку, ми включили в приклад OperationsController. який запитує всі види типу IOperation. а також OperationService. Метод дії Index потім відображає всі значення OperationId контролерів і сервісів.

Тепер ми робимо два окремих запиту до методу дії контролера:

Впровадження залежностей (dependency injection) - документація core 1
Впровадження залежностей (dependency injection) - документація core 1

Подивіться, як значення OperationId відрізняються при запиті і між запитами.

  • Об'єкти Transient завжди розрізняються; новий екземпляр надається кожного контролера і кожному сервісу.
  • Об'єкти Scoped завжди однакові при одному запиті, але розрізняються за різних запитах.
  • Об'єкти Singleton однакові для кожного запиту і для кожного об'єкта.

Сервіси, доступні ASP.NET запитом з HttpContext. входять в RequestServices.

Колекція RequestServices представляє сервіси, які ви налаштовуєте і запитуєте як частину свого додатка. Коли в ваших об'єктах вказуються залежності, їм потрібні типи з RequestServices. а не з ApplicationServices.

Загалом, ви не повинні використовувати ці властивості безпосередньо, а замість цього краще запросити тип, який потрібно класу, через конструктор і дозволити фреймворку впровадити ці залежності. Такі класи легше тестувати (див. Тестування), і вони не залежать один від одного.

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

А що якщо в ваших класах занадто багато впроваджених залежностей? Зазвичай це означає те, що ваш клас занадто перевантажений і порушує принцип SRP - принцип єдиної обов'язки. Ви повинні змінити цей клас, перемістивши деякі його функції в новий клас. Зверніть увагу, що класи Controller повинні бути сфокусовані на UI, так що бізнес-правила і доступ до даних повинні зберігатися в класах відповідно до розподілу обов'язків.

Що стосується доступу до даних, ви можете впроваджувати в контролери DbContext (якщо ви додали EF в контейнер сервісів в ConfigureServices). Деякі розробники вважають за краще використовувати інтерфейс сховища, замість впровадження DbContext безпосередньо. Використання інтерфейсу для інкапсулювання логіки доступу до даних в одному місці може зменшити число місць, куди ви повинні вносити зміни при наявності змін в БД.

Вбудований контейнер сервісів обробляє базові потреби фреймворка. Однак розробники можуть легко замінити цей контейнер іншим. Зазвичай метод ConfigureServices повертає void. але якщо він був змінений так, щоб повертати IServiceProvider. то можна налаштувати і повернути інший контейнер. Є багато IOC контейнерів, доступних для .NET. Ми спробуємо додати посилання на реалізації контейнерів DNX. У цьому прикладі використовується пакет Autofac.

По-перше, додайте відповідні пакети контейнерів в властивість dependencies в project.json:

Далі, налаштуйте контейнер в ConfigureServices і поверніть IServiceProvider: