Injeção de dependência Blazor 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 Rainer Stropek e Mike Rousos

Este artigo explica como aplicativos Blazor podem injetar serviços em componentes.

A DI (injeção de dependência) é uma técnica de acesso aos serviços configurados em um local central:

  • Os serviços registrados pela estrutura podem ser injetados diretamente em componentes Razor.
  • Aplicativos Blazor definem e registram serviços personalizados e os disponibilizam em todo o aplicativo por meio da DI.

Observação

Recomendamos a leitura de Injeção de dependência no ASP.NET Core antes de ler este tópico.

Serviços padrão

Os serviços mostrados na tabela a seguir normalmente são usados em aplicativos Blazor.

Serviço Tempo de vida Descrição
HttpClient Com escopo

Fornece um método de envio de solicitações HTTP e de recebimento de respostas HTTP de um recurso identificado por um URI.

Do lado do cliente, uma instância de HttpClient é registrada pelo aplicativo no arquivo Program e usa o navegador para lidar com o tráfego HTTP em segundo plano.

No lado do servidor, um HttpClient não está configurado como um serviço por padrão. No código do lado do servidor, forneça um HttpClient.

Para obter mais informações, consulte Chame uma API Web de um Blazoraplicativo ASP.NET Core.

Um HttpClient é registrado como um serviço com escopo, não singleton. Para obter mais informações, consulte a seção Tempo de vida do serviço.

IJSRuntime

Lado do cliente: singleton

Lado do servidor: com escopo

A Blazor estrutura se registra IJSRuntime no contêiner de serviço do aplicativo.

Representa uma instância de um runtime do JavaScript em que as chamadas JavaScript são enviadas. Para obter mais informações, consulte Chamar funções JavaScript de métodos .NET no ASP.NET Core Blazor.

Ao tentar injetar o serviço em um serviço singleton no servidor, siga uma das seguintes abordagens:

  • Altere o registro de serviço para com escopo a fim de corresponder ao registro de IJSRuntime, o que é apropriado se o serviço lidar com o estado específico do usuário.
  • Passe o IJSRuntime para a implementação do serviço singleton como argumento de suas chamadas de método em vez de injetá-lo no singleton.
NavigationManager

Lado do cliente: singleton

Lado do servidor: com escopo

A Blazor estrutura se registra NavigationManager no contêiner de serviço do aplicativo.

Contém auxiliares para o trabalho com URIs e estado de navegação. Para obter mais informações, consulte URI e Auxiliares de estado de navegação.

Serviços adicionais registrados pela estrutura de Blazor são descritos na documentação em que são usados para descrever recursos Blazor, como configuração e registro em log.

Um provedor de serviços personalizado não fornece automaticamente os serviços padrão listados na tabela. Se você usar um provedor de serviços personalizado e exigir qualquer um dos serviços mostrados na tabela, adicione os serviços necessários ao novo provedor de serviços.

Adicionar serviços do lado do cliente

Configure serviços para a coleção de serviços do aplicativo no arquivo Program. No exemplo a seguir, a implementação ExampleDependency é registrada para IExampleDependency:

var builder = WebAssemblyHostBuilder.CreateDefault(args);
...
builder.Services.AddSingleton<IExampleDependency, ExampleDependency>();
...

await builder.Build().RunAsync();

Após a criação do host, os serviços estarão disponíveis no escopo DI raiz antes que todos os componentes sejam renderizados. O recurso pode ser útil por executar a lógica de inicialização antes de renderizar o conteúdo:

var builder = WebAssemblyHostBuilder.CreateDefault(args);
...
builder.Services.AddSingleton<WeatherService>();
...

var host = builder.Build();

var weatherService = host.Services.GetRequiredService<WeatherService>();
await weatherService.InitializeWeatherAsync();

await host.RunAsync();

O host fornece uma instância de configuração central para o aplicativo. Com base no exemplo anterior, a URL do serviço meteorológico é passada de uma fonte de configuração padrão (por exemplo, appsettings.json) para InitializeWeatherAsync:

var builder = WebAssemblyHostBuilder.CreateDefault(args);
...
builder.Services.AddSingleton<WeatherService>();
...

