Skip to content

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: 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"] ?? "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

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.

  1. 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.

  2. Delete PSSXHangfireDbContextModelSnapshot.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 PSSXHangfireDbContext --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 PSSXHangfireDbContext(options, schemaName);
        {
            string targetMigration = "Hangfire" + schemaName;
            await dbContext.Database.GetService<IMigrator>().MigrateAsync(targetMigration);
        }
    }
    
  6. Add schema name to the appsettings.json in GridLab.PSSX.DbMigrator project.

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