Skip to content

GridLab Hangfire Background Worker Manager - Skills Guide

Skill Type: Background Processing & Job Scheduling
Technology Stack: .NET, ABP Framework, Hangfire, Entity Framework Core
Complexity Level: Advanced
Last Updated: 2024


📋 Overview

The GridLab Hangfire module provides enterprise-grade background job processing and scheduling capabilities, replacing ABP's default background worker manager with Hangfire's advanced features.

Key Capabilities

  • ✅ Advanced background job processing
  • ✅ Recurring job scheduling with CRON expressions
  • ✅ Job retry mechanisms and failure handling
  • ✅ Real-time monitoring dashboard
  • ✅ Multi-schema database support
  • ✅ Automatic worker discovery and loading
  • ✅ Unit of Work integration

Prerequisites

  • .NET 9.0 or higher
  • SQL Server or compatible database
  • NuGet package manager
  • Hangfire.SqlServer or Hangfire.EFCoreStorage

🚀 Quick Start

Step 1: Install NuGet Package

Install the GridLab.Abp.Hangfire package from NuGet:

Install-Package GridLab.Abp.Hangfire
Install-Package Hangfire.SqlServer  # or Hangfire.AspNetCore.EFCoreStorage

Package Information:

Step 2: Add Module Dependency

Add the AbpGridLabHangfireModule to your ABP module's dependency list:

[DependsOn(
    //...other dependencies
    typeof(AbpGridLabHangfireModule)
)]
public class YourModule : AbpModule
{
}

Step 3: Configure Settings

Add Hangfire configuration to appsettings.json:

{
  "ConnectionStrings": {
    "Default": "Server=(localdb)\\MSSQLLocalDB;Database=gmss-app;Trusted_Connection=True;TrustServerCertificate=True",
    "Hangfire": "Server=(localdb)\\MSSQLLocalDB;Database=gmss-hangfire;Trusted_Connection=True;"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "Hangfire": {
    "ServerName": "gmss-api-host",
    "SchemaName": "",
    "Schemas": []
  },
  "AllowedHosts": "*"
}

⚙️ Configuration

Automatic Worker Adapter

The Hangfire integration provides HangfirePeriodicBackgroundWorkerAdapter that automatically loads: - PeriodicBackgroundWorkerBase derived classes - AsyncPeriodicBackgroundWorkerBase derived classes

These are automatically registered as IHangfireGenericBackgroundWorker instances.

You can install any storage for Hangfire. The most common one is SQL Server (see the Hangfire.SqlServer NuGet package).

After you have installed these NuGet packages, you need to configure your project to use Hangfire.

First, we change the Module class (example: HttpApiHostModule) to add Hangfire configuration of the storage and connection string in the ConfigureServices method:

public override void ConfigureServices(ServiceConfigurationContext context)
{
   var configuration = context.Services.GetConfiguration();
   var hostingEnvironment = context.Services.GetHostingEnvironment();

   ...
   ConfigureHangfire(context, configuration);
}

private void ConfigureHangfire(ServiceConfigurationContext context, IConfiguration configuration)
{
    string serverName = configuration["Hangfire:ServerName"] ?? "gmss-api-host";
    string schemaName = configuration["Hangfire:SchemaName"] ?? string.Empty;

    string? connectionString = configuration.GetConnectionString(GmssDbProperties.ConnectionStringHangfireName);

    if (connectionString.IsNullOrEmpty())
        throw new ArgumentNullException(paramName: GmssDbProperties.ConnectionStringHangfireName);

    var serverStorageOptions = new EFCoreStorageOptions
    {
        CountersAggregationInterval = new TimeSpan(0, 5, 0),
        DistributedLockTimeout = new TimeSpan(0, 10, 0),
        JobExpirationCheckInterval = new TimeSpan(0, 30, 0),
        QueuePollInterval = new TimeSpan(0, 0, 15),
        Schema = schemaName,
        SlidingInvisibilityTimeout = new TimeSpan(0, 5, 0),
    };

    var options = new DbContextOptionsBuilder<GmssHangfireDbContext>()
        .UseSqlServer(connectionString)
        .Options;

    GlobalConfiguration.Configuration.UseEFCoreStorage(
         () => new GmssHangfireDbContext(options, schemaName),
         serverStorageOptions
    );

    Configure<AbpHangfireOptions>(options =>
    {
        options.Storage = EFCoreStorage.Current;
        options.ServerOptions = new BackgroundJobServerOptions() { ServerName = serverName };
    });
}

SQL Server storage implementation is available through the Hangfire.SqlServer NuGet package

public abstract class GmssHangfireDbContextBase<TDbContext> : AbpDbContext<TDbContext>
    where TDbContext : DbContext
{
    internal string Schema { get; }

    /*  Get a context referring SCHEMA1
     *  var context1 = new HangfireDbContext(options, "SCHEMA1");
     *  Get another context referring SCHEMA2
     *  var context1 = new HangfireDbContext(options, "SCHEMA2");
     */

    public GmssHangfireDbContextBase(DbContextOptions<TDbContext> options, string? schema)
        : base(options)
    {
        if (schema is null)
            throw new ArgumentNullException(nameof(schema));

        Schema = schema;
    }

    /* The DbContext type has a virtual OnConfiguring method which is designed to be overridden so that you can provide configuration information for the context via the method's DbContextOptionsBuilder parameter
     * The OnConfiguring method is called every time that an an instance of the context is created. 
     */

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        base.OnConfiguring(optionsBuilder);

        /* With the default implementation of IModelCacheKeyFactory the method OnModelCreating is executed only the first time the context is instantiated and then the result is cached.
         * Changing the implementation you can modify how the result of OnModelCreating is cached and retrieve.
         */
        optionsBuilder.ReplaceService<IModelCacheKeyFactory, GmssHangfireModelCacheKeyFactory>();
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);
        if (!string.IsNullOrEmpty(Schema))
            modelBuilder.HasDefaultSchema(Schema);

        modelBuilder.OnHangfireModelCreating();
    }
}

