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

Por Kirk Ltd, Steve Smith, Scott A meioe James Da ltda

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 de MVC, consulte 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, consulte Injeção de dependência no .NET.

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

Este tópico fornece informações sobre a injeção de dependência no ASP.NET Core. A documentação principal sobre como usar a injeção de dependência está contida 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 de que outro objeto depende. Examine a seguinte MyDependency classe com um método de que outras classes WriteMessage dependem:

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

Uma classe pode criar uma instância da MyDependency classe para usar seu método WriteMessage . No exemplo a seguir, MyDependency a classe é 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 MyDependency classe . 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 deve IndexModel ser modificada.
  • Se MyDependency tiver dependências, elas também deverão ser configuradas pela IndexModel classe . 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 do Startup.ConfigureServices 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 define o IMyDependency 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 IMyDependency serviço com o tipo concreto MyDependency . O AddScoped método 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 aplicativo de exemplo, o IMyDependency serviço é solicitado e usado 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:

  • Não usa o tipo concreto MyDependency , somente a interface que ele IMyDependency 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 IMyDependency interface pode ser aprimorada usando a API de registro em 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 ConfigureServices método atualizado registra a nova IMyDependency implementação:

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

    services.AddRazorPages();
}

MyDependency2 depende de ILogger<TCategoryName> , que ele solicita no construtor. ILogger<TCategoryName> é um serviço fornecido pelo Framework.

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 aproveitando ILogger<TCategoryName> os tipos abertos (genéricos), eliminando a necessidade de registrar cada 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 IMyDependency serviço.
  • O 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 IMyDependency implementações mostradas nos exemplos anteriores foram escritas para demonstrar a di básica, não para implementar o registro em log. A maioria dos aplicativos não deve precisar gravar agentes. O código a seguir demonstra como usar o log padrão, que não exige que nenhum serviço seja registrado no 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 há necessidade de atualizar ConfigureServices , porque o registro em log é fornecido pela estrutura.

Serviços injetados na inicialização

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

Somente os seguintes serviços podem ser injetados no Startup Construtor ao usar o host genérico ( IHostBuilder ):

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

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

Para obter mais informações, consulte Inicialização de aplicativo no ASP.NET Core e acessar a configuração na inicialização.

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

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

O código a seguir é gerado pelo modelo Páginas usando contas de usuário individuais e mostra como adicionar serviços adicionais ao contêiner usando os métodos Razor 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 seguinte ConfigureServices método, que registra os serviços e configura as 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 relacionados de registros 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;
        }
    }
}

Os serviços restantes são registrados em uma classe semelhante. O método a seguir ConfigureServices 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 services.Add{GROUP_NAME} método de extensão adiciona e potencialmente configura os serviços do. Por exemplo, AddControllersWithViews adiciona os serviços que os controladores MVC com exibições exigem e AddRazorPages adiciona os serviços que as Razor páginas exigem. Recomendamos que os aplicativos sigam essa Convenção de nomenclatura. Coloque os métodos de extensão no namespace Microsoft.Extensions.DependencyInjection para encapsular grupos de registros de serviço.

Tempos de vida do serviço

Consulte Tempo de vida do serviço na injeção de dependência no .NET

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

  • Injete o serviço no método Invoke ou do InvokeAsync middleware. O uso da injeção de construtor lança 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 InvokeAsync abordagem.
  • Use o middleware baseado em fábrica. O middleware registrado usando essa abordagem é ativado por solicitação do cliente (conexão), o que permite que os serviços com escopo sejam injetados no método do InvokeAsync middleware.

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

Métodos de registro do serviço

Consulte 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 testaro .

Registrar um serviço com apenas um tipo de implementação é equivalente a registrar esse serviço com a mesma implementação e tipo de 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 como o tipo de IMyDependency serviço. A segunda chamada para substitui a anterior quando resolvida como e adiciona à anterior quando AddSingleton IMyDependency 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

Consulte Comportamento de injeção de construtor na injeção de dependência no .NET

Contextos de Entity Framework

Por padrão, Entity Framework contextos 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 têm escopo para a solicitação do cliente. Para usar um tempo de vida diferente, especifique o tempo de vida usando uma AddDbContext sobrecarga. Os serviços de um determinado tempo de vida não devem usar um contexto de banco de dados com um tempo de vida mais curto 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 do serviço de uma operação é configurado para as seguintes interfaces, o contêiner fornece as mesmas instâncias ou instâncias diferentes do serviço quando solicitado por 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 Operation construtor 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 Startup.ConfigureServices método cria vários registros da classe de acordo com os tempo de vida Operation nomeados:

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

    services.AddRazorPages();
}

