Scrutor для C# (.NET): полное руководство с примерами
Scrutor — это библиотека с открытым исходным кодом для платформы .NET, которая расширяет встроенный механизм внедрения зависимостей (Dependency Injection, DI) возможностями автоматического сканирования сборок (assembly scanning). Она позволяет регистрировать сервисы в контейнере DI на основе соглашений об именах, атрибутов или интерфейсов, что значительно упрощает настройку IoC-контейнера в больших проектах.
Зачем нужна эта библиотека? В стандартном контейнере Microsoft.Extensions.DependencyInjection каждый сервис необходимо регистрировать вручную: services.AddScoped<IMyService, MyService>(). В проекте с десятками или сотнями сервисов это приводит к раздуванию кода в Startup.cs или Program.cs, снижает читаемость и увеличивает риск ошибок. Scrutor решает эту проблему, позволяя регистрировать все сервисы из сборки одной строкой: services.Scan(scan => scan.FromAssemblyOf<IMyService>()...).
Scrutor особенно полезен в архитектурах, где применяется Domain-Driven Design (DDD), Clean Architecture или модульные монолиты. Он автоматически находит все реализации интерфейсов, классы с определёнными атрибутами (например, [ServiceDescriptor]) или классы, соответствующие определённым паттернам именования (например, все классы, оканчивающиеся на "Repository"). Это делает код чище, а добавление новых сервисов — тривиальной задачей, не требующей изменения конфигурации DI.
Установка
Scrutor устанавливается через NuGet Package Manager. Выполните одну из следующих команд:
# Через .NET CLI
dotnet add package Scrutor
# Через Package Manager Console (Visual Studio)
Install-Package Scrutor
После установки в проект будет добавлена ссылка на библиотеку Scrutor. Убедитесь, что у вас установлен .NET SDK версии 6.0 или выше (рекомендуется .NET 8.0+).
Быстрый старт
Рассмотрим минимальный рабочий пример. Создадим консольное приложение с тремя файлами: интерфейс IGreetingService, его реализация GreetingService и точка входа Program.cs.
// IGreetingService.cs
public interface IGreetingService
{
string Greet(string name);
}
// GreetingService.cs
public class GreetingService : IGreetingService
{
public string Greet(string name) => $"Hello, {name}!";
}
// Program.cs
using Microsoft.Extensions.DependencyInjection;
using Scrutor;
var services = new ServiceCollection();
// Ручная регистрация (обычный подход)
// services.AddTransient<IGreetingService, GreetingService>();
// Регистрация через Scrutor (сканирование сборки)
services.Scan(scan => scan
.FromAssemblyOf<IGreetingService>() // Сканируем сборку, содержащую IGreetingService
.AddClasses(classes => classes.AssignableTo<IGreetingService>()) // Ищем классы, реализующие IGreetingService
.AsImplementedInterfaces() // Регистрируем как реализацию интерфейса
.WithTransientLifetime()); // Устанавливаем время жизни Transient
var serviceProvider = services.BuildServiceProvider();
var greetingService = serviceProvider.GetRequiredService<IGreetingService>();
Console.WriteLine(greetingService.Greet("World"));
// Вывод: Hello, World!
В этом примере Scrutor автоматически находит класс GreetingService в той же сборке, что и интерфейс IGreetingService, и регистрирует его как Transient. Если добавить новую реализацию IGreetingService, она будет зарегистрирована автоматически без изменения кода Program.cs.
Основные методы и классы
Scrutor предоставляет fluent API для построения цепочек вызовов. Рассмотрим 8 ключевых методов/классов с сигнатурами, описанием и примерами.
1. Scan(IServiceCollection, Action<ITypeSourceSelector>)
Сигнатура: public static IServiceCollection Scan(this IServiceCollection services, Action<ITypeSourceSelector> registration)
Описание: Точка входа в Scrutor. Вызывается на IServiceCollection и принимает делегат, в котором конфигурируется сканирование.
services.Scan(scan => scan
.FromAssemblyOf<MyClass>()
.AddClasses()
.AsMatchingInterface()
.WithScopedLifetime());
2. FromAssemblyOf<T>()
Сигнатура: public ITypeSourceSelector FromAssemblyOf<T>()
Описание: Указывает сборку для сканирования, содержащую тип T. Все типы из этой сборки будут проанализированы.
services.Scan(scan => scan
.FromAssemblyOf<Program>() // Сканируем сборку Program.cs
.AddClasses()
.AsSelf()
.WithSingletonLifetime());
3. FromAssembliesOf(params Type[] types)
Сигнатура: public ITypeSourceSelector FromAssembliesOf(params Type[] types)
Описание: Позволяет сканировать несколько сборок, передавая типы-маркеры из каждой сборки.
services.Scan(scan => scan
.FromAssembliesOf(typeof(Program), typeof(SomeOtherClass))
.AddClasses()
.AsImplementedInterfaces()
.WithTransientLifetime());
4. AddClasses(Action<IImplementationTypeFilter>? filter = null)
Сигнатура: public IImplementationTypeSelector AddClasses(Action<IImplementationTypeFilter>? filter = null)
Описание: Выбирает все неабстрактные классы из сканируемых сборок. Можно применить фильтр (например, по атрибутам или именам).
services.Scan(scan => scan
.FromAssemblyOf<Program>()
.AddClasses(classes => classes.Where(type => type.Name.EndsWith("Service")))
.AsImplementedInterfaces()
.WithScopedLifetime());
5. AssignableTo<T>()
Сигнатура: public IImplementationTypeFilter AssignableTo<T>()
Описание: Фильтр для AddClasses, который оставляет только классы, реализующие указанный интерфейс или наследующие базовый класс.
services.Scan(scan => scan
.FromAssemblyOf<Program>()
.AddClasses(classes => classes.AssignableTo<IRepository>())
.As<IRepository>()
.WithScopedLifetime());
6. AsImplementedInterfaces()
Сигнатура: public IServiceTypeSelector AsImplementedInterfaces()
Описание: Регистрирует класс как реализацию всех его интерфейсов (кроме IDisposable и подобных). Это самый распространённый способ регистрации.
services.Scan(scan => scan
.FromAssemblyOf<IUserService>()
.AddClasses(classes => classes.AssignableTo<IUserService>())
.AsImplementedInterfaces() // IUserService будет зарегистрирован
.WithTransientLifetime());
7. AsMatchingInterface()
Сигнатура: public IServiceTypeSelector AsMatchingInterface()
Описание: Регистрирует класс как реализацию интерфейса, имя которого совпадает с именем класса по шаблону "I" + имя класса. Например, UserService будет зарегистрирован как IUserService.
services.Scan(scan => scan
.FromAssemblyOf<UserService>()
.AddClasses(classes => classes.Where(type => type.Name.EndsWith("Service")))
.AsMatchingInterface() // UserService -> IUserService
.WithScopedLifetime());
8. WithTransientLifetime() / WithScopedLifetime() / WithSingletonLifetime()
Сигнатура: public IServiceLifetimeSelector WithTransientLifetime() (аналогично для Scoped и Singleton)
Описание: Задаёт время жизни сервиса: Transient (создаётся каждый раз), Scoped (один раз на запрос/скоуп), Singleton (один раз на всё приложение).
services.Scan(scan => scan
.FromAssemblyOf<Program>()
.AddClasses(classes => classes.AssignableTo<ISingletonService>())
.AsImplementedInterfaces()
.WithSingletonLifetime() // Регистрируем как Singleton
.AddClasses(classes => classes.AssignableTo<ITransientService>())
.AsImplementedInterfaces()
.WithTransientLifetime()); // Регистрируем как Transient
Реальный практический пример
Создадим приложение для управления заказами с использованием Clean Architecture. У нас будут слои: Domain, Application, Infrastructure и Web API. Scrutor автоматически зарегистрирует все сервисы и репозитории.
// Domain/IOrderRepository.cs
public interface IOrderRepository
{
Task<Order> GetByIdAsync(int id);
Task AddAsync(Order order);
}
// Domain/Order.cs
public class Order
{
public int Id { get; set; }
public string CustomerName { get; set; }
public decimal TotalAmount { get; set; }
}
// Application/IOrderService.cs
public interface IOrderService
{
Task<OrderDto> GetOrderAsync(int id);
Task CreateOrderAsync(CreateOrderDto dto);
}
// Application/OrderService.cs
public class OrderService : IOrderService
{
private readonly IOrderRepository _repository;
private readonly IMapper _mapper;
public OrderService(IOrderRepository repository, IMapper mapper)
{
_repository = repository;
_mapper = mapper;
}
public async Task<OrderDto> GetOrderAsync(int id)
{
var order = await _repository.GetByIdAsync(id);
return _mapper.Map<OrderDto>(order);
}
public async Task CreateOrderAsync(CreateOrderDto dto)
{
var order = _mapper.Map<Order>(dto);
await _repository.AddAsync(order);
}
}
// Infrastructure/OrderRepository.cs
public class OrderRepository : IOrderRepository
{
private readonly AppDbContext _context;
public OrderRepository(AppDbContext context)
{
_context = context;
}
public async Task<Order> GetByIdAsync(int id) => await _context.Orders.FindAsync(id);
public async Task AddAsync(Order order)
{
_context.Orders.Add(order);
await _context.SaveChangesAsync();
}
}
// Program.cs (Web API)
using Microsoft.Extensions.DependencyInjection;
using Scrutor;
var builder = WebApplication.CreateBuilder(args);
// Scrutor: автоматическая регистрация всех сервисов и репозиториев
builder.Services.Scan(scan => scan
.FromAssembliesOf(typeof(IOrderService), typeof(IOrderRepository))
.AddClasses(classes => classes.AssignableTo<IOrderService>())
.AsImplementedInterfaces()
.WithScopedLifetime()
.AddClasses(classes => classes.AssignableTo<IOrderRepository>())
.AsImplementedInterfaces()
.WithScopedLifetime());
// Регистрация AutoMapper (вручную, так как это сторонняя библиотека)
builder.Services.AddAutoMapper(typeof(Program));
builder.Services.AddControllers();
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
var app = builder.Build();
app.MapControllers();
app.Run();
// OrderController.cs
[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase
{
private readonly IOrderService _orderService;
public OrdersController(IOrderService orderService)
{
_orderService = orderService;
}
[HttpGet("{id}")]
public async Task<IActionResult> Get(int id)
{
var order = await _orderService.GetOrderAsync(id);
return Ok(order);
}
[HttpPost]
public async Task<IActionResult> Create(CreateOrderDto dto)
{
await _orderService.CreateOrderAsync(dto);
return Created();
}
}
В этом примере Scrutor сканирует сборки, содержащие IOrderService и IOrderRepository, находит все классы, реализующие эти интерфейсы (OrderService, OrderRepository), и регистрирует их с временем жизни Scoped. При добавлении нового сервиса, например INotificationService, достаточно просто создать интерфейс и его реализацию — Scrutor сам зарегистрирует их.
Советы по производительности и типичные ошибки
Производительность
- Кэшируйте результат сканирования: Scrutor выполняет рефлексию при старте приложения. Для больших проектов (100+ сборок) это может занять несколько секунд. Рассмотрите возможность кэширования зарегистрированных сервисов с помощью
services.BuildServiceProvider()и повторного использования. - Ограничивайте количество сканируемых сборок: Используйте
FromAssembliesOfс конкретными типами, а неFromApplicationDependencies(), который сканирует все сборки в папке bin. Это ускорит старт. - Используйте фильтры: Применяйте
Where()илиAssignableTo()вAddClasses, чтобы уменьшить количество обрабатываемых типов. Например,classes.Where(t => t.Name.EndsWith("Service")). - Избегайте дубли