Injeção de dependência no ASP.NET Core

Observação

Esta não é a versão mais recente deste artigo. Para informações sobre a versão vigente, confira a Versão do .NET 8 deste artigo.

Importante

Essas informações relacionam-se ao produto de pré-lançamento, que poderá ser substancialmente modificado antes do lançamento comercial. A Microsoft não oferece nenhuma garantia, explícita ou implícita, quanto às informações fornecidas aqui.

Para informações sobre a versão vigente, confira a Versão do .NET 8 deste artigo.

Por Kirk Larkin, Steve Smith e Brandon Dahler

O ASP.NET Core é compatível com o padrão de design de software de DI (injeção de dependência), que é uma técnica para alcançar a IoC (Inversão de Controle) entre classes e suas dependências.

Para obter mais informações específicas sobre injeção de dependência em controladores MVC, confira Injeção de dependência em controladores no ASP.NET Core.

Para obter informações sobre como usar a injeção de dependência em aplicativos diferentes de aplicativos Web, confira Injeção de dependência no .NET.

Para obter mais informações sobre a injeção de dependência de opções, confira Padrão de opções no ASP.NET Core.

Este tópico fornece informações sobre injeção de dependência no ASP.NET Core. A documentação principal sobre como usar injeção de dependência está incluída na Injeção de dependência no .NET.

Exibir ou baixar código de exemplo (como baixar)

Visão geral da injeção de dependência

Uma dependência é um objeto do qual outro objeto depende. Examine a classe MyDependency a seguir com um método WriteMessage do qual outras classes em um aplicativo dependem:

public class MyDependency
{
    public void WriteMessage(string message)
    {
        Console.WriteLine($"MyDependency.WriteMessage called. Message: {message}");
    }
}

Uma classe pode criar uma instância da classe MyDependency para usar seu método WriteMessage. No exemplo a seguir, a classe MyDependency é uma dependência da classe IndexModel:


public class IndexModel : PageModel
{
    private readonly MyDependency _dependency = new MyDependency();

    public void OnGet()
    {
        _dependency.WriteMessage("IndexModel.OnGet");
    }
}

A classe cria e depende diretamente da instância MyDependency. As dependências de código, como no exemplo anterior, são problemáticas e devem ser evitadas pelos seguintes motivos:

  • Para substituir MyDependency por uma implementação diferente, a classe IndexModel deve ser modificada.
  • Se MyDependency tiver dependências, elas também deverão ser configuradas pela classe IndexModel. Em um projeto grande com várias classes dependendo da MyDependency, o código de configuração fica pulverizado por todo o aplicativo.
  • É difícil testar a unidade dessa implementação.

Injeção de dependência trata desses problemas da seguinte maneira:

  • O uso de uma interface ou classe base para abstrair a implementação da dependência.
  • Registrando a dependência em um contêiner de serviço. O ASP.NET Core fornece um contêiner de serviço interno, o IServiceProvider. Normalmente, os serviços são registrados no arquivo Program.cs do aplicativo.
  • Injeção do serviço no construtor da classe na qual ele é usado. A estrutura assume a responsabilidade de criar uma instância da dependência e de descartá-la quando não for mais necessária.

No aplicativo de exemplo, a interface IMyDependency define o método WriteMessage:

public interface IMyDependency
{
    void WriteMessage(string message);
}

Essa interface é implementada por um tipo concreto, MyDependency:

public class MyDependency : IMyDependency
{
    public void WriteMessage(string message)
    {
        Console.WriteLine($"MyDependency.WriteMessage Message: {message}");
    }
}

O aplicativo de exemplo registra o serviço IMyDependency com o tipo concreto MyDependency. O método AddScoped registra o serviço com um tempo de vida com escopo, o tempo de vida de uma única solicitação. Descreveremos posteriormente neste tópico os tempos de vida do serviço.

using DependencyInjectionSample.Interfaces;
using DependencyInjectionSample.Services;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.AddScoped<IMyDependency, MyDependency>();

var app = builder.Build();

No exemplo de aplicativo, o serviço IMyDependency é solicitada e usada para chamar o método WriteMessage:

public class Index2Model : PageModel
{
    private readonly IMyDependency _myDependency;

    public Index2Model(IMyDependency myDependency)
    {
        _myDependency = myDependency;            
    }

    public void OnGet()
    {
        _myDependency.WriteMessage("Index2Model.OnGet");
    }
}

Usando o padrão DI, o controlador ou a Razor Page:

  • Não usa o tipo concreto MyDependency, apenas a interface IMyDependency que o implementa. Isso facilita a alteração da implementação sem modificar o controlador ou a Razor Page.
  • Não cria uma instância do MyDependency; ela é criada pelo contêiner de DI.

A implementação da interface IMyDependency pode ser aprimorada usando a API de log interna:

public class MyDependency2 : IMyDependency
{
    private readonly ILogger<MyDependency2> _logger;

    public MyDependency2(ILogger<MyDependency2> logger)
    {
        _logger = logger;
    }

    public void WriteMessage(string message)
    {
        _logger.LogInformation( $"MyDependency2.WriteMessage Message: {message}");
    }
}

O Program.cs atualizado registra a nova implementação de IMyDependency:

using DependencyInjectionSample.Interfaces;
using DependencyInjectionSample.Services;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.AddScoped<IMyDependency, MyDependency2>();

var app = builder.Build();

MyDependency2 depende ILogger<TCategoryName>do que ele solicita no construtor. ILogger<TCategoryName> é um serviço fornecido pela estrutura.

Não é incomum usar a injeção de dependência de uma maneira encadeada. Por sua vez, cada dependência solicitada solicita suas próprias dependências. O contêiner resolve as dependências no grafo e retorna o serviço totalmente resolvido. O conjunto de dependências que precisa ser resolvido normalmente é chamado de árvore de dependência, grafo de dependência ou grafo de objeto.

O contêiner resolve ILogger<TCategoryName> aproveitando os tipos abertos (genéricos), eliminando a necessidade de registrar todo tipo construído (genérico).

Na terminologia de injeção de dependência, um serviço:

  • Normalmente, é um objeto que fornece um serviço para outros objetos, como o serviço IMyDependency.
  • Não está relacionado a um serviço Web, embora o serviço possa usar um serviço Web.

A estrutura fornece um sistema de registro em log robusto. As implementações IMyDependency mostradas nos exemplos anteriores foram gravadas para demonstrar a DI básica, não para implementar o registro em log. A maioria dos aplicativos não deve precisar escrever agentes. O código a seguir demonstra o uso do registro em log padrão, que não exige que nenhum serviço seja registrado:

public class AboutModel : PageModel
{
    private readonly ILogger _logger;

    public AboutModel(ILogger<AboutModel> logger)
    {
        _logger = logger;
    }
    
    public string Message { get; set; } = string.Empty;

    public void OnGet()
    {
        Message = $"About page visited at {DateTime.UtcNow.ToLongTimeString()}";
        _logger.LogInformation(Message);
    }
}

Usando o código anterior, não é necessário atualizar Program.cs, pois o registro em log é fornecido pela estrutura.

Registrar grupos de serviços com métodos de extensão

O Microsoft Extensions usa uma convenção para registrar um grupo de serviços relacionados. A convenção é usar um único método de extensão Add{GROUP_NAME} para registrar todos os serviços exigidos por um recurso de estrutura. Por exemplo, o método de extensão AddControllers registra os serviços necessários para controladores MVC.

O código a seguir é gerado pelo modelo Razor Pages usando contas de usuário individuais e mostra como adicionar serviços adicionais ao contêiner usando os métodos de extensão AddDbContext e AddDefaultIdentity:

using DependencyInjectionSample.Data;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();
builder.Services.AddRazorPages();

var app = builder.Build();

Considere o seguinte ao registrar serviços e configurar opções:

using ConfigSample.Options;
using Microsoft.Extensions.DependencyInjection.ConfigSample.Options;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.Configure<PositionOptions>(
    builder.Configuration.GetSection(PositionOptions.Position));
builder.Services.Configure<ColorOptions>(
    builder.Configuration.GetSection(ColorOptions.Color));

builder.Services.AddScoped<IMyDependency, MyDependency>();
builder.Services.AddScoped<IMyDependency2, MyDependency2>();

var app = builder.Build();

Grupos de registros relacionados podem ser movidos para um método de extensão para registrar serviços. Por exemplo, os serviços de configuração são adicionados à seguinte classe:

using ConfigSample.Options;
using Microsoft.Extensions.Configuration;

namespace Microsoft.Extensions.DependencyInjection
{
    public static class MyConfigServiceCollectionExtensions
    {
        public static IServiceCollection AddConfig(
             this IServiceCollection services, IConfiguration config)
        {
            services.Configure<PositionOptions>(
                config.GetSection(PositionOptions.Position));
            services.Configure<ColorOptions>(
                config.GetSection(ColorOptions.Color));

            return services;
        }

        public static IServiceCollection AddMyDependencyGroup(
             this IServiceCollection services)
        {
            services.AddScoped<IMyDependency, MyDependency>();
            services.AddScoped<IMyDependency2, MyDependency2>();

            return services;
        }
    }
}

Os serviços restantes são registrados em uma classe similar. O código a seguir usa os novos métodos de extensão para registrar os serviços:

using Microsoft.Extensions.DependencyInjection.ConfigSample.Options;

var builder = WebApplication.CreateBuilder(args);

builder.Services
    .AddConfig(builder.Configuration)
    .AddMyDependencyGroup();

builder.Services.AddRazorPages();

var app = builder.Build();

Observação: Cada método de extensão services.Add{GROUP_NAME} adiciona e possivelmente configura serviços. Por exemplo, AddControllersWithViews adiciona os serviços que os controladores MVC com visualizações requerem e AddRazorPages adiciona os serviços que o Razor Pages requer.

Tempos de vida do serviço

Confira os Tempos de vida do serviço na Injeção de dependência no .NET

