En este post vamos a ver cómo crear tareas en segundo plano dentro de nuestras aplicaciones C# y cuáles son algunas de las diferentes opciones que tenemos.
El motivo por el que he elegido estas tres librerías es porque las he utilizado todas en el ambiente profesional. Y en mi opinión, cada una tiene su caso de uso.
Índice
Como siempre el código está en GitHub y para este post en concreto he creado un proyecto web API básico, para así poder ver todas las opciones juntas.
1 - Background workers en C# de forma nativa
La primera opción es la “nativa” en mi opinión es una buena opción cuando tienes que tener algo ejecutándose todo el tiempo, una vez detrás de otra.
Para ello C# nos trae un template llamado “Worker Service” aunque obviamente también se puede poner en un proyecto cualquiera. En este caso vamos a añadir un worker a una web api;
Lo primero es crear dicho worker, lo principal aquí es que herede de la clase BackgroundService
:
public class NativeWorker : BackgroundService
{
private readonly ISampleUseCase _sampleUseCase;
public NativeWorker(ISampleUseCase sampleUseCase)
{
_sampleUseCase = sampleUseCase;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
_sampleUseCase.Execute();
await Task.Delay(1000, stoppingToken);
}
}
}
Este código se ejecutará continuamente una vez la aplicación empiece a ejecutarse. Una vez termina, vuelve a empezar.
Y simplemente se configura en el startup como un HostedService
y ya funciona:
builder.Services.AddHostedService<NativeWorker>();
Esta funcionalidad es muy util por ejemplo cuando tienes que hacer un pull continuado de cierta informacion, o vaya, cualquier cosa que tenga que estar siendo ejecutada 24/7.
Por cierto, los BackgroundServices son singleton lo que quiere decir que todo lo que inyectes a través de la inyección de dependencias tiene que ser singleton y normalmente no lo será, así que para ese escenario tenemos el workaround the inyectar el IServiceProvider
y crear un scope del mismo, de esta forma si funciona:
using (IServiceScope scope = _serviceProvider.CreateScope())
{
ISampleUseCase sampleUseCase = scope.ServiceProvider
.GetRequiredService<ISampleUseCase>();
sampleUseCase.Execute();
}
Ahora vamos a pasar a las opciones alternativas, que son librerías las cuales están preparadas para más escenarios, obviamente podrías poner en el BackgroundService
un timer o cierta lógica por ejemplo para que se ejecute una vez cada hora, pero para eso, como digo hay otras opciones.
2 - Tareas en segundo plano en C# con HangFire
Dentro de C# Tenemos dos librerías que son más populares que el resto, la primera de ellas es HangFire. Personalmente es la que más me gusta y en la que más experiencia tengo, de hecho, es la que uso en este blog para enviar las notificaciones, newsletter etc.
Instalar hangfire es muy sencillo, una vez tenemos el paquete Hangfire.AspNetcore
únicamente debemos indicar en nuestro startup donde vamos a “almacenar” la información a enviar.
Me explico, cuando creo un post, envio una notificación de la web (típica campana arriba) y un email a aquellas personas que siguen el curso donde ese post hace parte (se puede configurar para no recibir nada). Esto lo hago con hangfire, utilizando su funcionalidad de Fire&Forget.
Para esta comunicación necesitas almacenar que quieres enviar X, en el caso de hangfire se puede configurar tanto en memoria, como en redis, como en bases de datos, etc. instalas el paquete que necesites, en mi caso Hangfire.MemoryStorage
y se hace de la siguiente manera:
builder.Services.AddHangfire(config => config.UseMemoryStorage());
builder.Services.AddHangfireServer(); //esto es necesario porque este es el "servidor"
Luego únicamente tienes que invocar hangfire en el código y especificar el método que quieres ejecutar con los parámetros correspondientes, y ya está, con eso funciona:
[ApiController]
[Route("[controller]")]
public class HangFireController : ControllerBase
{
private readonly SendPostCreatedNotification _sendPostNotification;
public HangFireController(SendPostCreatedNotification sendPostNotification)
{
_sendPostNotification = sendPostNotification;
}
[HttpGet("hangifre-execution")]
public bool HangfireExecution()
{
BackgroundJob.Enqueue(() => _sendPostNotification.Execute("identifier-1"));
return true;
}
}
En este caso lanzamos el job desde dentro del controlador con el tipo BackgroundJob
, y luego utlizamos Enqueue
para incluirlo en la cola de ejecución.
Por supuesto el código no va a esperar a que el job finalice para devolver el resultado del controlador.
Obviamente esto es solo un ejemplo de muchos, como puede ser lanzar el trabajo en X tiempo, que se repitan o mandar varias de golpe (en la versión pro).
Si alguien es familiar con ruby, Hangifre es el equivalente a sidekiq.
3 - Tareas en segundo plano en C# con Quarz.NET
La segunda opción que vamos a ver ahora es quarz.NET, similar al anterior, pero algo diferente, Originariamente esta librería es un port de otra librería de Java.
Pero bueno vamos al lío, para utilizarlo necesitamos, obviamente instalar la librería Quartz
y el paquete Quartz.Extensions.Hosting
, por supuesto lo tenemos que definir en el startup de la aplicación:
builder.Services.AddQuartz();
Anteriormente se pasaba UseMicrosoftDependencyInjectionScopedJobFactory
en la configuración, pero ya no hace falta al ser el método por defecto.
Luego por seguridad, podemos indicar el siguente código, el cual lo que hace es, que si intentas apagar la aplicación y hay un job siendo ejecutado, pues espera hasta que termina, así que la configuración del startup nos queda así:
builder.Services.AddQuartz()
.AddQuartzHostedService(config=>config.WaitForJobsToComplete = true);
Ahora lo que vamos a hacer es crear un job, que es básicamente la pieza de código que vamos a ejecutar en segundo plano. Por cierto, podemos utilizar inyección de dependencias:
public class QuartzExampleJob : IJob
{
private readonly ISampleUseCase _sampleUseCase;
public QuartzExampleJob(ISampleUseCase sampleUseCase)
{
_sampleUseCase = sampleUseCase;
}
public Task Execute(IJobExecutionContext context)
{
_sampleUseCase.Execute();
return Task.CompletedTask;
}
}
Como podemos ver, tenemos que utilizar la interfaz IJob
; lo cual si tenemos el escenario que he comentado antes de mi blog, pues no cuadra exactamente con lo que yo quiero. Pero es una alternativa totalmente viable y hasta elegante a utilizar en vez del BackgroundService que viene por defecto.
Por ese motivo, donde sí me parece que acierta es en crear cron jobs, Y que es un cron job? Pues básicamente una tarea que se ejecuta o bien cada X tiempo o a cierta hora cada día.
Esta funcionalidad es muy fácil de configurar se puede hacer de una forma muy sencilla al indicar AddQuartz
:
builder.Services.AddQuartz(config =>
{
JobKey key = new JobKey("QuartzExampleJob");
config.AddJob<QuartzExampleJob>(jobConfig => jobConfig.WithIdentity(key));
config.AddTrigger(opts => opts
.ForJob(key)
.WithIdentity("QuartzExampleJob-trigger")
.WithCronSchedule("10 * * * * ?"));
})
.AddQuartzHostedService(config=>config.WaitForJobsToComplete = true);
Podemos ver que estamos creando una key la cual va a “hacer link” entre el propio IJob que hemos creado y la configuración de Quartz. Además en la última línea lo configuramos como un cron job para que se ejecute cada 10 minutos.
Alternativamente puedes poner la configuración en el appsettings e incluir dicha configuración a través del fichero de configuración, o por ejemplo configurarlo para que se ejecute una vez al dia:
config.AddTrigger(opts => opts
.ForJob(key)
.WithIdentity("QuartzExampleJob-trigger")
.WithDailyTimeIntervalSchedule(daily =>
daily.WithIntervalInHours(24)
.OnEveryDay()
.StartingDailyAt(TimeOfDay.HourAndMinuteOfDay(10, 00))
));
Como vemos requiere de algo mas de configuración, así que si quieres ir a algo más complejo posiblemente te toque mirar la documentación oficial.
Personalmente, me gusta mucho ya que es muy común ver esta acción creada en el Task scheduler de windows, lo cual es obviamente una solución terrible.