The GmssHangfireDbContext class is a specialized Entity Framework Core (EF Core) DbContext designed to integrate with Hangfire, a popular library for background job processing in .NET applications.

The constructor of GmssHangfireDbContext accepts a schema parameter, which allows the context to be configured to use a specific database schema. This is useful for multi-tenant applications or scenarios where different schemas are used to isolate data.

[ConnectionStringName(GmssDbProperties.ConnectionStringHangfireName)]
public class GmssHangfireDbContext : GmssHangfireDbContextBase<GmssHangfireDbContext>
{
    public GmssHangfireDbContext(DbContextOptions<GmssHangfireDbContext> options, string? schema)
        : base(options, schema)
    {

    }
}

The GmssHangfireModelCacheKeyFactory class customizes the model caching mechanism in EF Core for the GmssHangfireDbContext. By including the schema and design time flag in the cache key, it ensures that different schemas or configurations result in different cached models.

public class GmssHangfireModelCacheKeyFactory : IModelCacheKeyFactory
{
    public object Create(DbContext context, bool designTime)
       => context is GmssHangfireDbContext hangfireContext
           ? (context.GetType(), hangfireContext.Schema, designTime)
           : (object)context.GetType();

    public object Create(DbContext context)
        => Create(context, false);
}

The GmssDbProperties static class provides a centralized and consistent way to manage database-related properties and constants.

public const string ConnectionStringHangfireName = "Hangfire";

You can register services with client-only mode

Configure<AbpHangfireOptions>(options =>
{
    // When true, only the Hangfire client (enqueue/schedule) is registered.
    // No BackgroundJobServer will be started.
    options.ClientOnly = true;
});

In your consuming module or Program.cs / host module:

public override void PreConfigureServices(ServiceConfigurationContext context, IConfiguration configuration)
{
    var connectionString = configuration.GetConnectionString("HangfireConnection");

    // Configure storage (client needs this to enqueue jobs into the shared DB)
    PreConfigure<IGlobalConfiguration>(configuration =>
    {
        configuration
            .SetDataCompatibilityLevel(CompatibilityLevel.Version_180)
            .UseSimpleAssemblyNameTypeSerializer()
            .UseRecommendedSerializerSettings()
            .UsePostgreSqlStorage(options =>
                 options.UseNpgsqlConnection(connectionString));
    });

    // Enable client-only mode — no BackgroundJobServer will start
    Configure<AbpGridLabHangfireOptions>(options =>
    {
        options.ClientOnly = true;
    });
}