Para usar serviços com escopo em middleware, use uma das seguintes abordagens:

  • Injete o serviço no método Invoke ou InvokeAsync do middleware. O uso da injeção de construtor gera uma exceção de runtime porque força o serviço com escopo a se comportar como um singleton. O exemplo na seção Opções de tempo de vida e registro demonstra a abordagem InvokeAsync.
  • Use Middleware de fábrica. O middleware registrado usando essa abordagem é ativado por solicitação de cliente (conexão), o que permite que os serviços com escopo sejam injetados no construtor do middleware.

Para obter mais informações, confira Escrever middleware do ASP.NET Core personalizado.

Métodos de registro do serviço

Confira Métodos de registro de serviço na Injeção de dependência no .NET

É comum usar várias implementações ao simular tipos para teste.

Registrar um serviço com apenas um tipo de implementação equivale a registrar esse serviço com o mesmo tipo de implementação e serviço. É por isso que várias implementações de um serviço não podem ser registradas usando os métodos que não usam um tipo de serviço explícito. Esses métodos podem registrar várias instâncias de um serviço, mas todos eles terão o mesmo tipo de implementação.

Qualquer um dos métodos de registro de serviço acima pode ser usado para registrar várias instâncias de serviço do mesmo tipo de serviço. No exemplo a seguir, AddSingleton é chamado duas vezes com IMyDependency como tipo de serviço. A segunda chamada para AddSingleton substitui a anterior quando resolvida como IMyDependency e adiciona à anterior quando vários serviços são resolvidos por meio de IEnumerable<IMyDependency>. Os serviços aparecem na ordem em que foram registrados quando resolvidos por meio de IEnumerable<{SERVICE}>.

services.AddSingleton<IMyDependency, MyDependency>();
services.AddSingleton<IMyDependency, DifferentDependency>();

public class MyService
{
    public MyService(IMyDependency myDependency, 
       IEnumerable<IMyDependency> myDependencies)
    {
        Trace.Assert(myDependency is DifferentDependency);

        var dependencyArray = myDependencies.ToArray();
        Trace.Assert(dependencyArray[0] is MyDependency);
        Trace.Assert(dependencyArray[1] is DifferentDependency);
    }
}

Serviços com chave

Os serviços com chave referem-se a um mecanismo para registrar e recuperar serviços de Injeção de Dependência (DI) por meio de chaves. Um serviço é associado a uma chave chamando AddKeyedSingleton (ou AddKeyedScoped ou AddKeyedTransient) para registrá-la. Acesse um serviço registrado especificando a chave com o atributo [FromKeyedServices]. O código a seguir mostra como usar serviços com chave:

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SignalR;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddKeyedSingleton<ICache, BigCache>("big");
builder.Services.AddKeyedSingleton<ICache, SmallCache>("small");
builder.Services.AddControllers();

var app = builder.Build();

app.MapGet("/big", ([FromKeyedServices("big")] ICache bigCache) => bigCache.Get("date"));
app.MapGet("/small", ([FromKeyedServices("small")] ICache smallCache) =>
                                                               smallCache.Get("date"));

app.MapControllers();

app.Run();

public interface ICache
{
    object Get(string key);
}
public class BigCache : ICache
{
    public object Get(string key) => $"Resolving {key} from big cache.";
}

public class SmallCache : ICache
{
    public object Get(string key) => $"Resolving {key} from small cache.";
}

[ApiController]
[Route("/cache")]
public class CustomServicesApiController : Controller
{
    [HttpGet("big-cache")]
    public ActionResult<object> GetOk([FromKeyedServices("big")] ICache cache)
    {
        return cache.Get("data-mvc");
    }
}

public class MyHub : Hub
{
    public void Method([FromKeyedServices("small")] ICache cache)
    {
        Console.WriteLine(cache.Get("signalr"));
    }
}

Comportamento da injeção de construtor

Confira o Comportamento de injeção do construtor na Injeção de dependência no .NET

Contextos de Entity Framework

Por padrão, os contextos do Entity Framework são adicionados ao contêiner de serviço usando o tempo de vida com escopo, pois as operações de banco de dados do aplicativo Web normalmente estão focadas na solicitação do cliente. Para usar um tempo de vida diferente, especifique o tempo de vida usando uma sobrecarga AddDbContext. Os serviços de um determinado tempo de vida não devem usar um contexto de banco de dados com um tempo de vida menor do que o tempo de vida do serviço.

Opções de tempo de vida e de registro

Para demonstrar a diferença entre os tempos de vida do serviço e suas opções de registro, considere as interfaces a seguir que representam uma tarefa como uma operação com um identificador OperationId. Dependendo de como o tempo de vida de um serviço de operações é configurado para as interfaces a seguir, o contêiner fornece a mesma instância do serviço, ou outras diferentes, quando recebe uma solicitação de uma classe:

public interface IOperation
{
    string OperationId { get; }
}

public interface IOperationTransient : IOperation { }
public interface IOperationScoped : IOperation { }
public interface IOperationSingleton : IOperation { }

A classe Operation a seguir implementa todas as interfaces anteriores. O construtor Operation gera um GUID e armazena os últimos 4 caracteres na propriedade OperationId:

public class Operation : IOperationTransient, IOperationScoped, IOperationSingleton
{
    public Operation()
    {
        OperationId = Guid.NewGuid().ToString()[^4..];
    }

    public string OperationId { get; }
}

O código a seguir cria vários registros da classe Operation de acordo com os tempos de vida nomeados:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.AddTransient<IOperationTransient, Operation>();
builder.Services.AddScoped<IOperationScoped, Operation>();
builder.Services.AddSingleton<IOperationSingleton, Operation>();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseMyMiddleware();
app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();

O exemplo de aplicativo demonstra os tempos de vida do objeto dentro e entre solicitações individuais. O IndexModel e o middleware solicitam cada tipo de IOperation e registram a OperationId para cada um:

public class IndexModel : PageModel
{
    private readonly ILogger _logger;
    private readonly IOperationTransient _transientOperation;
    private readonly IOperationSingleton _singletonOperation;
    private readonly IOperationScoped _scopedOperation;

    public IndexModel(ILogger<IndexModel> logger,
                      IOperationTransient transientOperation,
                      IOperationScoped scopedOperation,
                      IOperationSingleton singletonOperation)
    {
        _logger = logger;
        _transientOperation = transientOperation;
        _scopedOperation    = scopedOperation;
        _singletonOperation = singletonOperation;
    }

    public void  OnGet()
    {
        _logger.LogInformation("Transient: " + _transientOperation.OperationId);
        _logger.LogInformation("Scoped: "    + _scopedOperation.OperationId);
        _logger.LogInformation("Singleton: " + _singletonOperation.OperationId);
    }
}

Semelhante ao IndexModel, o middleware resolve os mesmos serviços:

public class MyMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger _logger;

    private readonly IOperationSingleton _singletonOperation;

    public MyMiddleware(RequestDelegate next, ILogger<MyMiddleware> logger,
        IOperationSingleton singletonOperation)
    {
        _logger = logger;
        _singletonOperation = singletonOperation;
        _next = next;
    }

    public async Task InvokeAsync(HttpContext context,
        IOperationTransient transientOperation, IOperationScoped scopedOperation)
    {
        _logger.LogInformation("Transient: " + transientOperation.OperationId);
        _logger.LogInformation("Scoped: " + scopedOperation.OperationId);
        _logger.LogInformation("Singleton: " + _singletonOperation.OperationId);

        await _next(context);
    }
}

public static class MyMiddlewareExtensions
{
    public static IApplicationBuilder UseMyMiddleware(this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<MyMiddleware>();
    }
}

Os serviços com escopo e transitórios devem ser resolvidos no método InvokeAsync:

public async Task InvokeAsync(HttpContext context,
    IOperationTransient transientOperation, IOperationScoped scopedOperation)
{
    _logger.LogInformation("Transient: " + transientOperation.OperationId);
    _logger.LogInformation("Scoped: " + scopedOperation.OperationId);
    _logger.LogInformation("Singleton: " + _singletonOperation.OperationId);

    await _next(context);
}

A saída do agente mostra:

  • Os objetos transitórios sempre são diferentes. O valor transitório da OperationId é diferente no IndexModel e no middleware.
  • Os objetos com escopo são os mesmos para uma determinada solicitação, mas diferem em cada nova solicitação.
  • Os objetos singleton são os mesmos para cada solicitação.

Para reduzir a saída de log, defina "Logging:LogLevel:Microsoft:Error" no arquivo appsettings.Development.json:

{
  "MyKey": "MyKey from appsettings.Developement.json",
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "System": "Debug",
      "Microsoft": "Error"
    }
  }
}

Resolver um serviço na inicialização do aplicativo

O código a seguir mostra como resolver um serviço com escopo por uma duração limitada quando o aplicativo é iniciado:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddScoped<IMyDependency, MyDependency>();

var app = builder.Build();

using (var serviceScope = app.Services.CreateScope())
{
    var services = serviceScope.ServiceProvider;

    var myDependency = services.GetRequiredService<IMyDependency>();
    myDependency.WriteMessage("Call services from main");
}

app.MapGet("/", () => "Hello World!");

app.Run();

Validação de escopo

Confira o Comportamento de injeção do construtor na Injeção de dependência no .NET

Para obter mais informações, confira Validação de escopo.

Serviços de solicitação

Os serviços e suas dependências em uma solicitação do ASP.NET Core são expostos por meio de HttpContext.RequestServices.

A estrutura cria um escopo por solicitação, e RequestServices expõe o provedor de serviços com escopo. Todos os serviços com escopo são válidos enquanto a solicitação estiver ativa.

Observação

Prefira solicitar dependências como parâmetros de construtor em vez de resolver serviços em RequestServices. Solicitar dependências como parâmetros de construtor produz classes mais fáceis de testar.