O aplicativo de exemplo demonstra os tempos de vida do objeto dentro e entre solicitações. O IndexModel e o middleware solicitam cada tipo IOperation de tipo e registram o para cada OperationId 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 InvokeAsync método :

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 logger mostra:

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

Para reduzir a saída de log, de definir "Logging:LogLevel:Microsoft:Error" no arquivoappsettings.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 com escopo IMyDependency e chamar seu WriteMessage método 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

Consulte comportamento de injeção de 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 disponíveis em uma solicitação de ASP.NET Core são expostos por meio da coleção HttpContext. Requestservices . Quando os serviços são solicitados de dentro de uma solicitação, os serviços e suas dependências são resolvidos na RequestServices coleção.

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

Observação

Prefira solicitar dependências como parâmetros de construtor para resolver serviços da RequestServices coleção. Isso resulta em classes que são mais fáceis de testar.

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

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

  • Evite classes e membros com estado e estáticos. Evite criar o estado global criando aplicativos para usar os 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.
  • Torne os serviços pequenos, bem fatorados e facilmente testados.

Se uma classe tiver muitas dependências injetadas, pode ser um sinal de que a classe tem muitas responsabilidades e viola o princípio de responsabilidade única (SRP). Tente refatorar a classe movendo algumas de suas responsabilidades para novas classes. Tenha em mente que as classes Razor de modelo de página de páginas e as classes do controlador MVC devem se concentrar nas preocupações da 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 fábrica for registrado como um 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 saída a seguir após cada atualização da página de índice:

Service1: IndexModel.OnGet
Service2: IndexModel.OnGet
Service3: IndexModel.OnGet
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

Consulte IDisposable guidance for Transient and shared instance in Dependency injection in .NET (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

Consulte Substituição de contêiner de serviço padrão na injeção de dependência no .NET

Recomendações

Consulte 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 BuildServiceProvider chamada normalmente ocorre quando o desenvolvedor deseja resolver um serviço no ConfigureServices . Por exemplo, considere o caso em que o LoginPath é carregado da configuração. Evite a seguinte abordagem:

    código ruim chamando BuildServiceProvider

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

    A chamada de ASP0000 'BuildServiceProvider' do código do aplicativo resulta em uma cópia adicional dos serviços singleton que estão sendo criados. Considere alternativas como a injeção de dependência de serviços como parâmetros para "Configurar".

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

    Uma maneira correta de obter é usar o suporte integrado do padrão de LoginPath 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 um vazamento de memória se resolvido do contêiner de nível superior.

  • Habilita a validação de escopo para garantir que o aplicativo não tenha singletons que capturam 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. Exceções são raras, principalmente 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 aplicativos para a criação de aplicativos modulares multi tenant no ASP.NET Core. Para obter mais informações, consulte a Documentação do Orchard Core.

Consulte os exemplos do Orchard Core para ver exemplos de como criar aplicativos modulares e multi-locatários usando apenas o Orchard Core Framework sem qualquer um de seus recursos específicos do CMS.

Serviços fornecidos pela estrutura

O método registra serviços que o aplicativo usa, incluindo recursos de Startup.ConfigureServices plataforma, como Entity Framework Core e ASP.NET Core MVC. Inicialmente, o IServiceCollection fornecido para tem serviços ConfigureServices definidos pela estrutura, dependendo de como o host foi configurado. Para aplicativos baseados nos ASP.NET Core, a estrutura registra mais de 250 serviços.

A tabela a seguir lista um pequeno exemplo desses serviços registrados em 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 Steve Smith, Scott A ltdae Scott Da ltda

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 de MVC, consulte Injeção de dependência em controladores no ASP.NET Core.

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

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

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

public class MyDependency
{
    public MyDependency()
    {
    }

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

        return Task.FromResult(0);
    }
}

Uma instância da classe MyDependency pode ser criada para tornar o método WriteMessage disponível para uma classe. A classe é uma dependência da classe:

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

    public async Task OnGetAsync()
    {
        await _dependency.WriteMessage(
            "IndexModel.OnGetAsync 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 por estes motivos:

  • Para substituir MyDependency por uma implementação diferente, a classe deve ser modificada.
  • Se MyDependency tiver dependências, elas deverão ser configuradas pela classe. 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. 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 exemplo de aplicativo, a interface IMyDependency define um método que o serviço fornece ao aplicativo:

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

Essa interface é implementada por um tipo concreto, MyDependency:

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

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

    public Task WriteMessage(string message)
    {
        _logger.LogInformation(
            "MyDependency.WriteMessage called. Message: {Message}", 
            message);

        return Task.FromResult(0);
    }
}

MyDependency solicita um ILogger<TCategoryName> em seu construtor. 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.

IMyDependency e ILogger<TCategoryName> devem ser registrados no contêiner do serviço. IMyDependency está registrado em Startup.ConfigureServices. ILogger<TCategoryName> é registrado pela infraestrutura de abstrações de registro em log, portanto, é um serviço fornecido por estrutura registrado por padrão pela estrutura.

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

services.AddSingleton(typeof(ILogger<>), typeof(Logger<>));

No exemplo de aplicativo, o serviço IMyDependency está registrado com o tipo concreto MyDependency. O registro define o escopo do tempo de vida do serviço para 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.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

    services.AddScoped<IMyDependency, MyDependency>();
    services.AddTransient<IOperationTransient, Operation>();
    services.AddScoped<IOperationScoped, Operation>();
    services.AddSingleton<IOperationSingleton, Operation>();
    services.AddSingleton<IOperationSingletonInstance>(new Operation(Guid.Empty));

    // OperationService depends on each of the other Operation types.
    services.AddTransient<OperationService, OperationService>();
}