var host = builder.Build();

var weatherService = host.Services.GetRequiredService<WeatherService>();
await weatherService.InitializeWeatherAsync(
    host.Configuration["WeatherServiceUrl"]);

await host.RunAsync();

Adicionar serviços do lado do servidor

Após criar um novo aplicativo, examine o bloco do arquivo Program:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorComponents()
    .AddInteractiveServerComponents();
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();
builder.Services.AddSingleton<WeatherForecastService>();

A variável builder representa um WebApplicationBuilder com um IServiceCollection, que é uma lista de objetos descritores de serviço. Os serviços são adicionados fornecendo descritores de serviço à coleção de serviços. O exemplo a seguir demonstra o conceito com a interface IDataAccess e sua implementação DataAccess concreta:

builder.Services.AddSingleton<IDataAccess, DataAccess>();

Após criar um novo aplicativo, examine o método Startup.ConfigureServices em Startup.cs:

using Microsoft.Extensions.DependencyInjection;

...

public void ConfigureServices(IServiceCollection services)
{
    ...
}

O método ConfigureServices é passado por um IServiceCollection, que é uma lista de objetos descritores de serviço. Os serviços são adicionados no método ConfigureServices, fornecendo descritores de serviço à coleção de serviços. O exemplo a seguir demonstra o conceito com a interface IDataAccess e sua implementação DataAccess concreta:

public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton<IDataAccess, DataAccess>();
}

Registrar serviços em comum

Se um ou mais serviços em comum forem necessários no lado do cliente e do servidor, você poderá colocar os registros dos serviço comuns em um lado do cliente do método e chamar o método para registrar os serviços em ambos os projetos.

Primeiro, decomponha registros de serviço comuns em um método separado. Por exemplo, crie um método ConfigureCommonServices do lado do cliente:

public static void ConfigureCommonServices(IServiceCollection services)
{
    services.Add...;
}

Para o arquivo Program do lado do cliente, chame ConfigureCommonServices para registrar os serviços comuns:

var builder = WebAssemblyHostBuilder.CreateDefault(args);

...

ConfigureCommonServices(builder.Services);

No arquivo Program do lado do servidor, chame ConfigureCommonServices para registrar os serviços comuns:

var builder = WebApplication.CreateBuilder(args);

...

Client.Program.ConfigureCommonServices(builder.Services);

Para obter um exemplo dessa abordagem, consulte cenários de segurança adicionais Blazor WebAssembly do ASP.NET Core.

Serviços do lado do cliente que falham durante a pré-renderização

Esta seção se aplica somente a componentes do WebAssembly em Blazor Aplicativos Web.

Blazor Os Aplicativos Web normalmente pré-renderizam componentes WebAssembly do lado do cliente. Se um aplicativo for executado com um serviço necessário registrado apenas no projeto .Client, a execução do aplicativo resultará em um erro de runtime semelhante ao seguinte quando um componente tentar usar o serviço necessário durante a pré-renderização:

InvalidOperationException: não é possível fornecer um valor para {PROPERTY} no tipo '{ASSEMBLY}}.Client.Pages.{COMPONENT NAME}'. Não há nenhum serviço registrado do tipo '{SERVICE}'.

Use ou das seguintes abordagens para resolver esse problema:

  • Registre o serviço no projeto principal para torná-lo disponível durante a pré-renderização do componente.
  • Se a pré-renderização não for necessária para o componente, desabilite-o seguindo as diretrizes em ASP.NET Core Blazor modos de renderização. Se você adotar essa abordagem, não precisará registrar o serviço no projeto principal.

Para obter mais informações, confira Serviços do lado do cliente não resolvidos durante a pré-renderização.

Tempo de vida do serviço

Os serviços podem ser configurados com os tempos de vida mostrados na tabela a seguir.

Tempo de vida Descrição
Scoped

No momento, o lado do cliente não tem um conceito de escopos de DI. Os serviços registrados Scoped se comportam como serviços Singleton.

