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:
- Package Name: GridLab.Abp.Hangfire
- Repository: GitLab - GridLab ABP Framework
- NuGet: GridLab.Abp.Hangfire
- Official Site: Hangfire.io
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: 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
-
RecurringJobId Is an optional parameter, see Hangfire document
-
CronExpression Is a CRON expression, see CRON expression
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.
-
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.
-
Delete
GmssHangfireDbContextModelSnapshot.cs -
XXXX_HangfireAuthServermigration file will be created in theMigrations\Hangfirefolder 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; .... } -
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.
-
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); } } -
Add schema names to the
appsettings.jsoninGridLab.Gmss.DbMigratorproject."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.csafter 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" + schemaNamepattern - Solution: Verify schemas are listed in
appsettings.json