Вы находитесь на странице: 1из 15

25 сентября 2018 г. в ASP.NET CORE .NET CORE DEPENDENCY INJECTION ~ 10 мин чтения.

Использование Scrutor для


автоматической регистрации
ваших сервисов в контейнере
ASP.NET Core DI
Поделись:

В этом посте я опишу, как использовать библиотеку с открытым исходным


кодом Scrutor от Кристиана Хелланга, чтобы добавить возможности
сканирования сборок в контейнер ASP.NET Core DI. Scrutor сам по себе не
является контейнером внедрения зависимостей (DI), он добавляет
дополнительные возможности во встроенный контейнер. Библиотека работает
уже более 2 лет - см. Этот пост для оригинального объявления и мотивации для
библиотеки Кристианом .

Этот пост оказался довольно длинным, поэтому для вашего удобства


оглавление:

Контейнер ASP.NET Core DI


Scrutor против сторонних DI-контейнеров
Сканирование сборки с помощью Scrutor
Выбор и фильтрация реализаций для регистрации
Обработка дублированных сервисов с помощью
ReplacementStrategy
Регистрация внедрений как сервис
Указание времени жизни зарегистрированных классов
Объединение нескольких селекторов вместе
Резюме

Контейнер ASP.NET Core DI


ASP.NET Core использует внедрение зависимостей во всем ядре фреймворка.
Следовательно, инфраструктура включает в себя простой контейнер DI,
который обеспечивает минимальные возможности, требуемые самой
платформой. Этот контейнер можно найти в пакете NuGet
Microsoft.Extensions.DependencyInjection .

Существует также множество сторонних библиотек .NET DI, которые


предоставляют гораздо больше возможностей и возможностей. Я писал
довольно давно об использовании StructureMap в ASP.NET Core (хотя, если вы
используете StructureMap, вам, вероятно, следует взглянуть на Lamar ), но есть
много контейнеров на выбор , например:

AutoFac
Виндзор
StructureMap / Lamar
Простой инжектор
Ninject
Dryloc

Все эти контейнеры предоставляют различные функции и фокусы:


конфигурация на основе атрибутов, регистрация на основе соглашений,
внедрение свойств, производительность ... список можно продолжить.

Общей особенностью является автоматическая регистрация служб путем


сканирования типов в сборке и поиска тех, которые соответствуют соглашению.
Это может значительно уменьшить конфигурацию шаблона, требуемую в
вашем Startup.ConfigureServices методе.

Например, если ваши регистрации на данный момент выглядят так:

services.AddScoped<IFoo, Foo>();
services.AddScoped<IBar, Bar>();
services.AddScoped<IBaz, Baz>();

тогда вы можете упростить настройку DI, используя сканирование сборок,


чтобы это было примерно так:

services.Scan(scan =>
scan.FromCallingAssembly()
.AddClasses()
.AsMatchingInterface());
Scrutor против сторонних DI-контейнеров

Scrutor - это не новый DI-контейнер. Под капотом используется встроенный


контейнер ASP.NET Core DI. Это как плюсы, так и минусы для вас, как
разработчика приложения:

Плюсы:

Это просто, чтобы добавить к существующему приложению


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

Минусы:

Уменьшенная функциональность . Поскольку он использует


встроенный контейнер, Scrutor всегда будет ограничен
функциональностью встроенного контейнера. Встроенный контейнер
намеренно поддерживается очень простым и вряд ли получит
значительные дополнительные функции.

Я уверен, что есть больше минусов, но ограниченная функциональность


действительно большая! Если вы привыкли к одному из более
полнофункциональных DI-контейнеров, вы, вероятно, захотите придерживаться
их. С другой стороны, если вы в настоящее время используете только
встроенный контейнер, Scrutor стоит посмотреть, может ли он упростить ваш
код.

Чтобы установить Scrutor, запустите dotnet add package Scrutor из .NET CLI или
Install-Package Scrutor из консоли диспетчера пакетов. В следующем разделе
я покажу некоторые примитивы сканирования сборки, которые вы можете
использовать со Scrutor.

Сканирование сборки с помощью Scrutor

Scrutor API состоит из двух методов расширения IServiceCollection : Scan() и


Decorate() . В этом посте я просто собираюсь посмотреть на Scan метод и
некоторые опции, которые он предоставляет.

