Start Debugging

Correção: Unable to resolve service for type 'X' while attempting to activate 'Y'

O ASP.NET Core lança esta exceção quando um construtor pede um tipo que nunca foi registrado, foi registrado no contêiner errado ou foi adicionado depois que o host foi construído. Três correções concretas cobrem quase todos os casos.

A correção: o ActivatorUtilities do ASP.NET Core percorreu o construtor de Y, pediu ao IServiceProvider um X e não recebeu nada. Ou você esqueceu de chamar services.AddScoped<X, XImpl>() (ou AddSingleton / AddTransient), registrou a implementação mas pediu uma interface ou classe base que o contêiner não conhece, ou o registro vive em um IServiceCollection diferente daquele que o host realmente construiu. Adicione o registro faltante em Program.cs antes de builder.Build() e confirme que os nomes dos tipos batem exatamente.

System.InvalidOperationException: Unable to resolve service for type 'MyApp.Data.IUserRepository' while attempting to activate 'MyApp.Api.Controllers.UsersController'.
   at Microsoft.Extensions.DependencyInjection.ActivatorUtilities.ConstructorMatcher.CreateInstance(IServiceProvider provider)
   at Microsoft.Extensions.DependencyInjection.ActivatorUtilities.CreateInstance(IServiceProvider provider, Type instanceType, Object[] parameters)
   at Microsoft.AspNetCore.Mvc.Controllers.ServiceBasedControllerActivator.Create(ControllerContext context)
   at Microsoft.AspNetCore.Mvc.Controllers.ControllerActivatorProvider.<>c__DisplayClass4_0.<CreateActivator>b__0(ControllerContext controllerContext)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)

Este guia foi escrito contra .NET 11 preview 4, Microsoft.AspNetCore.App 11.0.0-preview.4 e Microsoft.Extensions.DependencyInjection 11.0.0-preview.4. O texto da exceção tem sido estável desde o ASP.NET Core 2.1, então cada correção abaixo se aplica diretamente até .NET Core 3.1, .NET 5, 6, 8 e 10.

Os dois nomes de tipos na mensagem são a parte mais útil: o primeiro nome (X) é o tipo que o contêiner não conseguiu encontrar, e o segundo nome (Y) é o consumidor que o pediu. Leia-os nessa ordem antes de fazer qualquer outra coisa, porque a consulta de busca que te trouxe até aqui vai bater com a metade errada da mensagem na metade das vezes.

Por que o contêiner não encontrou o tipo

Três causas explicam quase todas as ocorrências:

  1. Nada registrado para aquele tipo. Você escreveu public UsersController(IUserRepository repo) mas nunca chamou services.AddScoped<IUserRepository, UserRepository>(). O contêiner não tem nenhum mapeamento da interface para a implementação.
  2. Você registrou com a chave errada. Chamou services.AddScoped<UserRepository>() (tipo concreto) mas o controller pede IUserRepository (interface). O contêiner só resolve o que você registrou, pelo tipo exato usado como parâmetro genérico ou argumento serviceType.
  3. Você registrou em um IServiceCollection diferente. Comum em testes onde o host de testes constrói sua própria coleção, ou em casos incomuns onde você muta builder.Services depois de builder.Build(). O host tira um snapshot da coleção no momento da build.

Existem algumas variantes menos comuns que vale a pena nomear: um tipo genérico aberto registrado sem o tipo fechado correspondente (AddScoped(typeof(IRepo<>), typeof(Repo<>)) está OK; AddScoped<IRepo<User>>(...) não é o mesmo registro), um serviço keyed solicitado por um consumidor sem chave, e um Action ou delegate de factory passado a um construtor que a injeção de dependência não consegue sintetizar. Cubra primeiro as três principais.

Reprodução mínima

Esta é a menor API mínima de .NET 11 que lança a exceção:

// .NET 11 preview 4, Microsoft.AspNetCore.App 11.0.0-preview.4
using Microsoft.AspNetCore.Builder;

var builder = WebApplication.CreateBuilder(args);

// Notice: no services.AddScoped<IUserRepository, UserRepository>();

builder.Services.AddControllers();

var app = builder.Build();
app.MapControllers();
app.Run();

public interface IUserRepository
{
    string GetName(int id);
}

public sealed class UserRepository : IUserRepository
{
    public string GetName(int id) => $"user-{id}";
}
// .NET 11 preview 4
using Microsoft.AspNetCore.Mvc;

[ApiController]
[Route("users")]
public sealed class UsersController(IUserRepository repo) : ControllerBase
{
    [HttpGet("{id:int}")]
    public IActionResult Get(int id) => Ok(repo.GetName(id));
}

Faça GET /users/1 e a requisição falha com a exceção da seção acima. O contêiner nunca viu IUserRepository, então quando o ServiceBasedControllerActivator do MVC tenta construir o controller, o parâmetro do construtor não pode ser satisfeito.

Correção um: registre o serviço faltante

A primeira coisa a tentar, e a resposta em 80% das vezes:

// .NET 11 preview 4
builder.Services.AddScoped<IUserRepository, UserRepository>();

Escolha o tempo de vida que combina com como o serviço é usado:

Erre isto e você troca a exceção de hoje pela InvalidOperationException: Cannot consume scoped service ... from singleton ... de amanhã, ou pior, por um bug silencioso de segurança de threads onde duas requisições compartilham um DbContext. A orientação oficial na documentação de Microsoft.Extensions.DependencyInjection é a referência; opte por AddScoped na dúvida quando existir um escopo de requisição.

Correção dois: registre o tipo de serviço que o construtor de fato pede

Se você tem um registro mas ainda recebe a exceção, o tipo registrado e o tipo consumido não batem. Verifique o construtor contra o registro:

// Wrong: only the concrete type is registered
builder.Services.AddScoped<UserRepository>();

// Right: register both the interface and the implementation,
// or just the interface mapped to the implementation
builder.Services.AddScoped<IUserRepository, UserRepository>();

O contêiner não faz inferência de interface. Se UserRepository implementa IUserRepository, registrar apenas UserRepository não registra IUserRepository. Se o consumidor pede a interface, registre a interface.

Se você genuinamente precisa de ambos (“injete UserRepository aqui, mas IUserRepository ali”), registre os dois e encaminhe a interface ao concreto:

// .NET 11 preview 4
builder.Services.AddScoped<UserRepository>();
builder.Services.AddScoped<IUserRepository>(sp => sp.GetRequiredService<UserRepository>());

Esse padrão importa para serviços hospedados e o pattern de opções, onde consumidores às vezes pedem o MyOptions concreto e às vezes IOptions<MyOptions>.

Correção três: registre antes do host ser construído

builder.Build() é a linha de corte. Qualquer coisa que você adicione a builder.Services depois desse ponto é silenciosamente descartada para o host em execução:

// .NET 11 preview 4
var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

// Too late. The container was snapshotted in builder.Build().
builder.Services.AddScoped<IUserRepository, UserRepository>();

app.MapControllers();
app.Run();

Reordene para que cada registro rode antes de Build. Esse padrão morde com mais frequência quando uma refatoração move uma chamada Add* para um método, e o método é chamado do lugar errado. Um padrão útil é colocar cada chamada services.Add* dentro de um método de extensão sobre IServiceCollection e chamá-lo de um único lugar perto do topo de Program.cs:

// .NET 11 preview 4
public static class DependencyInjectionExtensions
{
    public static IServiceCollection AddDataServices(this IServiceCollection services)
    {
        services.AddScoped<IUserRepository, UserRepository>();
        services.AddScoped<IOrderRepository, OrderRepository>();
        return services;
    }
}

// Program.cs
builder.Services.AddDataServices();
var app = builder.Build();

Em testes de integração contra WebApplicationFactory<TEntryPoint>, a regra equivalente é que ConfigureTestServices roda antes do host de teste ser construído. Se você muta o contêiner de dentro do corpo de um método de teste depois de factory.CreateClient(), está mutando uma coleção descartada.

Variantes que parecem o mesmo erro

Vários erros parecidos se resolvem de formas diferentes e desperdiçam tempo se forem tratados como o mesmo bug:

Como confirmar que o registro está conectado corretamente

Três verificações rápidas vencem qualquer quantidade de adivinhação:

  1. IServiceProvider.GetService<T>() do console de desenvolvimento. Coloque uma única linha logo após app.Build() e antes de app.Run():

    // .NET 11 preview 4 - remove before commit
    using (var scope = app.Services.CreateScope())
    {
        var repo = scope.ServiceProvider.GetService<IUserRepository>();
        Console.WriteLine(repo is null ? "NOT REGISTERED" : repo.GetType().FullName);
    }

    Se GetService<T>() retorna null, o registro está faltando ou está scoped a um contêiner diferente. GetRequiredService<T>() lançaria a mesma InvalidOperationException que você está depurando.

  2. Valide os escopos no startup. Passe ValidateScopes = true e ValidateOnBuild = true à factory do provider de serviços e o host se recusará a iniciar se algum registro estiver quebrado:

    // .NET 11 preview 4
    builder.Host.UseDefaultServiceProvider(options =>
    {
        options.ValidateScopes = true;
        options.ValidateOnBuild = true;
    });

    ValidateOnBuild percorre cada registro uma vez no momento da build e falha rápido se algum parâmetro de construtor não pode ser satisfeito. No ambiente de desenvolvimento o ASP.NET Core habilita isso para você, razão pela qual a exceção frequentemente aparece no momento em que você inicia a aplicação em vez do momento em que atinge um endpoint.

  3. Imprima os registros. Quando o registro parece correto mas a exceção continua disparando, despeje a coleção em si antes de Build:

    // .NET 11 preview 4
    foreach (var sd in builder.Services.Where(s => s.ServiceType.Name.Contains("UserRepository")))
    {
        Console.WriteLine($"{sd.Lifetime}: {sd.ServiceType.FullName} -> {sd.ImplementationType?.FullName ?? "factory"}");
    }

    Isso pega o caso em que você registrou MyApp.OldNamespace.IUserRepository e o controller importa MyApp.NewNamespace.IUserRepository. A mensagem da exceção mostra o namespace completo, mas o olho passa por cima.

Casos extremos que pegam desenvolvedores experientes

Alguns padrões disparam essa exceção em código que parece correto à inspeção:

Relacionados

Fontes

Comments

Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.

< Voltar