Es este artículo vamos a desarrollar la estructura de una aplicación web en .NET Core, para analizar cómo se integra la librería Hangfire (https://www.hangfire.io) para gestionar tareas de fondo.

Puntos Importantes
  1. Crear atributos para especificar metadata al usar las clases.

  2. Incorporar NLog para realizar registro de eventos.

  3. Cargar módulos en forma dinámica

  4. Incorporar Hangfire como gestor de tareas en background.

  5. Usar reflection para identificar clases en un assembly.

  6. Crear/Actualizar la base de datos al arrancar la aplicación.

Programas fuente

Artículo: HangFireCoreWebApp-1.0.zip (release 1.0 del repositorio)
Repositorio: https://github.com/mvelosop/HangFireCoreWebApp

Contexto

Al desarrollar aplicaciones web es frecuente que sea necesario ejecutar tareas de fondo, como envío de correos o tareas de mantenimiento y, según lo comentado por Scott Hanselman, aunque esto puede parecer sencillo cuando se utilizan librerías como Quartz.NET, puede llegar a ser complicado por la cantidad de escenarios que se deben manejar.

En su artículo, Hanseman menciona la librería HangFire (https://www.hangfire.io) y la verdad es que ofrece una solución muy completa, incluso viene con una interfaz web para gestionar los procesos programados. Esta interfaz facilita la gestión de las tareas programas y requiere muy poco esfuerzo ponerla en funcionamiento.

Además, creo que la interfaz de gestión se ve muy bien y, dado que está principalmente orientada a usuarios de sistemas, tampoco es tan importante que el aspecto sea diferente del resto de la aplicación.

Entonces, la idea en este artículo es desarrollar una estructura donde sea muy sencillo manejar las tareas programadas para una aplicación modular, donde cada módulo puede tener assemblies con una convención de nombre que permita incorporarlos y programar sus tareas, con solo copiar esos assemblies en la carpeta con los ejecutables (.dll) de la aplicación.

Herramientas utilizadas

Paso a paso

1 - Crear la solución HangFireCoreWebApp

  1. Crear una solución “blank” llamada HangFireCoreWebApp
  2. Crear el “solution folder” “src”
  3. Crear la carpeta “src” dentro de la carpeta de la solución

2 - Desarrollar atributo para facilitar la programación de las tareas

Importante

Los atributos nos permiten especificar metadata asociada a casi cualquier componente de una aplicación, para luego utilizarla usando “reflection”

Aquí vemos cuan fácil es crear atributos personalizados, para más información vea Attributes.

La idea es utilizar el atributo [HangfireJobMinutes(#)] para, para indicar la frecuencia de ejecución cuando se declara la clase.

Este es un atributo muy sencillo, a modo de ejemplo, para aplicaciones reales seguramente habrá que manejar escenarios más complejos.

2.1 - Crear proyecto “src\HangFireCore.Core”

2.2 - Agregar archivo “HangfireJobMinutesAttribute.cs”

src\HangFireCore.Core\HangfireJobMinutesAttribute.cs
[HangFireCoreWebApp-1.0]
 1 2 3 4 5 6 7 8 91011121314151617181920212223
using System;

namespace HangFireCore.Core
{
    [System.AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
    public sealed class HangfireJobMinutesAttribute : Attribute
    {
        // See the attribute guidelines at 
        //  http://go.microsoft.com/fwlink/?LinkId=85236
        readonly int minutes;

        // This is a positional argument
        public HangfireJobMinutesAttribute(int minutes)
        {
            this.minutes = minutes;
        }

        public int Minutes
        {
            get { return minutes; }
        }
    }
}

3 - Desarrollar un “módulo” con tareas programadas

Importante

Aquí vemos cómo se incluye la posibilidad de generar trazas de log en una clase.

Este es un “módulo” que sólo tiene una tarea programada, para demostrar la funcionalidad básica.

En el repositorio se incluye también el módulo HangFire.Job.Two, aunque no se incluye en este texto.

Advertencia

Cuando hablamos de módulos dinámicos es importante tener en cuenta que al compilarlos no se van a copiar los .dll a la carpeta de la aplicación web, entonces hay copiarlos a mano para ver los cambios.

3.1 - Crear proyecto “src\HangFireCore.Job.One”

3.2 - Incluir referencias y paquetes necesarios

  • referencia al proyecto src\HangFireCore.Core
  • Paquete NLog.5.0.0-beta06

3.3 - Incluir archivo “JobOne.cs”

src\HangFireCore.Job.One\JobOne.cs
[HangFireCoreWebApp-1.0]
 1 2 3 4 5 6 7 8 910111213141516171819202122
using HangFireCore.Core;
using NLog;
using System;
using System.Threading.Tasks;

namespace HangFireCore.Job.One
{
    [HangfireJobMinutes(1)]
    public class JobOne
    {
        // Get logger
        private static Logger logger = LogManager.GetCurrentClassLogger();

        private static DateTime start = DateTime.Now;

        public static Task Execute()
        {
            // Use logger at INFO level
            return Task.Run(() => logger.Info(@"Executing Job One - {0:hh\:mm\:ss\.fff}", DateTime.Now - start));
        }
    }
}

4 - Crear el proyecto src\HangFireCore.WebApp

En la aplicación web se integran de forma ligera (loosely coupled) los módulos de la solución, en este caso eso significa, que, si la aplicación encuentra los módulos de las tareas programadas, los carga y programa la ejecución de las tareas y si no los encuentra no pasa nada.

La idea fundamental de esta estructura es que no existe una referencia directa entre la aplicación web y los proyectos (assemblies) de tareas programadas, sino que, al arrancar, la aplicación busca los assemblies y usa los que encuentre.

Como no hay una referencia directa a esos assemblies, no serán incluidos directamente por el proceso de “Build” y entonces es necesario copiarlos a mano en la carpeta de la aplicación.

Crear un projecto .NET Core Web Application (.NET Core)

Tareas de fondo en aplicaciones ASP.NET MVC Core /posts/images/devenv_2017-04-25_11-01-58.png

Usamos el proyecto con autenticación por cuentas individuales, para que se cree todo lo relacionado con la conexión a la base de datos, porque la vamos a necesitar para el próximo paso.

5 - Agregar carga dinámica de “módulos”

Importante

Aquí vemos una implementación sencilla de la carga dinámica de módulos en una aplicación y el uso de Reflection para identificar las clases en un assembly.

Carga dinámica significa que si encuentra los módulos se usan y si no, no pasa nada.

Se implementa la carga de “modulos” como un ExtensionMethod de IHostingEnvironment para facilitar su invocación desde Startup.cs.

Agregar la clase “helpers\ScheduleJobsHelpers.cs**

src\HangFireCore.WebApp\Helpers\ScheduleJobsHelpers.cs
[HangFireCoreWebApp-1.0]
 1 2 3 4 5 6 7 8 9101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990
using Hangfire;
using HangFireCore.Core;
using Microsoft.AspNetCore.Hosting;
using NLog;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.Loader;

namespace HangFireCore.WebApp.Helpers
{
    public static class ScheduleJobsHelpers
    {
        static Logger logger = LogManager.GetCurrentClassLogger();

        public static void ScheduleRecurringJobs(this IHostingEnvironment env)
        {
            try
            {
                logger.Info("Scheduling recurring jobs...");
                logger.Trace("Loading job modules...");

                // Get the current executing assembly path
                string location = Assembly.GetEntryAssembly().Location;
                string directory = Path.GetDirectoryName(location);

                // Find modules that follow the job module name convention
                var jobModules = Directory.EnumerateFiles(directory, "HangFireCore.Job.*.dll", SearchOption.TopDirectoryOnly);

                if (!jobModules.Any())
                {
                    logger.Info("Didn't find any job module.");
                }

                foreach (var module in jobModules)
                {
                    try
                    {
                        logger.Info("Loading Job assembly: {0}", module);
                        Assembly assembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(module);

                        logger.Trace("Getting jobs...");

                        // Get types using the HangfireJobMinutes attribute
                        var recurringJobs = assembly
                            .ExportedTypes
                            .Where(et => et.GetTypeInfo().GetCustomAttribute<HangfireJobMinutesAttribute>() != null);

                        if (!recurringJobs.Any())
                        {
                            logger.Info("Didn't find any recurring job.");
                        }

                        foreach (Type job in recurringJobs)
                        {
                            int minutes = job.GetTypeInfo().GetCustomAttribute<HangfireJobMinutesAttribute>().Minutes;

                            logger.Trace(@"Scheduling recurring job ""{0}"" with {1} minutes interval", job.Name, minutes);

                            // We expect every job to have an "Execute" method
                            MethodInfo executeMethod = job.GetMethod("Execute");

                            if (executeMethod != null)
                            {
                                // Get lambda expression to call the STATIC "Execute" method
                                Expression<Action> expression = Expression.Lambda<Action>(Expression.Call(executeMethod));

                                // Add the job to Hangfire's queue
                                RecurringJob.AddOrUpdate(job.FullName, expression, Cron.MinuteInterval(minutes));
                            }
                        }

                    }
                    catch (Exception ex)
                    {
                        logger.Error(ex);
                    }

                }
            }
            catch (Exception ex)
            {
                logger.Error(ex);
            }
        }
    }
}

6 - Incluir Hangfire en la aplicación web

Importante

Aquí vemos cómo se incluyen los componentes de Hangfire en una aplicación, más adelante vemos las configuraciones necesarias.

Vamos a orientarnos por lo indicado en Integrate HangFire With ASP.NET Core, para configurar Hangfire:

6.1 - Incluir el paquete NuGet Hangfire 1.6.12

6.2 - Modificar el archivo de configuración para trabajar con SQL Server en vez de LocalDb

src\HangFireCore.WebApp\appsettings.json
[HangFireCoreWebApp-1.0]
 1 2 3 4 5 6 7 8 91011
{
  "ConnectionStrings": {
    "DefaultConnection": "Data Source=localhost;Initial Catalog=HangFireCoreWebApp;Integrated Security=SSPI;MultipleActiveResultSets=true"
  },
  "Logging": {
    "IncludeScopes": false,
    "LogLevel": {
      "Default": "Warning"
    }
  }
}

Prefiero trabajar con SQL Server Developer Edition, para que sea lo más parecido posible al ambiente de producción.

6.3 - Crear un AuthorizationFilter para poder acceder al dashboard de Hangfire

src\HangFireCore.WebApp\Helpers\HangfireDashboardAuthorizationFilter.cs
[HangFireCoreWebApp-1.0]
 1 2 3 4 5 6 7 8 9101112131415
using Hangfire.Annotations;
using Hangfire.Dashboard;

namespace HangFireCore.WebApp.Helpers
{
    public class HangfireDashboardAuthorizationFilter : IDashboardAuthorizationFilter
    {
        public bool Authorize([NotNull] DashboardContext context)
        {
            var httpcontext = context.GetHttpContext();

            return httpcontext.User.Identity.IsAuthenticated;
        }
    }
}

Esto es un filtro básico que sólo verifica que el usuario esté autenticado para permitir el acceso, en la práctica se debe aplicar algún criterio más estricto para permitirlo.

7 - Incluir NLog en la aplicación web

Importante

Aquí vemos cómo se incluyen los componentes de NLog en una aplicación y cómo se configura le genereción de los registros, más adelante se muestra la configuración necesaria en el arranque.

Vamos a utilizar NLog (http://nlog-project.org/) para verificar el funcionamiento del sistema de tareas en background y como buena práctica general del desarrollo de aplicaciones.

7.1 - Incluir paquetes NLog en la aplicación web

  • NLog.Web.AspNetCore 4.3.1
  • NLog.Config 4.4.7

7.2 - Resolver incompatibilidad con ASP.NET Core

Según lo indicado en https://github.com/NLog/NLog.Extensions.Logging/blob/master/README.md (para el 25/04/2017), la instalación de NLog.Config no copia el archivo NLog.xsd, así que hay que extraerlo manualmente del nuget y copiarlo en la raíz del proyecto HangFireCore.WebApp.

Para esto se debe buscar el paquete el la carpeta .nuget dentro del perfil del usuario y abrir el .nupkg correspondiente con un gestor de archivos .zip como WinRAR o WinZIP o cambiando temporalmente el nombre del .nupkg a .zip para usar el explorador de Windows.

7.3 - Crear archivo NLog.config en la raíz de la aplicación web

src\HangFireCore.WebApp\NLog.config
[HangFireCoreWebApp-1.0]
 1 2 3 4 5 6 7 8 910111213141516171819202122232425262728293031323334353637383940414243444546
<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      autoReload="true"
      internalLogLevel="Warn"
      internalLogFile="c:\temp\logs\internal-nlog.log">

    <extensions>
        <add assembly="NLog.Web.AspNetCore"/>
    </extensions>


    <!-- define various log targets -->
    <targets>
        <!-- write logs to file -->
        <target xsi:type="File"
                name="allfile"
                fileName="c:\temp\logs\nlog-all-current.log"
                archiveFileName="c:\temp\logs\nlog-all-archive${#}.log"
                archiveEvery="Day"
                archiveNumbering="Rolling"
                maxArchiveFiles="7"
                layout="${longdate}|${event-properties:item=EventId.Id}|${logger}|${uppercase:${level}}|${message} ${exception}" />


        <target xsi:type="File"
                name="ownFile-web"
                fileName="c:\temp\logs\nlog-HangFireCoreApp-current.log"
                archiveFileName="c:\temp\logs\nlog-HangFireCoreApp-archive${#}.log"
                archiveEvery="Day"
                archiveNumbering="Rolling"
                maxArchiveFiles="7"
                layout="${longdate}|${event-properties:item=EventId.Id}|${logger}|${uppercase:${level}}|  ${message} ${exception}|url: ${aspnet-request-url}|action: ${aspnet-mvc-action}" />

        <target xsi:type="Null" name="blackhole" />
    </targets>

    <rules>
        <!--All logs, including from Microsoft-->
        <logger name="*" minlevel="Info" writeTo="allfile" />

        <!--Skip Microsoft logs and so log only own logs-->
        <logger name="Microsoft.*" minlevel="Trace" writeTo="blackhole" final="true" />
        <logger name="*" minlevel="Info" writeTo="ownFile-web" />
    </rules>
</nlog>

Según lo indicado en https://github.com/NLog/NLog.Extensions.Logging/blob/master/README.md, todavía no está soportado el ${basedir}, para manejar rutas relativas en los archivos .log, así que por ahora es necesario configurar una ruta absoluta (c:\temp\logs).

Un aspecto interesante de NLog es que se puede modificar el archivo de configuración sin necesidad de detener la aplicación, por ejemplo para ajustar el nivel de detalle que se registra, para ver los resultados de inmediato.

Esta configuración crea archivos de log rotativos en c:\temp\logs. Una ventaja importante de usar archivos rotativos es que no es necesario estar pendiente de borrar los logs viejos:

  • nlog-all-current.log día en curso para todas las aplicaciones (bueno para detectar interferencias)
  • nlog-all-archive.1.log histórico del día -1 para todas las aplicaciones
  • nlog-HangFireCoreApp-current.log día en curso para nuestra aplicación
  • nlog-HangFireCoreApp-archive.1.log histórico del día -1 para nuestra aplicación

8 - Configurar los componentes en el arranque de la aplicación web

Aquí se detallan las modificaciones individuales y luego se muestra el archivo Startup.cs resultante.

Importante

Aquí se muestra cómo crear o actualizar la base de datos durante el arranque de la aplicación.

8.1 - Agregar el método InitDb en Startup.cs para crear/actualizar la base de datos

private void InitDb()
{
    var optionsBuilder = new DbContextOptionsBuilder<ApplicationDbContext>();

    optionsBuilder.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"));

    using (var dbContext = new ApplicationDbContext(optionsBuilder.Options))
    {
        dbContext.Database.Migrate();
    };
}

Este método se usa para crear/actualizar la base de datos cuando arranque la aplicación y no con el primer request, porque debe existir la base de datos antes de iniciar Hangfire.

8.2 - Inicializar Hangfire

Estos son los cambios necesarios, en los distintos métodos de Startup.cs, para configurar Hangfire:

using Hangfire;
using HangFireCore.WebApp.Data;
using HangFireCore.WebApp.Helpers;
using HangFireCore.WebApp.Models;
using HangFireCore.WebApp.Services;

public void ConfigureServices(IServiceCollection services)
{
    // Add Hangfire
    services.AddHangfire(config => config.UseSqlServerStorage(Configuration.GetConnectionString("DefaultConnection")));
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    // Initialize Dabatabase
    InitDb();

    // Configure Hangfire
    app.UseHangfireServer();

    app.UseHangfireDashboard("/hangfire", new DashboardOptions()
    {
        Authorization = new[] { new HangfireDashboardAuthorizationFilter() }
    });
}

8.3 - Cargar módulos de forma dinámica

Estos son los cambios necesarios para configurar la carga dinámica de módulos en Startup.cs, En este caso es importante cargar los módulos después que esté inicializada la base de datos y el servidor de Hangfire:

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    // Initialize Dabatabase
    InitDb();

    // Configure Hangfire
    app.UseHangfireServer();

    app.UseHangfireDashboard("/hangfire", new DashboardOptions()
    {
        Authorization = new[] { new HangfireDashboardAuthorizationFilter() }
    });

    // Recurring jobs
    env.ScheduleRecurringJobs();
}

8.4 - Inicializar NLog

Estos son los cambios necesarios, en los distintos métodos de Startup.cs, para configurar NLog:

using Microsoft.AspNetCore.Http;
using NLog.Extensions.Logging;
using NLog.Web;

public Startup(IHostingEnvironment env)
{
    env.ConfigureNLog("NLog.config");
}

public void ConfigureServices(IServiceCollection Services)
{
    //call this in case you need aspnet-user-authtype/aspnet-user-identity
    services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
}

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    //add NLog to ASP.NET Core
    loggerFactory.AddNLog();

    //add NLog.Web
    app.AddNLogWeb();
}

8.5 - Verificar cambios en Startup.cs

Con los cambios anteriores el archivo de arranque debe quedar así:

src\HangFireCore.WebApp\Startup.cs
[HangFireCoreWebApp-1.0]
  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133
using Hangfire;
using HangFireCore.WebApp.Data;
using HangFireCore.WebApp.Helpers;
using HangFireCore.WebApp.Models;
using HangFireCore.WebApp.Services;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using NLog;
using NLog.Extensions.Logging;
using NLog.Web;

namespace HangFireCore.WebApp
{
    public class Startup
    {
        private Logger logger = LogManager.GetCurrentClassLogger();

        public Startup(IHostingEnvironment env)
        {
            var builder = new ConfigurationBuilder()
                .SetBasePath(env.ContentRootPath)
                .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
                .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true);

            if (env.IsDevelopment())
            {
                // For more details on using the user secret store see https://go.microsoft.com/fwlink/?LinkID=532709
                builder.AddUserSecrets<Startup>();
            }

            builder.AddEnvironmentVariables();

            Configuration = builder.Build();

            env.ConfigureNLog("NLog.config");
        }

        public IConfigurationRoot Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            //call this in case you need aspnet-user-authtype/aspnet-user-identity
            services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();

            // Add framework services.
            services.AddDbContext<ApplicationDbContext>(options =>
                options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

            services.AddIdentity<ApplicationUser, IdentityRole>()
                .AddEntityFrameworkStores<ApplicationDbContext>()
                .AddDefaultTokenProviders();

            services.AddMvc();

            // Add application services.
            services.AddTransient<IEmailSender, AuthMessageSender>();
            services.AddTransient<ISmsSender, AuthMessageSender>();

            // Add Hangfire
            services.AddHangfire(config => config.UseSqlServerStorage(Configuration.GetConnectionString("DefaultConnection")));
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
        {
            loggerFactory.AddConsole(Configuration.GetSection("Logging"));
            loggerFactory.AddDebug();

            //add NLog to ASP.NET Core
            loggerFactory.AddNLog();

            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
                app.UseDatabaseErrorPage();
                app.UseBrowserLink();
            }
            else
            {
                app.UseExceptionHandler("/Home/Error");
            }

            app.UseStaticFiles();

            app.UseIdentity();

            // Add external authentication middleware below. To configure them please see https://go.microsoft.com/fwlink/?LinkID=532715

            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}/{id?}");
            });

            //add NLog.Web
            app.AddNLogWeb();

            // Initialize Dabatabase
            InitDb();

            // Configure Hangfire
            app.UseHangfireServer();

            app.UseHangfireDashboard("/hangfire", new DashboardOptions()
            {
                Authorization = new[] { new HangfireDashboardAuthorizationFilter() }
            });

            // Recurring jobs
            env.ScheduleRecurringJobs();
        }

        private void InitDb()
        {
            var optionsBuilder = new DbContextOptionsBuilder<ApplicationDbContext>();

            optionsBuilder.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"));

            using (var dbContext = new ApplicationDbContext(optionsBuilder.Options))
            {
                dbContext.Database.Migrate();
            };
        }
    }
}

