Найкращі практики, підказки та поради

У цій статті я поділюсь своїм досвідом та пропозиціями щодо використання залежної ін'єкції у додатках ASP.NET Core. Мотивація цих принципів є;

  • Ефективно проектування послуг та їх залежностей.
  • Запобігання проблемам із багатопотоковою передачею.
  • Запобігання витоку пам'яті.
  • Попередження потенційних помилок.

Ця стаття передбачає, що ви вже знайомі з введенням залежності та ASP.NET Core на базовому рівні. Якщо ні, спочатку ознайомтеся з документацією щодо введення базової залежності ASP.NET.

Основи

Інжекція конструктора

Інжекція конструктора використовується для оголошення та отримання залежності служби від побудови послуги. Приклад:

ProductService для громадського класу
{
    приватний лише для читання IProductRepository _productRepository;
    загальнодоступне обслуговування продуктів (репозиторій продукту IProductRepository)
    {
        _productRepository = productRepository;
    }
    публічна недійсність Видалити (int id)
    {
        _productRepository.Delete (id);
    }
}

ProductService вводить IProductRepository як залежність у свій конструктор, а потім використовує його всередині методу Delete.

Хороші практики:

  • Визначте необхідні залежності чітко в конструкторі послуг. Таким чином, послугу неможливо побудувати без її залежностей.
  • Призначте введену залежність полю / властивості лише для читання (щоб запобігти випадковому призначенню іншого значення в методі).

Вприскування майна

Стандартний контейнер для введення залежності ASP.NET Core не підтримує введення властивостей. Але ви можете використовувати інший контейнер, що підтримує введення властивості. Приклад:

за допомогою Microsoft.Extensions.Logging;
використання Microsoft.Extensions.Logging.Abstractions;
простір імен MyApp
{
    ProductService для громадського класу
    {
        public ILogger  Logger {get; набір; }
        приватний лише для читання IProductRepository _productRepository;
        загальнодоступне обслуговування продуктів (репозиторій продукту IProductRepository)
        {
            _productRepository = productRepository;
            Logger = NullLogger  .Instance;
        }
        публічна недійсність Видалити (int id)
        {
            _productRepository.Delete (id);
            Logger.LogInformation (
                $ "Видалено продукт з id = {id}");
        }
    }
}

ProductService оголошує властивість Logger із загальнодоступним сетером. Контейнер для ін'єкційних залежностей може встановити реєстратор, якщо він доступний (зареєстрований у контейнері DI раніше).

Хороші практики:

  • Використовуйте введення властивостей лише для необов'язкових залежностей. Це означає, що ваша послуга може нормально працювати без цих залежностей.
  • Якщо можливо, використовуйте Null Object Pattern (як, наприклад, у цьому прикладі). В іншому випадку завжди перевіряйте наявність нуля, використовуючи залежність.

Локатор обслуговування

Модель локатора обслуговування - ще один спосіб отримання залежностей. Приклад:

ProductService для громадського класу
{
    приватний лише для читання IProductRepository _productRepository;
    приватний лише для читання ILogger  _logger;
    загальнодоступне обслуговування продуктів (сервіс IServiceProviderProvider)
    {
        _productRepository = serviceProvider
          .GetRequiredService  ();
        _logger = serviceProvider
          .GetService > () ??
            NullLogger  .Instance;
    }
    публічна недійсність Видалити (int id)
    {
        _productRepository.Delete (id);
        _logger.LogInformation ($ "Видалено продукт з id = {id}");
    }
}

ProductService вводить IServiceProvider і вирішує залежності, використовуючи його. GetRequiredService викидає виняток, якщо запитувана залежність не була зареєстрована раніше. З іншого боку, GetService просто повертає нуль у такому випадку.

Коли ви вирішуєте послуги всередині конструктора, вони звільняються, коли служба випускається. Отже, вам не байдуже звільняти / розпоряджатись послугами, вирішеними всередині конструктора (як і конструктор та введення властивостей).

Хороші практики:

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

Термін служби служби Times

