Inserción de dependencias de Blazor de ASP.NET Core

Nota

Esta no es la versión más reciente de este artículo. Para la versión actual, consulta la versión .NET 8 de este artículo.

Advertencia

Esta versión de ASP.NET Core ya no se admite. Para obtener más información, consulta la Directiva de soporte técnico de .NET y .NET Core. Para la versión actual, consulta la versión .NET 8 de este artículo.

Importante

Esta información hace referencia a un producto en versión preliminar, el cual puede sufrir importantes modificaciones antes de que se publique la versión comercial. Microsoft no proporciona ninguna garantía, expresa o implícita, con respecto a la información proporcionada aquí.

Para la versión actual, consulte la versión .NET 8 de este artículo.

Por Rainer Stropek y Mike Rousos

En este artículo se explica cómo las aplicaciones Blazor pueden insertar servicios en componentes.

Inserción de dependencias (DI) es una técnica para acceder a los servicios configurados en una ubicación central:

  • Los servicios registrados en el marco pueden inyectarse directamente en los componentes Razor.
  • Las aplicaciones de Blazor definen y registran servicios personalizados y los ponen a disposición de toda la aplicación a través de la inserción de dependencias.

Nota:

Se recomienda leer Inserción de dependencias en ASP.NET Core antes de leer este tema.

Servicios predeterminados

Los servicios que se muestran en la tabla siguiente se usan normalmente en aplicaciones de Blazor.

web de Office Período de duración Descripción
HttpClient Con ámbito

Proporciona métodos para enviar solicitudes HTTP y recibir respuestas HTTP de un recurso identificado por un URI.

En el lado del cliente, la aplicación registra una instancia de HttpClient en el archivo Program y utiliza el navegador para administrar el tráfico HTTP en segundo plano.

Del lado del servidor, un HttpClient no está configurado como servicio por defecto. En el código del lado servidor, proporcione un HttpClient.

Para obtener más información, vea Llamada a una API web desde una aplicación Blazor de ASP.NET Core.

Un HttpClient se registra como un servicio con ámbito, no como singleton. Para más información, consulte la sección Duración del servicio.

IJSRuntime

Lado de cliente: Singleton

Lado de servidor: Ámbito

El marco Blazor registra IJSRuntime en el contenedor de servicios de la aplicación.

Representa una instancia de un entorno de ejecución de JavaScript en la que se envían las llamadas de JavaScript. Para más información, vea Llamada a funciones de JavaScript desde métodos de .NET en Blazor de ASP.NET Core.

Si desea inyectar el servicio en un servicio singleton del servidor, adopte uno de los siguientes enfoques:

  • Cambie el registro del servicio a con ámbito para que coincida con el registro de IJSRuntime, que es adecuado si el servicio se ocupa del estado específico del usuario.
  • Pase IJSRuntime a la implementación del servicio singleton como argumento de sus llamadas de método en lugar de insertarlo en el singleton.
NavigationManager

Lado de cliente: Singleton

Lado de servidor: Ámbito

El marco Blazor registra NavigationManager en el contenedor de servicios de la aplicación.

Contiene asistentes para trabajar con URI y el estado de navegación. Para obtener más información, vea Asistentes de URI y estado de navegación.

Los servicios adicionales registrados por el marco Blazor se describen en la documentación donde se usan para describir características de Blazor, como la configuración y el registro.

Un proveedor de servicios personalizado no proporciona automáticamente los servicios predeterminados que aparecen en la tabla. Si usa un proveedor de servicios personalizado y necesita cualquiera de los servicios que se muestran en la tabla, agregue los servicios necesarios al nuevo proveedor de servicios.

Agregar servicios del lado del cliente

Configure los servicios para la colección de servicios de la aplicación en el archivo Program. En el ejemplo siguiente, la implementación de ExampleDependency se registra para IExampleDependency:

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

await builder.Build().RunAsync();

