Scrutor для C# (.NET): полное руководство с примерами

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

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")).
  • Избегайте дубли

Рекомендаций