Search code examples
.net.net-6.0hangfire

HangFire as Windows Service for .NET 6


I've been searching for using HangFire as a Windows Service for .NET 6, the official document is like 10 years old. Other samples don't specify how to set up the worker service. Anyway, this is my environment -- I have a web app with api app. The api app is where background jobs will be queued to HangFire, but I'd like the actual processing to be on a different server like an app server. So my goal is to create a windows service to simply run HangFire server and continue to let api app to manage job creations.

I've created a new Worker Service project and here is my code:

public class Program
{
    public static void Main(string[] args) => CreateHostBuilder(args).Build().Run();

    public static IHostBuilder CreateHostBuilder(string[] args) =>
       Host.CreateDefaultBuilder(args)
           .ConfigureLogging(logging =>
           {
               logging.ClearProviders();
               logging.AddConsole();
               logging.AddEventLog();
           })
           // Essential to run this as a window service
           .UseWindowsService()
           .ConfigureServices(configureServices);

    private static void configureServices(HostBuilderContext context, IServiceCollection services)
    {
        var defaultConnection = context.Configuration.GetConnectionString("DefaultConnection");
        var hangFireConnection = context.Configuration.GetConnectionString("HangFireConnection");
        AppSettings appSettings = context.Configuration.GetSection("AppSettings").Get<AppSettings>();

        services.AddLogging();
        services.AddHangfire(configuration => configuration
            .SetDataCompatibilityLevel(CompatibilityLevel.Version_170)
            .UseSimpleAssemblyNameTypeSerializer()
            .UseRecommendedSerializerSettings()
            .UseSqlServerStorage(hangFireConnection, new SqlServerStorageOptions
            {
                CommandBatchMaxTimeout = TimeSpan.FromMinutes(5),
                SlidingInvisibilityTimeout = TimeSpan.FromMinutes(5),
                QueuePollInterval = TimeSpan.Zero,
                UseRecommendedIsolationLevel = true,
                DisableGlobalLocks = true
            }));
        services.AddHangfireServer();

        services.AddDbContext<PpContext>(options => options.UseSqlServer(defaultConnection), ServiceLifetime.Transient);
        services.AddScoped<ExceptionNotifier>();
        services.AddHostedService<HangFireWorker>();


        JobStorage.Current = new SqlServerStorage(hangFireConnection);
        RecurringJob.AddOrUpdate<ExceptionNotifier>("exception-notification", x => x.NotifyByHour(), "0 * * * *"); //runs every hour on the hour
    }
}

As you can see, I do have one recurring job that occurs every hour, on the hour.

Then for the class HangFireWorker, this is what I have:

public class HangFireWorker : BackgroundService
{
    private readonly ILogger<HangFireWorker> _logger;

    public HangFireWorker(ILogger<HangFireWorker> logger)
    {
        _logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        //while (!stoppingToken.IsCancellationRequested)
        //{
        //    _logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
        //    await Task.Delay(1000, stoppingToken);
        //}


        //there is nothing to do here, hangfire already takes cares of all?
        await Task.Delay(0);
    }
}

So my question is, do I need to do anything in the main worker class? In ExecuteAsync() function? I mean the project works just fine as is right now. I see the server is registered successfully in the dashboard (the api app). It seems odd that I just have an empty worker class.

Any advice is appreciated.


Solution

  • You don't need that empty worker class. Simply doing a call to AddHangfireServer will handle creating a worker.

    You can actually see that the server is registered if you open the source of AddHangfireServer:

    public static IServiceCollection AddHangfireServer(
      [NotNull] this IServiceCollection services,
      [NotNull] Action<IServiceProvider, BackgroundJobServerOptions> optionsAction)
    {
      if (services == null)
        throw new ArgumentNullException(nameof (services));
      return optionsAction != null ? HangfireServiceCollectionExtensions.AddHangfireServerInner(services, (JobStorage) null, (IEnumerable<IBackgroundProcess>) null, optionsAction) : throw new ArgumentNullException(nameof (optionsAction));
    }
    
    ...
    
    private static IServiceCollection AddHangfireServerInner(
      [NotNull] IServiceCollection services,
      [CanBeNull] JobStorage storage,
      [CanBeNull] IEnumerable<IBackgroundProcess> additionalProcesses,
      [NotNull] Action<IServiceProvider, BackgroundJobServerOptions> optionsAction)
    {
      services.AddTransient<IHostedService, BackgroundJobServerHostedService>((Func<IServiceProvider, BackgroundJobServerHostedService>) (provider =>
      {
        BackgroundJobServerOptions options = new BackgroundJobServerOptions();
        optionsAction(provider, options);
        return HangfireServiceCollectionExtensions.CreateBackgroundJobServerHostedService(provider, storage, additionalProcesses, options);
      }));
      return services;
    }
    
    ...
    
    private static BackgroundJobServerHostedService CreateBackgroundJobServerHostedService(
      IServiceProvider provider,
      JobStorage storage,
      IEnumerable<IBackgroundProcess> additionalProcesses,
      BackgroundJobServerOptions options)
    {
      HangfireServiceCollectionExtensions.ThrowIfNotConfigured(provider);
      storage = storage ?? provider.GetService<JobStorage>() ?? JobStorage.Current;
      additionalProcesses = additionalProcesses ?? provider.GetServices<IBackgroundProcess>();
      options.Activator = options.Activator ?? provider.GetService<JobActivator>();
      options.FilterProvider = options.FilterProvider ?? provider.GetService<IJobFilterProvider>();
      options.TimeZoneResolver = options.TimeZoneResolver ?? provider.GetService<ITimeZoneResolver>();
      IBackgroundJobFactory factory;
      IBackgroundJobStateChanger stateChanger;
      IBackgroundJobPerformer performer;
      HangfireServiceCollectionExtensions.GetInternalServices(provider, out factory, out stateChanger, out performer);
      IHostApplicationLifetime service = provider.GetService<IHostApplicationLifetime>();
      return new BackgroundJobServerHostedService(storage, options, additionalProcesses, factory, performer, stateChanger, service);
    }
    
    ...
    
    public class BackgroundJobServerHostedService : IHostedService, IDisposable
    {
    

    AddHangfireServer will simply register a BackgroundJobServerHostedService and it will handle everything.

    A good strategy to figure stuff like this out is to drill down into the actual source code sometime.

    This is also kindof documented on the Hangfire.AspNetCore package's github:

    Process background tasks inside a web application…

    You can process background tasks in any OWIN-compatible application framework, including ASP.NET MVC, ASP.NET Web API, FubuMvc, Nancy, etc. Forget about AppDomain unloads, Web Garden & Web Farm issues – Hangfire is reliable for web applications from scratch, even on shared hosting.

    app.UseHangfireServer(); … or anywhere else

    In console applications, Windows Service, Azure Worker Role, etc.

    using (new BackgroundJobServer())
    {
        Console.WriteLine("Hangfire Server started. Press ENTER to exit...");
        Console.ReadLine();
    }
    

    https://github.com/HangfireIO/Hangfire