Después de compilar el host, los servicios están disponibles en el ámbito de inserción de dependencias raíz antes de que se representen los componentes. Esto puede ser útil para ejecutar la lógica de inicialización antes de representar el contenido:

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();

El host también proporciona una instancia de configuración central para la aplicación. En función del ejemplo anterior, la dirección URL del servicio meteorológico se pasa de un origen de configuración predeterminado (por ejemplo, appsettings.json) a 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();

Agregar servicios del lado del servidor

Después de crear una aplicación, examine parte del archivo 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>();

La variable builder representa un WebApplicationBuilder con un IServiceCollection, que es una lista de objetos de descriptor de servicio. Para agregar los servicios, se proporcionan descriptores de servicio a la colección de servicios. En el ejemplo siguiente se muestra el concepto con la interfaz IDataAccess y su implementación concreta de DataAccess:

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

Después de crear una aplicación, examine el método Startup.ConfigureServices en Startup.cs:

using Microsoft.Extensions.DependencyInjection;

...

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

Al método ConfigureServices se le pasa una interfaz IServiceCollection, que es una lista de objetos de descriptor de servicio. Para agregar los servicios al método ConfigureServices, se proporcionan descriptores de servicio a la colección de servicios. En el ejemplo siguiente se muestra el concepto con la interfaz IDataAccess y su implementación concreta de DataAccess:

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

Registrar servicios comunes

Si se requieren uno o más servicios comunes del lado del cliente y del lado del servidor, puede colocar los registros de servicios comunes en un método del lado del cliente y llamar al método para registrar los servicios en ambos proyectos.

En primer lugar, factorice los registros de servicio comunes en un método independiente. Por ejemplo, cree un método del lado del cliente ConfigureCommonServices:

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

Para el archivo Program del lado del cliente, llame a ConfigureCommonServices para registrar los servicios comunes:

var builder = WebAssemblyHostBuilder.CreateDefault(args);

...

ConfigureCommonServices(builder.Services);

En el archivo Program del lado del servidor, llame a ConfigureCommonServices para registrar los servicios comunes:

var builder = WebApplication.CreateBuilder(args);

...

Client.Program.ConfigureCommonServices(builder.Services);

Para ver un ejemplo de este enfoque, consulte los escenarios de seguridad adicionales de ASP.NET Core Blazor WebAssembly.

Los servicios del lado cliente no se resuelven durante la representación previa

Esta sección solo se aplica a componentes WebAssembly en Blazor Web App.

Blazor Web App normalmente representa previamente los componentes WebAssembly del lado cliente. Si una aplicación se ejecuta con un servicio obligatorio registrado solo en el proyecto .Client, la ejecución de la aplicación genera un error en tiempo de ejecución similar al siguiente cuando un componente intenta usar el servicio obligatorio durante la representación previa:

InvalidOperationException: no se puede proporcionar un valor para {PROPIEDAD} en el tipo "{ENSAMBLADO}}. Client.Pages. {NOMBRE DEL COMPONENTE}". No hay ningún servicio registrado de tipo "{SERVICIO}".

Para resolver este problema, use uno de los enfoques siguientes:

  • Registre el servicio en el proyecto principal para que esté disponible durante la representación previa del componente.
  • Si la representación previa no es necesaria para el componente, deshabilítela con las instrucciones de Modos de representación de ASP.NET Core Blazor. Si adopta este enfoque, no es necesario registrar el servicio en el proyecto principal.

Para más información, vea Los servicios del lado cliente no se resuelven durante la representación previa.

Duración del servicio

Los servicios se pueden configurar con las duraciones que se muestran en la tabla siguiente.

Período de duración Descripción
Scoped

El lado del cliente no tiene actualmente un concepto de ámbitos DI. Los servicios registrados con Scoped se comportan como servicios Singleton.