Observação

Cada services.Add{SERVICE_NAME} método de extensão adiciona e potencialmente configura serviços. Por exemplo, services.AddControllersWithViews , e adiciona os serviços que ASP.NET services.AddRazorPages services.AddControllers Aplicativos Principais exigem. Recomendamos que os aplicativos sigam essa convenção. Coloque os métodos de extensão no namespace Microsoft.Extensions.DependencyInjection para encapsular grupos de registros de serviço. Incluindo a parte do namespace Microsoft.Extensions.DependencyInjection para métodos de extensão de DI também:

  • Permite que eles sejam exibidos no IntelliSense sem adicionar blocos using adicionais.
  • Impede instruções using excessivas na Startup classe da qual esses métodos de extensão normalmente são chamados.

Se o construtor do serviço exigir um tipo interno, como string, o tipo poderá ser injetado usando a configuração ou o padrão de opções:

public class MyDependency : IMyDependency
{
    public MyDependency(IConfiguration config)
    {
        var myStringValue = config["MyStringKey"];

        // Use myStringValue
    }

    ...
}

Uma instância do serviço é solicitada por meio do construtor de uma classe, onde o serviço é usado e atribuído a um campo particular. O campo é usado para acessar o serviço conforme o necessário na classe.

No exemplo de aplicativo, a instância IMyDependency é solicitada e usada para chamar o método WriteMessage do serviço:

public class IndexModel : PageModel
{
    private readonly IMyDependency _myDependency;

    public IndexModel(
        IMyDependency myDependency, 
        OperationService operationService,
        IOperationTransient transientOperation,
        IOperationScoped scopedOperation,
        IOperationSingleton singletonOperation,
        IOperationSingletonInstance singletonInstanceOperation)
    {
        _myDependency = myDependency;
        OperationService = operationService;
        TransientOperation = transientOperation;
        ScopedOperation = scopedOperation;
        SingletonOperation = singletonOperation;
        SingletonInstanceOperation = singletonInstanceOperation;
    }

    public OperationService OperationService { get; }
    public IOperationTransient TransientOperation { get; }
    public IOperationScoped ScopedOperation { get; }
    public IOperationSingleton SingletonOperation { get; }
    public IOperationSingletonInstance SingletonInstanceOperation { get; }

    public async Task OnGetAsync()
    {
        await _myDependency.WriteMessage(
            "IndexModel.OnGetAsync created this message.");
    }
}

Serviços injetados na inicialização

Somente os seguintes tipos de serviço podem ser injetados no Startup Construtor ao usar o host genérico ( IHostBuilder ):

Os serviços podem ser injetados em Startup.Configure :

public void Configure(IApplicationBuilder app, IOptions<MyOptions> options)
{
    ...
}

Para obter mais informações, consulte Inicialização de aplicativo no ASP.NET Core.

Serviços fornecidos pela estrutura

O Startup.ConfigureServices método é responsável por definir os serviços que o aplicativo usa, incluindo recursos de plataforma, como Entity Framework Core e ASP.NET Core MVC. Inicialmente, o IServiceCollection fornecido para o ConfigureServices tem serviços definidos pela estrutura, dependendo de como o host foi configurado. Não é incomum um aplicativo baseado em um modelo de ASP.NET Core ter centenas de serviços registrados pela estrutura. Uma pequena amostra de serviços registrados na estrutura é listada na tabela a seguir.

Tipo de Serviço Tempo de vida
Microsoft.AspNetCore.Hosting.Builder.IApplicationBuilderFactory Transitório
Microsoft.AspNetCore.Hosting.IApplicationLifetime Singleton
Microsoft.AspNetCore.Hosting.IHostingEnvironment 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

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

