Lanzar acciones al iniciar y terminar nuestras aplicaciones ASP.NET

  • net-core
  • net
  • api
  • dotnet
  • hostedservice

Si queremos lanzar al iniciar y/o finalizar una aplicación asp.net, la solución la tenemos en los IHostedServices. En términos sencillos, _IHostedService_ permite definir acciones que deben ejecutarse en segundo plano y proporciona métodos para iniciar y detener esas operaciones de manera controlada. Esos métodos son los siguiente:

  • Task StartAsync(CancellationToken cancellationToken)

  • Task StopAsync(CancellationToken cancellationToken)

La manera de implementarla es bastante sencilla. A continuación se muestra un ejemplo en el que tenemos dos hosted service.

public class HostedServiceOne : IHostedService
{
    public async Task StartAsync(CancellationToken cancellationToken)
    {
        Console.WriteLine($"***** => {DateTime.Now} Hosted service one starting...");
        await Task.Delay(1_000, cancellationToken);
        Console.WriteLine($"***** => {DateTime.Now} Hosted service one started");
    }

    public async Task StopAsync(CancellationToken cancellationToken)
    {
        Console.WriteLine($"***** => {DateTime.Now} Hosted service one stopping...");
        await Task.Delay(1_000, cancellationToken);
        Console.WriteLine($"***** => {DateTime.Now} Hosted service one stopped");
    }
}

public class HostedServiceTwo : IHostedService
{
    public async Task StartAsync(CancellationToken cancellationToken)
    {
        Console.WriteLine($"***** => {DateTime.Now} Hosted service two starting...");
        await Task.Delay(1_000, cancellationToken);
        Console.WriteLine($"***** => {DateTime.Now} Hosted service two started");
    }

    public async Task StopAsync(CancellationToken cancellationToken)
    {
        Console.WriteLine($"***** => {DateTime.Now} Hosted service two stopping...");
        await Task.Delay(1_000, cancellationToken);
        Console.WriteLine($"***** => {DateTime.Now} Hosted service two stopped");
    }
}

Una vez creados todos los hosted services que necesitamos, solo faltaría añadirlos a nuestro IServiceCollection

...
builder.Services.AddHostedService<HostedServiceOne>();
builder.Services.AddHostedService<HostedServiceTwo>();
...

Consideraciones

Al usar los hosted services es necesario tener en cuenta las siguientes consideraciones, ya que, de no hacerlo, puede que nos encontramos con problemas para implementar nuestro servicio.

Order de ejecución de los servicios

Es posible que necesitemos crear varios hosted service para realizar distintas tareas al inicial el servidor. Por lo que puede ser importante conocer el orden en el que se ejecutan esas tareas.

A la hora de iniciar los servicios, es decir, de ejecutar los StartAsync, se hace en el orden en el que se añadieron al IServiceCollection.

Para el ejemplo anterior, está sería la traza que dejan los servicios al arrancarse.

***** => 20/10/2023 11:58:17 Hosted service one starting...
***** => 20/10/2023 11:58:18 Hosted service one started
***** => 20/10/2023 11:58:18 Hosted service two starting...
***** => 20/10/2023 11:58:19 Hosted service two started

Por el contrario, al detener los servicios, es decir, ejecutar los StopAsync, la ejecución se hace en el orden inverso, tal y como muestra la siguiente traza.

***** => 20/10/2023 12:01:18 Hosted service two stopping...
***** => 20/10/2023 12:01:19 Hosted service two stopped
***** => 20/10/2023 12:01:19 Hosted service one stopping...
***** => 20/10/2023 12:01:20 Hosted service one stopped

Es necesario tener en cuenta que un hosted service no se inicia hasta que el anterior ya ha terminado su inicialización, es decir, el método StartAsync de un servicio no se ejecuta hasta que ese mismo método del servicio anterior no ha terminado.

Lo mismo sucede a la hora de finalizar un hosted service. Hasta que no se haya finalizado el anterior, no se finaliza el siguiente.

Otro punto a tener en cuenta en la inicialización, es que nuestro servicio no se arranca hasta que todos los hosted services se han iniciado. Por lo que, nuestro servidor no aceptará peticiones mientras haya un hosted service iniciándose. Asimismo, si se produce un error no controlado en uno de los hosted services el servidor no arrancará.

Duración de las tareas

Siempre se recomienda que estos hosted services empleen el mínimo tiempo posible. No obstante, a la hora de arrancar no hay limitación en el tiempo de duración de los métodos _StartAsyn_c. Eso sí, tal y como se ha indicado antes, el servidor no termina su arranque hasta que todos los hosted services han sido iniciados, por lo que la duración de esos métodos StartAsync afecta directamente al tiempo que necesitará el servidor para estar disponible y poder atender peticiones.

En cambio, a la hora de detener los hosted services, sí que tenemos una limitación. Ésta es de 30 segundos para la detención de todos los servicios. En caso de que la duración se extienda más allá de esos 30 segundos, la ejecución de los hosted services pendientes se cancelará.

Ese valor de 30 segundos puede modificarse, si así lo necesitamos. Basta con modificar el valor de ShutdownTimeout del HostOptions e indicar el valor que queramos. En el siguiente ejemplo, lo modifico para que sean 50 segundos.

...
builder.Services.Configure<HostOptions>(opts => opts.ShutdownTimeout = TimeSpan.FromSeconds(50));
...

Ámbito de los servicios utilizados

Otro detalle a tener en cuenta es que, dado que los hosted services no tienen ámbito, no podremos recibir inyectados en el constructor ningún servicio registrado como scope. No obstante, existe la alternativa de recibir por constructor un IServiceScopeFactory y obtener la instancia del servicio en el momento que lo necesitemos de la siguiente manera.

private readonly IServiceScopeFactory _serviceScopeFactory;

public HostedServiceOne(IServiceScopeFactory serviceScopeFactory)
{
    _serviceScopeFactory = serviceScopeFactory;
}

public async Task StartAsync(CancellationToken cancellationToken)
{
    ...
    using var scope = _serviceScopeFactory.CreateScope();
    var services = scope.ServiceProvider;
    var environment = services.GetService<{YOUR_INTERFACE}>(); // Useful to be able to use an scoped service
    ...
}

Tras todo lo visto anteriormente, ya estamos en condiciones de utilizar hosted services, teniendo en cuenta todas las consideraciones que nos pueden afectar.

Foto de Scott Graham en Unsplash