Hangfire Background Worker Manager¶
Hangfire is an advanced background jobs and worker manager. You can integrate Hangfire with the PSS®X to use it instead of the ABP default background worker manager.
Installation¶
If you want to install hangfire background manager;
-
Add the GridLab.Abp.Hangfire NuGet package to your project:
Install-Package GridLab.Abp.Hangfire
-
Add the
AbpHangfireModule
to the dependency list of your module:[DependsOn( //...other dependencies typeof(AbpHangfireModule) // <-- Add module dependency like that )] public class YourModule : AbpModule { }
-
Add a new section for hangfire settings.
{ "ConnectionStrings": { "Default": "Server=(localdb)\\MSSQLLocalDB;Database=pssx-app;Trusted_Connection=True;TrustServerCertificate=True", "Hangfire": "Server=(localdb)\\MSSQLLocalDB;Database=pssx-hangfire;Trusted_Connection=True;" }, "Logging": { "LogLevel": { "Default": "Information", "Microsoft": "Warning", "Microsoft.Hosting.Lifetime": "Information" } }, "Hangfire": { "ServerName": "pssx-api-host", "SchemaName": "" }, "AllowedHosts": "*" }
Hangfire background worker integration provides an adapter HangfirePeriodicBackgroundWorkerAdapter
to automatically load any PeriodicBackgroundWorkerBase
and AsyncPeriodicBackgroundWorkerBase
derived classes as IHangfireGenericBackgroundWorker
Configuration¶
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"] ?? "pssx-api-host";
string schemaName = configuration["Hangfire:SchemaName"] ?? string.Empty;
string? connectionString = configuration.GetConnectionString(PSSXDbProperties.ConnectionStringHangfireName);
if (connectionString.IsNullOrEmpty())
throw new ArgumentNullException(paramName: PSSXDbProperties.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<PSSXHangfireDbContext>()
.UseSqlServer(connectionString)
.Options;
GlobalConfiguration.Configuration.UseEFCoreStorage(
() => new PSSXHangfireDbContext(options, schemaName),
serverStorageOptions
);
Configure<HangfireOptions>(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 PSSXHangfireDbContextBase<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 PSSXHangfireDbContextBase(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, PSSXHangfireModelCacheKeyFactory>();
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
if (!string.IsNullOrEmpty(Schema))
modelBuilder.HasDefaultSchema(Schema);
modelBuilder.OnHangfireModelCreating();
}
}
The PSSXHangfireDbContext
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 PSSXHangfireDbContext
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(PSSXDbProperties.ConnectionStringHangfireName)]
public class PSSXHangfireDbContext : PSSXHangfireDbContextBase<PSSXHangfireDbContext>
{
public PSSXHangfireDbContext(DbContextOptions<PSSXHangfireDbContext> options, string? schema)
: base(options, schema)
{
}
}
The PSSXHangfireModelCacheKeyFactory
class customizes the model caching mechanism in EF Core for the PSSXHangfireDbContext
. 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 PSSXHangfireModelCacheKeyFactory : IModelCacheKeyFactory
{
public object Create(DbContext context, bool designTime)
=> context is PSSXHangfireDbContext hangfireContext
? (context.GetType(), hangfireContext.Schema, designTime)
: (object)context.GetType();
public object Create(DbContext context)
=> Create(context, false);
}
The PSSXDbProperties
static class provides a centralized and consistent way to manage database-related properties and constants.
public const string ConnectionStringHangfireName = "Hangfire";
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 PSSXHangfireContextDbSchemaMigrator
class is responsible for managing the database schema migrations for the Hangfire context.
public class PSSXHangfireContextDbSchemaMigrator : IContextDbSchemaMigrator, ITransientDependency
{
private readonly IServiceProvider _serviceProvider;
private readonly ILogger<PSSXHangfireContextDbSchemaMigrator> _logger;
public PSSXHangfireContextDbSchemaMigrator(IServiceProvider serviceProvider, ILogger<PSSXHangfireContextDbSchemaMigrator> 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(PSSXDbProperties.ConnectionStringHangfireName);
_logger.LogInformation($"Current connection string for hangfire database: {connectionString}");
var options = new DbContextOptionsBuilder<PSSXHangfireDbContext>()
.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 PSSXHangfireDbContext(options, schemaName);
{
string targetMigration = "Hangfire" + schemaName;
await dbContext.Database.GetService<IMigrator>().MigrateAsync(targetMigration);
}
}
}
else
{
using var dbContext = new PSSXHangfireDbContext(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 PSSXHangfireDbContext --output-dir Migrations\Hangfire -- "AuthServer"
In this case, the migration will be created with the schema name AuthServer.
-
Delete
PSSXHangfireDbContextModelSnapshot.cs
-
XXXX_HangfireAuthServer
migration file will be created in theMigrations\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; .... }
-
Run dotnet migration with schema name argument
dotnet ef migrations add HangfireApiHost --context PSSXHangfireDbContext --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 PSSXHangfireDbContext(options, schemaName); { string targetMigration = "Hangfire" + schemaName; await dbContext.Database.GetService<IMigrator>().MigrateAsync(targetMigration); } }
-
Add schema name to the
appsettings.json
inGridLab.PSSX.DbMigrator
project."Hangfire": { "Schemas": [ "AuthServer", "ApiHost" ] }