El desarrollo del lado del servidor admite el tiempo de vida Scoped a través de peticiones HTTP pero no a través SignalR de mensajes de conexión/circuito entre componentes que se cargan en el cliente. Por lo general, Razor Razor o la parte de MVC de la aplicación trata los servicios con ámbito y vuelve a crear los servicios en cada solicitud HTTP al navegar entre páginas o vistas, o desde una página o vista a un componente. Los servicios con ámbito no se reconstruyen al navegar entre los componentes del cliente, donde la comunicación con el servidor se realiza a través de la conexión SignalR del circuito del usuario, no a través de solicitudes HTTP. En los escenarios de componentes del cliente siguientes, los servicios con ámbito se reconstruyen porque se crea un circuito nuevo para el usuario:

  • El usuario cierra la ventana del explorador. El usuario abre una ventana nueva y vuelve a la aplicación.
  • El usuario cierra una pestaña de la aplicación en una ventana del explorador. El usuario abre una pestaña nueva y vuelve a la aplicación.
  • El usuario selecciona el botón de recargar o actualizar del explorador.

Para obtener más información sobre la conservación del estado del usuario en aplicaciones del lado del servidor, consulte Administración del estado de BlazorASP.NET Core.

Singleton La inserción de dependencias crea una sola instancia del servicio. Todos los componentes que requieren un servicio Singleton reciben la misma instancia del servicio.
Transient Cada vez que un componente obtiene una instancia de un servicio Transient del contenedor de servicios, recibe una nueva instancia del servicio.

El sistema de inserción de dependencias se basa en el sistema de inserción de dependencias de ASP.NET Core. Para más información, consulte Inserción de dependencias en ASP.NET Core.

Solicitud de un servicio en un componente

Para insertar servicios en componentes, Blazor admite la inserción de constructores y la inserción de propiedades.

Inserción de constructores

Una vez agregados los servicios a la colección de servicios, inserte uno o varios servicios en componentes con la inyección de constructor. En el ejemplo siguiente se inserta el servicio NavigationManager.

ConstructorInjection.razor:

@page "/constructor-injection"

<button @onclick="HandleClick">
    Take me to the Counter component
</button>

ConstructorInjection.razor.cs:

using Microsoft.AspNetCore.Components;

public partial class ConstructorInjection(NavigationManager navigation)
{
    private void HandleClick()
    {
        navigation.NavigateTo("/counter");
    }
}

Inserción de propiedades

Una vez agregados los servicios a la colección de servicios, inserte uno o varios servicios en componentes con la directiva @injectRazor, que tiene dos parámetros:

  • Tipo: el tipo de servicio que se va a insertar.
  • Propiedad: el nombre de la propiedad que recibe el servicio de aplicación insertado. La propiedad no requiere la creación manual. El compilador crea la propiedad.

Para más información, consulte Inserción de dependencias en vistas de ASP.NET Core.

Use varias instrucciones @inject para insertar distintos servicios.

En el ejemplo siguiente se muestra cómo usar la directiva @inject. El servicio que implementa Services.NavigationManager se inserta en la propiedad Navigation del componente. Observe cómo el código solo usa la abstracción de NavigationManager.

PropertyInjection.razor:

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

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

De forma interna, la propiedad generada (Navigation) usa el atributo [Inject]. Normalmente, este atributo no se usa de manera directa. Si se necesita una clase base para los componentes y las propiedades insertadas también son necesarias para la clase base, agregue manualmente el atributo [Inject]:

using Microsoft.AspNetCore.Components;

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

    ...
}

Nota

Dado que se espera que los servicios inyectados estén disponibles, en .NET 6 o posterior se asigna el literal por defecto con el operador de perdón de nulos (default!). Para obtener más información, consulte Tipos de referencia anulables (NRT) y análisis estático de estado nulo del compilador .NET.

En los componentes derivados de una clase base, la directiva @inject no es necesaria. Es suficiente con el objeto InjectAttribute de la clase base. El componente solo requiere la directiva @inherits. En el ejemplo siguiente, los servicios insertados de CustomComponentBase están disponibles para el componente Demo:

@page "/demo"
@inherits CustomComponentBase

Uso de la inserción de dependencias en servicios

Es posible que los servicios complejos requieran servicios adicionales. En el ejemplo siguiente, DataAccess requiere el servicio predeterminado HttpClient. @inject (o el atributo [Inject]) no está disponible para su uso en los servicios. En su lugar se debe usar la inserción de constructores. Los servicios necesarios se agregan mediante la adición de parámetros al constructor del servicio. Cuando la inserción de dependencias crea el servicio, reconoce los servicios que requiere en el constructor y los proporciona en consecuencia. En el ejemplo siguiente, el constructor recibe HttpClient a través de DI. HttpClient es un servicio predeterminado.

using System.Net.Http;

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

    ...
}

La inserción de constructores se admite con constructores principales en C# 12 (.NET 8) o posterior:

using System.Net.Http;

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

Requisitos previos para la inserción de constructores:

  • Debe existir un constructor cuyos argumentos se puedan cumplir mediante la inserción de dependencias. Se permiten parámetros adicionales que no estén incluidos en la inserción de dependencias si especifican valores predeterminados.
  • El constructor aplicable debe ser public.
  • Debe existir un constructor aplicable. En caso de ambigüedad, la inserción de dependencias inicia una excepción.

Inserción de servicios con clave en componentes

Blazor admite la inserción de servicios con clave mediante el atributo [Inject]. Las claves permiten determinar el ámbito del registro y el consumo de servicios al usar la inserción de dependencias. Use la propiedad InjectAttribute.Key para especificar la clave del servicio que se va a insertar:

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

Clases de componentes base de utilidad para administrar un ámbito de inserción de dependencias

En las aplicaciones ASP.NET Core de Blazor, los servicios con ámbito y transitorios suelen estar asociados a la solicitud actual. Una vez que se ha completado la solicitud, el sistema de inserción de dependencias elimina los servicios con ámbito y transitorios.