8.6 - Agregar opción Hangfire la barra de navegación

Modificar el archivo Views\Shared_Layout.cshtml para agregar la opción HangFire, como se indica a continuación:

<div class="navbar-collapse collapse">
    <ul class="nav navbar-nav">
        <li><a asp-area="" asp-controller="Home" asp-action="Index">Home</a></li>
        <li><a asp-area="" asp-controller="Home" asp-action="About">About</a></li>
        <li><a asp-area="" asp-controller="Home" asp-action="Contact">Contact</a></li>
        <li><a asp-area="" asp-controller="Hangfire" asp-action="Index">Hangfire</a></li>
    </ul>
    @await Html.PartialAsync("_LoginPartial")
</div>

9 - Probar la aplicación

Ejecutar la aplicación con [Ctrl]+[F5] y navegar hasta /hangfire, si es la primera vez le pedirá que se registre y después de unos minutos debe ver algo similar a esto:

Tareas de fondo en aplicaciones ASP.NET MVC Core /posts/images/chrome_2017-04-27_21-49-34.png

Se puede ver que en este caso hay dos tareas recurrentes, una se ejecuta cada minuto y la otra cada dos minutos.

Eventualmente se puede ver más de un servidor activo. Esto ocurre porque las tareas en background corren en threads independientes y no se cierran inmediatamente al reiniciar la aplicación, pero el gestor de tareas de Hangfire se encarga de hacerlo después de un timeout (ver logs).