Projetar serviços para injeção de dependência

Ao criar serviços para injeção de dependência:

  • Evite membros e classes estáticos com estado. Evite criar um estado global projetando aplicativos para usar serviços singleton.
  • Evite a instanciação direta das classes dependentes em serviços. A instanciação direta acopla o código a uma implementação específica.
  • Deixe os serviços pequenos, bem fatorados e fáceis de serem testados.

Se uma classe tiver muitas dependências injetadas, isso poderá ser um sinal de que a classe tem muitas responsabilidades e violará o SRP (princípio de responsabilidade única). Tente refatorar a classe movendo algumas das responsabilidades para uma nova classe. Lembre-se de que as classes de modelo de página do Razor Pages e as classes do controlador MVC devem se concentrar em questões de interface do usuário.

Descarte de serviços

O contêiner chama Dispose para os tipos IDisposable criados por ele. Os serviços resolvidos do contêiner nunca devem ser descartados pelo desenvolvedor. Se um tipo ou um alocador for registrado como singleton, o contêiner descartará o singleton automaticamente.

No exemplo a seguir, os serviços são criados pelo contêiner de serviço e descartados automaticamente: dependency-injection\samples\6.x\DIsample2\DIsample2\Services\Service1.cs

public class Service1 : IDisposable
{
    private bool _disposed;

    public void Write(string message)
    {
        Console.WriteLine($"Service1: {message}");
    }

    public void Dispose()
    {
        if (_disposed)
            return;

        Console.WriteLine("Service1.Dispose");
        _disposed = true;
    }
}

public class Service2 : IDisposable
{
    private bool _disposed;

    public void Write(string message)
    {
        Console.WriteLine($"Service2: {message}");
    }

    public void Dispose()
    {
        if (_disposed)
            return;

        Console.WriteLine("Service2.Dispose");
        _disposed = true;
    }
}

public interface IService3
{
    public void Write(string message);
}

public class Service3 : IService3, IDisposable
{
    private bool _disposed;

    public Service3(string myKey)
    {
        MyKey = myKey;
    }

    public string MyKey { get; }

    public void Write(string message)
    {
        Console.WriteLine($"Service3: {message}, MyKey = {MyKey}");
    }

    public void Dispose()
    {
        if (_disposed)
            return;

        Console.WriteLine("Service3.Dispose");
        _disposed = true;
    }
}
using DIsample2.Services;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.AddScoped<Service1>();
builder.Services.AddSingleton<Service2>();

var myKey = builder.Configuration["MyKey"];
builder.Services.AddSingleton<IService3>(sp => new Service3(myKey));

var app = builder.Build();
public class IndexModel : PageModel
{
    private readonly Service1 _service1;
    private readonly Service2 _service2;
    private readonly IService3 _service3;

    public IndexModel(Service1 service1, Service2 service2, IService3 service3)
    {
        _service1 = service1;
        _service2 = service2;
        _service3 = service3;
    }

    public void OnGet()
    {
        _service1.Write("IndexModel.OnGet");
        _service2.Write("IndexModel.OnGet");
        _service3.Write("IndexModel.OnGet");
    }
}

O console de depuração mostra a seguinte saída após cada atualização da página Índice:

Service1: IndexModel.OnGet
Service2: IndexModel.OnGet
Service3: IndexModel.OnGet, MyKey = MyKey from appsettings.Developement.json
Service1.Dispose

Serviços não criados pelo contêiner de serviço

Considere o seguinte código:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.AddSingleton(new Service1());
builder.Services.AddSingleton(new Service2());

No código anterior:

  • As instâncias de serviço não são criadas pelo contêiner de serviço.
  • A estrutura não descarta os serviços automaticamente.
  • O desenvolvedor é responsável por descartar os serviços.

Diretrizes IDisposable para instâncias transitórias e compartilhadas

Confira Diretrizes IDisposable para instância transitória e compartilhada em Injeção de dependência no .NET

Substituição do contêiner de serviço padrão

Confira Substituição do contêiner de serviço padrão na Injeção de dependência no .NET

Recomendações

Confira Recomendações na Injeção de dependência no .NET

  • Evite usar o padrão do localizador de serviço. Por exemplo, não invoque GetService para obter uma instância de serviço quando for possível usar a DI:

    Incorreto:

    Código incorreto

    Correto:

    public class MyClass
    {
        private readonly IOptionsMonitor<MyOptions> _optionsMonitor;
    
        public MyClass(IOptionsMonitor<MyOptions> optionsMonitor)
        {
            _optionsMonitor = optionsMonitor;
        }
    
        public void MyMethod()
        {
            var option = _optionsMonitor.CurrentValue.Option;
    
            ...
        }
    }
    
  • Outra variação de localizador de serviço a ser evitada é injetar um alocador que resolve as dependências em runtime. Essas duas práticas misturam estratégias de inversão de controle.

  • Evite o acesso estático a HttpContext (por exemplo, IHttpContextAccessor.HttpContext).

A DI é uma alternativa aos padrões de acesso a objeto estático/global. Talvez você não obtenha os benefícios da DI se combiná-lo com o acesso a objeto estático.

O Orchard Core é uma estrutura de aplicativo para a criação de aplicativos modulares multilocatários no ASP.NET Core. Para obter mais informações, confira a Documentação do Orchard Core.

Confira os exemplos do Orchard Core para obter exemplos de como criar aplicativos modulares e multilocatários usando apenas o Orchard Core Framework sem nenhum de seus recursos específicos do CMS.

Serviços fornecidos pela estrutura

O Program.cs registra as serviços usados pelo aplicativo, incluindo recursos de plataforma, como o Entity Framework Core e o ASP.NET Core MVC. Inicialmente, o IServiceCollection fornecido para Program.cs possui serviços definidos pela estrutura dependendo de como o host foi configurado. Para aplicativos baseados nos modelos do ASP.NET Core, a estrutura registra mais de 250 serviços.

A tabela a seguir lista um pequeno exemplo desses serviços registrados por estrutura:

Tipo de Serviço Tempo de vida
Microsoft.AspNetCore.Hosting.Builder.IApplicationBuilderFactory Transitório
IHostApplicationLifetime Singleton
IWebHostEnvironment Singleton
Microsoft.AspNetCore.Hosting.IStartup Singleton
Microsoft.AspNetCore.Hosting.IStartupFilter Transitório
Microsoft.AspNetCore.Hosting.Server.IServer Singleton
Microsoft.AspNetCore.Http.IHttpContextFactory Transitório
Microsoft.Extensions.Logging.ILogger<TCategoryName> Singleton
Microsoft.Extensions.Logging.ILoggerFactory Singleton
Microsoft.Extensions.ObjectPool.ObjectPoolProvider Singleton
Microsoft.Extensions.Options.IConfigureOptions<TOptions> Transitório
Microsoft.Extensions.Options.IOptions<TOptions> Singleton
System.Diagnostics.DiagnosticSource Singleton
System.Diagnostics.DiagnosticListener Singleton

Recursos adicionais

Por Kirk Larkin, Steve Smith e Brandon Dahler

O ASP.NET Core é compatível com o padrão de design de software de DI (injeção de dependência), que é uma técnica para alcançar a IoC (Inversão de Controle) entre classes e suas dependências.

Para obter mais informações específicas sobre injeção de dependência em controladores MVC, confira Injeção de dependência em controladores no ASP.NET Core.

Para obter informações sobre como usar a injeção de dependência em aplicativos diferentes de aplicativos Web, confira Injeção de dependência no .NET.

Para obter mais informações sobre a injeção de dependência de opções, confira Padrão de opções no ASP.NET Core.

Este tópico fornece informações sobre injeção de dependência no ASP.NET Core. A documentação principal sobre como usar injeção de dependência está incluída na Injeção de dependência no .NET.

Exibir ou baixar código de exemplo (como baixar)

Visão geral da injeção de dependência

Uma dependência é um objeto do qual outro objeto depende. Examine a classe MyDependency a seguir com um método WriteMessage do qual outras classes em um aplicativo dependem:

public class MyDependency
{
    public void WriteMessage(string message)
    {
        Console.WriteLine($"MyDependency.WriteMessage called. Message: {message}");
    }
}

Uma classe pode criar uma instância da classe MyDependency para usar seu método WriteMessage. No exemplo a seguir, a classe MyDependency é uma dependência da classe IndexModel:


public class IndexModel : PageModel
{
    private readonly MyDependency _dependency = new MyDependency();

    public void OnGet()
    {
        _dependency.WriteMessage("IndexModel.OnGet");
    }
}

A classe cria e depende diretamente da instância MyDependency. As dependências de código, como no exemplo anterior, são problemáticas e devem ser evitadas pelos seguintes motivos:

  • Para substituir MyDependency por uma implementação diferente, a classe IndexModel deve ser modificada.
  • Se MyDependency tiver dependências, elas também deverão ser configuradas pela classe IndexModel. Em um projeto grande com várias classes dependendo da MyDependency, o código de configuração fica pulverizado por todo o aplicativo.
  • É difícil testar a unidade dessa implementação.

Injeção de dependência trata desses problemas da seguinte maneira:

  • O uso de uma interface ou classe base para abstrair a implementação da dependência.
  • Registrando a dependência em um contêiner de serviço. O ASP.NET Core fornece um contêiner de serviço interno, o IServiceProvider. Normalmente, os serviços são registrados no arquivo Program.cs do aplicativo.
  • Injeção do serviço no construtor da classe na qual ele é usado. A estrutura assume a responsabilidade de criar uma instância da dependência e de descartá-la quando não for mais necessária.

No aplicativo de exemplo, a interface IMyDependency define o método WriteMessage:

public interface IMyDependency
{
    void WriteMessage(string message);
}

Essa interface é implementada por um tipo concreto, MyDependency:

public class MyDependency : IMyDependency
{
    public void WriteMessage(string message)
    {
        Console.WriteLine($"MyDependency.WriteMessage Message: {message}");
    }
}

