Най-добри практики, съвети и трикове за инжектиране на основна зависимост ASP.NET

В тази статия ще споделя моя опит и предложения за използване на Dependency Injection в ASP.NET Core приложения. Мотивацията зад тези принципи е;

  • Ефективно проектиране на услуги и техните зависимости.
  • Предотвратяване на проблеми с многократно нанизване.
  • Предотвратяване на изтичане на памет.
  • Предотвратяване на потенциални грешки.

Тази статия предполага, че вече сте запознати с Dependency Injection и ASP.NET Core на основно ниво. Ако не, моля, първо прочетете документацията за инжектиране на основни зависимости ASP.NET.

Основи

Инжектиране на конструктор

Инжекцията на конструктора се използва за деклариране и получаване на зависимости на услугата от сервизната конструкция. Пример:

обществена класа ProductService
{
    частен само за четене IProductRepository _productRepository;
    обществен ProductService (IProductRepository productRepository)
    {
        _productRepository = productRepository;
    }
    public void Изтриване (int id)
    {
        _productRepository.Delete (Id);
    }
}

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

Добри практики:

  • Определете изрично необходимите зависимости в конструктора на услуги. По този начин услугата не може да бъде изградена без нейните зависимости.
  • Присвойте инжектирана зависимост на поле / свойство само за четене (за да предотвратите случайно присвояване на друга стойност към него в метод).

Инжектиране на имоти

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

използване на Microsoft.Extensions.Logging;
използване на Microsoft.Extensions.Logging.Abstractions;
пространство за имена MyApp
{
    обществена класа ProductService
    {
        обществен ILogger  Регистратор {get; комплект; }
        частен само за четене IProductRepository _productRepository;
        обществен ProductService (IProductRepository productRepository)
        {
            _productRepository = productRepository;
            Logger = NullLogger  .Instance;
        }
        public void Изтриване (int id)
        {
            _productRepository.Delete (Id);
            Logger.LogInformation (
                $ "Изтрихте продукт с id = {id}");
        }
    }
}

ProductService декларира свойство на Logger с публичен сетер. Контейнерът за инжектиране на зависимост може да зададе Logger, ако е наличен (регистриран в DI контейнер преди).

Добри практики:

  • Използвайте инжектиране на свойства само за незадължителни зависимости. Това означава, че вашата услуга може да работи правилно без тези зависимости.
  • Използвайте Null Object Pattern (както е в този пример), ако е възможно. В противен случай винаги проверявайте за нула, докато използвате зависимостта.

Локатор на услуги

Моделът за локализация на услуги е друг начин за получаване на зависимости. Пример:

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

ProductService инжектира IServiceProvider и разрешава зависимости, като го използва. GetRequiredService хвърля изключение, ако заявената зависимост не е регистрирана преди. От друга страна, GetService просто връща нула в този случай.

Когато разрешите услугите в конструктора, те се освобождават, когато услугата е пусната. Така че, не се интересувате от освобождаване / изхвърляне на услуги, разрешени вътре в конструктора (точно като конструктор и инжектиране на свойства).

Добри практики:

  • Не използвайте шаблона на локатор на услуги, когато е възможно (ако видът на услугата е известен по време на разработка). Защото прави зависимостите неявни. Това означава, че не е възможно лесно да видите зависимостите, докато създавате екземпляр от услугата. Това е особено важно за единични тестове, където може да искате да се подигравате с някои зависимости на услугата.
  • Разрешете зависимостите в сервизния конструктор, ако е възможно. Разрешаването на метод на обслужване прави приложението ви по-сложно и податливо на грешки. Ще разгледам проблемите и решенията в следващите раздели.

Service Life Times

Има три експлоатационни времена в инжектирането на ASP.NET Core Dependency Injection:

  1. Преходните услуги се създават всеки път, когато се инжектират или поискат.
  2. Обхватните услуги се създават по обхват. В уеб приложение всяка уеб заявка създава нов отделен обхват на услугата. Това означава, че обхванатите услуги обикновено се създават на уеб запитване.
  3. Singleton услугите се създават за DI контейнер. Това обикновено означава, че те са създадени само един път на приложение и след това се използват за целия период на приложение на приложението.

