Seguro que alguna vez os habéis encontrado con la necesidad de usar un servicio registrado como scoped desde el código de un servicio registrado como singleton. Un caso típico es cuando en una aplicación ASP.NET Core tenemos un servicio corriendo en segundo plano (vaya, un BackgroundService) y necesitamos acceder a servicios que usan bases de datos o cualquier otro recurso que está normalmente asociado al ámbito de una petición HTTP.
Si intentamos inyectar directamente un servicio scoped dentro de un servicio singleton, obtendremos un error al iniciar la aplicación, durante el proceso de validación del árbol de dependencias, ya que el contenedor de .NET no permite esta combinación:
System.AggregateException: 'Some services are not able to be constructed
(Error while validating the service descriptor 'ServiceType:
Microsoft.Extensions.Hosting.IHostedService Lifetime: Singleton ImplementationType:
MyBackgroundService': Cannot consume scoped service 'MyDbContext' from
singleton 'Microsoft.Extensions.Hosting.IHostedService'.)'
Una posibilidad (peligrosa: ¡no hacer!) es deshabilitar esta validación inicial, pero esto solo ocultaría el problema y casi con toda seguridad nos llevaría más tarde a errores en tiempo de ejecución que podrían ser difíciles de diagnosticar y, en el peor de los casos, incluso tumbarnos la aplicación.
En este post vamos a ver la forma correcta de resolver el problema creando ámbitos personalizados dentro del servicio singleton, y usándolos para resolver los servicios scoped que necesitemos.
Entendiendo los ámbitos (scopes) en .NET
En .NET, la vida de un servicio registrado como scoped está asociada obligatoriamente a un ámbito (o scope), una abstracción que define el contexto de ejecución de una serie de operaciones durante un periodo de tiempo claramente delimitado.
Cuando desde cualquier punto de la aplicación se solicita un servicio de este tipo, el contenedor de dependencias creará una instancia y la asociará al ámbito actual. Luego, si vuelven a solicitarse instancias del mismo servicio dentro del mismo ámbito, se devolverá la misma instancia. Y finalmente, cuando el ámbito finalice, todas las instancias asociadas a él serán destruidas automáticamente, invocando al método Dispose() si el servicio implementa la interfaz IDisposable.
El concepto "ámbito" es flexible y depende del tipo de aplicación que estemos desarrollando.
Por ejemplo, en ASP.NET Core, un ámbito se crea automáticamente para cada petición HTTP. Esto permite que cada petición tenga su propia instancia de servicios scoped sin mezclarse con instancias creadas para satisfacer otras peticiones.
En Blazor, sin embargo, los ámbitos están muy ligados al modelo de hosting usado por los componentes. En Blazor Server Side Rendering, se creará un ámbito para cada petición; en Blazor Server, cada usuario conectado tendrá su propio ámbito, que permanecerá activo mientras dure la conexión; en Blazor WebAssembly, dado que no existen ámbitos separados para cada usuario o petición, por lo que todos los servicios scoped se comportan como singleton.
Pero la parte interesante es que podemos crear nuestros propios ámbitos o scopes en cualquier parte de la aplicación.
Creando ámbitos personalizados
Para crear un ámbito personalizado, necesitamos usar el servicio IServiceScopeFactory, que está registrado automáticamente en el contenedor de dependencias de .NET. Este servicio nos permite crear nuevos ámbitos bajo demanda, y luego usar estos ámbitos para resolver servicios scoped, con una vida limitada al ámbito que hemos creado.
Veamos un ejemplo práctico. Vamos a partir de un servicio scoped llamado MyDbContext que proporciona acceso a una base de datos, y un servicio singleton llamado MyBackgroundService que necesita guardar datos en la base de datos periódicamente.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHostedService<MyBackgroundService>();
builder.Services.AddScoped<MyDbContext>();
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run();
public class MyBackgroundService (MyDbContext dbContext) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
await dbContext.SaveToDatabaseAsync();
await Task.Delay(1000, stoppingToken);
}
}
}
public class MyDbContext: IDisposable
{
public Task SaveToDatabaseAsync()
{
Console.WriteLine("Saving to database...");
return Task.CompletedTask;
}
public void Dispose() { } // TODO release managed resources here
}
Si intentamos ejecutar este código, obtendremos el error mencionado anteriormente, ya que MyBackgroundService es un servicio singleton que intenta inyectar directamente un servicio scoped (MyDbContext).
Veamos cómo solucionarlo de forma correcta. En primer lugar, en lugar de hacer que el servicio singleton inyecte directamente el servicio scoped, haremos que inyecte el servicio IServiceScopeFactory:
public class MyBackgroundService (IServiceScopeFactory scopeFactory) : BackgroundService
{
...
}
A continuación, podemos utilizar el método CreateScope() de esta factoría para crear un scope cuando lo necesitemos. El objeto devuelto es un IServiceScope definida en el framework de la siguiente manera:
public interface IServiceScope : IDisposable
{
IServiceProvider ServiceProvider { get; }
}
El hecho de que implemente IDisposable ya nos da pistas de que el scope debe ser destruido cuando ya no lo necesitemos. Además, la propiedad ServiceProvider nos proporciona un contenedor de dependencias específico para ese ámbito, que podremos utilizar para obtener los servicios que necesitemos. En general, el patrón de uso es el siguiente:
using (var scope = scopeFactory.CreateScope())
{
// Solicitamos servicios _scoped_ dentro de este ámbito
// usando métodos como scope.ServiceProvider.GetService<T>() o GetRequiredService<T>()
}
Volviendo al ejemplo anterior, ahora podríamos decidir la duración del ámbito. Podríamos optar por:
- Crear un ámbito fuera del bucle
while, de modo que la misma instancia deMyDbContextse reutilice en cada iteración. - O bien, crear un ámbito nuevo en cada iteración del bucle, de modo que cada vez que necesitemos
MyDbContext, obtengamos una nueva instancia.
En nuestro caso, la segunda opción es la más adecuada, ya que nos aseguramos de que cada operación de guardado en la base de datos se realice con una instancia fresca de MyDbContext, evitando posibles problemas de concurrencia o estado compartido, además de minimizar el tiempo de vida de las conexiones a la base de datos.
Por tanto, el código final de MyBackgroundService quedaría así:
public class MyBackgroundService (IServiceScopeFactory scopeFactory) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
using(var scope = scopeFactory.CreateScope())
{
var dbContext = scope.ServiceProvider.GetRequiredService<MyDbContext>();
await dbContext.SaveToDatabaseAsync();
}
await Task.Delay(1000, stoppingToken);
}
}
}
¡Eso es todo! Ahora MyBackgroundService puede usar el servicio scoped MyDbContext sin problemas, creando y destruyendo ámbitos personalizados según sea necesario.
Publicado en Variable not found.


Aún no hay comentarios, ¡sé el primero!
Enviar un nuevo comentario