O aplicativo de exemplo registra o serviço IMyDependency com o tipo concreto MyDependency. O método AddScoped registra o serviço com um tempo de vida com escopo, o tempo de vida de uma única solicitação. Descreveremos posteriormente neste tópico os tempos de vida do serviço.

using DependencyInjectionSample.Interfaces;
using DependencyInjectionSample.Services;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.AddScoped<IMyDependency, MyDependency>();

var app = builder.Build();

No exemplo de aplicativo, o serviço IMyDependency é solicitada e usada para chamar o método WriteMessage:

public class Index2Model : PageModel
{
    private readonly IMyDependency _myDependency;

    public Index2Model(IMyDependency myDependency)
    {
        _myDependency = myDependency;            
    }

    public void OnGet()
    {
        _myDependency.WriteMessage("Index2Model.OnGet");
    }
}

Usando o padrão DI, o controlador ou a Razor Page:

  • Não usa o tipo concreto MyDependency, apenas a interface IMyDependency que o implementa. Isso facilita a alteração da implementação sem modificar o controlador ou a Razor Page.
  • Não cria uma instância do MyDependency; ela é criada pelo contêiner de DI.

A implementação da interface IMyDependency pode ser aprimorada usando a API de log interna:

public class MyDependency2 : IMyDependency
{
    private readonly ILogger<MyDependency2> _logger;

    public MyDependency2(ILogger<MyDependency2> logger)
    {
        _logger = logger;
    }

    public void WriteMessage(string message)
    {
        _logger.LogInformation( $"MyDependency2.WriteMessage Message: {message}");
    }
}

O Program.cs atualizado registra a nova implementação de IMyDependency:

using DependencyInjectionSample.Interfaces;
using DependencyInjectionSample.Services;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.AddScoped<IMyDependency, MyDependency2>();

var app = builder.Build();

MyDependency2 depende ILogger<TCategoryName>do que ele solicita no construtor. ILogger<TCategoryName> é um serviço fornecido pela estrutura.

Não é incomum usar a injeção de dependência de uma maneira encadeada. Por sua vez, cada dependência solicitada solicita suas próprias dependências. O contêiner resolve as dependências no grafo e retorna o serviço totalmente resolvido. O conjunto de dependências que precisa ser resolvido normalmente é chamado de árvore de dependência, grafo de dependência ou grafo de objeto.

O contêiner resolve ILogger<TCategoryName> aproveitando os tipos abertos (genéricos), eliminando a necessidade de registrar todo tipo construído (genérico).

Na terminologia de injeção de dependência, um serviço:

  • Normalmente, é um objeto que fornece um serviço para outros objetos, como o serviço IMyDependency.
  • Não está relacionado a um serviço Web, embora o serviço possa usar um serviço Web.

A estrutura fornece um sistema de registro em log robusto. As implementações IMyDependency mostradas nos exemplos anteriores foram gravadas para demonstrar a DI básica, não para implementar o registro em log. A maioria dos aplicativos não deve precisar escrever agentes. O código a seguir demonstra o uso do registro em log padrão, que não exige que nenhum serviço seja registrado:

public class AboutModel : PageModel
{
    private readonly ILogger _logger;

    public AboutModel(ILogger<AboutModel> logger)
    {
        _logger = logger;
    }
    
    public string Message { get; set; } = string.Empty;

    public void OnGet()
    {
        Message = $"About page visited at {DateTime.UtcNow.ToLongTimeString()}";
        _logger.LogInformation(Message);
    }
}

Usando o código anterior, não é necessário atualizar Program.cs, pois o registro em log é fornecido pela estrutura.

Registrar grupos de serviços com métodos de extensão

O Microsoft Extensions usa uma convenção para registrar um grupo de serviços relacionados. A convenção é usar um único método de extensão Add{GROUP_NAME} para registrar todos os serviços exigidos por um recurso de estrutura. Por exemplo, o método de extensão AddControllers registra os serviços necessários para controladores MVC.

O código a seguir é gerado pelo modelo Razor Pages usando contas de usuário individuais e mostra como adicionar serviços adicionais ao contêiner usando os métodos de extensão AddDbContext e AddDefaultIdentity:

using DependencyInjectionSample.Data;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();
builder.Services.AddRazorPages();

var app = builder.Build();

Considere o seguinte ao registrar serviços e configurar opções:

using ConfigSample.Options;
using Microsoft.Extensions.DependencyInjection.ConfigSample.Options;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.Configure<PositionOptions>(
    builder.Configuration.GetSection(PositionOptions.Position));
builder.Services.Configure<ColorOptions>(
    builder.Configuration.GetSection(ColorOptions.Color));

builder.Services.AddScoped<IMyDependency, MyDependency>();
builder.Services.AddScoped<IMyDependency2, MyDependency2>();

var app = builder.Build();

Grupos de registros relacionados podem ser movidos para um método de extensão para registrar serviços. Por exemplo, os serviços de configuração são adicionados à seguinte classe:

using ConfigSample.Options;
using Microsoft.Extensions.Configuration;

namespace Microsoft.Extensions.DependencyInjection
{
    public static class MyConfigServiceCollectionExtensions
    {
        public static IServiceCollection AddConfig(
             this IServiceCollection services, IConfiguration config)
        {
            services.Configure<PositionOptions>(
                config.GetSection(PositionOptions.Position));
            services.Configure<ColorOptions>(
                config.GetSection(ColorOptions.Color));

            return services;
        }

        public static IServiceCollection AddMyDependencyGroup(
             this IServiceCollection services)
        {
            services.AddScoped<IMyDependency, MyDependency>();
            services.AddScoped<IMyDependency2, MyDependency2>();

            return services;
        }
    }
}

Os serviços restantes são registrados em uma classe similar. O código a seguir usa os novos métodos de extensão para registrar os serviços:

using Microsoft.Extensions.DependencyInjection.ConfigSample.Options;

var builder = WebApplication.CreateBuilder(args);

builder.Services
    .AddConfig(builder.Configuration)
    .AddMyDependencyGroup();

builder.Services.AddRazorPages();

var app = builder.Build();

Observação: Cada método de extensão services.Add{GROUP_NAME} adiciona e possivelmente configura serviços. Por exemplo, AddControllersWithViews adiciona os serviços que os controladores MVC com visualizações requerem e AddRazorPages adiciona os serviços que o Razor Pages requer.

Tempos de vida do serviço

Confira os Tempos de vida do serviço na Injeção de dependência no .NET

Para usar serviços com escopo em middleware, use uma das seguintes abordagens:

  • Injete o serviço no método Invoke ou InvokeAsync do middleware. O uso da injeção de construtor gera uma exceção de runtime porque força o serviço com escopo a se comportar como um singleton. O exemplo na seção Opções de tempo de vida e registro demonstra a abordagem InvokeAsync.
  • Use Middleware de fábrica. O middleware registrado usando essa abordagem é ativado por solicitação de cliente (conexão), o que permite que os serviços com escopo sejam injetados no construtor do middleware.

Para obter mais informações, confira Escrever middleware do ASP.NET Core personalizado.

Métodos de registro do serviço

Confira Métodos de registro de serviço na Injeção de dependência no .NET

É comum usar várias implementações ao simular tipos para teste.

Registrar um serviço com apenas um tipo de implementação equivale a registrar esse serviço com o mesmo tipo de implementação e serviço. É por isso que várias implementações de um serviço não podem ser registradas usando os métodos que não usam um tipo de serviço explícito. Esses métodos podem registrar várias instâncias de um serviço, mas todos eles terão o mesmo tipo de implementação.

Qualquer um dos métodos de registro de serviço acima pode ser usado para registrar várias instâncias de serviço do mesmo tipo de serviço. No exemplo a seguir, AddSingleton é chamado duas vezes com IMyDependency como tipo de serviço. A segunda chamada para AddSingleton substitui a anterior quando resolvida como IMyDependency e adiciona à anterior quando vários serviços são resolvidos por meio de IEnumerable<IMyDependency>. Os serviços aparecem na ordem em que foram registrados quando resolvidos por meio de IEnumerable<{SERVICE}>.

services.AddSingleton<IMyDependency, MyDependency>();
services.AddSingleton<IMyDependency, DifferentDependency>();

public class MyService
{
    public MyService(IMyDependency myDependency, 
       IEnumerable<IMyDependency> myDependencies)
    {
        Trace.Assert(myDependency is DifferentDependency);

        var dependencyArray = myDependencies.ToArray();
        Trace.Assert(dependencyArray[0] is MyDependency);
        Trace.Assert(dependencyArray[1] is DifferentDependency);
    }
}

Comportamento da injeção de construtor

Confira o Comportamento de injeção do construtor na Injeção de dependência no .NET

Contextos de Entity Framework

Por padrão, os contextos do Entity Framework são adicionados ao contêiner de serviço usando o tempo de vida com escopo, pois as operações de banco de dados do aplicativo Web normalmente estão focadas na solicitação do cliente. Para usar um tempo de vida diferente, especifique o tempo de vida usando uma sobrecarga AddDbContext. Os serviços de um determinado tempo de vida não devem usar um contexto de banco de dados com um tempo de vida menor do que o tempo de vida do serviço.

Opções de tempo de vida e de registro

Para demonstrar a diferença entre os tempos de vida do serviço e suas opções de registro, considere as interfaces a seguir que representam uma tarefa como uma operação com um identificador OperationId. Dependendo de como o tempo de vida de um serviço de operações é configurado para as interfaces a seguir, o contêiner fornece a mesma instância do serviço, ou outras diferentes, quando recebe uma solicitação de uma classe:

public interface IOperation
{
    string OperationId { get; }
}

public interface IOperationTransient : IOperation { }
public interface IOperationScoped : IOperation { }
public interface IOperationSingleton : IOperation { }