En las aplicaciones de Blazor interactivas del lado del servidor, el ámbito de DI dura el curso del circuito (la conexión de SignalR entre el cliente y el servidor), lo que puede dar lugar a que los servicios transitorios disponibles y con ámbito funcionen durante más tiempo de lo esperado durante la duración de un solo componente. Por lo tanto, no inserte directamente un servicio con ámbito en un componente si quiere que la duración del servicio coincida con la duración del componente. Los servicios transitorios insertados en un componente que no implementan IDisposable se recopilan como elementos no utilizados cuando se elimina el componente. Sin embargo, el contenedor de DI mantiene los servicios transitorios insertados que implementan IDisposable durante la vigencia del circuito, lo que impide la recolección de elementos no utilizados del servicio cuando el componente se elimina y da como resultado una fuga de memoria. Un enfoque alternativo para los servicios con ámbito basados en el tipo OwningComponentBase se describe más adelante en esta sección y los servicios transitorios descartables no se deben usar en absoluto. Para obtener más información, vea Diseño para resolver descartables transitorios en Blazor Server (dotnet/aspnetcore #26676).

Incluso en las aplicaciones Blazor del lado del cliente que no operan a través de un circuito, los servicios registrados con una duración con ámbito se tratan como singletons, por lo que duran más tiempo que los servicios con ámbito en aplicaciones típicas de ASP.NET Core. Los servicios transitorios descartables del lado cliente también residen más tiempo que los componentes en los que se insertan porque el contenedor de inserción de dependencias, que contiene referencias a servicios descartables, se conserva durante la vigencia de la aplicación, lo que impide la recolección de elementos no utilizados en los servicios. Aunque los servicios transitorios descartables de larga duración son más importantes en el servidor, deben evitarse como registros de servicio de cliente. El uso del tipo OwningComponentBase también se recomienda para los servicios de ámbito de cliente, a fin de controlar la duración del servicio y los servicios transitorios descartables no se deben usar en absoluto.

Un enfoque que limita la vida útil de un servicio es el uso del tipo OwningComponentBase. OwningComponentBase es un tipo abstracto derivado de ComponentBase, que crea un ámbito de inserción de dependencias que se corresponde a la duración del componente. Con este ámbito, un componente puede insertar servicios con una duración con ámbito y mantenerlos activos durante el mismo tiempo que el componente. Cuando el componente se destruye, también se eliminan los servicios del proveedor de servicios con ámbito del componente. Esto puede ser útil para los servicios reutilizados dentro de un componente, pero no se puede compartir entre componentes.

Hay dos versiones de tipo OwningComponentBase disponibles y se describen en las dos secciones siguientes:

OwningComponentBase

OwningComponentBase es un elemento secundario abstracto y descartable del tipo ComponentBase con una propiedad ScopedServices protegida de tipo IServiceProvider. El proveedor se puede usar para resolver los servicios cuyo ámbito es la duración del componente.

Los servicios de inserción de dependencias insertados en el componente mediante @inject o el atributo [Inject] no se crean en el ámbito del componente. Para usar el ámbito del componente, los servicios se deben resolver mediante ScopedServices con GetRequiredService o GetService. Los servicios que se resuelvan mediante el proveedor de ScopedServices reciben sus dependencias en el ámbito del componente.

El siguiente ejemplo demuestra la diferencia entre inyectar un servicio de ámbito directamente y resolver un servicio utilizando ScopedServices en el servidor. La siguiente interfaz e implementación de una clase de viaje en el tiempo incluyen una propiedad DT para contener un valor DateTime. La implementación llama a DateTime.Now para establecer DT cuando se crea una instancia de la clase TimeTravel.

ITimeTravel.cs:

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

TimeTravel.cs:

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

El servicio se registra como ámbito en el archivo Program del lado del servidor. Los servicios del lado del servidor tienen una vida útil igual a la duración del circuito.

En el archivo Program:

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

En el componente TimeTravel siguiente:

  • El servicio de viaje de tiempo se inserta directamente con @inject como TimeTravel1.
  • El servicio también se resuelve por separado con ScopedServices y 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>();
    }
}

Al navegar inicialmente al componente TimeTravel, se crea una instancia del servicio de viaje en el tiempo dos veces cuando se carga el componente y TimeTravel1 y TimeTravel2 tiene el mismo valor inicial:

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

Al alejarse del componente TimeTravel a otro componente y volver al componente TimeTravel:

  • TimeTravel1 se proporciona la misma instancia de servicio que se creó cuando el componente se cargó por primera vez, por lo que el valor de DT sigue siendo el mismo.
  • TimeTravel2 obtiene una nueva instancia de servicio ITimeTravel en TimeTravel2 con un nuevo valor DT.

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

TimeTravel1 está vinculado al circuito del usuario, que permanece intacto y no se elimina hasta que se desmantela el circuito subyacente. Por ejemplo, el servicio se elimina si el circuito está desconectado durante el período de retención del circuito desconectado.

A pesar del registro del servicio en el archivo Program y de la longevidad del circuito del usuario, TimeTravel2 recibe una nueva ITimeTravel instancia de servicio cada vez que se inicializa el componente.

OwningComponentBase<TService>

OwningComponentBase<TService> deriva de OwningComponentBase y agrega una propiedad Service que devuelve una instancia de T desde el proveedor de inserción de dependencias con ámbito. Este tipo es una manera cómoda de acceder a los servicios con ámbito sin usar una instancia de IServiceProvider cuando hay un servicio principal que la aplicación necesita del contenedor de inserción de dependencias que usa el ámbito del componente. La propiedad ScopedServices está disponible, por lo que la aplicación puede obtener servicios de otros tipos, si es necesario.

@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>

Detección de descartables transitorios del lado del cliente

