O Scrutor acrescenta duas coisas que faltam ao contêiner nativo de injeção de dependência (DI) do .NET: a varredura de assemblies, que registra todos os nossos serviços em uma linha em vez de dezenas, e a decoração, que envolve um serviço registrado com logging, cache ou validação sem modificá-lo.

O repositório do Scrutor descreve o projeto em uma linha, “Extensões de varredura de assemblies e decoração para Microsoft.Extensions.DependencyInjection”, e esse é exatamente o escopo dele. Não é um contêiner de injeção de dependência. Ele estende IServiceCollection, então nada muda na configuração que já temos.

Para baixar o código-fonte deste artigo, você pode acessar nosso repositório no GitHub.

O que é o Scrutor?

O Scrutor é um pacote NuGet que adiciona os dois recursos de que a maioria das equipes sente falta na injeção de dependência nativa do .NET: a varredura de assemblies e a decoração. A varredura encontra as implementações dos nossos serviços por convenção e as registra em uma única chamada a Scan(), substituindo dezenas de linhas AddScoped() escritas à mão que, de outra forma, esqueceríamos de atualizar quando adicionamos uma classe.

A decoração envolve um serviço já registrado em outra implementação da mesma interface, para logging, cache ou validação, sem modificar a classe original nem o registro dela.

O Scrutor não é um contêiner de injeção de dependência e não substitui Microsoft.Extensions.DependencyInjection. Ele registra tudo por meio de métodos de extensão em IServiceCollection, então os registros existentes, os tempos de vida e o comportamento do contêiner continuam exatamente como estão. O pacote tem licença MIT e é mantido ativamente; a versão atual é o Scrutor 7.0.0.

Se precisarmos de módulos, interceptação ou injeção por propriedade, esse é um trabalho para um contêiner completo, como o Autofac. Para varredura e decoração, basta o Scrutor sobre o contêiner nativo.

Instalação e primeira varredura

Instalamos o Scrutor pelo NuGet em um projeto Web API criado com dotnet new webapi --use-controllers:

dotnet add package Scrutor

Em seguida, varremos o assembly atual e registramos cada classe encontrada para as interfaces que ela implementa:

builder.Services.Scan(scan => scan
    .FromAssembliesOf(typeof(Program))
    .AddClasses()
    .AsImplementedInterfaces()
    .WithScopedLifetime());

Isso registra cada classe concreta e pública do assembly da nossa aplicação para cada interface que ela implementa, com o tempo de vida scoped.

Todo o resto deste artigo é um refinamento dessa única chamada: restringir quais classes pegamos, escolher como elas são mapeadas para as interfaces e controlar os tempos de vida delas.

Varredura de assemblies com o Scrutor

O Scrutor ajuda a simplificar o código de injeção de dependência, ao procurar tipos dinamicamente dentro dos assemblies e registrá-los em tempo de execução. Com a varredura de assemblies, podemos automatizar parcialmente o código de registro das dependências.

Além disso, podemos usar a varredura de assemblies para construir sistemas extensíveis. Com ela, podemos construir um programa capaz de encontrar e carregar módulos adicionais em tempo de execução.

Automação do registro de dependências

No nosso projeto Web API do ASP.NET, vamos definir uma entidade User:

public class User
{
    public required int Id { get; init; }
    public required string FirstName { get; init; }
    public required string LastName { get; init; }
}

Depois, vamos criar uma interface de serviço para gerenciar a entidade User:

public interface IUserService
{
    User GetUser(int id);
}

No namespace IntroductionToScrutorInDotNet.Services.Implementations, vamos definir uma implementação de IUserService:

public class UserService : IUserService
{
    public User GetUser(int id)
    {
        return new User
        {
            Id = id,
            FirstName = "John",
            LastName = "Doe"
        };
    }
}

Por fim, vamos criar um UsersController que nos permita consultar o usuário com uma requisição HTTP GET:

using Microsoft.AspNetCore.Mvc;

[Route("[controller]")]
[ApiController]
public class UsersController : ControllerBase
{
    private readonly IUserService _userService;