У системі ASP.NET Core Dependency Injection є три періоди служби:

  1. Перехідні сервіси створюються щоразу, коли їх вводять або запитують.
  2. Обсяги послуг створюються в межах сфери. У веб-додатку кожен веб-запит створює нову окрему сферу обслуговування. Це означає, що широкомасштабні послуги, як правило, створюються на веб-запит.
  3. Послуги Singleton створюються для контейнера DI. Це, як правило, означає, що вони створюються лише один раз на додаток, а потім використовуються протягом усього часу роботи програми.

Контейнер DI відстежує всі вирішені послуги. Послуги звільняються та розпоряджаються, коли закінчується їхнє життя:

  • Якщо служба має залежності, вони також автоматично звільняються та утилізуються.
  • Якщо сервіс реалізує інтерфейс ID, що використовується для одноразового використання, при випуску служби автоматично викликається метод Dispose.

Хороші практики:

  • Реєструйте свої послуги як можна тимчасовіше, де це можливо. Тому що створити перехідні сервіси просто. Ви, як правило, не дбаєте про багаторізну та витоку пам'яті, і знаєте, що служба має короткий термін служби.
  • Обережно використовуйте масштаб служби, оскільки це може бути складним, якщо ви створюєте сфери обслуговування дітей або використовуєте ці послуги з не веб-програми.
  • Обережно використовуйте одинарне життя з тих пір, вам потрібно мати справу з багатопотоковими проблемами та можливими проблемами витоку пам'яті.
  • Не залежати від тимчасової або широкомасштабної послуги від одиночної послуги. Оскільки транзиторна служба стає одиничним екземпляром, коли служба одиночного вводить її, і це може спричинити проблеми, якщо перехідна служба не призначена для підтримки такого сценарію. Контейнер DI за замовчуванням ASP.NET Core вже викидає винятки в таких випадках.

Розв’язання послуг у методичному органі

У деяких випадках може знадобитися вирішити іншу послугу методом вашої послуги. У таких випадках переконайтеся, що ви звільняєте послугу після використання. Найкращий спосіб забезпечити це - створити сферу обслуговування. Приклад:

публічний клас PriceCalculator
{
    приватний лише для читання IServiceProvider _serviceProvider;
    загальнодоступний ціновий калькулятор (сервіс IServiceProviderProvider)
    {
        _serviceProvider = serviceProvider;
    }
    public float Розрахувати (продукт продукту, кількість int,
      Введіть податокStrategyServiceType)
    {
        використовуючи (var Oblast = _serviceProvider.CreateScope ())
        {
            var taxStrategy = (ITaxStrategy) obseg.ServiceProvider
              .GetRequiredService (taxStrategyServiceType);
            вар ціна = продукт.Ціна * кількість;
            ціна повернення + податокStrategy.CalculateTax (ціна);
        }
    }
}

PriceCalculator вводить IServiceProvider у свій конструктор і присвоює йому поле. Потім PriceCalculator використовує його всередині методу Calculate для створення сфери обслуговування дітей. Для розв’язання служб він використовує domain.ServiceProvider замість введеного екземпляра _serviceProvider. Таким чином, усі послуги, вирішені з області дії, автоматично вивільняються / розпоряджаються в кінці оператора use.

Хороші практики:

  • Якщо ви вирішуєте службу в тілі методу, завжди створюйте область дочірньої служби, щоб гарантувати належне звільнення розв'язаних служб.
  • Якщо метод отримує IServiceProvider як аргумент, ви можете безпосередньо вирішувати послуги з нього, не піклуючись про звільнення / розпорядження. Створення / керування сферою обслуговування - це відповідальність коду, що викликає ваш метод. Дотримуючись цього принципу, ваш код стає чистішим.
  • Не тримайте посилання на вирішену послугу! В іншому випадку це може спричинити витік пам'яті, і ви отримаєте доступ до розпорядженої служби, коли пізніше будете використовувати посилання на об'єкт (якщо тільки вирішена послуга не є однотонною).

Послуги Singleton

Послуги Singleton, як правило, призначені для збереження стану програми. Кеш - хороший приклад станів програми. Приклад:

FileService публічного класу
{
    приватний лише для читання ConcurrentDictionary  _cache;
    загальнодоступна FileService ()
    {
        _cache = новий ConcurrentDictionary <рядок, байт []> ();
    }
    публічний байт [] GetFileContent (рядок filePath)
    {
        return _cache.GetOrAdd (filePath, _ =>
        {
            повернути File.ReadAllBytes (filePath);
        });
    }
}

