docs/en/framework/infrastructure/background-jobs/index.md
//[doc-seo]
{
"Description": "Learn how to implement background jobs in ABP Framework for efficient task execution, ensuring persistence and reliability in your applications."
}
Background jobs are used to queue some tasks to be executed in the background. You may need background jobs for several reasons. Here are some examples:
Background jobs are persistent that means they will be re-tried and executed later even if your application crashes.
ABP provides an abstraction module and several implementations for background jobs. It has a built-in/default implementation as well as Hangfire, RabbitMQ and Quartz integrations.
Volo.Abp.BackgroundJobs.Abstractions NuGet package provides needed services to create background jobs and queue background job items. If your module only depend on this package, it can be independent from the actual implementation/integration.
Volo.Abp.BackgroundJobs.Abstractionspackage is installed to the startup templates by default.
A background job is a class that implements the IBackgroundJob<TArgs> interface or derives from the BackgroundJob<TArgs> class. TArgs is a simple plain C# class to store the job data.
This example is used to send emails in background. First, define a class to store arguments of the background job:
namespace MyProject
{
public class EmailSendingArgs
{
public string EmailAddress { get; set; }
public string Subject { get; set; }
public string Body { get; set; }
}
}
Then create a background job class that uses an EmailSendingArgs object to send an email:
using System.Threading.Tasks;
using Volo.Abp.BackgroundJobs;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Emailing;
namespace MyProject
{
public class EmailSendingJob
: AsyncBackgroundJob<EmailSendingArgs>, ITransientDependency
{
private readonly IEmailSender _emailSender;
public EmailSendingJob(IEmailSender emailSender)
{
_emailSender = emailSender;
}
public override async Task ExecuteAsync(EmailSendingArgs args)
{
await _emailSender.SendAsync(
args.EmailAddress,
args.Subject,
args.Body
);
}
}
}
This job simply uses IEmailSender to send emails (see email sending document).
AsyncBackgroundJobis used to create a job needs to perform async calls. You can inherit fromBackgroundJob<TJob>and override theExecutemethod if the method doesn't need to perform any async call.
A background job should not hide exceptions. If it throws an exception, the background job is automatically re-tried after a calculated waiting time. Hide exceptions only if you don't want to re-run the background job for the current argument.
If your background task is cancellable, then you can use the standard Cancellation Token system to obtain a CancellationToken to cancel your job when requested. See the following example that uses the ICancellationTokenProvider to obtain the cancellation token:
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Threading;
namespace MyProject
{
public class LongRunningJob : AsyncBackgroundJob<LongRunningJobArgs>, ITransientDependency
{
private readonly ICancellationTokenProvider _cancellationTokenProvider;
public LongRunningJob(ICancellationTokenProvider cancellationTokenProvider)
{
_cancellationTokenProvider = cancellationTokenProvider;
}
public override async Task ExecuteAsync(LongRunningJobArgs args)
{
foreach (var id in args.Ids)
{
_cancellationTokenProvider.Token.ThrowIfCancellationRequested();
await ProcessAsync(id); // code omitted for brevity
}
}
}
}
A cancellation operation might be needed if the application is shutting down and we don't want to block the application in the background job. This example throws an exception if the cancellation is requested. So, the job will be retried the next time the application starts. If you don't want that, just return from the
ExecuteAsyncmethod without throwing any exception (you can simply check the_cancellationTokenProvider.Token.IsCancellationRequestedproperty).
Each background job has a name. Job names are used in several places. For example, RabbitMQ provider uses job names to determine the RabbitMQ Queue names.
Job name is determined by the job argument type. For the EmailSendingArgs example above, the job name is MyProject.EmailSendingArgs (full name, including the namespace). You can use the BackgroundJobName attribute to set a different job name.
Example
using Volo.Abp.BackgroundJobs;
namespace MyProject
{
[BackgroundJobName("emails")]
public class EmailSendingArgs
{
public string EmailAddress { get; set; }
public string Subject { get; set; }
public string Body { get; set; }
}
}
You can configure GetBackgroundJobName delegate of the AbpBackgroundJobOptions to change the default job name.
Configure<AbpBackgroundJobOptions>(options =>
{
options.GetBackgroundJobName = (jobType) =>
{
if (jobType == typeof(EmailSendingArgs))
{
return "emails";
}
return BackgroundJobNameAttribute.GetName(jobType);
};
});
Now, you can queue an email sending job using the IBackgroundJobManager service:
public class RegistrationService : ApplicationService
{
private readonly IBackgroundJobManager _backgroundJobManager;
public RegistrationService(IBackgroundJobManager backgroundJobManager)
{
_backgroundJobManager = backgroundJobManager;
}
public async Task RegisterAsync(string userName, string emailAddress, string password)
{
//TODO: Create new user in the database...
await _backgroundJobManager.EnqueueAsync(
new EmailSendingArgs
{
EmailAddress = emailAddress,
Subject = "You've successfully registered!",
Body = "..."
}
);
}
}
Just injected IBackgroundJobManager service and used its EnqueueAsync method to add a new job to the queue.
Enqueue method gets some optional arguments to control the background job:
BackgroundJobPriority enum which has Low, BelowNormal, Normal (default), AboveNormal and Hight fields.TimeSpan) before first try.You may want to disable background job execution for your application. This is generally needed if you want to execute background jobs in another process and disable it for the current process.
Use AbpBackgroundJobOptions to configure the job execution:
[DependsOn(typeof(AbpBackgroundJobsModule))]
public class MyModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
Configure<AbpBackgroundJobOptions>(options =>
{
options.IsJobExecutionEnabled = false; //Disables job execution
});
}
}
ABP includes a simple IBackgroundJobManager implementation that;
Volo.Abp.BackgroundJobsnuget package contains the default background job manager and it is installed to the startup templates by default.
Use AbpBackgroundJobWorkerOptions in your module class to configure the default background job manager. The example below changes the timeout duration for background jobs:
[DependsOn(typeof(AbpBackgroundJobsModule))]
public class MyModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
Configure<AbpBackgroundJobWorkerOptions>(options =>
{
options.DefaultTimeout = 864000; //10 days (as seconds)
});
}
}
JobPollPeriod is used to determine the interval between two job polling operations. Default is 5000 ms (5 seconds).MaxJobFetchCount is used to determine the maximum job count to fetch in a single polling operation. It is also used as the batch size for the retention cleanup deletions. Default is 1000.DefaultFirstWaitDuration is used to determine the duration to wait before the first retry. Default is 60 seconds.DefaultTimeout is used to determine the timeout duration for a job. Default is 172800 seconds (2 days).DefaultWaitFactor is used to determine the factor to increase the wait duration between retries. Default is 2.0.DistributedLockName is used to determine the distributed lock name to use. Default is AbpBackgroundJobWorker.StoreSuccessfulJobs is used to determine whether to keep successfully completed jobs in the store instead of deleting them. Default is false. See the Storing Successful Jobs section.SuccessfulJobRetentionTime is used to determine how long a kept job is retained before the cleanup deletes it. Default is 7 days. Set to null to keep completed jobs forever. Only relevant when StoreSuccessfulJobs is enabled.CleanSuccessfulJobsPeriod is used to determine the interval between cleanup runs that delete expired completed jobs. Default is 3600000 ms (1 hour).CleanupDistributedLockName is used to determine the distributed lock name for the cleanup worker. Default is AbpBackgroundJobCleanup.MaxParallelJobExecutionCount is used to determine the maximum number of jobs a worker executes in parallel within one poll cycle. Default is 1. See the Parallel Job Execution section.PerJobDistributedLockPrefix is used to determine the prefix of the per-job distributed lock name used when MaxParallelJobExecutionCount is greater than 1. Default is AbpBackgroundJob:.By default, the background job manager deletes a job from the store as soon as it runs successfully. If you want to keep completed jobs (for auditing or history), enable StoreSuccessfulJobs:
Configure<AbpBackgroundJobWorkerOptions>(options =>
{
options.StoreSuccessfulJobs = true;
options.SuccessfulJobRetentionTime = TimeSpan.FromDays(30); //null to keep forever
});
When enabled, a successful job is not deleted; instead its CompletionTime is set and it stays in the store. Completed jobs are excluded from the waiting jobs query, so they are not executed again. A cleanup worker periodically deletes completed jobs older than SuccessfulJobRetentionTime.
Note: The
IBackgroundJobStoreinterface has new overloads (aGetWaitingJobsAsyncoverload that takes a job name filter and aDeleteAsyncoverload for cleanup). If you have a customIBackgroundJobStoreimplementation, you must implement them for your code to compile. The built-in stores already implement them.
By default, a single worker processes all job types. If you want to process certain job types separately (for example, slow or high-volume jobs), you can register dedicated workers, each handling only the specified job argument types with its own distributed lock:
Configure<AbpBackgroundJobWorkerOptions>(options =>
{
options.AddDedicatedWorker<EmailJobArgs, SmsJobArgs>("NotificationWorkerLock");
options.AddDedicatedWorker<ReportJobArgs>("ReportWorkerLock");
});
Each dedicated worker processes only its configured job types. An additional default worker is automatically started to process all the remaining job types. In sequential mode, each worker (including the default one) runs independently under its own distributed lock (see Parallel Job Execution for how this changes when running jobs in parallel).
If you don't want to specify a lock name, use the overloads without the lockName parameter; a stable, length-bounded lock name is then derived from the job argument types:
Configure<AbpBackgroundJobWorkerOptions>(options =>
{
options.AddDedicatedWorker<EmailJobArgs, SmsJobArgs>();
options.AddDedicatedWorker<ReportJobArgs>();
});
Note: Each job type can be handled by only one dedicated worker, and each worker must have a unique lock name;
AddDedicatedWorkerthrows if this is violated. Dedicated workers require anIBackgroundJobStorethat can filter jobs by name (the built-in stores can).
By default, a worker executes waiting jobs one by one under a single worker-level distributed lock, so only one job runs at a time across all application instances. If you want to execute multiple jobs concurrently, set MaxParallelJobExecutionCount to a value greater than 1:
Configure<AbpBackgroundJobWorkerOptions>(options =>
{
options.MaxParallelJobExecutionCount = 4;
});
When it is greater than 1, the worker-level lock is not used. Instead, each job is claimed with its own distributed lock, so multiple application instances can execute different jobs at the same time. With a properly configured distributed lock provider, a job is not executed by more than one instance at a time.
MaxParallelJobExecutionCount is a per-worker, per-poll-cycle limit — it is not a cluster-wide limit. A worker first fetches up to MaxJobFetchCount waiting jobs, then executes up to MaxParallelJobExecutionCount of them in parallel, so a single worker runs up to min(MaxJobFetchCount, MaxParallelJobExecutionCount) jobs per cycle; keep MaxJobFetchCount at least as large as MaxParallelJobExecutionCount to avoid capping the parallelism. When you also configure dedicated workers, each worker runs its own timer and claims up to MaxParallelJobExecutionCount jobs, so the effective concurrency is up to (number of workers) × MaxParallelJobExecutionCount per application instance, and up to (number of application instances) × (number of workers) × MaxParallelJobExecutionCount across the whole cluster.
Important: Configure
MaxParallelJobExecutionCountandPerJobDistributedLockPrefixconsistently across all application instances. Mixing sequential (worker lock) and parallel (per-job lock) instances removes the common mutual exclusion, and a different prefix produces a different per-job lock name for the same job — either case may let the same job run on more than one instance. As with the sequential mode, configure a real distributed lock provider for clustered deployments.
The default background job manager needs a data store to save and read jobs. It defines IBackgroundJobStore as an abstraction to store the jobs.
Background Jobs module implements IBackgroundJobStore using various data access providers. See its own documentation. If you don't want to use this module, you should implement the IBackgroundJobStore interface yourself.
Background Jobs module is already installed to the startup templates by default and it works based on your ORM/data access choice.
If multiple applications share the same storage for background jobs and workers (Default, Hangfire, RabbitMQ, and Quartz), you should configure the provider options to use the application name for isolation.
Set ApplicationName property in AbpBackgroundJobWorkerOptions to your application's name:
public override void ConfigureServices(ServiceConfigurationContext context)
{
Configure<AbpBackgroundJobWorkerOptions>(options =>
{
options.ApplicationName = context.Services.GetApplicationName()!;
});
}
Set DefaultQueuePrefix property in AbpHangfireOptions to your application's name:
public override void ConfigureServices(ServiceConfigurationContext context)
{
Configure<AbpHangfireOptions>(options =>
{
options.DefaultQueuePrefix = context.Services.GetApplicationName()!;
});
}
Set the quartz.scheduler.instanceName property to your application's name:
public override void PreConfigureServices(ServiceConfigurationContext context)
{
var configuration = context.Services.GetConfiguration();
PreConfigure<AbpQuartzOptions>(options =>
{
options.Properties = new NameValueCollection
{
["quartz.scheduler.instanceName"] = context.Services.GetApplicationName(),
["quartz.jobStore.dataSource"] = "BackgroundJobsDemoApp",
["quartz.jobStore.type"] = "Quartz.Impl.AdoJobStore.JobStoreTX, Quartz",
["quartz.jobStore.tablePrefix"] = "QRTZ_",
["quartz.serializer.type"] = "json",
["quartz.dataSource.BackgroundJobsDemoApp.connectionString"] = configuration.GetConnectionString("Default"),
["quartz.dataSource.BackgroundJobsDemoApp.provider"] = "SqlServer",
["quartz.jobStore.driverDelegateType"] = "Quartz.Impl.AdoJobStore.SqlServerDelegate, Quartz",
};
});
}
Set DefaultQueueNamePrefix and DefaultDelayedQueueNamePrefix properties in AbpRabbitMqBackgroundJobOptions to your application's name:
public override void ConfigureServices(ServiceConfigurationContext context)
{
Configure<AbpRabbitMqBackgroundJobOptions>(options =>
{
options.DefaultQueueNamePrefix = context.Services.GetApplicationName()!.EnsureEndsWith('.') + options.DefaultQueueNamePrefix;
options.DefaultDelayedQueueNamePrefix = context.Services.GetApplicationName()!.EnsureEndsWith('.') + options.DefaultDelayedQueueNamePrefix;
});
}
The default background job manager is compatible with clustered environments (where multiple instances of your application run concurrently). It uses a distributed lock to ensure that the jobs are executed only in a single application instance at a time.
However, the distributed lock system works in-process by default. That means it is not distributed actually, unless you configure a distributed lock provider. So, please follow the distributed lock document to configure a provider for your application, if it is not already configured.
If you don't want to use a distributed lock provider, you may go with the following options:
AbpBackgroundJobOptions.IsJobExecutionEnabled to false as explained in the Disable Job Execution section) in all application instances except one of them, so only the single instance executes the jobs (while other application instances can still queue jobs).AbpBackgroundJobOptions.IsJobExecutionEnabled to false as explained in the Disable Job Execution section) in all application instances and create a dedicated application (maybe a console application running in its own container or a Windows Service running in the background) to execute all the background jobs. This can be a good option if your background jobs consume high system resources (CPU, RAM or Disk), so you can deploy that background application to a dedicated server and your background jobs don't affect your application's performance.ABP provides IDynamicBackgroundJobManager for scenarios where you need to enqueue jobs by name at runtime, without requiring a strongly-typed job args class at compile time. This is useful for plugin systems, dynamic workflows, or any case where job types are not known ahead of time.
If a typed job is already registered (e.g., via [BackgroundJobName("emails")]), you can enqueue it by name:
public class MyService : ApplicationService
{
private readonly IDynamicBackgroundJobManager _dynamicJobManager;
public MyService(IDynamicBackgroundJobManager dynamicJobManager)
{
_dynamicJobManager = dynamicJobManager;
}
public async Task DoSomethingAsync()
{
await _dynamicJobManager.EnqueueAsync("emails", new
{
EmailAddress = "[email protected]",
Subject = "Hello",
Body = "World"
});
}
}
The IDynamicBackgroundJobManager will look up the typed job configuration, deserialize the args to the expected type, and enqueue through the standard typed pipeline.
You can also register dynamic handlers at runtime for jobs that don't have a pre-defined typed job class:
public override void OnApplicationInitialization(ApplicationInitializationContext context)
{
var dynamicJobManager = context.ServiceProvider
.GetRequiredService<IDynamicBackgroundJobManager>();
dynamicJobManager.RegisterHandler("ProcessOrder", async (context, ct) =>
{
var json = context.JsonData;
var serviceProvider = context.ServiceProvider;
// Process the order using JsonData and resolved services...
});
}
Then enqueue jobs using the registered name:
await _dynamicJobManager.EnqueueAsync("ProcessOrder", new
{
OrderId = "ORD-001",
Amount = 99.99
});
// Check if a handler is registered
bool exists = _dynamicJobManager.IsHandlerRegistered("ProcessOrder");
// Unregister a handler
bool removed = _dynamicJobManager.UnregisterHandler("ProcessOrder");
IBackgroundJobManager.EnqueueAsync<TArgs>.DynamicBackgroundJobArgs (a public transport type used internally by the framework) and enqueued through IBackgroundJobManager.EnqueueAsync<DynamicBackgroundJobArgs>. When the job executes, the framework looks up the handler by name and invokes it.Note: If the job name matches both a registered typed job configuration and a dynamic handler, the typed job takes priority and the dynamic handler is ignored. To avoid confusion, use distinct names for dynamic handlers that do not conflict with existing typed job names.
Important: Dynamic job handlers are stored in memory only and are not persisted across application restarts. When using a persistent provider (Hangfire, Quartz, RabbitMQ, TickerQ), enqueued jobs survive a restart but if no handler is re-registered, the job executor will throw an exception when the job is picked up. To ensure handlers are always available, register them in
OnApplicationInitializationso they are re-registered on every startup.
Background job system is extensible and you can change the default background job manager with your own implementation or on of the pre-built integrations.
See pre-built job manager alternatives: