It is possible to run recurring code using a recurring background job. Background job classes should implement the IRecurringBackgroundJob interface, which controls when and where the job gets run.
Once you've created your background job class, register it using a Composer. It will be detected at startup and a new HostedService will be created to run your job.
Be aware you may or may not want this background job to run on all servers. If you are using Load Balancing with multiple servers, see load balancing documentation for more information
IRecurringBackgroundJob
Period - this property is a TimeSpan that tells Umbraco how often to run your job.
// Run this job every 5 minutesTimeSpan Period =TimeSpan.FromMinutes(5);
Delay - this property is also a TimeSpan that tells Umbraco how long to wait after starting up before running the task for the first time. The default is 3 minutes.
// Wait 3 minutes after application startup before starting to run this job.TimeSpan Delay =TimeSpan.FromMinutes(3);
ServerRoles - a list roles that should run this job. In a multi-server setup you may want your job to run on all servers, or only on one of your servers. For example, a temporary file cleanup task might need to run on all servers. A database import job might be better to be run once per day on a single server.
The default value is: { Single, SchedulingPublisher }, which will cause the job to only run on one server.
// Run this job on ALL serversServerRole[] ServerRoles =Enum.GetValues<ServerRole>();
For more information about server roles, see the Load Balancing documentation.
PeriodChanged - an event you can trigger to tell the background job service that the period has changed. For example, if the period for your job is controlled by a configuration file setting.
// No-op event as the period never changes on this jobpublic event EventHandler PeriodChanged { add { } remove { } }
See below for a full example of how to implement the PeriodChanged event.
RunJobAsync() - the main method of your job.
publicTaskRunJobAsync() { // your job code goes here}
Minimal example
This example shows the minimum code necessary to implement the IRecurringBackgroundJob interface. The job runs every 60 minutes on all servers.
usingUmbraco.Cms.Core;usingUmbraco.Cms.Core.Logging;usingUmbraco.Cms.Core.Scoping;usingUmbraco.Cms.Core.Services;usingUmbraco.Cms.Core.Sync;usingUmbraco.Cms.Infrastructure.BackgroundJobs;namespaceUmbraco.Docs.Samples.Web.RecurringBackgroundJob;publicclassCleanUpYourRoom:IRecurringBackgroundJob{publicTimeSpan Period { get=>TimeSpan.FromMinutes(60); } // Runs on all serverspublicServerRole[] ServerRoles { get=>Enum.GetValues<ServerRole>(); } // No-op event as the period never changes on this jobpubliceventEventHandler PeriodChanged { add { } remove { } }publicasyncTaskRunJobAsync() { // YOUR CODE GOES HERE }}
Example with dependency injection
This example shows how to inject other Umbraco services into your background job. This example cleans the recycle bin every 60 minutes. In order to do so it injects an an IContentService to access the Recycle bin, and an IScopeProvider to provide an ambient scope for the EmptyRecycleBin method.
usingUmbraco.Cms.Core;usingUmbraco.Cms.Core.Logging;usingUmbraco.Cms.Core.Scoping;usingUmbraco.Cms.Core.Services;usingUmbraco.Cms.Core.Sync;usingUmbraco.Cms.Infrastructure.BackgroundJobs;namespaceUmbraco.Docs.Samples.Web.RecurringBackgroundJob;publicclassCleanUpYourRoom:IRecurringBackgroundJob{publicTimeSpan Period { get=>TimeSpan.FromMinutes(60); } // Runs on all serverspublicServerRole[] ServerRoles { get=>Enum.GetValues<ServerRole>(); } // No-op event as the period never changes on this jobpubliceventEventHandler PeriodChanged { add { } remove { } }privatereadonlyIContentService _contentService;privatereadonlyICoreScopeProvider _scopeProvider;publicCleanUpYourRoom(IContentService contentService,ICoreScopeProvider scopeProvider) { _contentService = contentService; _scopeProvider = scopeProvider; }publicTaskRunJobAsync() { // Wrap the three content service calls in a scope to do it all in one transaction.usingICoreScope scope =_scopeProvider.CreateCoreScope();int numberOfThingsInBin =_contentService.CountChildren(Constants.System.RecycleBinContent);if (_contentService.RecycleBinSmells()) {_contentService.EmptyRecycleBin(userId:-1); } // Remember to complete the scope when done.scope.Complete();returnTask.CompletedTask; }}
Complex example
The complex example builds on the previous one by injecting additional services. It includes a logger to log error messages, a profiler to capture timings, and an IServerRoleAccessor to log the current server role. Additionally, it injects an IOptionsMonitor to allow the period to be updated while the server is running. It also demonstrates how to trigger the PeriodChanged event to signal the job's host.
usingUmbraco.Cms.Core;usingUmbraco.Cms.Core.Logging;usingUmbraco.Cms.Core.Scoping;usingUmbraco.Cms.Core.Services;usingUmbraco.Cms.Core.Sync;usingUmbraco.Cms.Infrastructure.BackgroundJobs;namespaceUmbraco.Docs.Samples.Web.RecurringBackgroundJob;publicclassCleanUpYourRoom:IRecurringBackgroundJob{publicTimeSpan Period { get; set; } =TimeSpan.FromMinutes(60); // Runs on all serverspublicServerRole[] ServerRoles { get=>Enum.GetValues<ServerRole>(); } //privateeventEventHandler? _periodChanged;publiceventEventHandler PeriodChanged {add { _periodChanged += value; }remove { _periodChanged -= value; } }privatereadonlyIContentService _contentService;privatereadonlyIServerRoleAccessor _serverRoleAccessor;privatereadonlyIProfilingLogger _profilingLogger;privatereadonlyILogger<CleanUpYourRoom> _logger;privatereadonlyICoreScopeProvider _scopeProvider;publicCleanUpYourRoom(IContentService contentService,IServerRoleAccessor serverRoleAccessor,IProfilingLogger profilingLogger,ILogger<CleanUpYourRoom> logger,ICoreScopeProvider scopeProvider,IOptionsMonitor<HealthChecksSettings> healthChecksSettings) { _contentService = contentService; _serverRoleAccessor = serverRoleAccessor; _profilingLogger = profilingLogger; _logger = logger; _scopeProvider = scopeProvider; // if the settings are updated trigger the event to let the job host knowhealthChecksSettings.OnChange(x => { Period =x.Notification.Period;_periodChanged?.Invoke(this,EventArgs.Empty); }); }publicTaskRunJobAsync() { // Wrap the three content service calls in a scope to do it all in one transaction.usingICoreScope scope =_scopeProvider.CreateCoreScope();int numberOfThingsInBin =_contentService.CountChildren(Constants.System.RecycleBinContent);_logger.LogInformation("Go clean your room - {ServerRole}",_serverRoleAccessor.CurrentServerRole);_logger.LogInformation("You have {NumberOfThingsInTheBin} items to clean", numberOfThingsInBin);if (_contentService.RecycleBinSmells()) { // Take out the trashusing (_profilingLogger.TraceDuration<CleanUpYourRoom>("Mum, I am emptying out the bin","It's all clean now")) {_contentService.EmptyRecycleBin(userId:-1); } } // Remember to complete the scope when done.scope.Complete();returnTask.CompletedTask; }}
Registering with a composer
All we need to do here is to create the composer where we register the background job with AddRecurringBackgroundJob.
RecurringHostedServiceBase is a low-level base class. It implements the dotnetcore interface IHostedService to run itself in the background, and creates and manages the timer that runs the job on a recurring basis.
RecurringBackgroundJobHostedService is an Umbraco specific Hosted Service that extends RecurringHostedServiceBase. It uses some system-level Umbraco services to ensure that your jobs only execute once Umbraco is up and running. It checks:
Server Roles - see above for more discussion about Server roles
MainDom - The MainDom lock ensures that only one instance of Umbraco is running at a time on a given machine. This ensures the integrity of certain files used by Umbraco. See Host Synchronization for more details.
Runtime State - On a fresh install or when waiting for a database upgrade, Umbraco may be fully up and running yet.
Notifications
The RecurringBackgroundJobHostedService publishes a number of notifications that can be hooked to report on the status of background jobs. All notifications extend from the base Umbraco.CMS.Infrastructure.Notifications.RecurringBackgroundJobNotification class.
The following notifications are available:
Starting
Started
Stopping
Stopped
Executing
Executed
Failed
Ignored
Start/Stop
The Starting/Started and Stopping/Stopped notification pairs are published when the RecurringBackgroundJobHostedService is started or stopped. The start event normally occurs soon after application start as part of the .Net WebHost startup process. Similarly the stop event would happen as part of application shutdown.
These notifications are there to support low-level debugging of background jobs to ensure they are starting/stopping correctly. Due to the timing of the notification, all handlers associated with these notifications should not depend on any Umbraco services, including database access.
Ignored
The Ignored notification is published when a background job's schedule is triggered, but the Umbraco runtime checks prevent it from running.
This notification is there to support low-level debugging of background jobs to ascertain why they are/aren't running. As the runtime checks include runtime state readiness, this event may be triggered during the install phase. Any notification handlers associated with this notification should also conduct their own checks before relying on Umbraco services, including database access.
Executing/Executed/Failed
These notifications will be triggered in pairs depending on the success/failure of the job itself.
The executing notification is triggered before the job is run.
The executed notification is triggered after the job is completed.
The failed notification is triggered from the catch block if an exception is thrown.
For successful job runs, the following notifications will be published:
Executing
Executed
For failed job runs, the following notifications will be published:
Executing
Failed
// Do not run the code on subscribers or unknown role servers// ONLY run for SchedulingPublisher server or Single server rolesswitch (_serverRoleAccessor.CurrentServerRole){caseServerRole.Subscriber:_logger.LogDebug("Does not run on subscriber servers.");returnTask.CompletedTask; // We return Task.CompletedTask to try again as the server role may change!caseServerRole.Unknown:_logger.LogDebug("Does not run on servers with unknown role.");returnTask.CompletedTask; // We return Task.CompletedTask to try again as the server role may change!}