A classe Operation a seguir implementa todas as interfaces anteriores. O construtor Operation gera um GUID e armazena os últimos 4 caracteres na propriedade OperationId:

public class Operation : IOperationTransient, IOperationScoped, IOperationSingleton
{
    public Operation()
    {
        OperationId = Guid.NewGuid().ToString()[^4..];
    }

    public string OperationId { get; }
}

O código a seguir cria vários registros da classe Operation de acordo com os tempos de vida nomeados:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.AddTransient<IOperationTransient, Operation>();
builder.Services.AddScoped<IOperationScoped, Operation>();
builder.Services.AddSingleton<IOperationSingleton, Operation>();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseMyMiddleware();
app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();

O exemplo de aplicativo demonstra os tempos de vida do objeto dentro e entre solicitações individuais. O IndexModel e o middleware solicitam cada tipo de IOperation e registram a OperationId para cada um:

public class IndexModel : PageModel
{
    private readonly ILogger _logger;
    private readonly IOperationTransient _transientOperation;
    private readonly IOperationSingleton _singletonOperation;
    private readonly IOperationScoped _scopedOperation;

    public IndexModel(ILogger<IndexModel> logger,
                      IOperationTransient transientOperation,
                      IOperationScoped scopedOperation,
                      IOperationSingleton singletonOperation)
    {
        _logger = logger;
        _transientOperation = transientOperation;
        _scopedOperation    = scopedOperation;
        _singletonOperation = singletonOperation;
    }

    public void  OnGet()
    {
        _logger.LogInformation("Transient: " + _transientOperation.OperationId);
        _logger.LogInformation("Scoped: "    + _scopedOperation.OperationId);
        _logger.LogInformation("Singleton: " + _singletonOperation.OperationId);
    }
}

Semelhante ao IndexModel, o middleware resolve os mesmos serviços:

public class MyMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger _logger;

    private readonly IOperationSingleton _singletonOperation;

    public MyMiddleware(RequestDelegate next, ILogger<MyMiddleware> logger,
        IOperationSingleton singletonOperation)
    {
        _logger = logger;
        _singletonOperation = singletonOperation;
        _next = next;
    }

    public async Task InvokeAsync(HttpContext context,
        IOperationTransient transientOperation, IOperationScoped scopedOperation)
    {
        _logger.LogInformation("Transient: " + transientOperation.OperationId);
        _logger.LogInformation("Scoped: " + scopedOperation.OperationId);
        _logger.LogInformation("Singleton: " + _singletonOperation.OperationId);

        await _next(context);
    }
}

public static class MyMiddlewareExtensions
{
    public static IApplicationBuilder UseMyMiddleware(this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<MyMiddleware>();
    }
}

Os serviços com escopo e transitórios devem ser resolvidos no método InvokeAsync:

public async Task InvokeAsync(HttpContext context,
    IOperationTransient transientOperation, IOperationScoped scopedOperation)
{
    _logger.LogInformation("Transient: " + transientOperation.OperationId);
    _logger.LogInformation("Scoped: " + scopedOperation.OperationId);
    _logger.LogInformation("Singleton: " + _singletonOperation.OperationId);

    await _next(context);
}

A saída do agente mostra:

  • Os objetos transitórios sempre são diferentes. O valor transitório da OperationId é diferente no IndexModel e no middleware.
  • Os objetos com escopo são os mesmos para uma determinada solicitação, mas diferem em cada nova solicitação.
  • Os objetos singleton são os mesmos para cada solicitação.

Para reduzir a saída de log, defina "Logging:LogLevel:Microsoft:Error" no arquivo appsettings.Development.json:

{
  "MyKey": "MyKey from appsettings.Developement.json",
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "System": "Debug",
      "Microsoft": "Error"
    }
  }
}

Resolver um serviço na inicialização do aplicativo

O código a seguir mostra como resolver um serviço com escopo por uma duração limitada quando o aplicativo é iniciado:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddScoped<IMyDependency, MyDependency>();

var app = builder.Build();

using (var serviceScope = app.Services.CreateScope())
{
    var services = serviceScope.ServiceProvider;

    var myDependency = services.GetRequiredService<IMyDependency>();
    myDependency.WriteMessage("Call services from main");
}

app.MapGet("/", () => "Hello World!");

app.Run();

Validação de escopo

Confira o Comportamento de injeção do construtor na Injeção de dependência no .NET

Para obter mais informações, confira Validação de escopo.

Serviços de solicitação

Os serviços e suas dependências em uma solicitação do ASP.NET Core são expostos por meio de HttpContext.RequestServices.

A estrutura cria um escopo por solicitação, e RequestServices expõe o provedor de serviços com escopo. Todos os serviços com escopo são válidos enquanto a solicitação estiver ativa.

Observação

Prefira solicitar dependências como parâmetros de construtor em vez de resolver serviços em RequestServices. Solicitar dependências como parâmetros de construtor produz classes mais fáceis de testar.

Projetar serviços para injeção de dependência

Ao criar serviços para injeção de dependência:

  • Evite membros e classes estáticos com estado. Evite criar um estado global projetando aplicativos para usar serviços singleton.
  • Evite a instanciação direta das classes dependentes em serviços. A instanciação direta acopla o código a uma implementação específica.
  • Deixe os serviços pequenos, bem fatorados e fáceis de serem testados.

Se uma classe tiver muitas dependências injetadas, isso poderá ser um sinal de que a classe tem muitas responsabilidades e violará o SRP (princípio de responsabilidade única). Tente refatorar a classe movendo algumas das responsabilidades para uma nova classe. Lembre-se de que as classes de modelo de página do Razor Pages e as classes do controlador MVC devem se concentrar em questões de interface do usuário.

Descarte de serviços

O contêiner chama Dispose para os tipos IDisposable criados por ele. Os serviços resolvidos do contêiner nunca devem ser descartados pelo desenvolvedor. Se um tipo ou um alocador for registrado como singleton, o contêiner descartará o singleton automaticamente.

No exemplo a seguir, os serviços são criados pelo contêiner de serviço e descartados automaticamente: dependency-injection\samples\6.x\DIsample2\DIsample2\Services\Service1.cs

public class Service1 : IDisposable
{
    private bool _disposed;

    public void Write(string message)
    {
        Console.WriteLine($"Service1: {message}");
    }

    public void Dispose()
    {
        if (_disposed)
            return;

        Console.WriteLine("Service1.Dispose");
        _disposed = true;
    }
}

public class Service2 : IDisposable
{
    private bool _disposed;

    public void Write(string message)
    {
        Console.WriteLine($"Service2: {message}");
    }

    public void Dispose()
    {
        if (_disposed)
            return;

        Console.WriteLine("Service2.Dispose");
        _disposed = true;
    }
}

public interface IService3
{
    public void Write(string message);
}

public class Service3 : IService3, IDisposable
{
    private bool _disposed;

    public Service3(string myKey)
    {
        MyKey = myKey;
    }

    public string MyKey { get; }

    public void Write(string message)
    {
        Console.WriteLine($"Service3: {message}, MyKey = {MyKey}");
    }

    public void Dispose()
    {
        if (_disposed)
            return;

        Console.WriteLine("Service3.Dispose");
        _disposed = true;
    }
}
using DIsample2.Services;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.AddScoped<Service1>();
builder.Services.AddSingleton<Service2>();

var myKey = builder.Configuration["MyKey"];
builder.Services.AddSingleton<IService3>(sp => new Service3(myKey));

var app = builder.Build();
public class IndexModel : PageModel
{
    private readonly Service1 _service1;
    private readonly Service2 _service2;
    private readonly IService3 _service3;

    public IndexModel(Service1 service1, Service2 service2, IService3 service3)
    {
        _service1 = service1;
        _service2 = service2;
        _service3 = service3;
    }

    public void OnGet()
    {
        _service1.Write("IndexModel.OnGet");
        _service2.Write("IndexModel.OnGet");
        _service3.Write("IndexModel.OnGet");
    }
}

O console de depuração mostra a seguinte saída após cada atualização da página Índice:

Service1: IndexModel.OnGet
Service2: IndexModel.OnGet
Service3: IndexModel.OnGet, MyKey = MyKey from appsettings.Developement.json
Service1.Dispose

Serviços não criados pelo contêiner de serviço

Considere o seguinte código:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.AddSingleton(new Service1());
builder.Services.AddSingleton(new Service2());

No código anterior:

  • As instâncias de serviço não são criadas pelo contêiner de serviço.
  • A estrutura não descarta os serviços automaticamente.
  • O desenvolvedor é responsável por descartar os serviços.

Diretrizes IDisposable para instâncias transitórias e compartilhadas

Confira Diretrizes IDisposable para instância transitória e compartilhada em Injeção de dependência no .NET

Substituição do contêiner de serviço padrão

Confira Substituição do contêiner de serviço padrão na Injeção de dependência no .NET

Recomendações

Confira Recomendações na Injeção de dependência no .NET

  • Evite usar o padrão do localizador de serviço. Por exemplo, não invoque GetService para obter uma instância de serviço quando for possível usar a DI:

    Incorreto:

    Código incorreto

    Correto:

    public class MyClass
    {
        private readonly IOptionsMonitor<MyOptions> _optionsMonitor;
    
        public MyClass(IOptionsMonitor<MyOptions> optionsMonitor)
        {
            _optionsMonitor = optionsMonitor;
        }
    
        public void MyMethod()
        {
            var option = _optionsMonitor.CurrentValue.Option;
    
            ...
        }
    }
    
  • Outra variação de localizador de serviço a ser evitada é injetar um alocador que resolve as dependências em runtime. Essas duas práticas misturam estratégias de inversão de controle.

  • Evite o acesso estático a HttpContext (por exemplo, IHttpContextAccessor.HttpContext).

A DI é uma alternativa aos padrões de acesso a objeto estático/global. Talvez você não obtenha os benefícios da DI se combiná-lo com o acesso a objeto estático.

