
Como sabemos, es muy sencillo implementar servicios en segundo plano alojados en el interior de aplicaciones ASP.NET Core porque el framework nos proporciona infraestructura que simplifica la tarea, permitiendo que nos enfoquemos en la lógica de negocio que queremos ejecutar en background en lugar de tener que preocuparnos de los aspectos de más bajo nivel necesarios para hacer que funcione.
En la práctica, basta con heredar de la clase BackgroundService
y sobreescribir el método ExecuteAsync
para implementar la lógica que queremos ejecutar en segundo plano. Por ejemplo, el siguiente servicio se encarga de escribir un mensaje en la consola cada segundo:
public class MyBackgroundService : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
Console.WriteLine($"Current time: {DateTime.Now:T}");
await Task.Delay(1000, stoppingToken);
}
}
}
Luego tendríamos registrar este servicio en el contenedor de dependencias de ASP.NET Core, de forma que el framework lo detectará automáticamente y lo lanzará cuando la aplicación sea iniciada:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHostedService<MyBackgroundService>();
...
Al ejecutar, veremos que, en la consola donde hemos lanzado la aplicación, se escribirá periódicamente la hora desde la tarea en segundo plano:
Current time: 13:34:06
info: Microsoft.Hosting.Lifetime[14]
Now listening on: https://localhost:7080
info: Microsoft.Hosting.Lifetime[14]
Now listening on: http://localhost:5148
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
Content root path: D:\Projects\BackgroundServiceCrash
Current time: 13:34:07
Current time: 13:34:08
Pero bueno, no es esto de lo que quería hablar, sino de lo que ocurre cuando en uno de estos servicios en segundo plano se produce un error... porque ojo, que si no tenemos cuidado puede tumbar completamente nuestra aplicación.
¿Qué ocurre cuando un BackgroundService
falla?
Podemos verlo fácilmente si modificamos el servicio anterior para que lance una excepción bajo una condición determinada:
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
if(DateTime.Now.Second % 10 == 0)
throw new Exception("Background service crashed!");
Console.WriteLine($"Current time: {DateTime.Now:T}");
await Task.Delay(1000, stoppingToken);
}
}
Al ejecutar la aplicación, veremos que al cabo de unos segundos el servicio en segundo plano falla y la aplicación se detiene por completo:
Current time: 13:39:54
info: Microsoft.Hosting.Lifetime[14]
Now listening on: https://localhost:7080
info: Microsoft.Hosting.Lifetime[14]
Now listening on: http://localhost:5148
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
Content root path: D:\Projects\BackgroundServiceCrash
Current time: 13:39:55
Current time: 13:39:56
Current time: 13:39:57
Current time: 13:39:58
Current time: 13:39:59
fail: Microsoft.Extensions.Hosting.Internal.Host[9]
BackgroundService failed
System.Exception: Background service crashed!
at MyBackgroundService.ExecuteAsync(CancellationToken stoppingToken)
in D:\Projects\BackgroundServiceCrash\Program.cs:line 18
at Microsoft.Extensions.Hosting.Internal.Host
.TryExecuteBackgroundServiceAsync(BackgroundService backgroundService)
crit: Microsoft.Extensions.Hosting.Internal.Host[10]
The HostOptions.BackgroundServiceExceptionBehavior is configured to StopHost.
A BackgroundService has thrown an unhandled exception, and the IHost instance
is stopping.
To avoid this behavior, configure this to Ignore; however the BackgroundService
will not be restarted.
System.Exception: Background service crashed!
at MyBackgroundService.ExecuteAsync(CancellationToken stoppingToken)
in D:\Projects\BackgroundServiceCrash\Program.cs:line 18
at Microsoft.Extensions.Hosting.Internal.Host
.TryExecuteBackgroundServiceAsync(BackgroundService backgroundService)
info: Microsoft.Hosting.Lifetime[0]
Application is shutting down...
D:\Projects\BackgroundServiceCrash\bin\Debug\net9.0\BackgroundServiceCrash.exe
(process 15432) exited with code 0 (0x0).
To automatically close the console when debugging stops, enable
Tools->Options->Debugging->Automatically close the console when debugging stops.
Press any key to close this window . . .
_
Pues efectivamente, el lanzamiento de la excepción no controlada ha provocado que la aplicación se detenga por completo 😱
Pero esto no ha sido así siempre. En versiones anteriores a .NET 6, si fallaba un servicio en segundo plano, la excepción se ignoraba y la aplicación seguía funcionando normalmente, aunque sin el servicio crasheado.O sea, básicamente no nos enterábamos de que había ocurrido.
En .NET 6 se introdujo un breaking change que cambió este comportamiento, haciendo que, por defecto, la aplicación se detenga si un servicio en segundo plano lanza una excepción.
Vale... ¿y cómo lo evitamos?
Afortunadamente, esto tiene fácil remedio. Sin duda, la solución más recomendable sería hacer que no falle, o bien capturar las excepciones en el interior del servicio para evita que se propaguen hacia arriba, como en el siguiente ejemplo:
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
try
{
if (DateTime.Now.Second % 10 == 0)
throw new Exception("Background service crashed!");
Console.WriteLine($"Current time: {DateTime.Now:T}");
await Task.Delay(1000, stoppingToken);
}
catch (Exception ex)
{
// Gestionar la excepción apropiadamente
}
}
}
La pista sobre otra forma de evitar la parada completa de la aplicación en estos casos la encontramos en el cuerpo del mensaje de error que hemos visto antes en la consola:
The
HostOptions.BackgroundServiceExceptionBehavior
is configured toStopHost
. ABackgroundService
has thrown an unhandled exception, and the IHost instance is stopping. To avoid this behavior, configure this toIgnore
; however theBackgroundService
will not be restarted.
Es decir, si por cualquier motivo preferimos que la aplicación siga funcionando aunque un servicio en segundo plano falle, tal y como se comportaba antes de .NET 6, podemos establecer la propiedad BackgroundServiceExceptionBehavior
de la clase HostOptions
a Ignore
, algo que podemos conseguir fácilmente en el código de inicialización de la aplicación, en Program.cs
:
var builder = WebApplication.CreateBuilder(args);
builder.Host.ConfigureHostOptions(options =>
{
options.BackgroundServiceExceptionBehavior = BackgroundServiceExceptionBehavior.Ignore;
});
...
Con esta configuración, si un BackgroundService
falla, la aplicación no se detendrá, aunque obviamente el servicio que falló no continuará funcionando.
¡Espero que os resulte útil! 😊
Publicado en: www.variablenotfound.com.
Aún no hay comentarios, ¡sé el primero!
Enviar un nuevo comentario