El código personalizado se puede agregar a una aplicación Blazor del lado del cliente para detectar servicios transitorios descartables en una aplicación que debe usar OwningComponentBase. Este enfoque es útil si le preocupa que el código agregado a la aplicación en el futuro consuma uno o varios servicios descartables transitorios, incluidos los servicios agregados por las bibliotecas. El código de demostración está disponible en el Blazorrepositorio de GitHub de ejemplos de (cómo descargar).

Inspeccione lo siguiente en .NET 6 o versiones posteriores del ejemplo BlazorSample_WebAssembly:

  • DetectIncorrectUsagesOfTransientDisposables.cs
  • Services/TransientDisposableService.cs
  • En: Program.cs
    • El espacio de nombres Services de la aplicación se proporciona en la parte superior del archivo (using BlazorSample.Services;).
    • DetectIncorrectUsageOfTransients se llama inmediatamente después de asignar builder desde WebAssemblyHostBuilder.CreateDefault.
    • TransientDisposableService está registrado (builder.Services.AddTransient<TransientDisposableService>();).
    • EnableTransientDisposableDetection se llama a en el host integrado en la canalización de procesamiento de la aplicación (host.EnableTransientDisposableDetection();).
  • La aplicación registra el servicio TransientDisposableService sin iniciar una excepción. Sin embargo, al intentar resolver el servicio en TransientService.razor, se produce InvalidOperationException cuando el marco intenta construir una instancia de TransientDisposableService.

Detectar desechables transitorios del lado del servidor

El código personalizado se puede agregar a una aplicación Blazor del lado del servidor para detectar servicios transitorios descartables del lado del servidor en una aplicación que debe usar OwningComponentBase. Este enfoque es útil si le preocupa que el código agregado a la aplicación en el futuro consuma uno o varios servicios descartables transitorios, incluidos los servicios agregados por las bibliotecas. El código de demostración está disponible en el Blazorrepositorio de GitHub de ejemplos de (cómo descargar).

Inspeccione lo siguiente en .NET 8 o versiones posteriores del ejemplo BlazorSample_BlazorWebApp:

Inspeccione lo siguiente en las versiones de .NET 6 o .NET 7 del ejemplo BlazorSample_Server:

  • DetectIncorrectUsagesOfTransientDisposables.cs
  • Services/TransitiveTransientDisposableDependency.cs:
  • En: Program.cs
    • El espacio de nombres Services de la aplicación se proporciona en la parte superior del archivo (using BlazorSample.Services;).
    • Se llama a DetectIncorrectUsageOfTransients en el generador de hosts (builder.DetectIncorrectUsageOfTransients();).
    • El servicio TransientDependency está registrado (builder.Services.AddTransient<TransientDependency>();).
    • TransitiveTransientDisposableDependency está registrado para ITransitiveTransientDisposableDependency (builder.Services.AddTransient<ITransitiveTransientDisposableDependency, TransitiveTransientDisposableDependency>();).
  • La aplicación registra el servicio TransientDependency sin iniciar una excepción. Sin embargo, al intentar resolver el servicio en TransientService.razor, se produce InvalidOperationException cuando el marco intenta construir una instancia de TransientDependency.

Se recomiendan los registros de servicio transitorios para los controladores IHttpClientFactory/HttpClient

Se recomiendan los registros de servicio transitorios para los controladores IHttpClientFactory/HttpClient. Si la aplicación contiene controladores IHttpClientFactory/HttpClient y usa IRemoteAuthenticationBuilder<TRemoteAuthenticationState,TAccount> para agregar compatibilidad con la autenticación, también se detectan los siguientes descartables transitorios para la autenticación del lado del cliente. Este es un comportamiento esperado y se puede omitir:

También se detectan otras instancias de IHttpClientFactory/HttpClient. Estas instancias también se pueden omitir.

Las Blazoraplicaciones de Blazorejemplo en el repositorio de GitHub (cómo descargar) muestran el código para detectar descartables transitorios. Sin embargo, el código se desactiva porque las aplicaciones de ejemplo incluyen controladores IHttpClientFactory/HttpClient.

