Советы и рекомендации по внедрению основных зависимостей 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
    {
        public ILogger  Logger {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 (как в этом примере). В противном случае всегда используйте null при использовании зависимости.

Сервисный локатор

Шаблон локатора службы - это еще один способ получения зависимостей. Пример:

открытый класс ProductService
{
    приватный только для чтения IProductRepository _productRepository;
    частное чтение только для ILogger  _logger;
    public ProductService (сервис-провайдер IServiceProvider)
    {
        _productRepository = serviceProvider
          .GetRequiredService  ();
        _logger = serviceProvider
          .GetService > () ??
            NullLogger  .Instance;
    }
    public void Удалить (int id)
    {
        _productRepository.Delete (ID);
        _logger.LogInformation ($ "Удален продукт с id = {id}");
    }
}

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

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

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

  • Не используйте шаблон локатора службы везде, где это возможно (если тип службы известен во время разработки). Потому что это делает зависимости неявными. Это означает, что невозможно легко увидеть зависимости при создании экземпляра службы. Это особенно важно для модульных тестов, где вы можете смоделировать некоторые зависимости службы.
  • Разрешите зависимости в конструкторе службы, если это возможно. Разрешение в методе обслуживания делает ваше приложение более сложным и подверженным ошибкам. Я расскажу о проблемах и решениях в следующих разделах.

Срок службы

В инъекции зависимостей ядра ASP.NET есть три срока службы:

  1. Временные услуги создаются каждый раз, когда они вводятся или запрашиваются.
  2. Услуги с определенной областью создаются для каждой области. В веб-приложении каждый веб-запрос создает новую отдельную область обслуживания. Это означает, что сервисы с определенной областью обычно создаются для веб-запроса.
  3. Синглтон-сервисы создаются для каждого DI-контейнера. Как правило, это означает, что они создаются только один раз для каждого приложения, а затем используются в течение всего срока службы приложения.

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

  • Если у службы есть зависимости, они также автоматически освобождаются и удаляются.
  • Если служба реализует интерфейс IDisposable, метод Dispose автоматически вызывается при выпуске службы.

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

  • Зарегистрируйте свои услуги как временные, где это возможно. Потому что проектировать временные сервисы просто. Как правило, вы не заботитесь о многопоточности и утечках памяти, и вы знаете, что служба имеет короткий срок службы.
  • Тщательно используйте срок службы сервисов с областями, так как это может быть сложно, если вы создаете дочерние сервисы или используете эти сервисы из не-веб-приложения.
  • Тщательно используйте время жизни синглтона, так как тогда вам придется иметь дело с многопоточностью и потенциальными утечками памяти.
  • Не зависит от переходного или ограниченного сервиса от одноэлементного сервиса. Потому что временная служба становится одноэлементным экземпляром, когда его внедряет одноэлементная служба, и это может вызвать проблемы, если временная служба не предназначена для поддержки такого сценария. Контейнер DI по умолчанию в ASP.NET Core уже генерирует исключения в таких случаях.

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

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

общедоступный класс PriceCalculator
{
    только для чтения IServiceProvider _serviceProvider;
    общедоступный PriceCalculator (IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }
    public float Calculate (Товарный продукт, int count,
      Тип taxStrategyServiceType)
    {
        использование (var scope = _serviceProvider.CreateScope ())
        {
            var taxStrategy = (ITaxStrategy) scope.ServiceProvider
              .GetRequiredService (taxStrategyServiceType);
            var price = product.Price * count;
            возвратная цена + taxStrategy.CalculateTax (цена);
        }
    }
}

PriceCalculator вставляет IServiceProvider в его конструктор и присваивает его полю. Затем PriceCalculator использует его внутри метода Calculate для создания дочерней области обслуживания. Он использует scope.ServiceProvider для разрешения служб вместо внедренного экземпляра _serviceProvider. Таким образом, все службы, разрешенные из области, автоматически освобождаются / удаляются в конце оператора использования.

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

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

Синглтон Сервис

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

открытый класс FileService
{
    приватное чтение только ConcurrentDictionary  _cache;
    public FileService ()
    {
        _cache = new ConcurrentDictionary  ();
    }
    public byte [] GetFileContent (строка filePath)
    {
        return _cache.GetOrAdd (filePath, _ =>
        {
            return File.ReadAllBytes (filePath);
        });
    }
}

FileService просто кэширует содержимое файла, чтобы уменьшить чтение с диска. Эта услуга должна быть зарегистрирована как синглтон. В противном случае, кэширование не будет работать, как ожидалось.

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

  • Если служба содержит состояние, она должна получить доступ к этому состоянию потокобезопасным способом. Потому что все запросы одновременно используют один и тот же экземпляр службы. Я использовал ConcurrentDictionary вместо словаря, чтобы обеспечить безопасность потока.
  • Не используйте ограниченные или временные сервисы из одноэлементных сервисов. Потому что временные сервисы могут не быть ориентированными на многопоточность. Если вам нужно их использовать, тогда позаботьтесь о многопоточности при использовании этих сервисов (например, используйте блокировку).
  • Утечки памяти, как правило, вызваны синглтон-сервисами. Они не освобождаются / не утилизируются до конца заявки. Таким образом, если они создают экземпляры классов (или внедряют), но не освобождают / удаляют их, они также будут оставаться в памяти до конца приложения. Убедитесь, что вы отпускаете / утилизируете их в нужное время. См. Раздел «Службы разрешения» в разделе «Тело метода» выше.
  • Если вы кэшируете данные (содержимое файла в этом примере), вы должны создать механизм для обновления / аннулирования кэшированных данных при изменении исходного источника данных (когда в этом примере изменяется кэшированный файл на диске).

Сфера обслуживания

Время жизни с заданной областью видимости является хорошим кандидатом для хранения данных веб-запроса. Потому что ASP.NET Core создает область обслуживания для каждого веб-запроса. Таким образом, если вы зарегистрируете сервис как ограниченный, им можно будет поделиться во время веб-запроса. Пример:

открытый класс RequestItemsService
{
    частный только для чтения словарь <строка, объект> _items;
    public RequestItemsService ()
    {
        _items = новый словарь <строка, объект> ();
    }
    public void Set (имя строки, значение объекта)
    {
        _items [name] = значение;
    }
    открытый объект Get (имя строки)
    {
        вернуть _items [имя];
    }
}

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

Но .. факт не всегда такой. Если вы создадите дочернюю область обслуживания и разрешите RequestItemsService из дочерней области, то вы получите новый экземпляр RequestItemsService, и он не будет работать так, как вы ожидаете. Таким образом, сервис с областью действия не всегда означает экземпляр на веб-запрос.

Вы можете подумать, что вы не совершаете такой очевидной ошибки (разрешение области действия внутри дочерней области). Но это не ошибка (очень регулярное использование), и дело может быть не таким простым. Если между вашими сервисами имеется большой граф зависимостей, вы не можете знать, создал ли кто-либо дочернюю область и разрешил ли сервис, который внедряет другой сервис… который, наконец, внедряет сервис с ограниченным объемом.

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

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

Заключение

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