Scan Метод принимает один аргумент: действие конфигурации , в котором


определяются четыре вещи:

1. Селектор - какие реализации (конкретные классы) зарегистрировать


2. Стратегия регистрации - как работать с дублирующимися
сервисами или реализациями
3. Сервисы - какие сервисы (т.е. интерфейсы) каждая реализация
должна быть зарегистрирована как
4. Время жизни - какое время жизни использовать для регистрации

Например, Scan метод, который просматривает вызывающую сборку и


добавляет все конкретные классы, поскольку временные службы будут
выглядеть следующим образом:

services.Scan(scan => scan


.FromCallingAssembly() // 1. Find the concrete classes
.AddClasses() // to register
.UsingRegistrationStrategy(RegistrationStrategy.Skip) // 2. Define
.AsSelf() // 2. Specify which services they are registered as
.WithTransientLifetime()); // 3. Set the lifetime for the services

Итак, нам есть что обсудить, давайте представим, что у нас есть следующие
сервисы и реализации в сборке:

public interface IService { }


public class Service1 : IService { }
public class Service2 : IService { }
public class Service : IService { }

public interface IFoo {}


public interface IBar {}
public class Foo: IFoo, IBar {}

Предыдущий Scan() код будет регистрировать Service1 , Service2 , Service и ,


Foo как они сами, что эквивалентно следующие утверждения с помощью
встроенных контейнера:

services.AddTransient<Service1>();
services.AddTransient<Service2>();
services.AddTransient<Service>();
services.AddTransient<Foo>();

В следующем разделе я рассмотрю некоторые опции, доступные вам для


пункта 1 - выбор реализации для регистрации.

Выбор и фильтрация реализаций для регистрации

Самый первый шаг настройки в конце сканирования сборки - определить, какие


конкретные классы вы хотите зарегистрировать в своем приложении. Scrutor
предоставляет множество различных способов поиска типов, ориентируясь в
первую очередь на сканирование сборок для типов и фильтрацию списка. В
этом разделе я опишу различные доступные варианты.

Если вы когда-либо использовали возможности сканирования сборок


других DI-контейнеров, таких как StructureMap или Autofac, то вам
это должно быть знакомо.

Указание типов явно

Простейший селектор типов предполагает явное предоставление типов.


Например, чтобы зарегистрироваться Service1 и Service2 как временные
услуги:

services.Scan(scan => scan


.AddTypes<Service1, Service2>()
.AsSelf()
.WithTransientLifetime());
Это эквивалентно

services.AddTransient<Service1>();
services.AddTransient<Service2>();

Доступны три AddTypes<> метода, с 1, 2 или 3 общими параметрами. Вы,


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

Сканирование сборки на наличие типов

Реальная точка продажи Scrutor - это методы сканирования ваших сборок и


автоматической регистрации найденных типов. Scrutor имеет несколько
вариантов, которые позволяют вам передавать экземпляры Assembly для
сканирования, получать список Assembly экземпляров на основе зависимостей
вашего приложения или использовать вызывающую или исполняющую сборку.
Лично я предпочитаю методы, которые позволяют вам передавать a Type , и
пусть Scrutor находит все классы в сборке, которая содержит Type . Например:

services.Scan(scan => scan


.FromAssemblyOf<IService>()
.AddClasses()
.AsSelf()
.WithTransientLifetime());

Приведенный выше код найдет сборку, которая содержит, IService и


отсканирует все содержащиеся в ней классы. Для текущей версии Scrutor, 3.0.0
на момент написания (названной Third Essential Scarecrow 😂) , доступны
следующие методы сканирования сборок:

FromAssemblyOf<> , FromAssembliesOf - Сканирование сборок ,


содержащих поставляемый Type или Type S
FromCallingAssembly , FromExecutingAssembly , FromEntryAssembly -
Сканирование призвания, выполнение или запись сборки! Смотрите
на Assembly статические методы для получения подробной
информации о различиях между ними.
FromAssemblyDependencies - Сканирование всех сборок, от которых
Assembly зависит предоставленное

FromApplicationDependencies , FromDependencyContext - сканирование


библиотеки времени выполнения. Я ничего не знаю о
DependencyContext s, так что вы сами по себе с этими!

Фильтрация найденных классов

Какой бы подход к сканированию сборки вы ни выбрали, вам нужно