Now you can enqueue/schedule jobs from this app:

// Enqueue a fire-and-forget job
BackgroundJob.Enqueue<IMyEmailService>(x => x.SendWelcomeEmail("user@example.com"));

These jobs are written to the shared SQL database and will be picked up by whatever separate app is running the BackgroundJobServer (your "server" deployment that has ClientOnly = false or the default).

Create a Background Worker

HangfireGenericBackgroundWorkerBase is an easy way to create a background worker.

[ExposeServices(typeof(IMyWorker))]
public class MyWorker : HangfireGenericBackgroundWorkerBase, IMyWorker
{
    public static string Name = "My.Worker";

    protected IMySuperService MySuperService { get; }

    public MyWorker(IMySuperService mySuperService)
    {
        MySuperService = mySuperService;
    }

    public override async Task DoWorkAsync(params object[] arguments)
    {
        if (arguments != null)
        {
            string myArgument = arguments[0].ToString();

            await MySuperService.ExecuteAsync(myArgument);
        }
        else
        {
            throw new ArgumentException("My Worker has invalid arguments", nameof(arguments));
        }

        await Task.CompletedTask;
    }
}

You can directly implement the IHangfireGenericBackgroundWorker, but HangfireGenericBackgroundWorkerBase provides some useful properties like Logger, RecurringJobId and CronExpression

UnitOfWork

[ExposeServices(typeof(IMyWorker))]
public class MyWorker : HangfireGenericBackgroundWorkerBase, IMyWorker
{
    public static string Name = "My.Worker";

    protected IMySuperService MySuperService { get; }

    public MyWorker(IMySuperService mySuperService)
    {
        MySuperService = mySuperService;
        RecurringJobId = nameof(MyWorker);
        CronExpression = Cron.Daily();
    }

    public override async Task DoWorkAsync(params object[] arguments)
    {
        if (arguments != null)
        {
          using (var uow = LazyServiceProvider.LazyGetRequiredService<IUnitOfWorkManager>().Begin())
          {
              Logger.LogInformation("Executed My Worker..!");
          }
        }
        else
        {
            throw new ArgumentException("My Worker has invalid arguments", nameof(arguments));
        }

        await Task.CompletedTask;
    }
}

Register Background Worker Manager

After creating a background worker class, you should add it to the IBackgroundWorkerManager. The most common place is the OnApplicationInitializationAsync method of your module class:

[DependsOn(typeof(AbpBackgroundWorkersModule))]
public class YourModule : AbpModule
{
    public override async Task OnApplicationInitializationAsync(
        ApplicationInitializationContext context)
    {
        await context.AddBackgroundWorkerAsync<MyWorker>();
    }
}

context.AddBackgroundWorkerAsync(...) is a shortcut extension method for the expression below:

context.ServiceProvider
    .GetRequiredService<IBackgroundWorkerManager>()
    .AddAsync(
        context
            .ServiceProvider
            .GetRequiredService<MyWorker>()
    );

So, it resolves the given background worker and adds to the IBackgroundWorkerManager.

While we generally add workers in OnApplicationInitializationAsync, there are no restrictions on that. You can inject IBackgroundWorkerManager anywhere and add workers at runtime. Background worker manager will stop and release all the registered workers when your application is being shut down.

Hangfire Schema Support

The GmssHangfireContextDbSchemaMigrator class is responsible for managing the database schema migrations for the Hangfire context.

public class GmssHangfireContextDbSchemaMigrator : IContextDbSchemaMigrator, ITransientDependency
{
    private readonly IServiceProvider _serviceProvider;

    private readonly ILogger<GmssHangfireContextDbSchemaMigrator> _logger;

    public GmssHangfireContextDbSchemaMigrator(IServiceProvider serviceProvider, ILogger<GmssHangfireContextDbSchemaMigrator> logger)
    {
        _serviceProvider = serviceProvider;
        _logger = logger;
    }