Quando um método de extensão de coleta do serviço estiver disponível para registrar um serviço (e seus serviços dependentes, se for necessário), a convenção é usar um único método de extensão Add{SERVICE_NAME} para registrar todos os serviços exigidos por esse serviço. O código a seguir é um exemplo de como adicionar serviços adicionais ao contêiner usando os métodos de extensão AddDbContext <TContext> e AddIdentityCore :

public void ConfigureServices(IServiceCollection services)
{
    ...

    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

    services.AddIdentity<ApplicationUser, IdentityRole>()
        .AddEntityFrameworkStores<ApplicationDbContext>()
        .AddDefaultTokenProviders();

    ...
}

Para saber mais, confira a classe ServiceCollection na documentação da API.

Tempos de vida do serviço

Escolha um tempo de vida apropriado para cada serviço registrado. Os serviços do ASP.NET Core podem ser configurados com os seguintes tempos de vida:

Transitório

Serviços temporários de tempo de vida (AddTransient) são criados cada vez que são solicitados pelo contêiner de serviço. Esse tempo de vida funciona melhor para serviços leves e sem estado.

Em aplicativos que processam solicitações, os serviços transitórios são descartados no final da solicitação.

Com escopo

Os serviços com tempo de vida (AddScoped) com escopo são criados uma vez por solicitação de cliente (conexão).

Em aplicativos que processam solicitações, os serviços com escopo são descartados no final da solicitação.

Aviso

Ao usar um serviço com escopo em um middleware, injete o serviço no método Invoke ou InvokeAsync. Não injetar via injeção de Construtor porque força o serviço a se comportar como um singleton. Para obter mais informações, consulte Escrever middleware do ASP.NET Core personalizado.

Singleton

Serviços de tempo de vida singleton (AddSingleton) são criados na primeira solicitação (ou quando Startup.ConfigureServices é executado e uma instância é especificada com o registro do serviço). Cada solicitação subsequente usa a mesma instância. Se o aplicativo exigir um comportamento de singleton, recomendamos permitir que o contêiner do serviço gerencie o tempo de vida do serviço. Não implemente o padrão de design singleton e forneça o código de usuário para gerenciar o tempo de vida do objeto na classe.

Em aplicativos que processam solicitações, os serviços singleton são descartados quando o ServiceProvider é Descartado no desligamento do aplicativo.

Aviso

É perigoso resolver um serviço com escopo de um singleton. Pode fazer com que o serviço tenha um estado incorreto durante o processamento das solicitações seguintes.

Métodos de registro do serviço

Os métodos de extensão de registro de serviço oferecem sobrecargas que são úteis em cenários específicos.

Método Automática
objeto
descarte
Vários
implementações
Passar argumentos
Add{LIFETIME}<{SERVICE}, {IMPLEMENTATION}>()
Exemplo:
services.AddSingleton<IMyDep, MyDep>();
Sim Sim Não
Add{LIFETIME}<{SERVICE}>(sp => new {IMPLEMENTATION})
Exemplos:
services.AddSingleton<IMyDep>(sp => new MyDep());
services.AddSingleton<IMyDep>(sp => new MyDep("A string!"));
Sim Sim Sim
Add{LIFETIME}<{IMPLEMENTATION}>()
Exemplo:
services.AddSingleton<MyDep>();
Sim Não Não
AddSingleton<{SERVICE}>(new {IMPLEMENTATION})
Exemplos:
services.AddSingleton<IMyDep>(new MyDep());
services.AddSingleton<IMyDep>(new MyDep("A string!"));
Não Sim Yes
AddSingleton(new {IMPLEMENTATION})
Exemplos:
services.AddSingleton(new MyDep());
services.AddSingleton(new MyDep("A string!"));
Não Não Sim

Para obter mais informações sobre o descarte de tipos, consulte a seção Descarte de serviços. Um cenário comum para várias implementações é a simulação de tipos para teste.

O registro de um serviço com apenas um tipo de implementação é equivalente ao registro desse serviço com a mesma implementação e tipo de 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 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 o tipo de serviço. A segunda chamada para substitui a anterior quando resolvida como e adiciona à anterior quando AddSingleton IMyDependency 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);
    }
}

A estrutura também fornece TryAdd{LIFETIME} métodos de extensão, que registram o serviço somente se ainda não houver uma implementação registrada.

No exemplo a seguir, a chamada para AddSingleton se registra como uma implementação para MyDependency IMyDependency . A chamada para TryAddSingleton não tem nenhum efeito porque já tem uma implementação IMyDependency registrada.

services.AddSingleton<IMyDependency, MyDependency>();
// The following line has no effect:
services.TryAddSingleton<IMyDependency, DifferentDependency>();

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

Para obter mais informações, consulte:

Métodos TryAddEnumerable(ServiceDescriptor) apenas registrarão o serviço se ainda não houver uma implementação do mesmo tipo. Vários serviços são resolvidos via IEnumerable<{SERVICE}>. Ao registrar os serviços, só convém ao desenvolvedor adicionar uma instância se até o momento nenhuma do mesmo tipo foi adicionada. Em geral, esse método é usado por autores de biblioteca para evitar registrar duas cópias de uma instância no contêiner.

No exemplo a seguir, a primeira linha registra MyDep para IMyDep1. A segunda linha registra MyDep para IMyDep2. A terceira linha não tem nenhum efeito porque IMyDep1 já tem uma implementação registrada de MyDep:

public interface IMyDep1 {}
public interface IMyDep2 {}

public class MyDep : IMyDep1, IMyDep2 {}

services.TryAddEnumerable(ServiceDescriptor.Singleton<IMyDep1, MyDep>());
services.TryAddEnumerable(ServiceDescriptor.Singleton<IMyDep2, MyDep>());
// Two registrations of MyDep for IMyDep1 is avoided by the following line:
services.TryAddEnumerable(ServiceDescriptor.Singleton<IMyDep1, MyDep>());

Comportamento da injeção de construtor

Os serviços podem ser resolvidos por dois mecanismos:

  • IServiceProvider
  • ActivatorUtilities: permite a criação de objeto sem registro de serviço no contêiner de injeção de dependência. ActivatorUtilities é usado com abstrações voltadas ao usuário, como Auxiliares de Marca, controladores MVC e associadores de modelo.

Os construtores podem aceitar argumentos que não são fornecidos pela injeção de dependência, mas que precisam atribuir valores padrão.

Quando os serviços são resolvidos IServiceProvider por ou , a ActivatorUtilities injeção de construtor requer um construtor público.

Quando os serviços são resolvidos pelo ActivatorUtilities , a injeção de construtor requer que apenas um construtor aplicável exista. Há suporte para sobrecargas de construtor, mas somente uma sobrecarga pode existir, cujos argumentos podem ser todos atendidos pela injeção de dependência.

Contextos de Entity Framework

Contextos do Entity Framework geralmente são adicionados ao contêiner de serviço usando o tempo de vida com escopo, pois o escopo de operações de banco de dados de aplicativo Web normalmente é para a solicitação do cliente. O tempo de vida padrão será definido se um tempo de vida não for especificado por uma sobrecarga AddDbContext <TContext> ao registrar o contexto do banco de dados. Serviços com um determinado tempo de vida não devem usar um contexto de banco de dados com um tempo de vida mais curto que aquele do serviço.

Opções de tempo de vida e de registro

Para demonstrar a diferença entre as opções de tempo de vida e de registro, considere as interfaces a seguir, que representam tarefas como uma operação com um identificador exclusivo, 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 outra diferente, quando recebe uma solicitação de uma classe:

public interface IOperation
{
    Guid OperationId { get; }
}

public interface IOperationTransient : IOperation
{
}

public interface IOperationScoped : IOperation
{
}

public interface IOperationSingleton : IOperation
{
}

public interface IOperationSingletonInstance : IOperation
{
}

As interfaces são implementadas na classe Operation. O construtor Operation gera um GUID, caso um não seja fornecido:

public class Operation : IOperationTransient, 
    IOperationScoped, 
    IOperationSingleton, 
    IOperationSingletonInstance
{
    public Operation() : this(Guid.NewGuid())
    {
    }

    public Operation(Guid id)
    {
        OperationId = id;
    }

    public Guid OperationId { get; private set; }
}

Há um OperationService registrado que depende de cada um dos outros Operation tipos. Quando OperationService é solicitado por meio da injeção de dependência, ele recebe a uma nova instância de cada serviço, ou uma instância existente com base no tempo de vida do serviço dependente.

  • Quando os serviços temporários forem criados mediante solicitação do contêiner, o OperationId do serviço IOperationTransient será diferente do OperationId do OperationService. OperationService recebe uma nova instância da classe IOperationTransient. A nova instância produz outro OperationId.
  • Quando os serviços com escopo forem criados por solicitação do cliente, o OperationId do serviço IOperationScoped será o mesmo que o OperationService dentro de uma solicitação do cliente. Em todas as solicitações do cliente, os dois serviços compartilham um valor de OperationId diferente.
  • Quando os serviços singleton e de instância singleton forem criados uma vez e usados em todas as solicitações de cliente e em todos os serviços, o OperationId será constante entre todas as solicitações de serviço.
public class OperationService
{
    public OperationService(
        IOperationTransient transientOperation,
        IOperationScoped scopedOperation,
        IOperationSingleton singletonOperation,
        IOperationSingletonInstance instanceOperation)
    {
        TransientOperation = transientOperation;
        ScopedOperation = scopedOperation;
        SingletonOperation = singletonOperation;
        SingletonInstanceOperation = instanceOperation;
    }