AddClasses() впоследствии позвонить , чтобы выбрать конкретные типы для
добавления в контейнер. Этот метод имеет несколько перегрузок, которые вы
можете использовать для фильтрации выбранных классов:

AddClasses() - Добавить все публичные, неабстрактные классы

AddClasses(publicOnly) - Добавить все неабстрактные классы.


Установить publicOnly=false добавить internal /
private вложенные классы тоже

AddClass(predicate) - Запустите произвольное действие, чтобы


отфильтровать, какие классы включают. Это очень полезно и широко
используется, как показано ниже.
AddClasses(predicate, publicOnly) - Сочетание двух предыдущих
методов.

Возможность запуска предиката для каждого обнаруженного конкретного


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

services.Scan(scan => scan


.FromAssemblyOf<IService>()
.AddClasses(classes => classes.AssignableTo<IService>())
.AsImplementedInterfaces()
.WithTransientLifetime());

Или вы можете ограничиться только этими классами в определенном


пространстве имен:

services.Scan(scan => scan


.FromAssemblyOf<IService>()
.AddClasses(classes => classes.InNamespaces("MyApp"))
.AsImplementedInterfaces()
.WithTransientLifetime());

В качестве альтернативы вы можете использовать произвольный фильтр на


основе самого Type себя:
services.Scan(scan => scan
.FromAssemblyOf<IService>()
.AddClasses(classes => classes.Where(type => type.Name.EndsWith("Repo
.AsImplementedInterfaces()
.WithTransientLifetime());

После того как вы определили конкретный селектор классов, вы можете при


желании определить стратегию замены.

Обработка дублированных сервисов с помощью


ReplacementStrategy

Scrutor позволяет вам контролировать, как обрабатывать случай, когда служба


(например IService ) уже зарегистрирована в контейнере DI, указав a
ReplacementStrategy . В настоящее время вы можете использовать пять
различных стратегий замены:

Append - Не беспокойтесь о дублирующих регистрациях, добавьте


новые регистрации для существующих сервисов. Это поведение по
умолчанию, если вы не указали стратегию регистрации.
Skip - Если услуга уже зарегистрирована в контейнере, не
добавляйте новую регистрацию.
Replace(ReplacementBehavior.ServiceType) - Если служба уже
зарегистрирована в контейнере, удалите все предыдущие
регистрации для этой службы перед созданием новой регистрации.
Replace(ReplacementBehavior.ImplementationType) - Если реализация
уже зарегистрирована в контейнере, удалите все предыдущие
регистрации, если реализация соответствует новой регистрации,
перед созданием новой регистрации.
Replace(ReplacementBehavior.All) - Примените оба предыдущих
поведения. Если служба или реализация ранее были
зарегистрированы, сначала удалите все эти регистрации.

Чтобы выбрать стратегию замены, используйте


UsingRegistrationStrategy() метод после указания вашего селектора типа:

services.Scan(scan => scan


.FromAssemblyOf<IService>()
.AddClasses()
.UsingRegistrationStrategy(RegistrationStrategy.Skip)
.AsSelf()
.WithTransientLifetime());

Обдумать разницу между стратегиями замены (в частности, тремя последними)


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

services.AddTransient<ITransientService, TransientService>();
services.AddScoped<IScopedService, ScopedService>();

Впоследствии во время сканирования Scrutor находит следующие классы для


регистрации в качестве переходных пар службы / реализации:

public class TransientService : IFooService {}


public class AnotherService : IScopedService {}

Каков будет конечный результат с каждой из стратегий замены? С


Append ответом легко, вы получите все:

services.AddTransient<ITransientService, TransientService>(); // From pre


services.AddScoped<IScopedService, ScopedService>(); // From previous reg
services.AddTransient<IFooSerice, TransientService>();
services.AddScoped<IScopedService, AnotherService>();

С Skip , дубликат IScopedService игнорируется, но мы добавляем


TransientService / IFooService пару:

services.AddTransient<ITransientService, TransientService>(); // From pre


services.AddScoped<IScopedService, ScopedService>(); // From previous reg
services.AddTransient<IFooSerice, TransientService>();

На более хитрых. Replace(ReplacementBehavior.ServiceType) заменяется типом


сервиса (т.е. интерфейсом), поэтому в этом случае IScopedService
ScopedService регистрация будет заменена на AnotherService :

services.AddTransient<ITransientService, TransientService>(); // From pre


services.AddTransient<IFooSerice, TransientService>();
services.AddScoped<IScopedService, AnotherService>(); // Replaces ScopedS
Если вы замените типом реализации Replace(ReplacementBehavior.
ImplementationType) , то TransientService регистрация изменится с
ITransientService на IFooService . Дубликат IScopedService прилагается:

services.AddScoped<IScopedService, ScopedService>(); // From previous reg


services.AddTransient<IFooSerice, TransientService>(); // Changed from IT
services.AddScoped<IScopedService, AnotherService>();

Наконец, если вы используете Replace(ReplacementBehavior.All) , обе


предыдущие регистрации будут удалены и заменены новыми:

services.AddTransient<IFooSerice, TransientService>();
services.AddScoped<IScopedService, AnotherService>();

Замена стратегии, вероятно, является одним из самых сложных аспектов,


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

Регистрация внедрений как сервис

Мы обсудили, как найти реализации для добавления и стратегию реализации,


но нам все еще нужно выбрать, как классы регистрируются в контейнере.

Scrutor предоставляет множество различных вариантов регистрации данной


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

AsSelf()

AsMatchingInterface()

AsImplementedInterfaces()

AsSelfWithInterfaces()

As<>()

Вы вызываете каждый метод после, AddClasses() если вы используете


стратегию регистрации по умолчанию, или после,
UsingRegistrationStrategy() если нет:
services.Scan(scan => scan
.FromAssemblyOf<IService>()
.AddClasses()
.AsSelf() // Specify how the to register the implementations/servic
.WithSingletonLifetime());

Для примеров в этом разделе я предполагаю, что Scrutor обнаружил


следующий класс как часть своего сканирования сборки:

public class TestService: ITestService, IService {}

Регистрация реализации как таковой

Для классов, которые не реализуют интерфейс, или которые вы хотите, чтобы


они были непосредственно доступны для внедрения, вы можете использовать
AsSelf() метод. В нашем примере это эквивалентно следующей ручной
регистрации:

services.AddSingleton<TestService>();

Регистрация сервисов, соответствующих стандартному


соглашению об именах

Я часто вижу шаблон, в котором каждый конкретный класс Class имеет


эквивалентный интерфейс с именем IClass . Вы можете зарегистрировать все
серклассы, которые соответствуют этому шаблону, используя
AsMatchingInterface() метод. Для нашего примера это эквивалентно
следующему:

services.AddSingleton<ITestService, TestService>();

Регистрация реализации как всех реализованных интерфейсов

Если класс реализует более одного интерфейса, иногда вы хотите, чтобы класс
был зарегистрирован во всех этих службах. Вы можете достичь этого с
помощью Scrutor, используя AsImplementedInterfaces() :

services.AddSingleton<ITestService, TestService>();
services.AddSingleton<IService, TestService>();
Как я уже говорил в своем последнем сообщении , важно понимать,
что эти регистрации могут привести к ошибкам, когда у вас есть
два экземпляра «синглтона». Если это не то, что вы хотите,
читайте дальше!

Регистрация реализации с использованием перенаправленных


сервисов

В моем предыдущем посте я обсуждал, как регистрация реализации более


одного раза в контейнере DI может привести к нескольким экземплярам
«синглтонных» или «ограниченных» сервисов. Контейнер ASP.NET Core DI не
поддерживает «переадресованные» типы сервисов, поэтому обычно вам нужно
достичь этого вручную, используя фабрику объектов. Например:

services.AddSingleton<TestService>();
services.AddSingleton<ITestService>(x => x.GetRequiredService<TestService
services.AddSingleton<IService>(x => x.GetRequiredService<TestService>())

С Scrutor вы можете теперь ( начиная с 3.0.0) легко использовать этот шаблон,


используя AsSelfWithInterfaces() метод.

Регистрация реализации в качестве произвольной службы

Последний вариант регистрации - указать конкретную услугу, например,


IMyService используя As<T>() функцию, например As<IMyService>() . Это
регистрирует все классы, найденные как этот сервис:

services.AddSingleton<IMyService, TestService>();

Обратите внимание, что если вы попытаетесь зарегистрировать


конкретный тип в службе, которую он не может предоставить, вы
получите InvalidOperationException во время выполнения.

Это охватывает все различные варианты регистрации. Вы, вероятно,


обнаружите, что используете различные методы для различных служб в своем
приложении (за исключением As<T>() функции, в которой я лично не нашел
необходимости).
Последний аспект, который следует учитывать при регистрации служб в
контейнерах DI, - это время жизни. К счастью, поскольку Scrutor использует
встроенный контейнер DI, это довольно очевидно, если вы знакомы с DI в
ASP.NET Core.

Указание времени жизни зарегистрированных


классов

Всякий раз, когда вы регистрируете класс в контейнере ASP.NET Core DI, вам
нужно указать время жизни службы. У Scrutor есть методы, соответствующие
трем временам жизни в ASP.NET Core:

WithTransientLifetime() - Transient - это время жизни по умолчанию,


если вы его не указали.
WithScopedLifetime() - Используйте тот же сервис, ограниченный
временем жизни запроса.
WithSingletonLifetime() - Используйте один экземпляр службы на
весь срок службы приложения.

Со всеми этими частями (сканирование, фильтрация, стратегия регистрации,


тип регистрации и срок действия) вы можете автоматизировать регистрацию
своих служб с помощью контейнера DI.

Scrutor также позволяет вам украшать ваши классы


[ServiceDescriptor] атрибутами, чтобы определить, как следует регистрировать
сервисы, но так как это кажется мерзостью, я не буду вдаваться в подробности
здесь 😉.

Более важным аспектом является рассмотрение того, как вы можете


объединить разные правила для разных классов в одном Scan() методе.

Объединение нескольких селекторов вместе

Маловероятно, что вы захотите зарегистрировать все классы в своем


приложении, используя одну и ту же стратегию жизни или регистрации. Вместо
этого гораздо более вероятно, что вы захотите зарегистрировать
подмножества своего приложения, используя ту же стратегию.

API Scrutor позволяет объединять несколько сканирований сборки, задавая


правила для подмножества классов одновременно. Это кажется мне очень
естественным и позволяет писать такие вещи как:

services.Scan(scan => scan


.FromAssemblyOf<CombinedService>()
.AddClasses(classes => classes.AssignableTo<ICombinedService>()) // F
.AsSelfWithInterfaces()
.WithSingletonLifetime()

.AddClasses(x=> x.AssignableTo(typeof(IOpenGeneric<>))) // Can close g


.AsMatchingInterface()

.AddClasses(x=> x.InNamespaceOf<MyClass>())
.UsingRegistrationStrategy(RegistrationStrategy.Replace()) // Defau
.AsMatchingInterface()
.WithScopedLifetime()

.FromAssemblyOf<DatabaseContext>() // Can load from multiple assembli


.AddClasses()
.AsImplementedInterfaces()
);

В общем, scrutor позволяет вам достичь всего, что вы могли бы сделать


вручную с помощью контейнера ASP.NET Core DI, но более кратким способом.
Если вы пропустите сканирование сборки из сторонних DI-контейнеров, но по
какой-либо причине хотите использовать встроенный DI-контейнер, я
настоятельно рекомендую проверить Scrutor .

Целый аспект, который я не обсуждал в этом сообщении, является


Украшением. Я расскажу об этом в моем следующем посте, но пока
вы можете увидеть примеры его использования в проекте Readme .

Резюме

Scrutor добавляет возможности сканирования сборок в контейнер DI


Microsoft.Extensions.DependencyInjection , используемый в ASP.NET Core. Он
не является сторонним DI-контейнером, а расширяет встроенный контейнер,
упрощая регистрацию ваших служб.

Для того, чтобы зарегистрировать свои услуги, вызов Scan() на


IServiceCollection ин Startup.ConfigureServices . Вы должны определить
четыре вещи:
1. Селектор - как найти типы для регистрации (как правило, путем
сканирования сборки)
2. Стратегия регистрации - как обрабатывать дубликаты сервисов
(по умолчанию Scrutor добавляет новые регистрации для дубликатов
сервисов)
3. Сервисы - какие сервисы (т.е. интерфейсы) каждая реализация
должна быть зарегистрирована как
4. Время жизни - какое время жизни для регистрации (переходный по
умолчанию)

Вы можете объединить несколько сканов, чтобы применить разные правила к


подмножествам ваших классов. Если Scrutor вам интересен, проверьте это на
GitHub , загрузите пакет NuGet и следите за Кристианом в Twitter !

Вам также может понравиться