    public UsersController(IUserService userService)
    {
        _userService = userService;
    }

    [HttpGet("{id:int}")]
    public IActionResult GetUser(int id)
    {
        return Ok(_userService.GetUser(id));
    }
}

Agora, precisamos conectar UsersController a UserService, e podemos fazer isso com o nosso framework de injeção de dependência.

Em vez de registrar UserService manualmente, podemos usar o método de extensão Scan() do Scrutor para registrar todos os serviços automaticamente:

builder.Services.Scan(selector => selector
    .FromAssembliesOf(typeof(Program))
    .AddClasses(
        classSelector =>
            classSelector.InNamespaces("IntroductionToScrutorInDotNet.Services.Implementations")
    )
    .AsImplementedInterfaces()
);

Aqui, chamamos o método de extensão Scan() do Scrutor, passando uma ação seletora como argumento. O método faz a varredura dos assemblies e seleciona os serviços a registrar com base no seletor que fornecemos.

Antes de tudo, precisamos selecionar o assembly em que ficam os tipos que queremos registrar; aqui, apontamos o Scrutor para o assembly que contém Program usando FromAssembliesOf(typeof(Program)). Em seguida, restringimos ainda mais a seleção, para registrar apenas classes, chamando o método AddClasses().

O Scrutor 6.0.0 removeu o antigo método auxiliar FromCallingAssembly(), então nomeamos o assembly explicitamente. FromAssembliesOf(typeof(Program)) também continua funcionando se, mais tarde, movermos esse registro para um método de extensão em outro assembly e passarmos typeof(Program) como argumento, situação em que FromCallingAssembly() teria varrido o assembly errado.

O método AddClasses() também recebe um seletor como parâmetro, e o usamos para selecionar apenas as classes do namespace IntroductionToScrutorInDotNet.Services.Implementations.

The Web API Production Checklist, e-book gratuito em inglês

E-book gratuito

Sua Web API está pronta para produção?

33 itens para verificar antes de implantá-la, com a correção de cada um. Um PDF gratuito de 76 páginas para .NET 10.

O e-book está em inglês.

Baixe o checklist gratuito

PDF gratuito. Um único e-mail para enviá-lo. Cancele a inscrição quando quiser.

Por fim, registramos os tipos selecionados para responder por todas as interfaces que eles implementam, chamando AsImplementedInterfaces(). Fazemos isso porque o nosso UsersController depende de IUserService, e não diretamente de UserService.

Com a grande variedade de métodos que o Scrutor oferece, podemos ser muito precisos sobre os serviços que queremos registrar.

Registro de serviços em outro assembly

Vamos considerar outro projeto de biblioteca de classes .NET, IntroductionToScrutorInDotNet.Customers, que contém tipos para gerenciar a entidade Customer:

  • A definição da entidade Customer
  • A interface ICustomerService
  • A implementação CustomerService da interface ICustomerService

Copiamos esse projeto do repositório de exemplo, colocamos a pasta dele ao lado da pasta do nosso projeto Web API e adicionamos uma referência a ele:

dotnet add reference ../IntroductionToScrutorInDotNet.Customers/IntroductionToScrutorInDotNet.Customers.csproj

O nosso CustomersController precisa usar ICustomerService para criar e retornar um cliente para um determinado User:

using IntroductionToScrutorInDotNet.Customers.Services;
using Microsoft.AspNetCore.Mvc;

[Route("[controller]")]
[ApiController]
public class CustomersController : ControllerBase
{
    private readonly ICustomerService _customerService;

    public CustomersController(ICustomerService customerService)
    {
        _customerService = customerService;
    }

    [HttpPost]
    public IActionResult CreateCustomer(User user)
    {
        var fullName = string.Join(' ', user.FirstName, user.LastName);
        var customerId = Random.Shared.Next(1000);

        return Created(
            $"/Customers/{customerId}",
            _customerService.CreateCustomer(customerId, fullName)
        );
    }
}

Com o Scrutor, depois de adicionar using IntroductionToScrutorInDotNet.Customers.Services; no topo de Program.cs, podemos registrar os tipos do assembly do projeto IntroductionToScrutorInDotNet.Customers:

builder.Services.Scan(selector => selector
    .FromAssemblyOf<ICustomerService>()
    .AddClasses(classSelector =>
        classSelector.AssignableTo<ICustomerService>())
    .AsMatchingInterface());

Aqui, pedimos ao Scrutor que varra apenas o assembly em que fica a interface ICustomerService, usando o método FromAssemblyOf<ICustomerService>(). Depois, entre todas as implementações desse assembly, selecionamos apenas as que são atribuíveis a ICustomerService.

Por fim, instruímos o Scrutor a registrar as implementações selecionadas para as interfaces cujo nome corresponde ao nome da classe de implementação, chamando o método AsMatchingInterface().

Uso de tipos genéricos

Agora, queremos adicionar um repositório para obter a lista de Users. Primeiro, definimos a interface genérica de repositório:

public interface IRepository<T>
{
    IEnumerable<T> GetAll();
}

Depois, implementamos o repositório usando uma lista como armazenamento da entidade User:

public class UserRepository : IRepository<User>
{
    private readonly List<User> _users = new()
    {
        new User
        {
            Id = 1,
            FirstName = "John",
            LastName = "Doe"
        },
        new User
        {
            Id = 2,
            FirstName = "Janine",
            LastName = "Doe"
        }
    };

    public IEnumerable<User> GetAll()
    {
        return _users;
    }
}

Vamos atualizar o UsersController, mantendo os atributos dele e a ação GetUser(), para usar o repositório e obter todos os usuários que temos no sistema:

public class UsersController : ControllerBase
{
    private readonly IUserService _userService;
    private readonly IRepository<User> _userRepository;

    public UsersController(IUserService userService, IRepository<User> userRepository)
    {
        _userService = userService;
        _userRepository = userRepository;
    }

    [HttpGet]
    public IActionResult GetUsers()
    {
        return Ok(_userRepository.GetAll());
    }
}

Por último, pedimos ao Scrutor que registre todas as implementações do tipo genérico aberto IRepository<> como as interfaces que elas implementam:

builder.Services.Scan(selector => selector
    .FromAssembliesOf(typeof(Program))
    .AddClasses(classSelector => classSelector.AssignableTo(typeof(IRepository<>)))
    .AsImplementedInterfaces());

Isso registra o nosso UserRepository para responder por qualquer dependência de IRepository<User>.

Definição do tempo de vida das dependências

Como em qualquer framework de gerenciamento de dependências, especificar o tempo de vida das instâncias criadas pelo contêiner é uma parte essencial do registro de dependências. Com o Scrutor, podemos usar especificadores de tempo de vida:

builder.Services.Scan(selector => selector
    .FromAssemblyOf<ICustomerService>()
    .AddClasses(classSelector =>
        classSelector.AssignableTo<ICustomerService>())
    .AsMatchingInterface()
    .WithTransientLifetime()
);

Neste exemplo, registramos todas as implementações correspondentes com o tempo de vida transient, chamando WithTransientLifetime(). Há métodos semelhantes para os tempos de vida comuns do .NET, por exemplo WithSingletonLifetime() ou WithScopedLifetime().

Tratamento de várias implementações

Agora, vejamos como podemos definir a estratégia de registro desejada para cada seletor. A estratégia de registro é a forma como o Scrutor trata os casos em que existem várias implementações para a mesma interface. RegistrationStrategy fica no namespace Scrutor, então também adicionamos using Scrutor; no topo de Program.cs:

builder.Services.Scan(scan =>
    scan.FromAssemblyOf<ICustomerService>()
        .AddClasses(classes => classes.InExactNamespaceOf<ICustomerService>())
        .UsingRegistrationStrategy(RegistrationStrategy.Skip)
        .AsImplementedInterfaces()
);

Neste código, ao usar o método UsingRegistrationStrategy() com o parâmetro RegistrationStrategy.Skip, dizemos ao Scrutor que ignore implementações adicionais de qualquer interface depois que a primeira for registrada.