    public IOperationTransient TransientOperation { get; }
    public IOperationScoped ScopedOperation { get; }
    public IOperationSingleton SingletonOperation { get; }
    public IOperationSingletonInstance SingletonInstanceOperation { get; }
}

Em Startup.ConfigureServices, cada tipo é adicionado ao contêiner de acordo com seu tempo de vida nomeado:

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

    services.AddScoped<IMyDependency, MyDependency>();
    services.AddTransient<IOperationTransient, Operation>();
    services.AddScoped<IOperationScoped, Operation>();
    services.AddSingleton<IOperationSingleton, Operation>();
    services.AddSingleton<IOperationSingletonInstance>(new Operation(Guid.Empty));

    // OperationService depends on each of the other Operation types.
    services.AddTransient<OperationService, OperationService>();
}

O serviço IOperationSingletonInstance está usando uma instância específica com uma ID conhecida de Guid.Empty. Fica claro quando esse tipo está em uso (seu GUID contém apenas zeros).

O exemplo de aplicativo demonstra os tempos de vida do objeto dentro e entre solicitações individuais. O exemplo de aplicativo IndexModel solicita cada tipo IOperation e o OperationService. Depois, a página exibe todos os valores de OperationId da classe de modelo de página e do serviço por meio de atribuições de propriedade:

public class IndexModel : PageModel
{
    private readonly IMyDependency _myDependency;

    public IndexModel(
        IMyDependency myDependency, 
        OperationService operationService,
        IOperationTransient transientOperation,
        IOperationScoped scopedOperation,
        IOperationSingleton singletonOperation,
        IOperationSingletonInstance singletonInstanceOperation)
    {
        _myDependency = myDependency;
        OperationService = operationService;
        TransientOperation = transientOperation;
        ScopedOperation = scopedOperation;
        SingletonOperation = singletonOperation;
        SingletonInstanceOperation = singletonInstanceOperation;
    }

    public OperationService OperationService { get; }
    public IOperationTransient TransientOperation { get; }
    public IOperationScoped ScopedOperation { get; }
    public IOperationSingleton SingletonOperation { get; }
    public IOperationSingletonInstance SingletonInstanceOperation { get; }

    public async Task OnGetAsync()
    {
        await _myDependency.WriteMessage(
            "IndexModel.OnGetAsync created this message.");
    }
}

As duas saídas a seguir mostram os resultados das duas solicitações:

Primeira solicitação:

Operações do controlador:

Transitória: d233e165-f417-469b-a866-1cf1935d2518
Com escopo: 5d997e2d-55f5-4a64-8388-51c4e3a1ad19
Singleton: 01271bc1-9e31-48e7-8f7c-7261b040ded9
Instância: 00000000-0000-0000-0000-000000000000

OperationService operações:

Transitória: c6b049eb-1318-4e31-90f1-eb2dd849ff64
Com escopo: 5d997e2d-55f5-4a64-8388-51c4e3a1ad19
Singleton: 01271bc1-9e31-48e7-8f7c-7261b040ded9
Instância: 00000000-0000-0000-0000-000000000000

Segunda solicitação:

Operações do controlador:

Transitória: b63bd538-0a37-4ff1-90ba-081c5138dda0
Com escopo: 31e820c5-4834-4d22-83fc-a60118acb9f4
Singleton: 01271bc1-9e31-48e7-8f7c-7261b040ded9
Instância: 00000000-0000-0000-0000-000000000000

OperationService operações:

Transitória: c4cbacb8-36a2-436d-81c8-8c1b78808aaf
Com escopo: 31e820c5-4834-4d22-83fc-a60118acb9f4
Singleton: 01271bc1-9e31-48e7-8f7c-7261b040ded9
Instância: 00000000-0000-0000-0000-000000000000

Observe qual dos valores de OperationId varia em uma solicitação, e entre as solicitações:

  • Os objetos transitórios sempre são diferentes. O valor OperationId transitório da primeira e da segunda solicitações de cliente é diferente para as duas operações OperationService e em todas as solicitações de cliente. Uma nova instância é fornecida para cada solicitação de serviço e solicitação de cliente.
  • Objetos com escopo são os mesmos em uma solicitação de cliente, mas diferentes entre solicitações de cliente.
  • Os pbjetos singleton são os mesmos para cada objeto e solicitação, independentemente de uma instância Operation ser fornecida em Startup.ConfigureServices.

Chamar os serviços desde o principal