DI контейнерът следи всички разрешени услуги. Услугите се освобождават и изхвърлят, когато животът им приключи:

  • Ако услугата има зависимости, те също автоматично се освобождават и изхвърлят.
  • Ако услугата реализира интерфейса IDisposable, методът на разположение автоматично се извиква при пускане на услугата.

Добри практики:

  • Регистрирайте услугите си като преходни, когато е възможно. Защото е лесно да се проектират преходни услуги. Обикновено не ви интересуват течовете с много нишки и паметта и знаете, че услугата има кратък живот.
  • Използвайте внимателно експлоатационния живот внимателно, тъй като може да бъде сложно, ако създавате обхвати за услуги за деца или използвате тези услуги от приложение, което не е уеб.
  • Използвайте еднократния живот внимателно, тъй като тогава трябва да се справите с многонишкови и потенциални проблеми с изтичане на памет.
  • Не зависете от преходна или обхваната услуга от единична услуга. Тъй като преходната услуга се превръща в единичен случай, когато сингъл услугата я инжектира и това може да причини проблеми, ако преходната услуга не е проектирана да поддържа такъв сценарий. По подразбиране DI контейнерът на ASP.NET Core вече хвърля изключения в такива случаи.

Разрешаване на услуги в методичен орган

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

обществен клас PriceCalculator
{
    частен само за четене IServiceProvider _serviceProvider;
    обществен PriceCalculator (услуга IServiceProviderProvider)
    {
        _serviceProvider = serviceProvider;
    }
    публичен поплавък Изчислете (Продукт на продукта, брой на инт.,
      Въведете данъкStrategyServiceType)
    {
        използване (var Oblast = _serviceProvider.CreateScope ())
        {
            var taxStrategy = (ITaxStrategy) обхват.ServiceProvider
              .GetRequiredService (taxStrategyServiceType);
            вар цена = продукт.Цена * брой;
            цена на връщане + данъкStrategy.CalculateTax (цена);
        }
    }
}

PriceCalculator инжектира IServiceProvider в своя конструктор и го присвоява на поле. След това PriceCalculator го използва в рамките на метода Изчисляване, за да създаде обхват за обслужване на деца. Той използва range.ServiceProvider за разрешаване на услуги, вместо инжектирания екземпляр _serviceProvider. По този начин всички услуги, разрешени от обхвата, автоматично се освобождават / разпореждат в края на оператора за използване.

Добри практики:

  • Ако решавате услуга в орган на метод, винаги създавайте обхват на детска услуга, за да гарантирате, че разрешените услуги са пуснати правилно.
  • Ако метод получи IServiceProvider като аргумент, можете директно да разрешите услугите от него, без да се грижите за освобождаването / изхвърлянето. Създаването / управлението на обхвата на услугата е отговорност на кода, извикващ вашия метод. Следвайки този принцип прави вашия код по-чист.
  • Не притежавайте препратка към разрешена услуга! В противен случай това може да доведе до изтичане на памет и ще получите достъп до разположена услуга, когато използвате по-късно референцията на обекта (освен ако разрешената услуга е еднократна).

Singleton Services

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

обществена класа FileService
{
    private readonly ConcurrentDictionary  _cache;
    обществена FileService ()
    {
        _cache = нов ConcurrentDictionary  ();
    }
    обществен байт [] GetFileContent (string filePath)
    {
        return _cache.GetOrAdd (filePath, _ =>
        {
            върнете File.ReadAllBytes (filePath);
        });
    }
}

FileService просто кешира съдържанието на файла, за да намали четенето на диска. Тази услуга трябва да бъде регистрирана като единична. В противен случай кеширането няма да работи както се очаква.