O desenvolvimento do lado do servidor dá suporte ao tempo de vida Scoped entre solicitações HTTP, mas não entre mensagens de conexão/circuito do SignalR entre os componentes carregados no cliente. A parte Razor das Páginas ou do MVC do aplicativo trata os serviços com escopo normalmente e recria os serviços em cada solicitação HTTP ao navegar entre páginas ou exibições ou de uma página ou exibição para um componente. Os serviços com escopo não são reconstruídos ao navegar entre componentes no cliente, onde a comunicação com o servidor ocorre pela conexão SignalR do circuito do usuário, não por meio de solicitações HTTP. Nos seguintes cenários de componente no cliente, os serviços com escopo são reconstruídos porque um novo circuito é criado para o usuário:

  • O usuário fecha a janela do navegador. O usuário abre uma nova janela e navega de volta ao aplicativo.
  • O usuário fecha uma guia do aplicativo em uma janela do navegador. O usuário abre uma nova janela e navega de volta ao aplicativo.
  • O usuário seleciona o botão recarregar/atualizar do navegador.

Para obter mais informações sobre como preservar o estado do usuário em aplicativos do lado do servidor, consulte gerenciamento de estado do ASP.NET Core Blazor.

Singleton A DI cria uma única instância do serviço. Todos os componentes que exigem um serviço Singleton recebem a mesma instância do serviço.
Transient Sempre que um componente obtém uma instância de um serviço Transient do contêiner de serviço, ele recebe uma nova instância do serviço.

O sistema de DI é baseado no sistema DI no ASP.NET Core. Para obter mais informações, consulte Injeção de dependência no ASP.NET Core.

Solicite um serviço em um componente

Para serviços de injeção em componentes, o Blazor dá suporte à injeção de construtor e à injeção de propriedade.

Injeção de construção

Depois que os serviços forem adicionados à coleção de serviços, injete um ou mais serviços em componentes com a injeção do construtor. O exemplo a seguir injeta o serviço NavigationManager.

ConstructorInjection.razor:

@page "/constructor-injection"

<button @onclick="@(() => Navigation.NavigateTo("/counter"))">
    Take me to the Counter component
</button>

ConstructorInjection.razor.cs:

using Microsoft.AspNetCore.Components;

public partial class ConstructorInjection(NavigationManager navigation)
{
    protected NavigationManager Navigation { get; } = navigation;
}

Injeção de propriedade

Após a adição dos serviços à coleção de serviços, injete um ou mais serviços nos componentes usando a diretiva @injectRazor, que tem dois parâmetros:

  • Tipo: o tipo do serviço a ser injetado.
  • Propriedade: o nome da propriedade que recebe o serviço de aplicativo injetado. A propriedade não requer criação manual. O compilador cria a propriedade .

Para obter mais informações, consulte Injeção de dependência em exibições no ASP.NET Core.

Use várias instruções @inject para injetar serviços diferentes.

O exemplo a seguir demonstra como usar a diretiva @inject. A implementação Services.NavigationManager do serviço é injetada na propriedade Navigationdo componente. Observe como o código está usando apenas a abstração NavigationManager.

PropertyInjection.razor:

@page "/property-injection"
@inject NavigationManager Navigation

<button @onclick="@(() => Navigation.NavigateTo("/counter"))">
    Take me to the Counter component
</button>

Internamente, a propriedade gerada (Navigation) usa o atributo [Inject]. Normalmente, esse atributo não é usado diretamente. Se uma classe base for necessária para componentes e propriedades injetadas também forem necessárias para a classe base, adicione manualmente o [Inject] atributo:

using Microsoft.AspNetCore.Components;

public class ComponentBase : IComponent
{
    [Inject]
    protected NavigationManager Navigation { get; set; } = default!;

    ...
}

Observação

Como espera-se que os serviços injetados estejam disponíveis, o literal padrão com o operador de perdão nulo (default!) é atribuído no .NET 6 ou posterior. Para obter mais informações, consulte NRTs (tipos de referência anuláveis) e análise estática de estado nulo do compilador do .NET.

Em componentes derivados de uma classe base, a diretiva @inject não é necessária. O InjectAttribute da classe base é suficiente. O componente requer apenas a diretiva @inherits. No seguinte exemplo, todos os serviços injetados de CustomComponentBase estão disponíveis para o componente Demo:

@page "/demo"
@inherits CustomComponentBase

Use DI em serviços

Serviços complexos podem exigir serviços adicionais. No exemplo a seguir, DataAccess requer o serviço padrão HttpClient. @inject (ou o [Inject] atributo) não está disponível para uso em serviços. Em vez disso, a injeção de construtor deve ser usada. Os serviços necessários são acrescentados por meio da adição de parâmetros ao construtor do serviço. Quando cria o serviço, a DI reconhece os serviços necessários no construtor e os fornece adequadamente. No exemplo a seguir, o construtor recebe um HttpClient por meio de DI. HttpClient é um serviço padrão.

using System.Net.Http;

public class DataAccess : IDataAccess
{
    public DataAccess(HttpClient http)
    {
        ...
    }
}

Pré-requisitos para injeção de construtor:

  • Deve existir um único construtor cujos argumentos podem ser atendidos pela DI. Parâmetros adicionais não cobertos por DI serão permitidos se especificarem valores padrão.
  • O construtor aplicável deve ser public.
  • Deve existir um único construtor aplicável. Em caso de ambiguidade, a DI gera uma exceção.

Injetar serviços com chave em componentes

Blazor dá suporte à injeção de serviços chave usando o atributo [Inject]. As chaves permitem o escopo do registro e do consumo de serviços ao usar a injeção de dependência. Use a propriedade InjectAttribute.Key para especificar a chave do serviço a ser injetado:

[Inject(Key = "my-service")]
public IMyService MyService { get; set; }

Classes de componentes base do utilitário para gerenciar um escopo de DI

Em aplicativos não Blazor ASP.NET Core, os serviços com escopo e transitórios normalmente têm como escopo a solicitação atual. Após a conclusão da solicitação, os serviços com escopo e transitórios são descartados pelo sistema de DI.