Crie um IServiceScope com para resolver um serviço com escopo dentro do escopo do IServiceScopeFactory.CreateScope aplicativo. Essa abordagem é útil para acessar um serviço com escopo na inicialização com o tempo de vida de serviço correto para executar tarefas de inicialização. O exemplo a seguir mostra como obter um contexto para o MyScopedService em Program.Main:

using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

public class Program
{
    public static async Task Main(string[] args)
    {
        var host = CreateWebHostBuilder(args).Build();

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

            try
            {
                var serviceContext = services.GetRequiredService<MyScopedService>();
                // Use the context here
            }
            catch (Exception ex)
            {
                var logger = services.GetRequiredService<ILogger<Program>>();
                logger.LogError(ex, "An error occurred.");
            }
        }

        await host.RunAsync();
    }

    public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
        WebHost.CreateDefaultBuilder(args)
            .UseStartup<Startup>();
}

Não é necessário criar um escopo para serviços transitórios, incluindo para o no ILogger exemplo anterior (consulte: Como fazer registro em log no .NET Core e no ASP.NET Core ). Transitórios não resolvem inadvertidamente como singletons quando resolvidos da raiz, como os serviços com escopo resolveriam. Transitórios são criados quando solicitado. Se um serviço transitório for descartável, ele será enraizado pelo contêiner até o descarte. Por exemplo, veja Fazer solicitações HTTP usando IHttpClientFactory no ASP.NET Core.

Validação de escopo

Quando o aplicativo está em execução no ambiente de desenvolvimento, o provedor de serviço padrão executa verificações para saber se:

  • Os serviços com escopo não são resolvidos direta ou indiretamente pelo provedor de serviço raiz.
  • Os serviços com escopo não são injetados direta ou indiretamente em singletons.

O provedor de serviços raiz é criado quando BuildServiceProvider é chamado. O tempo de vida do provedor de serviço raiz corresponde ao tempo de vida do aplicativo/servidor quando o provedor começa com o aplicativo e é descartado quando o aplicativo é desligado.

Os serviços com escopo são descartados pelo contêiner que os criou. Se um serviço com escopo é criado no contêiner raiz, o tempo de vida do serviço é promovido efetivamente para singleton, porque ele só é descartado pelo contêiner raiz quando o aplicativo/servidor é desligado. A validação dos escopos de serviço detecta essas situações quando BuildServiceProvider é chamado.

Para obter mais informações, consulte Host da Web do ASP.NET Core.

Serviços de solicitação

Os serviços disponíveis em uma solicitação do ASP.NET de HttpContext são expostos por meio da coleção HttpContext.RequestServices.

Os Serviços de Solicitação representam os serviços configurados e solicitados como parte do aplicativo. Quando os objetos especificam dependências, elas são atendidas pelos tipos encontrados em RequestServices, não em ApplicationServices.

Em geral, o aplicativo não deve usar essas propriedades diretamente. Em vez disso, solicite os tipos exigidos pelas classes por meio de construtores de classe e permita que a estrutura injete as dependências. Isso resulta em classes que são mais fáceis de testar.

Observação

Prefira solicitar dependências como parâmetros de construtor para acessar a coleção RequestServices.

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

As melhores práticas são:

  • Projetar serviços para usar a injeção de dependência a fim de obter suas dependências.
  • Evite membros e classes estáticas com estado. Em vez disso, projete aplicativos para usar serviços singleton, o que evita a criação de estado global.
  • 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.
  • Verifique as classes de aplicativo pequenas, bem fatoradas e facilmente testadas.

Se parecer que uma classe tem muitas dependências injetadas, normalmente é um sinal de que a classe tem muitas responsabilidades e está violando o Princípio da Responsabilidade Única (SRP). Tente refatorar a classe movendo algumas de suas responsabilidades para uma nova classe. Tenha em mente que as classes de modelo de página Pages e as classes de controlador Razor MVC devem se concentrar em preocupações com a interface do usuário. Os detalhes de implementação de acesso aos dados e as regras de negócios devem ser mantidos em classes apropriadas a esses interesses separados.

Descarte de serviços

O contêiner chama Dispose para os tipos IDisposable criados por ele. Se uma instância for adicionada ao contêiner pelo código do usuário, ele não será descartado automaticamente.

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

public class Service1 : IDisposable {}
public class Service2 : IDisposable {}

public interface IService3 {}
public class Service3 : IService3, IDisposable {}

public void ConfigureServices(IServiceCollection services)
{
    services.AddScoped<Service1>();
    services.AddSingleton<Service2>();
    services.AddSingleton<IService3>(sp => new Service3());
}

No exemplo a seguir:

  • As instâncias de serviço não são criadas pelo contêiner de serviço.
  • Os tempos de vida de serviço pretendidos não são conhecidos pela estrutura.
  • A estrutura não descarta os serviços automaticamente.
  • Se os serviços não forem explicitamente descartados no código do desenvolvedor, eles persistirão até que o aplicativo seja desligado.