Добри практики:

  • Ако услугата има състояние, тя трябва да има достъп до това състояние по безопасен за нишки начин. Тъй като всички заявки едновременно използват един и същ екземпляр на услугата. Използвах ConcurrentDictionary вместо речник, за да осигуря безопасността на нишката.
  • Не използвайте обхванати или преходни услуги от единични услуги. Защото преходните услуги може да не са проектирани така, че да са безопасни за конци. Ако трябва да ги използвате, тогава се грижете за многократно нанизване, докато използвате тези услуги (използвайте заключване например).
  • Изтичането на паметта обикновено се причинява от услугите за единични. Те не се освобождават / изхвърлят до края на заявлението. Така че, ако инсталират класове (или инжектират), но не ги освободят / изхвърлят, те също ще останат в паметта до края на приложението. Уверете се, че ги освобождавате / изхвърляте в точното време. Вижте Разрешаващите услуги в раздел Метод на тялото по-горе.
  • Ако кеширате данни (съдържанието на файла в този пример), трябва да създадете механизъм за актуализиране / обезсилване на кешираните данни при промяна на оригиналния източник на данни (когато кешираният файл се променя на диска за този пример).

Обхват на услуги

Първият обхват на експлоатационния живот изглежда добър кандидат за съхранение на данни от уеб заявка. Тъй като ASP.NET Core създава обхват на услугата за всяка заявка в мрежата. Така че, ако регистрирате услуга като обхваната, тя може да бъде споделена по време на заявка в мрежата. Пример:

обществена класа RequestItemsService
{
    частен речник само за четене  _items;
    обществен RequestItemsService ()
    {
        _items = нов речник <низ, обект> ();
    }
    public void Set (име на низ, стойност на обекта)
    {
        _items [име] = стойност;
    }
    публичен обект Вземете (име на низ)
    {
        връщане _items [име];
    }
}

Ако регистрирате RequestItemsService като обхванат и го инжектирате в две различни услуги, тогава можете да получите елемент, който е добавен от друга услуга, тъй като те ще споделят същия екземпляр RequestItemsService. Това очакваме от обхванатите услуги.

Но .. фактът може да не е винаги такъв. Ако създадете обхват на услугата за деца и разрешите RequestItemsService от обхвата на детето, тогава ще получите нов екземпляр на RequestItemsService и той няма да работи както очаквате. Така че, обхватът на услугата не винаги означава пример за всяка заявка в мрежата.

Може да си мислите, че не правите такава очевидна грешка (разрешаване на обхвата в рамките на обхвата на детето). Но това не е грешка (много редовно използване) и случаят може да не е толкова прост. Ако има голяма графика на зависимост между вашите услуги, не можете да знаете дали някой е създал обхват на дете и е разрешил услуга, която инжектира друга услуга ... която накрая инжектира услуга с обхват.

Добра практика:

  • Обхватната услуга може да се мисли като оптимизация, когато тя се инжектира от твърде много услуги в уеб заявка. По този начин всички тези услуги ще използват единичен екземпляр от услугата по време на една и съща заявка в мрежата.
  • Обхватните услуги не е необходимо да бъдат проектирани като безопасни за конци. Защото те трябва да се използват обикновено от едно заявка / нишка за уеб. Но ... в този случай не трябва да споделяте обхвата на услугите между различни нишки!
  • Бъдете внимателни, ако проектирате услуга с обхват, за да споделяте данни между други услуги в уеб заявка (обяснено по-горе). Можете да съхранявате данни за уеб заявка вътре в HttpContext (инжектирайте IHttpContextAccessor за достъп до нея), което е по-сигурният начин да направите това. Животът на HttpContext не е обхванат. Всъщност той изобщо не е регистриран в DI (затова не го инжектирате, а вместо това инжектирате IHttpContextAccessor). Реализацията на HttpContextAccessor използва AsyncLocal за споделяне на същия HttpContext по време на уеб заявка.

заключение

Инжекцията на зависимостта изглежда проста за използване в началото, но има потенциални проблеми с много резби и изтичане на памет, ако не спазвате някои строги принципи. Споделих някои добри принципи, базирани на моя собствен опит по време на разработването на рамката на ASP.NET Boilerplate.