Outras estratégias de registro disponíveis incluem:

  • RegistrationStrategy.Append: acrescenta um novo registro para os serviços já existentes
  • RegistrationStrategy.Throw: lança uma exceção ao tentar registrar um serviço já existente

Encadeamento de vários registros

Até aqui, configuramos o Scrutor para cada parte da aplicação separadamente. Por simplicidade e clareza, é possível registrar tudo com uma única chamada ao método Scan().

Graças à possibilidade de encadear os diferentes métodos, podemos substituir todas as configurações do Scrutor que escrevemos por uma única chamada:

builder.Services.Scan(selector =>
    selector
        .FromAssembliesOf(typeof(Program))
        .AddClasses(
            classSelector =>
                classSelector.InNamespaces("IntroductionToScrutorInDotNet.Services.Implementations")
        )
        .AsImplementedInterfaces()
        .AddClasses(classSelector => classSelector.AssignableTo(typeof(IRepository<>)))
        .UsingRegistrationStrategy(RegistrationStrategy.Skip)
        .AsImplementedInterfaces()
        .FromAssemblyOf<ICustomerService>()
        .AddClasses(classSelector =>
            classSelector.AssignableTo<ICustomerService>())
        .AsMatchingInterface()
        .WithTransientLifetime()
);

Padrão Decorator com o Scrutor

Podemos usar o padrão de projeto Decorator para estender a funcionalidade de um objeto existente sem alterar o código dele. Para isso, o decorador é construído para envolver os objetos que precisam ser estendidos, de modo que consiga interceptar as chamadas aos métodos do objeto envolvido.

Implementação de um decorador simples

Para mostrar como podemos aproveitar o método de extensão Decorate() do Scrutor para gerenciar o registro de objetos decoradores, vamos definir um decorador do nosso repositório que registra um log no console quando acessamos uma lista de entidades.

Para isso, precisamos implementar a mesma interface que o nosso repositório implementa:

public class RepositoryLoggerDecorator<T> : IRepository<T>
{
    private readonly IRepository<T> _decoratedRepository;

    public RepositoryLoggerDecorator(IRepository<T> decoratedRepository)
    {
        _decoratedRepository = decoratedRepository;
    }

    public IEnumerable<T> GetAll()
    {
        Console.WriteLine("The list of all users has been retrieved from the DB");

        return _decoratedRepository.GetAll();
    }
}

O decorador deve receber no construtor a instância do repositório decorado; no nosso caso, é a instância de IRepository<T> que vamos envolver. No método GetAll(), registramos uma mensagem no console e depois chamamos o método GetAll() de _decoratedRepository.

The Web API Production Checklist, e-book gratuito em inglês

E-book gratuito

Sua Web API está pronta para produção?

33 itens para verificar antes de implantá-la, com a correção de cada um. Um PDF gratuito de 76 páginas para .NET 10.

O e-book está em inglês.

Baixe o checklist gratuito

PDF gratuito. Um único e-mail para enviá-lo. Cancele a inscrição quando quiser.

Registro de decoradores com o Scrutor

Vamos registrar o decorador no contêiner de DI com o método de extensão Decorate<,>() do Scrutor:

builder.Services.Decorate<IRepository<User>, RepositoryLoggerDecorator<User>>();

O primeiro argumento de tipo do método Decorate<,>() representa o tipo de serviço que queremos decorar, e o segundo argumento de tipo é o tipo do decorador.

Depois que registramos o decorador com o Scrutor, o RepositoryLoggerDecorator<User> intercepta todas as chamadas aos métodos de IRepository<User>. Se executarmos a aplicação e chamarmos o endpoint HTTP GET que obtém todos os usuários, veremos a mensagem de log escrita no console.

Neste exemplo, usamos o método Console.WriteLine() para registrar o log; em um caso real, deveríamos usar ILogger<T> e um framework de logging.

Armadilhas comuns com o Scrutor

As chamadas do Scrutor acima são curtas, mas alguns comportamentos pegam as equipes de surpresa na primeira vez que esbarram neles. Cada um dos itens a seguir foi verificado com o Scrutor 7.0.0.