O Orchard Core é uma estrutura de aplicativo para a criação de aplicativos modulares multilocatários no ASP.NET Core. Para obter mais informações, confira a Documentação do Orchard Core.

Confira os exemplos do Orchard Core para obter exemplos de como criar aplicativos modulares e multilocatários usando apenas o Orchard Core Framework sem nenhum de seus recursos específicos do CMS.

Serviços fornecidos pela estrutura

O Program.cs registra as serviços usados pelo aplicativo, incluindo recursos de plataforma, como o Entity Framework Core e o ASP.NET Core MVC. Inicialmente, o IServiceCollection fornecido para Program.cs possui serviços definidos pela estrutura dependendo de como o host foi configurado. Para aplicativos baseados nos modelos do ASP.NET Core, a estrutura registra mais de 250 serviços.

A tabela a seguir lista um pequeno exemplo desses serviços registrados por estrutura:

Tipo de Serviço Tempo de vida
Microsoft.AspNetCore.Hosting.Builder.IApplicationBuilderFactory Transitório
IHostApplicationLifetime Singleton
IWebHostEnvironment Singleton
Microsoft.AspNetCore.Hosting.IStartup Singleton
Microsoft.AspNetCore.Hosting.IStartupFilter Transitório
Microsoft.AspNetCore.Hosting.Server.IServer Singleton
Microsoft.AspNetCore.Http.IHttpContextFactory Transitório
Microsoft.Extensions.Logging.ILogger<TCategoryName> Singleton
Microsoft.Extensions.Logging.ILoggerFactory Singleton
Microsoft.Extensions.ObjectPool.ObjectPoolProvider Singleton
Microsoft.Extensions.Options.IConfigureOptions<TOptions> Transitório
Microsoft.Extensions.Options.IOptions<TOptions> Singleton
System.Diagnostics.DiagnosticSource Singleton
System.Diagnostics.DiagnosticListener Singleton

Recursos adicionais

Por Kirk Larkin, Steve Smith, Scott Addie e Brandon Dahler

O ASP.NET Core é compatível com o padrão de design de software de DI (injeção de dependência), que é uma técnica para alcançar a IoC (Inversão de Controle) entre classes e suas dependências.

Para obter mais informações específicas sobre injeção de dependência em controladores MVC, confira Injeção de dependência em controladores no ASP.NET Core.

Para obter informações sobre como usar a injeção de dependência em aplicativos diferentes de aplicativos Web, confira Injeção de dependência no .NET.

Para obter mais informações sobre a injeção de dependência de opções, confira Padrão de opções no ASP.NET Core.

Este tópico fornece informações sobre injeção de dependência no ASP.NET Core. A documentação principal sobre como usar injeção de dependência está incluída na Injeção de dependência no .NET.

Exibir ou baixar código de exemplo (como baixar)

Visão geral da injeção de dependência

Uma dependência é um objeto do qual outro objeto depende. Examine a classe MyDependency a seguir com um método WriteMessage do qual outras classes em um aplicativo dependem:

public class MyDependency
{
    public void WriteMessage(string message)
    {
        Console.WriteLine($"MyDependency.WriteMessage called. Message: {message}");
    }
}

Uma classe pode criar uma instância da classe MyDependency para usar seu método WriteMessage. No exemplo a seguir, a classe MyDependency é uma dependência da classe IndexModel:

public class IndexModel : PageModel
{
    private readonly MyDependency _dependency = new MyDependency();

    public void OnGet()
    {
        _dependency.WriteMessage("IndexModel.OnGet created this message.");
    }
}

A classe cria e depende diretamente da instância MyDependency. As dependências de código, como no exemplo anterior, são problemáticas e devem ser evitadas pelos seguintes motivos:

  • Para substituir MyDependency por uma implementação diferente, a classe IndexModel deve ser modificada.
  • Se MyDependency tiver dependências, elas também deverão ser configuradas pela classe IndexModel. Em um projeto grande com várias classes dependendo da MyDependency, o código de configuração fica pulverizado por todo o aplicativo.
  • É difícil testar a unidade dessa implementação. O aplicativo deve usar uma simulação ou stub da classe MyDependency, o que não é possível com essa abordagem.

Injeção de dependência trata desses problemas da seguinte maneira:

  • O uso de uma interface ou classe base para abstrair a implementação da dependência.
  • Registrando a dependência em um contêiner de serviço. O ASP.NET Core fornece um contêiner de serviço interno, o IServiceProvider. Normalmente, os serviços são registrados no método Startup.ConfigureServices do aplicativo.
  • Injeção do serviço no construtor da classe na qual ele é usado. A estrutura assume a responsabilidade de criar uma instância da dependência e de descartá-la quando não for mais necessária.

No aplicativo de exemplo, a interface IMyDependency define o método WriteMessage:

public interface IMyDependency
{
    void WriteMessage(string message);
}

Essa interface é implementada por um tipo concreto, MyDependency:

public class MyDependency : IMyDependency
{
    public void WriteMessage(string message)
    {
        Console.WriteLine($"MyDependency.WriteMessage Message: {message}");
    }
}

O aplicativo de exemplo registra o serviço IMyDependency com o tipo concreto MyDependency. O método AddScoped registra o serviço com um tempo de vida com escopo, o tempo de vida de uma única solicitação. Descreveremos posteriormente neste tópico os tempos de vida do serviço.

public void ConfigureServices(IServiceCollection services)
{
    services.AddScoped<IMyDependency, MyDependency>();

    services.AddRazorPages();
}

No exemplo de aplicativo, o serviço IMyDependency é solicitada e usada para chamar o método WriteMessage:

public class Index2Model : PageModel
{
    private readonly IMyDependency _myDependency;

    public Index2Model(IMyDependency myDependency)
    {
        _myDependency = myDependency;            
    }

    public void OnGet()
    {
        _myDependency.WriteMessage("Index2Model.OnGet");
    }
}

Usando o padrão DI, o controlador ou controlador:

  • Não usa o tipo concreto MyDependency, apenas a interface IMyDependency que o implementa. Isso facilita a alteração da implementação que o controlador usa sem modificar o controlador.
  • Não cria uma instância do MyDependency; ela é criada pelo contêiner de DI.

A implementação da interface IMyDependency pode ser aprimorada usando a API de log interna:

public class MyDependency2 : IMyDependency
{
    private readonly ILogger<MyDependency2> _logger;

    public MyDependency2(ILogger<MyDependency2> logger)
    {
        _logger = logger;
    }

    public void WriteMessage(string message)
    {
        _logger.LogInformation( $"MyDependency2.WriteMessage Message: {message}");
    }
}

O método atualizado ConfigureServices registra a nova implementação IMyDependency:

public void ConfigureServices(IServiceCollection services)
{
    services.AddScoped<IMyDependency, MyDependency2>();

    services.AddRazorPages();
}

MyDependency2 depende ILogger<TCategoryName>do que ele solicita no construtor. ILogger<TCategoryName> é um serviço fornecido pela estrutura.

Não é incomum usar a injeção de dependência de uma maneira encadeada. Por sua vez, cada dependência solicitada solicita suas próprias dependências. O contêiner resolve as dependências no grafo e retorna o serviço totalmente resolvido. O conjunto de dependências que precisa ser resolvido normalmente é chamado de árvore de dependência, grafo de dependência ou grafo de objeto.

O contêiner resolve ILogger<TCategoryName> aproveitando os tipos abertos (genéricos), eliminando a necessidade de registrar todo tipo construído (genérico).

Na terminologia de injeção de dependência, um serviço:

  • Normalmente, é um objeto que fornece um serviço para outros objetos, como o serviço IMyDependency.
  • Não está relacionado a um serviço Web, embora o serviço possa usar um serviço Web.

A estrutura fornece um sistema de registro em log robusto. As implementações IMyDependency mostradas nos exemplos anteriores foram gravadas para demonstrar a DI básica, não para implementar o registro em log. A maioria dos aplicativos não deve precisar escrever agentes. O código a seguir demonstra o uso do registro em log padrão, que não exige que nenhum serviço seja registrado em ConfigureServices:

public class AboutModel : PageModel
{
    private readonly ILogger _logger;

    public AboutModel(ILogger<AboutModel> logger)
    {
        _logger = logger;
    }
    
    public string Message { get; set; }

    public void OnGet()
    {
        Message = $"About page visited at {DateTime.UtcNow.ToLongTimeString()}";
        _logger.LogInformation(Message);
    }
}

Usando o código anterior, não é necessário atualizar ConfigureServices, pois o registro em log é fornecido pela estrutura.

Serviços injetados na Inicialização

Os serviços podem ser injetados no construtor Startup e no método Startup.Configure.

Somente os seguintes serviços podem ser injetados no construtor Startup ao usar o Host Genérico (IHostBuilder):

Qualquer serviço registrado com o contêiner de DI pode ser injetado no método Startup.Configure:

public void Configure(IApplicationBuilder app, ILogger<Startup> logger)
{
    ...
}

Para obter mais informações, confira Inicialização de aplicativo no ASP.NET Core e Configuração de acesso na Inicialização.

Registrar grupos de serviços com métodos de extensão

O Microsoft Extensions usa uma convenção para registrar um grupo de serviços relacionados. A convenção é usar um único método de extensão Add{GROUP_NAME} para registrar todos os serviços exigidos por um recurso de estrutura. Por exemplo, o método de extensão AddControllers registra os serviços necessários para controladores MVC.

O código a seguir é gerado pelo modelo Razor Pages usando contas de usuário individuais e mostra como adicionar serviços adicionais ao contêiner usando os métodos de extensão AddDbContext e AddDefaultIdentity:

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(
            Configuration.GetConnectionString("DefaultConnection")));
    services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
        .AddEntityFrameworkStores<ApplicationDbContext>();
    services.AddRazorPages();
}