Para activar el código de demostración y ver su operación:

  • Quite la marca de comentario de las líneas descartables transitorias en Program.cs.

  • Quite la comprobación condicional NavLink.razor que impide que el componente TransientService se muestre en la barra lateral de navegación de la aplicación:

    - else if (name != "TransientService")
    + else
    
  • Ejecute la aplicación de ejemplo y vaya al componente de TransientService en /transient-service.

Uso de un objeto DbContext de Entity Framework Core (EF Core) desde la inserción de dependencias

Para más información, vea ASP.NET Core Blazor con Entity Framework Core (EF Core).

Acceder a los servicios del Blazor servidor desde un ámbito DI diferente

Los controladores de actividad de circuito proporcionan un enfoque para acceder a los servicios de Blazor con ámbito desde otros ámbitos de inserción de dependencias (DI) que no son de Blazor, como los ámbitos creados mediante IHttpClientFactory.

Antes del lanzamiento de ASP.NET Core en .NET 8, para acceder a los servicios con ámbito de circuito desde otros ámbitos de inserción de dependencias era necesario usar un tipo de componente base personalizado. Con los controladores de actividad de circuito no se requiere un tipo de componente base personalizado, como se muestra en el ejemplo siguiente:

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

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

public class ServicesAccessorCircuitHandler(
    IServiceProvider services, CircuitServicesAccessor servicesAccessor) 
    : CircuitHandler
{
    public override Func<CircuitInboundActivityContext, Task> CreateInboundActivityHandler(
        Func<CircuitInboundActivityContext, Task> next) => 
            async context =>
            {
                servicesAccessor.Services = services;
                await next(context);
                servicesAccessor.Services = null;
            };
}

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

        return services;
    }
}

Acceda a los servicios con ámbito de circuito mediante la inserción de CircuitServicesAccessor donde sea necesario.

Para ver un ejemplo que muestra cómo acceder a AuthenticationStateProvider desde una configuración DelegatingHandler mediante IHttpClientFactory, consulte Escenarios de seguridad adicionales de Blazor de ASP.NET Core del lado del servidor.

Puede haber ocasiones en las que un componente Razor invoque métodos asincrónicos que ejecuten código en un ámbito de inserción de dependencias distinto. Sin el enfoque correcto, estos ámbitos de inserción de dependencias no tienen acceso a los servicios de Blazor, como IJSRuntime y Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage.

Por ejemplo, las instancias de HttpClient creadas con IHttpClientFactory tienen su propio ámbito de servicio de inserción de dependencias. Como resultado, las instancias de HttpMessageHandler configuradas en HttpClient no pueden insertar servicios de Blazor directamente.

Cree una clase BlazorServiceAccessor que defina un objeto AsyncLocal, que almacena el objeto IServiceProvider de Blazor para el contexto asincrónico actual. Una instancia de BlazorServiceAccessor puede adquirirse desde dentro de un ámbito de servicio de inserción de dependencias distinto para acceder a los servicios de 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 establecer el valor de BlazorServiceAccessor.Services de forma automática cuando se invoque un método de componente async, cree un componente base personalizado que reimplemente los tres puntos de entrada asincrónicos principales en el código del componente Razor:

En la clase siguiente se muestra la implementación del 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;
        }
    }
}

Los componentes que extienden CustomComponentBase de forma automática tienen el valor BlazorServiceAccessor.Services establecido en IServiceProvider en el ámbito de inserción de dependencias Blazor actual.

Por último, en el archivo Program, agregar el BlazorServiceAccessor como un servicio de alcance:

builder.Services.AddScoped<BlazorServiceAccessor>();

Por último, en Startup.ConfigureServices de Startup.cs, agregue BlazorServiceAccessor como servicio con ámbito:

services.AddScoped<BlazorServiceAccessor>();

Recursos adicionales