Toda chamada a Scan() acrescenta registros por padrão. Executar duas varreduras que encontram UserRepository deixa dois registros de IRepository<User>, e IEnumerable<IRepository<User>> passa a resolver os dois.

A solução é definir uma estratégia explícita: UsingRegistrationStrategy(RegistrationStrategy.Skip), ou RegistrationStrategy.Throw durante o desenvolvimento, para detectar cedo as varreduras sobrepostas.

Os registros feitos por varredura são transient, a menos que digamos o contrário. Um repositório que supúnhamos ser scoped é criado a cada resolução até adicionarmos um tempo de vida explícito.

A solução é encadear o tempo de vida que realmente queremos, como WithScopedLifetime(); se os tempos de vida da injeção de dependência importam para um serviço, nunca devemos contar com o padrão.

Decorar um serviço que nunca foi registrado lança uma exceção. Chamar Decorate<IRepository<User>, RepositoryLoggerDecorator<User>>() antes que algum Scan() ou Add*() registre IRepository<User> lança uma Scrutor.DecorationException com a mensagem “Could not find any registered services for type ‘IntroductionToScrutorInDotNet.Repositories.IRepository<IntroductionToScrutorInDotNet.Entities.User>'”.

A solução é a ordem: faça primeiro todos os registros com Scan() e Add*() e deixe toda chamada a Decorate() por último.

Genéricos abertos são decorados com a sobrecarga não genérica. Depois que a varredura registra o tipo fechado IRepository<User>, tanto Decorate<IRepository<User>, RepositoryLoggerDecorator<User>>() quanto a forma com genérico aberto fazem o contêiner resolver o decorador:

builder.Services.Decorate(typeof(IRepository<>), typeof(RepositoryLoggerDecorator<>));

A sobrecarga com genérico aberto é a que devemos usar quando registramos muitos tipos fechados de uma mesma interface genérica e não queremos nomear cada um.

Scrutor vs Autofac vs DI nativa

O Scrutor fica entre o contêiner nativo e um contêiner completo, como o Autofac. A tabela compara onde cada opção se posiciona:

CritérioDI nativaNativa + ScrutorAutofac
Varredura de assembliesNãoSimSim
DecoradoresLambdas de fábrica escritas à mãoUma chamada a Decorate()Sim
Injeção por propriedadeNãoNãoSim
Módulos / contêineres filhosNãoNãoSim
Serviços com chaveSim (.NET 8+)Sim (o mesmo contêiner)Sim
Dependência extraNenhumaUm pacote pequeno, com licença MITTroca completa do contêiner
Quando escolherPoucos serviços, registro explícitoVocê quer convenções, e não um novo contêinerVocê precisa de interceptação, módulos ou tempos de vida avançados

As diretrizes de injeção de dependência da Microsoft dizem isso com todas as letras: “Recomendamos usar o contêiner nativo, a menos que você precise de um recurso específico que ele não ofereça.” Os recursos citados são injeção por propriedade, contêineres filhos, gerenciamento personalizado do tempo de vida, suporte a Func<T> para inicialização lenta e registro baseado em convenções. O Scrutor ocupa o meio-termo: mantemos o contêiner nativo, com os serviços com chave do .NET e a integração completa com o ASP.NET Core, e adicionamos só as convenções.

Uma troca completa para o Autofac em um projeto .NET traz interceptação e módulos, ao preço de uma segunda API de registro que todo membro da equipe precisa aprender.

Quando recorrer ao Scrutor

Recorremos ao Scrutor quando o código de registro deixa de ser informativo, quando o Program.cs vira uma parede de linhas AddScoped() que ninguém lê e que todo mundo esquece de atualizar. A varredura resolve isso em uma chamada.

Também recorremos a ele no momento em que queremos um decorador sem escrever à mão o padrão Factory com injeção de dependência.

Não recorremos a ele para substituir o contêiner. Se o requisito for interceptação, módulos ou injeção por propriedade, isso é território do Autofac. Para varredura e decoração, o Scrutor é a menor ferramenta que resolve.

Testado com .NET 10.0.10 e Scrutor 7.0.0.