    public async Task MigrateAsync(string? connectionString = null)
    {
        if(!_serviceProvider.GetRequiredService<ICurrentTenant>().IsAvailable)
        {
            _logger.LogInformation($"Host side is detected. Trying to create hangfire tables if not already available");

            var _configuration = _serviceProvider.GetRequiredService<IConfiguration>();

            if (connectionString == null)
                connectionString = _configuration.GetConnectionString(GmssDbProperties.ConnectionStringHangfireName);

            _logger.LogInformation($"Current connection string for hangfire database: {connectionString}");

            var options = new DbContextOptionsBuilder<GmssHangfireDbContext>()
                .UseSqlServer(connectionString)
                .Options;

            var schemaNames = _configuration.GetSection("Hangfire:Schemas").Get<string[]>();

            if (schemaNames != null && schemaNames.Length > 0)
            {
                foreach (var schemaName in schemaNames!)
                {
                    using var dbContext = new GmssHangfireDbContext(options, schemaName);
                    {
                        string targetMigration = "Hangfire" + schemaName;
                        await dbContext.Database.GetService<IMigrator>().MigrateAsync(targetMigration);
                    }
                }
            }
            else
            {
                using var dbContext = new GmssHangfireDbContext(options, string.Empty);
                {
                    await dbContext.Database.MigrateAsync();
                }
            }
        }
    }
}

It ensures that the necessary tables and schema configurations are created and updated, supports multi-tenancy, and provides detailed logging of the migration process.

  1. Run dotnet migration with schema name argument

    dotnet ef migrations add HangfireAuthServer --context GmssHangfireDbContext --output-dir Migrations\Hangfire -- "AuthServer"
    

    In this case, the migration will be created with the schema name AuthServer.

  2. Delete GmssHangfireDbContextModelSnapshot.cs

  3. XXXX_HangfireAuthServer migration file will be created in the Migrations\Hangfire folder with the schema name AuthServer.

    Add return statement in the Up method to avoid the migration script execution.

    protected override void Up(MigrationBuilder migrationBuilder)
    {
      return;
      ....
    }
    
  4. Run dotnet migration with schema name argument

    dotnet ef migrations add HangfireApiHost --context GmssHangfireDbContext --output-dir Migrations\Hangfire -- "ApiHost"
    

    In this case, the migration will be created with the schema name ApiHost.

  5. Target migration names must match with the schema name at PSXHangfireContextDbSchemaMigrator service.

    foreach (var schemaName in schemaNames!)
    {
        using var dbContext = new GmssHangfireDbContext(options, schemaName);
        {
            string targetMigration = "Hangfire" + schemaName;
            await dbContext.Database.GetService<IMigrator>().MigrateAsync(targetMigration);
        }
    }
    
  6. Add schema names to the appsettings.json in GridLab.Gmss.DbMigrator project.

    "Hangfire": {
      "Schemas": [
        "AuthServer",
        "ApiHost"
      ]
    }
    

📚 Best Practices

✅ Do's

  • Use separate database for Hangfire to avoid contention
  • Implement idempotent job logic (safe to retry)
  • Use background jobs for long-running operations
  • Set appropriate retry policies
  • Monitor job execution through Hangfire Dashboard
  • Use recurring jobs for scheduled tasks
  • Implement proper logging in workers

❌ Don'ts

  • Don't use Hangfire for real-time processing
  • Don't store large objects in job arguments
  • Don't forget to handle exceptions in workers
  • Don't block the main thread with synchronous jobs
  • Don't use the same database for app and Hangfire in production
  • Don't ignore failed job notifications

🔍 Troubleshooting

Common Issues

Issue: Jobs not executing

  • Solution: Verify Hangfire server is running
  • Solution: Check database connection string
  • Solution: Ensure schema migrations are applied

Issue: Database migration errors

  • Solution: Delete GmssHangfireDbContextModelSnapshot.cs after each migration
  • Solution: Verify schema names match between migrations and configuration
  • Solution: Add return; statement in Up method of intermediate migrations

Issue: Schema-specific issues

  • Solution: Ensure target migration names match "Hangfire" + schemaName pattern
  • Solution: Verify schemas are listed in appsettings.json

📖 Additional Resources