Cultiv Hangfire missing Umbraco Context issue

Umb V13.7.2, Cultiv Hangfire V3.1.2

Trying to use the Cultiv Hangfire package to hit a local Umbraco API endpoint that I can hit with a button in the back office (CourseListImportAsync()). However, I’m getting an error saying :-

" Failed to process the job ‘5’: an exception occurred. Retry attempt 4 of 10 will be performed in 00:02:00.
System.InvalidOperationException: Wasn’t able to get an UmbracoContext
at Umbraco.Extensions.UmbracoContextAccessorExtensions.GetRequiredUmbracoContext(IUmbracoContextAccessor umbracoContextAccessor)
at Umbraco.Cms.Infrastructure.DependencyInjection.UmbracoBuilderExtensions.<>c.b__0_10(IServiceProvider factory)
at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSiteMain(ServiceCallSite callSite, TArgument argument)
"

This is the entire code:-

using Hangfire;
using Hangfire.Server;
using Umbraco.Cms.Core.Composing;
using Umbraco.Cms.Core.Web;
using Web.Core.Services;

namespace Web.Core.Composers;

public class CourseImportScheduleComposer : IComposer {
    
    public void Compose(IUmbracoBuilder builder) {            
        // Register the background job
        builder.Services.AddTransient<CourseImportJob>();

        // Register Hangfire recurring job
        RecurringJob.AddOrUpdate<CourseImportJob>(job => job.DoIt(null), "*/2 * * * *"); // Every 2 minutes
    }
        
    // Separate job class for Hangfire
    public class CourseImportJob {
        private readonly ImportCoursesService _importCoursesService;
        private readonly IUmbracoContextFactory _umbracoContextFactory;

        public CourseImportJob(ImportCoursesService importCoursesService,
            IUmbracoContextFactory umbracoContextFactory) {
            _importCoursesService = importCoursesService;
            _umbracoContextFactory = umbracoContextFactory;
        }

        public async Task DoIt(PerformContext? context) {
            using var umbracoContext = _umbracoContextFactory.EnsureUmbracoContext();
            
            await _importCoursesService.CourseListImportAsync();
        }
    }
}

I don’t quite understand why it’s failing to get the Umbraco context. Any clues or advice would be greatly appreciated.

Thanks

This job succeeds when I remove the import parts, so for example:

public async Task DoIt(PerformContext? context)
{
    using var umbracoContext = _umbracoContextFactory.EnsureUmbracoContext();
    umbracoContext.UmbracoContext.Content.GetById(1057);
}

I am not sure what’s going on here by the way, you’re getting ImportCourseService through DI and also not through DI?

What I suspect is that CourseListImportAsync is creating content nodes?

In which case:

I don’t know why you’re doing EnsureUmbracoContext here, if you’re just going to offload the rest ti the CourseListImportAsync method. I would make sure you’re doing everything in ImportCoursesService instead.

In any case you will need to instantiate a BackgroundScope and use IServiceProvider to create a scope to be able to do any create/update operations on content, as in the second example on the blog post above.

Thanks Seb,

I changed it to:-

    // Separate job class for Hangfire
    public class CourseImportJob(ImportCoursesService importCoursesService) {
        public async Task DoIt(PerformContext? context) {
            await importCoursesService.CourseListImportAsync();
        }
    }

But it still complains of no Umbraco Context. The CourseListImportAsync fetches a file from a remote server then reads the fiile off disk and creates or updates a load of Umbraco nodes. Would the scheduler need an Umbraco Context for that?

Make sure you ONLY DI an IServiceProvider and IUmbracoContextFactory.

Then you can:

  • Ensure context
  • Create service scope
  • Get your needed services through that scope

See Sebastiaans example:

using var _ = _umbracoContextFactory.EnsureUmbracoContext();
using var serviceScope = _serviceProvider.CreateScope();
            
var query = serviceScope.ServiceProvider.GetRequiredService<IPublishedContentQuery>();

In your case you’d get another service than IPublishedContentQuery. You would get your ImportCoursesService.

So I’ve changed it to this based on Seb’s article and your comments but it still doesn’t get an Umbraco context :frowning:

using Hangfire;
using Hangfire.Server;
using Umbraco.Cms.Core.Composing;
using Umbraco.Cms.Core.Web;
using Web.Core.Services;

namespace Web.Core.Composers;

public class CourseImportScheduleComposer : IComposer {

    public void Compose(IUmbracoBuilder builder) {
        // Register the background job
        builder.Services.AddTransient<CourseImportJob>();

        // Register Hangfire recurring job
        RecurringJob.AddOrUpdate<CourseImportJob>(job => job.DoIt(null),
            "*/2 * * * *"); //"*/2 * * * *" Every 2 minutes // "0 2 * * *" fires at 02:00 hrs, daily
    }
}

// Separate job class for Hangfire
public class CourseImportJob() {
    private readonly ImportCoursesService _importCoursesService;
    private readonly IUmbracoContextFactory _umbracoContextFactory;

    public CourseImportJob(ImportCoursesService importCoursesService, IUmbracoContextFactory _umbracoContextFactory) : this() {
        _importCoursesService = importCoursesService;
        this._umbracoContextFactory = _umbracoContextFactory;
    }
    
    public async Task DoIt(PerformContext? context) {
        using var _ = _umbracoContextFactory.EnsureUmbracoContext();
        
        await _importCoursesService.CourseListImportAsync();
    }
}

Finally got it working. Turns out it was because what was being run on a schedule needed the Umbraco context because it was using NuCache to find some data before modifying it with the Content Service. After modifying the code to just use the Content Service, it ran fine so was able to simplify the Hangfire code t this:-

using Hangfire;
using Hangfire.Server;
using Umbraco.Cms.Core.Composing;
using Web.Core.Services;

namespace Web.Core.Composers;

public class CourseImportScheduleComposer : IComposer {
    
    public void Compose(IUmbracoBuilder builder) {            
        // Register the background job
        builder.Services.AddTransient<CourseImportJob>();

        // Register Hangfire recurring job
        RecurringJob.AddOrUpdate<CourseImportJob>(job => job.DoIt(null), "*/2 * * * *"); //"*/2 * * * *" Every 2 minutes // "0 2 * * *" fires at 02:00 hrs, daily
    }
        
    // Separate job class for Hangfire
    public class CourseImportJob(ImportCoursesService importCoursesService) {
        public async Task DoIt(PerformContext? context) {
            await importCoursesService.CourseListImportAsync();
        }
    }
}

Thanks for all the helpful responses.