El archivo de log debe ser similar a este, pero ubicado en c:\temp\logs:

src\HangFireCore.WebApp\temp\nlog-HangFireCoreApp-current.log
[HangFireCoreWebApp-1.0]
 1 2 3 4 5 6 7 8 9101112131415
2017-04-27 21:44:33.9864|0|Hangfire.SqlServer.SqlServerStorage|INFO|  Start installing Hangfire SQL objects... |url: |action: 
2017-04-27 21:44:34.2044|0|Hangfire.SqlServer.SqlServerStorage|INFO|  Hangfire SQL objects installed. |url: |action: 
2017-04-27 21:44:34.2419|0|Hangfire.BackgroundJobServer|INFO|  Starting Hangfire Server |url: |action: 
2017-04-27 21:44:34.2504|0|Hangfire.BackgroundJobServer|INFO|  Using job storage: 'SQL Server: localhost@HangFireCoreWebApp' |url: |action: 
2017-04-27 21:44:34.2504|0|Hangfire.BackgroundJobServer|INFO|  Using the following options for SQL Server job storage: |url: |action: 
2017-04-27 21:44:34.2504|0|Hangfire.BackgroundJobServer|INFO|      Queue poll interval: 00:00:15. |url: |action: 
2017-04-27 21:44:34.2504|0|Hangfire.BackgroundJobServer|INFO|  Using the following options for Hangfire Server: |url: |action: 
2017-04-27 21:44:34.2504|0|Hangfire.BackgroundJobServer|INFO|      Worker count: 20 |url: |action: 
2017-04-27 21:44:34.2504|0|Hangfire.BackgroundJobServer|INFO|      Listening queues: 'default' |url: |action: 
2017-04-27 21:44:34.2504|0|Hangfire.BackgroundJobServer|INFO|      Shutdown timeout: 00:00:15 |url: |action: 
2017-04-27 21:44:34.2504|0|Hangfire.BackgroundJobServer|INFO|      Schedule polling interval: 00:00:15 |url: |action: 
2017-04-27 21:44:34.3054||ScheduleJobsHelpers|INFO|  Scheduling recurring jobs... |url: |action: 
2017-04-27 21:44:34.3054||ScheduleJobsHelpers|INFO|  Loading Job assembly: C:\Users\Miguel\Documents\Trabajo\Blog\coderepo.blog\repos\HangFireCoreWebApp\src\HangFireCore.WebApp\bin\Debug\netcoreapp1.1\HangFireCore.Job.One.dll |url: |action: 
2017-04-27 21:44:34.5750||ScheduleJobsHelpers|INFO|  Loading Job assembly: C:\Users\Miguel\Documents\Trabajo\Blog\coderepo.blog\repos\HangFireCoreWebApp\src\HangFireCore.WebApp\bin\Debug\netcoreapp1.1\HangFireCore.Job.Two.dll |url: |action: 
2017-04-27 21:45:00.7109||JobOne|INFO|  Executing Job One - 00:00:00.000 |url: |action: 


Espero que este artículo le haya resultado útil y le invito a darme su opinión en la sección de comentarios.

Gracias,

Miguel.

Enlaces relacionados

How to run Background Tasks in ASP.NET https://www.hanselman.com/blog/HowToRunBackgroundTasksInASPNET.aspx

The Dangers of Implementing Recurring Background Tasks In ASP.NET
http://haacked.com/archive/2011/10/16/the-dangers-of-implementing-recurring-background-tasks-in-asp-net.aspx

Integrate HangFire With ASP.NET Core
http://dotnetthoughts.net/integrate-hangfire-with-aspnet-core/