Saltar al contenido

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript... y lo que venga ;)

18 años online

el blog de José M. Aguilar

Inicio El autor Contactar

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web
ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript...

¡Microsoft MVP!
martes, 4 de febrero de 2025
Máquina compleja con piezas explotando

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

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!