Using Hangfire to update Umbraco content

If you've been reading this blog for a while, you must have realized that I am a huge Hangfire fan. It's an excellent tool to help in scheduling background tasks. The best part is that it includes a super useful dashboard allowing you to see exactly what is going on with your scheduled jobs and run them at will.

Note: this only applies to Umbraco 9 and up.

With the help of my colleague Paul Johnson, I recently updated my Hangfire for Umbraco package, which now runs great on Umbraco Cloud, and can be used with LocalDb as well.

The funny thing is, I have never used Hangfire for anything related to Umbraco. Usually I use it to talk to remote APIs, create a local cache and read that into a template. 

So after getting asked this from my colleague Tony, who found an interesting bit of example code, I thought I'd try it out.

As it turns out, that did the trick and I have a full example for you:

using System;
using System.Linq;
using Hangfire;
using Hangfire.Console;
using Hangfire.Server;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Composing;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Web;

namespace Collaborators.Web.JobScheduler
{
    public class Scheduler : IComposer
    {
        public void Compose(IUmbracoBuilder builder)
        {
            builder.Services.AddScoped<IJobs, Jobs>();
            RecurringJob.AddOrUpdate<IJobs>(x  => x.ManipulateContent(null), Cron.Hourly);
        }
    }

    public interface IJobs
    {
        void ManipulateContent(PerformContext context);
    }

    public class Jobs : IJobs
    {
        private readonly IServiceProvider _serviceProvider;
        private readonly IUmbracoContextFactory _umbracoContextFactory;

        public Jobs(IServiceProvider serviceProvider, 
            IUmbracoContextFactory umbracoContextFactory)
        {
            _serviceProvider = serviceProvider;
            _umbracoContextFactory = umbracoContextFactory;
        }

        public void ManipulateContent(PerformContext context)
        {
            using var _ = _umbracoContextFactory.EnsureUmbracoContext();
            using var serviceScope = _serviceProvider.CreateScope();
            
            var query = serviceScope.ServiceProvider.GetRequiredService<IPublishedContentQuery>();
            var rootNode = query.ContentAtRoot().FirstOrDefault();
            
            if (rootNode == null) return; 
            
            context.WriteLine($"Root node - Id: {rootNode.Id} | Name: {rootNode.Name}");
        }
    }
} 

To explain: in a Composer we tell Umbraco that the Jobs is to be used when we ask for an IJobs type. In the Jobs class we inject IServiceProvider and IUmbracoContextFactory to use later. These will create a proper UmbracoContext and then we can create a Scope to start doing content queries.

Of course just querying is super fun, but you might also want to actually update content. It's not difficult to inject an IContentService now and get rolling. But we've been getting sporadic reports of the content cache and Examine indexes not always updating accordingly when doing this from a background task.

To help with this problem, community member Chad (also known as nzdev) recently posted some code on our Discord server that makes sure that the DatabaseServerMessenger sends the message that content has been updated. 

So here's some updated code that doesn't just access the content cache but also saves and publishes our home page:

using System;
using System.Linq;
using Hangfire;
using Hangfire.Console;
using Hangfire.Server;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Composing;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Sync;
using Umbraco.Cms.Core.Web;
using Umbraco.Cms.Infrastructure.Sync;

namespace Collaborators.Web.JobScheduler
{
    public class Scheduler : IComposer
    {
        public void Compose(IUmbracoBuilder builder)
        {
            builder.Services.AddScoped<IJobs, Jobs>();
            RecurringJob.AddOrUpdate<IJobs>(x  => x.ManipulateContent(null), Cron.Hourly);
        }
    }

    public interface IJobs
    {
        void ManipulateContent(PerformContext context);
    }

    public class Jobs : IJobs
    {
        private readonly IServiceProvider _serviceProvider;
        private readonly IUmbracoContextFactory _umbracoContextFactory;
        private readonly IServerMessenger _serverMessenger;
        private readonly IContentService _contentService;

        public Jobs(IServiceProvider serviceProvider, 
            IUmbracoContextFactory umbracoContextFactory, 
            IServerMessenger serverMessenger, 
            IContentService contentService)
        {
            _serviceProvider = serviceProvider;
            _umbracoContextFactory = umbracoContextFactory;
            _serverMessenger = serverMessenger;
            _contentService = contentService;
        }

        public void ManipulateContent(PerformContext context)
        {
            using var backgroundScope = new BackgroundScope(_serverMessenger);        
            using var _ = _umbracoContextFactory.EnsureUmbracoContext();
            using var serviceScope = _serviceProvider.CreateScope();
            
            var query = serviceScope.ServiceProvider.GetRequiredService<IPublishedContentQuery>();
            var rootNode = query.ContentAtRoot().FirstOrDefault();
            
            if (rootNode == null) return; 
            
            context.WriteLine($"Root node - Id: {rootNode.Id} | Name: {rootNode.Name}");
            
            // Do something with ContentService
            var content = _contentService.GetById(rootNode.Id);
            content.Name = "Home " + DateTime.Now.ToUniversalTime();
            _contentService.SaveAndPublish(content);
            
            context.WriteLine($"Root node updated - Id: {content.Id} | Name: {content.Name}");
        }
    }
    
    public class BackgroundScope : IDisposable
    {
        private readonly IServerMessenger _serverMessenger;

        public BackgroundScope(IServerMessenger serverMessenger)
        {
            _serverMessenger = serverMessenger;
        }

        public void Dispose()
        {
            if (_serverMessenger is BatchedDatabaseServerMessenger batchedDatabaseServerMessenger)
            {
                batchedDatabaseServerMessenger.SendMessages();
            }
        }
    }
}

Just to make it more clear, we're now also injecting IServerMessenger and IContentService, the former to make sure we can create the BackgroundScope and the latter to be able to update the content.

And there we have it:

Hangfire logging shows us the nodename before update, after update and we've made sure caches get updated through the addition of the BackgroundScope. 

Hopefully this serves as some inspiration for your own background tasks and now I have a URL to point to for people trying to work with Umbraco content from Hangfire!

Sebastiaan Janssen

Dutch guy living in (and loving) Copenhagen, working at Umbraco HQ. Lifehacker, skeptic, music lover, cyclist, developer.