public class Service1 : IDisposable {}
public class Service2 : IDisposable {}

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

Diretrizes de IDisposable para instâncias transitórias e compartilhadas

Tempo de vida transitório, limitado

Cenário

O aplicativo requer uma IDisposable instância com um tempo de vida transitório para qualquer um dos seguintes cenários:

  • A instância é resolvida no escopo raiz.
  • A instância deve ser descartada antes do término do escopo.

Solução

Use o padrão de fábrica para criar uma instância fora do escopo pai. Nessa situação, o aplicativo geralmente teria um Create método que chamasse o construtor do tipo final diretamente. Se o tipo final tiver outras dependências, a fábrica poderá:

Instância compartilhada, tempo de vida limitado

Cenário

O aplicativo requer uma IDisposable instância compartilhada entre vários serviços, mas o IDisposable deve ter um tempo de vida limitado.

Solução

Registre a instância com um tempo de vida de escopo. Use IServiceScopeFactory.CreateScope para iniciar e criar um novo IServiceScope . Use o escopo IServiceProvider para obter os serviços necessários. Descarte o escopo quando o tempo de vida deve terminar.

Diretrizes gerais

  • Não registre IDisposable instâncias com um escopo transitório. Em vez disso, use o padrão de fábrica.
  • Não resolva instâncias IDisposable transitórias ou com escopo no escopo raiz. A única exceção geral é quando o aplicativo cria/recria e descarta IServiceProvider o , que não é um padrão ideal.
  • O recebimento IDisposable de uma dependência por meio da DI não exige que o receptor se IDisposable implemente. O receptor da IDisposable dependência não deve chamar nessa Dispose dependência.
  • Os escopos devem ser usados para controlar os tempo de vida dos serviços. Os escopos não são hierárquicos e não há nenhuma conexão especial entre escopos.

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

O contêiner de serviço integrado foi projetado para atender às necessidades da estrutura e da maioria dos aplicativos de consumidor. É recomendável usar o contêiner integrado, a menos que você precise de um recurso específico que o contêiner integrado não dá suporte, como:

  • Injeção de propriedade
  • Injeção com base no nome
  • Contêineres filho
  • Gerenciamento de tempo de vida personalizado
  • Suporte de Func<T> para inicialização lenta
  • Registro baseado em convenção

Os seguintes contêineres de terceiros podem ser usados com ASP.NET Core:

Acesso thread-safe

Crie serviços singleton thread-safe. Se um serviço singleton tiver uma dependência em um serviço transitório, o serviço transitório também precisará ter acesso thread-safe, dependendo de como ele é usado pelo singleton.

O método de fábrica de um único serviço, como o segundo argumento para AddSingservice <TService> (IServiceCollection, Func <IServiceProvider,TService> ), não precisa ser thread-safe. Como um construtor do tipo (static), ele tem garantia de ser chamado uma vez por um único thread.

Recomendações

  • A resolução de serviço baseada em async/await e Task não é compatível. O C# não é compatível com os construtores assíncronos. Portanto, o padrão recomendado é usar os métodos assíncronos após a resolução síncrona do serviço.

  • Evite armazenar dados e a configuração diretamente no contêiner do serviço. Por exemplo, o carrinho de compras de um usuário normalmente não deve ser adicionado ao contêiner do serviço. A configuração deve usar o padrão de opções. Da mesma forma, evite objetos de "suporte de dados" que existem somente para permitir o acesso a outro objeto. É melhor solicitar o item real por meio da DI.

  • Evite o acesso estático aos serviços. Por exemplo, evite digitar estaticamente IApplicationBuilder.ApplicationServices para uso em outro lugar.

  • Evite usar o padrão de localizador de serviço, que combina estratégias de Inversão de Controle.

    • Não invoque GetService para obter uma instância de serviço quando você puder usar a DI em vez disso:

      Incorreto:

      public class MyClass()
      
        public void MyMethod()
        {
            var optionsMonitor = 
                _services.GetService<IOptionsMonitor<MyOptions>>();
            var option = optionsMonitor.CurrentValue.Option;
      
            ...
        }
      

      Correto:

      public class MyClass
      {
          private readonly IOptionsMonitor<MyOptions> _optionsMonitor;
      
          public MyClass(IOptionsMonitor<MyOptions> optionsMonitor)
          {
              _optionsMonitor = optionsMonitor;
          }
      
          public void MyMethod()
          {
              var option = _optionsMonitor.CurrentValue.Option;
      
              ...
          }
      }
      
  • Evite injetar uma fábrica que resolva dependências em tempo de execução usando GetService .

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

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, principalmente os 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.

Recursos adicionais