FileService просто кешує вміст файлу, щоб зменшити читання дисків. Ця послуга має бути зареєстрована як одиночна. В іншому випадку кешування не буде працювати, як очікувалося.

Хороші практики:

  • Якщо служба має стан, він повинен отримувати доступ до цього стану безпечним потоком. Тому що всі запити одночасно використовують один і той же екземпляр служби. Я використовував ConcurrentDictionary замість словника, щоб забезпечити безпеку потоку.
  • Не використовуйте широкомасштабні або тимчасові послуги від одиночних служб. Тому що тимчасові послуги можуть бути розроблені не для безпечного потоку. Якщо вам доведеться їх використовувати, то подбайте про багатопотоковість, використовуючи ці послуги (наприклад, застосуйте замок).
  • Витоки пам'яті, як правило, спричиняються послугами одиночних. Вони не звільняються / розпоряджаються до кінця заявки. Таким чином, якщо вони створюють класи (або вводять), але не випускають / розпоряджаються ними, вони також залишаться в пам'яті до кінця програми. Переконайтесь, що ви звільнили / розпорядили їх у потрібний час. Див. Розв’язання Служб у розділі Метод тіла вище.
  • Якщо ви кешуєте дані (вміст файлу в цьому прикладі), вам слід створити механізм оновлення / недійсності кешованих даних при зміні оригінального джерела даних (коли кешований файл змінюється на диску для цього прикладу).

Обсяг послуг

Початок роботи в першу чергу здається хорошим кандидатом для зберігання даних на веб-запит. Оскільки ASP.NET Core створює сферу обслуговування за кожним запитом. Отже, якщо ви зареєструєте послугу як розширену, то нею можна поділитися під час веб-запиту. Приклад:

публічний клас RequestItemsService
{
    приватний словник для читання лише <рядок, об'єкт> _items;
    public RequestItemsService ()
    {
        _items = новий словник <рядок, об'єкт> ();
    }
    public void Set (ім'я рядка, значення об'єкта)
    {
        _items [ім'я] = значення;
    }
    публічний об'єкт Get (ім'я рядка)
    {
        return _items [ім'я];
    }
}

Якщо ви зареєструєте RequestItemsService як масштабований і введете його у дві різні служби, то ви можете отримати елемент, який буде додано з іншої служби, оскільки вони матимуть спільний доступ до того самого екземпляра RequestItemsService. Ось що ми очікуємо від масштабних послуг.

Але .. факт може бути не завжди таким. Якщо ви створите сферу обслуговування дітей і вирішите RequestItemsService з дочірньої області, ви отримаєте новий екземпляр RequestItemsService, і він не працюватиме так, як ви очікували. Отже, масштабований сервіс не завжди означає екземпляр для запиту в Інтернеті.

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

Гарна практика:

  • Об'ємна послуга може розглядатися як оптимізація, коли її вводить занадто багато сервісів у веб-запит. Таким чином, усі ці сервіси будуть використовувати один екземпляр послуги під час одного веб-запиту.
  • Поширені послуги не потрібно конструювати як безпечні для потоків. Тому що вони, як правило, повинні використовуватися одним веб-запитом / потоком. Але ... у такому випадку вам не слід ділитися сферами обслуговування між різними потоками!
  • Будьте обережні, якщо ви розробляєте послугу з масштабним обміном для обміну даними між іншими службами у веб-запиті (пояснено вище). Ви можете зберігати дані за запитом в Інтернеті всередині HttpContext (введіть IHttpContextAccessor для доступу до нього), що є більш безпечним способом зробити це. Термін служби HttpContext не визначений. Насправді він взагалі не зареєстрований в DI (саме тому ви не вводите його, а вводите IHttpContextAccessor). Реалізація HttpContextAccessor використовує AsyncLocal для обміну тим самим HttpContext під час веб-запиту.

Висновок

Спочатку введення залежно виглядає простим у використанні, але можливі проблеми з багатопотоковою резьбою та витоком пам'яті, якщо ви не дотримуєтесь суворих принципів. Я поділився деякими хорошими принципами, спираючись на власний досвід під час розробки рамки ASP.NET Boilerplate.