Considere o método ConfigureServices seguinte, que registra serviços e configura opções:

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<PositionOptions>(
        Configuration.GetSection(PositionOptions.Position));
    services.Configure<ColorOptions>(
        Configuration.GetSection(ColorOptions.Color));

    services.AddScoped<IMyDependency, MyDependency>();
    services.AddScoped<IMyDependency2, MyDependency2>();

    services.AddRazorPages();
}

Grupos de registros relacionados podem ser movidos para um método de extensão para registrar serviços. Por exemplo, os serviços de configuração são adicionados à seguinte classe:

using ConfigSample.Options;
using Microsoft.Extensions.Configuration;

namespace Microsoft.Extensions.DependencyInjection
{
    public static class MyConfigServiceCollectionExtensions
    {
        public static IServiceCollection AddConfig(
             this IServiceCollection services, IConfiguration config)
        {
            services.Configure<PositionOptions>(
                config.GetSection(PositionOptions.Position));
            services.Configure<ColorOptions>(
                config.GetSection(ColorOptions.Color));

            return services;
        }

        public static IServiceCollection AddMyDependencyGroup(
             this IServiceCollection services)
        {
            services.AddScoped<IMyDependency, MyDependency>();
            services.AddScoped<IMyDependency2, MyDependency2>();

            return services;
        }
    }
}

Os serviços restantes são registrados em uma classe similar. O método ConfigureServices a seguir usa os novos métodos de extensão para registrar os serviços:

public void ConfigureServices(IServiceCollection services)
{
    services.AddConfig(Configuration)
            .AddMyDependencyGroup();

    services.AddRazorPages();
}

Observação: Cada método de extensão services.Add{GROUP_NAME} adiciona e possivelmente configura serviços. Por exemplo, AddControllersWithViews adiciona os serviços que os controladores MVC com visualizações requerem e AddRazorPages adiciona os serviços que o Razor Pages requer. Recomendamos que os aplicativos sigam a convenção de nomenclatura de criação de métodos de extensão no namespace Microsoft.Extensions.DependencyInjection. Criar métodos de extensão no namespace Microsoft.Extensions.DependencyInjection:

  • Encapsula grupos de registros de serviço.
  • Fornece acesso IntelliSense conveniente ao serviço.

Tempos de vida do serviço

Confira os Tempos de vida do serviço na Injeção de dependência no .NET

Para usar serviços com escopo em middleware, use uma das seguintes abordagens:

  • Injete o serviço no método Invoke ou InvokeAsync do middleware. O uso da injeção de construtor gera uma exceção de runtime porque força o serviço com escopo a se comportar como um singleton. O exemplo na seção Opções de tempo de vida e registro demonstra a abordagem InvokeAsync.
  • Use Middleware de fábrica. O middleware registrado usando essa abordagem é ativado por solicitação de cliente (conexão), que permite que serviços com escopo sejam injetados no método InvokeAsync do middleware.

Para obter mais informações, confira Escrever middleware do ASP.NET Core personalizado.

Métodos de registro do serviço

Confira Métodos de registro de serviço na Injeção de dependência no .NET

É comum usar várias implementações ao simular tipos para teste.

Registrar um serviço com apenas um tipo de implementação equivale a registrar esse serviço com o mesmo tipo de implementação e serviço. É por isso que várias implementações de um serviço não podem ser registradas usando os métodos que não usam um tipo de serviço explícito. Esses métodos podem registrar várias instâncias de um serviço, mas todos eles terão o mesmo tipo de implementação.

Qualquer um dos métodos de registro de serviço acima pode ser usado para registrar várias instâncias de serviço do mesmo tipo de serviço. No exemplo a seguir, AddSingleton é chamado duas vezes com IMyDependency como tipo de serviço. A segunda chamada para AddSingleton substitui a anterior quando resolvida como IMyDependency e adiciona à anterior quando vários serviços são resolvidos por meio de IEnumerable<IMyDependency>. Os serviços aparecem na ordem em que foram registrados quando resolvidos por meio de IEnumerable<{SERVICE}>.

services.AddSingleton<IMyDependency, MyDependency>();
services.AddSingleton<IMyDependency, DifferentDependency>();

public class MyService
{
    public MyService(IMyDependency myDependency, 
       IEnumerable<IMyDependency> myDependencies)
    {
        Trace.Assert(myDependency is DifferentDependency);

        var dependencyArray = myDependencies.ToArray();
        Trace.Assert(dependencyArray[0] is MyDependency);
        Trace.Assert(dependencyArray[1] is DifferentDependency);
    }
}

Comportamento da injeção de construtor

Confira o Comportamento de injeção do construtor na Injeção de dependência no .NET

Contextos de Entity Framework

Por padrão, os contextos do Entity Framework são adicionados ao contêiner de serviço usando o tempo de vida com escopo, pois as operações de banco de dados do aplicativo Web normalmente estão focadas na solicitação do cliente. Para usar um tempo de vida diferente, especifique o tempo de vida usando uma sobrecarga AddDbContext. Os serviços de um determinado tempo de vida não devem usar um contexto de banco de dados com um tempo de vida menor do que o tempo de vida do serviço.

Opções de tempo de vida e de registro

Para demonstrar a diferença entre os tempos de vida do serviço e suas opções de registro, considere as interfaces a seguir que representam uma tarefa como uma operação com um identificador OperationId. Dependendo de como o tempo de vida de um serviço de operações é configurado para as interfaces a seguir, o contêiner fornece a mesma instância do serviço, ou outras diferentes, quando recebe uma solicitação de uma classe:

public interface IOperation
{
    string OperationId { get; }
}

public interface IOperationTransient : IOperation { }
public interface IOperationScoped : IOperation { }
public interface IOperationSingleton : IOperation { }

A classe Operation a seguir implementa todas as interfaces anteriores. O construtor Operation gera um GUID e armazena os últimos 4 caracteres na propriedade OperationId:

public class Operation : IOperationTransient, IOperationScoped, IOperationSingleton
{
    public Operation()
    {
        OperationId = Guid.NewGuid().ToString()[^4..];
    }

    public string OperationId { get; }
}

O método Startup.ConfigureServices cria vários registros da classe Operation de acordo com os tempos de vida nomeados:

public void ConfigureServices(IServiceCollection services)
{
    services.AddTransient<IOperationTransient, Operation>();
    services.AddScoped<IOperationScoped, Operation>();
    services.AddSingleton<IOperationSingleton, Operation>();

    services.AddRazorPages();
}

O exemplo de aplicativo demonstra os tempos de vida do objeto dentro e entre solicitações individuais. O IndexModel e o middleware solicitam cada tipo de IOperation e registram a OperationId para cada um:

public class IndexModel : PageModel
{
    private readonly ILogger _logger;
    private readonly IOperationTransient _transientOperation;
    private readonly IOperationSingleton _singletonOperation;
    private readonly IOperationScoped _scopedOperation;

    public IndexModel(ILogger<IndexModel> logger,
                      IOperationTransient transientOperation,
                      IOperationScoped scopedOperation,
                      IOperationSingleton singletonOperation)
    {
        _logger = logger;
        _transientOperation = transientOperation;
        _scopedOperation    = scopedOperation;
        _singletonOperation = singletonOperation;
    }

    public void  OnGet()
    {
        _logger.LogInformation("Transient: " + _transientOperation.OperationId);
        _logger.LogInformation("Scoped: "    + _scopedOperation.OperationId);
        _logger.LogInformation("Singleton: " + _singletonOperation.OperationId);
    }
}

Semelhante ao IndexModel, o middleware resolve os mesmos serviços:

public class MyMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger _logger;

    private readonly IOperationTransient _transientOperation;
    private readonly IOperationSingleton _singletonOperation;

    public MyMiddleware(RequestDelegate next, ILogger<MyMiddleware> logger,
        IOperationTransient transientOperation,
        IOperationSingleton singletonOperation)
    {
        _logger = logger;
        _transientOperation = transientOperation;
        _singletonOperation = singletonOperation;
        _next = next;
    }

    public async Task InvokeAsync(HttpContext context,
        IOperationScoped scopedOperation)
    {
        _logger.LogInformation("Transient: " + _transientOperation.OperationId);
        _logger.LogInformation("Scoped: "    + scopedOperation.OperationId);
        _logger.LogInformation("Singleton: " + _singletonOperation.OperationId);

        await _next(context);
    }
}

public static class MyMiddlewareExtensions
{
    public static IApplicationBuilder UseMyMiddleware(this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<MyMiddleware>();
    }
}

Os serviços com escopo devem ser resolvidos no método InvokeAsync:

public async Task InvokeAsync(HttpContext context,
    IOperationScoped scopedOperation)
{
    _logger.LogInformation("Transient: " + _transientOperation.OperationId);
    _logger.LogInformation("Scoped: "    + scopedOperation.OperationId);
    _logger.LogInformation("Singleton: " + _singletonOperation.OperationId);

    await _next(context);
}

A saída do agente mostra:

  • Os objetos transitórios sempre são diferentes. O valor transitório da OperationId é diferente no IndexModel e no middleware.
  • Os objetos com escopo são os mesmos para uma determinada solicitação, mas diferem em cada nova solicitação.
  • Os objetos singleton são os mesmos para cada solicitação.

Para reduzir a saída de log, defina "Logging:LogLevel:Microsoft:Error" no arquivo appsettings.Development.json:

{
  "MyKey": "MyKey from appsettings.Developement.json",
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "System": "Debug",
      "Microsoft": "Error"
    }
  }
}

Chamar os serviços desde o principal

Crie um IServiceScope com IServiceScopeFactory.CreateScope para resolver um serviço com escopo dentro do escopo do aplicativo. Essa abordagem é útil para acessar um serviço com escopo durante a inicialização para executar tarefas de inicialização.

O exemplo a seguir mostra como acessar o serviço IMyDependency com escopo e chamar seu método WriteMessage em Program.Main:

public class Program
{
    public static void Main(string[] args)
    {
        var host = CreateHostBuilder(args).Build();

        using (var serviceScope = host.Services.CreateScope())
        {
            var services = serviceScope.ServiceProvider;

            try
            {
                var myDependency = services.GetRequiredService<IMyDependency>();
                myDependency.WriteMessage("Call services from main");
            }
            catch (Exception ex)
            {
                var logger = services.GetRequiredService<ILogger<Program>>();
                logger.LogError(ex, "An error occurred.");
            }
        }

        host.Run();
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>();
            });
}

Validação de escopo

Confira o Comportamento de injeção do construtor na Injeção de dependência no .NET

Para obter mais informações, confira Validação de escopo.

Serviços de solicitação

Os serviços e suas dependências em uma solicitação do ASP.NET Core são expostos por meio de HttpContext.RequestServices.

A estrutura cria um escopo por solicitação, e RequestServices expõe o provedor de serviços com escopo. Todos os serviços com escopo são válidos enquanto a solicitação estiver ativa.

Observação

Prefira solicitar dependências como parâmetros de construtor em vez de resolver serviços em RequestServices. Solicitar dependências como parâmetros de construtor produz classes mais fáceis de testar.

Projetar serviços para injeção de dependência

Ao criar serviços para injeção de dependência:

  • Evite membros e classes estáticos com estado. Evite criar um estado global projetando aplicativos para usar serviços singleton.
  • Evite a instanciação direta das classes dependentes em serviços. A instanciação direta acopla o código a uma implementação específica.
  • Deixe os serviços pequenos, bem fatorados e fáceis de serem testados.

Se uma classe tiver muitas dependências injetadas, isso poderá ser um sinal de que a classe tem muitas responsabilidades e violará o SRP (princípio de responsabilidade única). Tente refatorar a classe movendo algumas das responsabilidades para uma nova classe. Lembre-se de que as classes de modelo de página do Razor Pages e as classes do controlador MVC devem se concentrar em questões de interface do usuário.

Descarte de serviços

O contêiner chama Dispose para os tipos IDisposable criados por ele. Os serviços resolvidos do contêiner nunca devem ser descartados pelo desenvolvedor. Se um tipo ou um alocador for registrado como singleton, o contêiner descartará o singleton automaticamente.

No exemplo a seguir, os serviços são criados pelo contêiner de serviço e descartados automaticamente:

public class Service1 : IDisposable
{
    private bool _disposed;

    public void Write(string message)
    {
        Console.WriteLine($"Service1: {message}");
    }

    public void Dispose()
    {
        if (_disposed)
            return;

        Console.WriteLine("Service1.Dispose");
        _disposed = true;
    }
}

public class Service2 : IDisposable
{
    private bool _disposed;

    public void Write(string message)
    {
        Console.WriteLine($"Service2: {message}");
    }

    public void Dispose()
    {
        if (_disposed)
            return;

        Console.WriteLine("Service2.Dispose");
        _disposed = true;
    }
}

public interface IService3
{
    public void Write(string message);
}

public class Service3 : IService3, IDisposable
{
    private bool _disposed;

    public Service3(string myKey)
    {
        MyKey = myKey;
    }

    public string MyKey { get; }

    public void Write(string message)
    {
        Console.WriteLine($"Service3: {message}, MyKey = {MyKey}");
    }

    public void Dispose()
    {
        if (_disposed)
            return;

        Console.WriteLine("Service3.Dispose");
        _disposed = true;
    }
}
public void ConfigureServices(IServiceCollection services)
{
    services.AddScoped<Service1>();
    services.AddSingleton<Service2>();
    
    var myKey = Configuration["MyKey"];
    services.AddSingleton<IService3>(sp => new Service3(myKey));

    services.AddRazorPages();
}
public class IndexModel : PageModel
{
    private readonly Service1 _service1;
    private readonly Service2 _service2;
    private readonly IService3 _service3;

    public IndexModel(Service1 service1, Service2 service2, IService3 service3)
    {
        _service1 = service1;
        _service2 = service2;
        _service3 = service3;
    }

    public void OnGet()
    {
        _service1.Write("IndexModel.OnGet");
        _service2.Write("IndexModel.OnGet");
        _service3.Write("IndexModel.OnGet");
    }
}

O console de depuração mostra a seguinte saída após cada atualização da página Índice:

Service1: IndexModel.OnGet
Service2: IndexModel.OnGet
Service3: IndexModel.OnGet, MyKey = My Key from config
Service1.Dispose

Serviços não criados pelo contêiner de serviço

Considere o seguinte código:

public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton(new Service1());
    services.AddSingleton(new Service2());

    services.AddRazorPages();
}

No código anterior:

  • As instâncias de serviço não são criadas pelo contêiner de serviço.
  • A estrutura não descarta os serviços automaticamente.
  • O desenvolvedor é responsável por descartar os serviços.

Diretrizes IDisposable para instâncias transitórias e compartilhadas

Confira Diretrizes IDisposable para instância transitória e compartilhada em Injeção de dependência no .NET

Substituição do contêiner de serviço padrão

Confira Substituição do contêiner de serviço padrão na Injeção de dependência no .NET

Recomendações

Confira Recomendações na Injeção de dependência no .NET

  • Evite usar o padrão do localizador de serviço. Por exemplo, não invoque GetService para obter uma instância de serviço quando for possível usar a DI:

    Incorreto:

    Código incorreto

    Correto:

    public class MyClass
    {
        private readonly IOptionsMonitor<MyOptions> _optionsMonitor;
    
        public MyClass(IOptionsMonitor<MyOptions> optionsMonitor)
        {
            _optionsMonitor = optionsMonitor;
        }
    
        public void MyMethod()
        {
            var option = _optionsMonitor.CurrentValue.Option;
    
            ...
        }
    }
    
  • Outra variação de localizador de serviço a ser evitada é injetar um alocador que resolve as dependências em runtime. Essas duas práticas misturam estratégias de inversão de controle.

  • Evite o acesso estático a HttpContext (por exemplo, IHttpContextAccessor.HttpContext).

  • Evite chamadas para BuildServiceProvider em ConfigureServices. A chamada para BuildServiceProvider normalmente acontece quando o desenvolvedor quer resolver um serviço em ConfigureServices. Por exemplo, considere o caso em que o LoginPath é carregado na configuração. Evite a seguinte abordagem:

    código incorreto chamando BuildServiceProvider

    Na imagem anterior, selecionar a linha ondulada verde em services.BuildServiceProvider mostra o seguinte aviso ASP0000:

    Asp0000 chamando “BuildServiceProvider” do código do aplicativo resulta em uma cópia adicional dos serviços singleton sendo criados. Considere alternativas como a injeção de serviços de dependência como parâmetros para "Configurar".

    Chamar BuildServiceProvider cria um segundo contêiner, que pode criar singletons rasgados e fazer referências a grafos de objeto em vários contêineres.

    Uma maneira correta de obter LoginPath é usar o suporte interno do padrão de opções para DI:

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
            .AddCookie();
    
        services.AddOptions<CookieAuthenticationOptions>(
                            CookieAuthenticationDefaults.AuthenticationScheme)
            .Configure<IMyService>((options, myService) =>
            {
                options.LoginPath = myService.GetLoginPath();
            });
    
        services.AddRazorPages();
    }
    
  • Os serviços transitórios descartáveis são capturados pelo contêiner para descarte. Isso pode se transformar em uma perda de memória quando resolvido por meio do contêiner de nível superior.

  • Habilite a validação de escopo para garantir que o aplicativo não tenha singletons que capturem serviços com escopo. Para obter mais informações, confira Validação de escopo.

Como todos os conjuntos de recomendações, talvez você encontre situações em que é necessário ignorar uma recomendação. As exceções são raras, ocorrendo em casos especiais dentro da própria estrutura.

A DI é uma alternativa aos padrões de acesso a objeto estático/global. Talvez você não obtenha os benefícios da DI se combiná-lo com o acesso a objeto estático.

O Orchard Core é uma estrutura de aplicativo para a criação de aplicativos modulares multilocatários no ASP.NET Core. Para obter mais informações, confira a Documentação do Orchard Core.

Confira os exemplos do Orchard Core para obter exemplos de como criar aplicativos modulares e multilocatários usando apenas o Orchard Core Framework sem nenhum de seus recursos específicos do CMS.

Serviços fornecidos pela estrutura

O Startup.ConfigureServices registra os serviços usados pelo aplicativo, incluindo recursos de plataforma, como o Entity Framework Core e o ASP.NET Core MVC. Inicialmente, o IServiceCollection fornecido para ConfigureServices possui serviços definidos pela estrutura dependendo de como o host foi configurado. Para aplicativos baseados nos modelos do ASP.NET Core, a estrutura registra mais de 250 serviços.

A tabela a seguir lista um pequeno exemplo desses serviços registrados por estrutura:

Tipo de Serviço Tempo de vida
Microsoft.AspNetCore.Hosting.Builder.IApplicationBuilderFactory Transitório
IHostApplicationLifetime Singleton
IWebHostEnvironment Singleton
Microsoft.AspNetCore.Hosting.IStartup Singleton
Microsoft.AspNetCore.Hosting.IStartupFilter Transitório
Microsoft.AspNetCore.Hosting.Server.IServer Singleton
Microsoft.AspNetCore.Http.IHttpContextFactory Transitório
Microsoft.Extensions.Logging.ILogger<TCategoryName> Singleton
Microsoft.Extensions.Logging.ILoggerFactory Singleton
Microsoft.Extensions.ObjectPool.ObjectPoolProvider Singleton
Microsoft.Extensions.Options.IConfigureOptions<TOptions> Transitório
Microsoft.Extensions.Options.IOptions<TOptions> Singleton
System.Diagnostics.DiagnosticSource Singleton
System.Diagnostics.DiagnosticListener Singleton

Recursos adicionais