Em aplicativos interativos do lado do servidor Blazor, o escopo de DI dura durante a duração do circuito (a SignalR conexão entre o cliente e o servidor), o que pode resultar em serviços transitórios e descartáveis com escopo que duram muito mais do que o tempo de vida de um único componente. Portanto, não injete diretamente um serviço com escopo em um componente se você pretende que o tempo de vida do serviço corresponda ao tempo de vida do componente. Os serviços transitórios injetados em um componente que não implementam IDisposable são coletados do lixo quando o componente é descartado. No entanto, os serviços transitórios injetados que implementam IDisposable são mantidos pelo contêiner de DI durante o tempo de vida do circuito, o que impede a coleta de lixo do serviço quando o componente é descartado e resulta em uma perda de memória. Uma abordagem alternativa para serviços com escopo baseados no tipo OwningComponentBase é descrita mais adiante nesta seção, e os serviços transientes descartáveis não devem ser usados de forma alguma. Para obter mais informações, confira Projeto para resolver descartáveis transitórios em Blazor Server (dotnet/aspnetcore #26676).

Mesmo no lado do cliente, os aplicativos Blazor que não operam em um circuito, os serviços registrados com um tempo de vida com escopo são tratados como singletons, para que eles vivam mais do que os serviços com escopo em aplicativos típicos do ASP.NET Core. Os serviços transitórios descartáveis do lado do cliente também duram mais do que os componentes em que são injetados porque o contêiner de DI, que contém referências a serviços descartáveis, persiste durante o tempo de vida do aplicativo, impedindo a coleta de lixo nos serviços. Embora os serviços transitórios descartáveis de longa duração sejam de maior preocupação no servidor, eles também devem ser evitados como registros de serviço do cliente. O uso do tipo OwningComponentBase também é recomendado para serviços com escopo do lado do cliente para controlar o tempo de vida do serviço, e os serviços transitórios descartáveis não devem ser usados de forma alguma.

Uma abordagem que limita o tempo de vida de um serviço é o uso do tipo OwningComponentBase. OwningComponentBase é um tipo abstrato derivado de ComponentBase que cria um escopo da DI correspondente ao tempo de vida do componente. Usando esse escopo, um componente pode injetar serviços com um tempo de vida com escopo e fazer com que eles durem tanto quanto o componente. Quando o componente é destruído, os serviços do provedor de serviços com escopo do componente também são descartados. Isso pode ser útil para serviços reutilizados em um componente, mas não compartilhados entre componentes.

Duas versões do tipo OwningComponentBase estão disponíveis e descritas nas duas seções a seguir:

OwningComponentBase

OwningComponentBase é um filho abstrato e descartável do tipo ComponentBase, com uma propriedade protegida ScopedServices do tipo IServiceProvider. O provedor pode ser usado para resolver serviços com escopo para o tempo de vida do componente.

Os serviços de DI injetados no componente usando o @injectatributo[Inject] ou não são criados no escopo do componente. Para usar o escopo do componente, os serviços devem ser resolvidos usando ScopedServices com GetRequiredService ou GetService. Todos os serviços resolvidos usando o provedor ScopedServices têm suas dependências fornecidas no escopo do componente.

O exemplo a seguir demonstra a diferença entre injetar um serviço com escopo diretamente e resolver um serviço usando ScopedServices no servidor. A interface e a implementação a seguir para uma classe de viagem no tempo incluem uma propriedade DTpara manter um valor DateTime. A implementação chama DateTime.Now para definir DT quando a classe da TimeTravel é instanciada.

ITimeTravel.cs:

public interface ITimeTravel
{
    public DateTime DT { get; set; }
}

TimeTravel.cs:

public class TimeTravel : ITimeTravel
{
    public DateTime DT { get; set; } = DateTime.Now;
}

O serviço é registrado como um serviço com escopo no arquivo Program do lado do servidor. Os serviços com escopo do lado do servidor têm um tempo de vida igual à duração do circuito.

No arquivo Program:

builder.Services.AddScoped<ITimeTravel, TimeTravel>();

No seguinte componente TimeTravel:

  • O serviço de viagem no tempo é injetado diretamente com @inject como TimeTravel1.
  • O serviço também é resolvido separadamente com ScopedServices e GetRequiredService como TimeTravel2.

TimeTravel.razor:

@page "/time-travel"
@inject ITimeTravel TimeTravel1
@inherits OwningComponentBase

<h1><code>OwningComponentBase</code> Example</h1>

<ul>
    <li>TimeTravel1.DT: @TimeTravel1?.DT</li>
    <li>TimeTravel2.DT: @TimeTravel2?.DT</li>
</ul>

@code {
    private ITimeTravel TimeTravel2 { get; set; } = default!;

    protected override void OnInitialized()
    {
        TimeTravel2 = ScopedServices.GetRequiredService<ITimeTravel>();
    }
}
@page "/time-travel"
@inject ITimeTravel TimeTravel1
@inherits OwningComponentBase

<h1><code>OwningComponentBase</code> Example</h1>

<ul>
    <li>TimeTravel1.DT: @TimeTravel1?.DT</li>
    <li>TimeTravel2.DT: @TimeTravel2?.DT</li>
</ul>

@code {
    private ITimeTravel TimeTravel2 { get; set; } = default!;

    protected override void OnInitialized()
    {
        TimeTravel2 = ScopedServices.GetRequiredService<ITimeTravel>();
    }
}

Inicialmente navegando até o componente TimeTravel, o serviço de viagem no tempo é instanciado duas vezes quando o componente é carregado, TimeTravel1 e TimeTravel2 têm o mesmo valor inicial:

TimeTravel1.DT: 8/31/2022 2:54:45 PM
TimeTravel2.DT: 8/31/2022 2:54:45 PM

Ao navegar do componente TimeTravela outro componente e voltar ao componente TimeTravel:

  • TimeTravel1 é fornecida a mesma instância de serviço que foi criada quando o componente foi carregado pela primeira vez, de modo que o valor de DT permanece o mesmo.
  • TimeTravel2 obtém uma nova instância de serviço ITimeTravel em TimeTravel2 com um novo valor de DT.

TimeTravel1.DT: 8/31/2022 2:54:45 PM
TimeTravel2.DT: 8/31/2022 2:54:48 PM

TimeTravel1 está vinculado ao circuito do usuário, que permanece intacto e não é descartado até que o circuito subjacente seja desconstruído. Por exemplo, o serviço será descartado se o circuito for desconectado durante o período de retenção de circuito desconectado.

Apesar do registro de serviço com escopo no arquivo Program e da longevidade do circuito do usuário, TimeTravel2 recebe uma nova instância de serviço ITimeTravel sempre que o componente é inicializado.

OwningComponentBase<TService>

OwningComponentBase<TService> deriva de OwningComponentBase e adiciona uma propriedade Serviceque retorna uma instância de T do provedor de DI com escopo. Esse tipo é uma maneira conveniente de acessar serviços com escopo sem usar uma instância de IServiceProvider quando há um serviço primário que o aplicativo requer do contêiner de DI usando o escopo do componente. Como a propriedade ScopedServices está disponível, o aplicativo pode obter serviços de outros tipos, se necessário.

@page "/users"
@attribute [Authorize]
@inherits OwningComponentBase<AppDbContext>

<h1>Users (@Service.Users.Count())</h1>

<ul>
    @foreach (var user in Service.Users)
    {
        <li>@user.UserName</li>
    }
</ul>

Detectar descartáveis transitórios do lado do cliente

O código personalizado pode ser adicionado a um aplicativo Blazor do lado do cliente para detectar serviços transitórios descartáveis em um aplicativo que deve usar OwningComponentBase. Essa abordagem é útil se você estiver preocupado que o código adicionado ao aplicativo no futuro consuma um ou mais serviços descartáveis transitórios, incluindo serviços adicionados por bibliotecas. O código de demonstração está disponível no Blazor repositório GitHub de amostras (como baixar).

Inspecione o seguinte no .NET 6 ou versões posteriores do exemplo BlazorSample_WebAssembly:

  • DetectIncorrectUsagesOfTransientDisposables.cs
  • Services/TransientDisposableService.cs
  • Em: Program.cs
    • O namespace do aplicativo Services é fornecido na parte superior do arquivo (using BlazorSample.Services;).
    • DetectIncorrectUsageOfTransients é chamado imediatamente após builder ser atribuído de WebAssemblyHostBuilder.CreateDefault.
    • O TransientDisposableService está registrado (builder.Services.AddTransient<TransientDisposableService>();).
    • EnableTransientDisposableDetection é chamado no host interno no pipeline de processamento do aplicativo (host.EnableTransientDisposableDetection();).
  • O aplicativo registra o serviço TransientDisposableService sem abrir uma exceção. No entanto, a tentativa de resolver o serviço em TransientService.razor lança um InvalidOperationException quando a estrutura tenta construir uma instância do TransientDisposableService.

Detectar descartáveis transitórios do lado do servidor

O código personalizado pode ser adicionado a um aplicativo Blazor do lado do servidor para detectar serviços transitórios descartáveis do lado do servidor em um aplicativo que deve usar OwningComponentBase. Essa abordagem é útil se você estiver preocupado que o código adicionado ao aplicativo no futuro consuma um ou mais serviços descartáveis transitórios, incluindo serviços adicionados por bibliotecas. O código de demonstração está disponível no Blazor repositório GitHub de amostras (como baixar).

Inspecione o seguinte no .NET 8 ou versões posteriores do exemplo BlazorSample_BlazorWebApp:

Inspecione o seguinte nas versões .NET 6 ou 7 do exemplo BlazorSample_Server:

  • DetectIncorrectUsagesOfTransientDisposables.cs
  • Services/TransitiveTransientDisposableDependency.cs:
  • Em: Program.cs
    • O namespace do aplicativo Services é fornecido na parte superior do arquivo (using BlazorSample.Services;).
    • DetectIncorrectUsageOfTransients é chamado no construtor de host (builder.DetectIncorrectUsageOfTransients();).
    • O serviço TransientDependency é registrado (builder.Services.AddTransient<TransientDependency>();).
    • O TransitiveTransientDisposableDependency é registrado para ITransitiveTransientDisposableDependency (builder.Services.AddTransient<ITransitiveTransientDisposableDependency, TransitiveTransientDisposableDependency>();).
  • O aplicativo registra o serviço TransientDependency sem abrir uma exceção. No entanto, a tentativa de resolver o serviço em TransientService.razor lança um InvalidOperationException quando a estrutura tenta construir uma instância do TransientDependency.

Registros de serviços transitórios para manipuladores IHttpClientFactory/HttpClient

Os registros de serviço transitórios para manipuladores IHttpClientFactory/HttpClient são recomendados. Se o aplicativo contiver manipuladores IHttpClientFactory/HttpClient e usar o IRemoteAuthenticationBuilder<TRemoteAuthenticationState,TAccount> para adicionar suporte à autenticação, os seguintes descartáveis transitórios para autenticação no lado do cliente também serão descobertos, o que é esperado e pode ser ignorado:

Outras instâncias de IHttpClientFactory/HttpClient também são descobertas. Essas instâncias também podem ser ignoradas.

Os aplicativos de amostra Blazor no Blazor repositório GitHub de amostras (como baixar) demonstram o código para detectar descartáveis transitórios. No entanto, o código está desativado porque os aplicativos de amostra incluem os manipuladores IHttpClientFactory/HttpClient.

Para ativar o código de demonstração e testemunhar sua operação:

  • Remova a marca de comentário das linhas descartáveis transitórias em Program.cs.

  • Remova a verificação condicional em NavLink.razor que impede que o componente TransientService seja exibido na barra lateral de navegação do aplicativo:

    - else if (name != "TransientService")
    + else
    
  • Execute o aplicativo de amostra e navegue até o componente TransientService em /transient-service.

Uso de um DbContext do Entity Framework Core (EF Core) do DI

Para obter mais informações, consulte ASP.NET Core Blazor com o Entity Framework Core (EF Core).

Acessar serviços Blazor do lado do servidor de um escopo de DI diferente

Os manipuladores de atividade de circuito fornecem uma abordagem para acessar serviços Blazor com escopo de outros escopos de DI (injeção de dependência de injeção) de não Blazor, como escopos criados usando IHttpClientFactory.

Antes do lançamento do ASP.NET Core no .NET 8, acessar serviços de escopo de circuito de outras escopos de injeção de dependência exigia o uso de um tipo de componente base personalizado. Com manipuladores de atividade de circuito, um tipo de componente base personalizado não é necessário, como demonstra o exemplo a seguir:

public class CircuitServicesAccessor
{
    static readonly AsyncLocal<IServiceProvider> blazorServices = new();

    public IServiceProvider? Services
    {
        get => blazorServices.Value;
        set => blazorServices.Value = value;
    }
}

public class ServicesAccessorCircuitHandler : CircuitHandler
{
    readonly IServiceProvider services;
    readonly CircuitServicesAccessor circuitServicesAccessor;

    public ServicesAccessorCircuitHandler(IServiceProvider services, 
        CircuitServicesAccessor servicesAccessor)
    {
        this.services = services;
        this.circuitServicesAccessor = servicesAccessor;
    }

    public override Func<CircuitInboundActivityContext, Task> CreateInboundActivityHandler(
        Func<CircuitInboundActivityContext, Task> next)
    {
        return async context =>
        {
            circuitServicesAccessor.Services = services;
            await next(context);
            circuitServicesAccessor.Services = null;
        };
    }
}

public static class CircuitServicesServiceCollectionExtensions
{
    public static IServiceCollection AddCircuitServicesAccessor(
        this IServiceCollection services)
    {
        services.AddScoped<CircuitServicesAccessor>();
        services.AddScoped<CircuitHandler, ServicesAccessorCircuitHandler>();

        return services;
    }
}

Acesse os serviços com escopo de circuito injetando o CircuitServicesAccessor onde for necessário.

Para ver um exemplo que mostra como acessar o AuthenticationStateProvider em um DelegatingHandler configurado por meio de IHttpClientFactory, confira Outros cenários de segurança do ASP.NET Core Blazor do servidor.

Pode haver momentos em que um componente Razor invoca métodos assíncronos que executam código em um escopo de DI diferente. Sem a abordagem correta, esses escopos de DI não têm acesso aos serviços de Blazor, como IJSRuntime e Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage.

Por exemplo, instâncias HttpClient criadas usando IHttpClientFactory têm seu próprio escopo de serviço DI. Como resultado, as instâncias HttpMessageHandler configuradas no HttpClient não são capazes de injetar serviços Blazor diretamente.

Crie uma classe BlazorServiceAccessor que define um AsyncLocal, que armazena o BlazorIServiceProvider para o contexto assíncrono atual. Uma instância BlazorServiceAcccessor pode ser adquirida de dentro de um escopo diferente de serviço DI para acessar serviços Blazor.

BlazorServiceAccessor.cs:

internal sealed class BlazorServiceAccessor
{
    private static readonly AsyncLocal<BlazorServiceHolder> s_currentServiceHolder = new();

    public IServiceProvider? Services
    {
        get => s_currentServiceHolder.Value?.Services;
        set
        {
            if (s_currentServiceHolder.Value is { } holder)
            {
                // Clear the current IServiceProvider trapped in the AsyncLocal.
                holder.Services = null;
            }

            if (value is not null)
            {
                // Use object indirection to hold the IServiceProvider in an AsyncLocal
                // so it can be cleared in all ExecutionContexts when it's cleared.
                s_currentServiceHolder.Value = new() { Services = value };
            }
        }
    }

    private sealed class BlazorServiceHolder
    {
        public IServiceProvider? Services { get; set; }
    }
}

Para definir o valor de BlazorServiceAccessor.Services automaticamente quando um método de componente async é invocado, crie um componente base personalizado que implementa novamente os três pontos de entrada assíncronos primários no código do componente Razor:

A classe a seguir demonstra a implementação do componente base.

CustomComponentBase.cs:

using Microsoft.AspNetCore.Components;

public class CustomComponentBase : ComponentBase, IHandleEvent, IHandleAfterRender
{
    private bool hasCalledOnAfterRender;

    [Inject]
    private IServiceProvider Services { get; set; } = default!;

    [Inject]
    private BlazorServiceAccessor BlazorServiceAccessor { get; set; } = default!;

    public override Task SetParametersAsync(ParameterView parameters)
        => InvokeWithBlazorServiceContext(() => base.SetParametersAsync(parameters));

    Task IHandleEvent.HandleEventAsync(EventCallbackWorkItem callback, object? arg)
        => InvokeWithBlazorServiceContext(() =>
        {
            var task = callback.InvokeAsync(arg);
            var shouldAwaitTask = task.Status != TaskStatus.RanToCompletion &&
                task.Status != TaskStatus.Canceled;

            StateHasChanged();

            return shouldAwaitTask ?
                CallStateHasChangedOnAsyncCompletion(task) :
                Task.CompletedTask;
        });

    Task IHandleAfterRender.OnAfterRenderAsync()
        => InvokeWithBlazorServiceContext(() =>
        {
            var firstRender = !hasCalledOnAfterRender;
            hasCalledOnAfterRender |= true;

            OnAfterRender(firstRender);

            return OnAfterRenderAsync(firstRender);
        });

    private async Task CallStateHasChangedOnAsyncCompletion(Task task)
    {
        try
        {
            await task;
        }
        catch
        {
            if (task.IsCanceled)
            {
                return;
            }

            throw;
        }

        StateHasChanged();
    }

    private async Task InvokeWithBlazorServiceContext(Func<Task> func)
    {
        try
        {
            BlazorServiceAccessor.Services = Services;
            await func();
        }
        finally
        {
            BlazorServiceAccessor.Services = null;
        }
    }
}

Todos os componentes que estendem CustomComponentBase automaticamente definiram BlazorServiceAccessor.Services para o IServiceProvider no escopo DI Blazor atual.

Por fim, no arquivo Program, adicione o BlazorServiceAccessor como um serviço com escopo:

builder.Services.AddScoped<BlazorServiceAccessor>();

Por fim, em Startup.ConfigureServices de Startup.cs, adicione o BlazorServiceAccessor como um serviço com escopo:

services.AddScoped<BlazorServiceAccessor>();

Recursos adicionais