Umbraco-cms: v8: Reading IPublishedContent in a background task fails

Created on 6 Feb 2019  路  7Comments  路  Source: umbraco/Umbraco-CMS

Hi,

I am trying to figure out some v8 stuff, including how to execute background jobs that want to read some Umbraco data as IPublishedContent. I was happy to see v8 provides an IUmbracoContextAccessor and a IPublishedSnapshotAccessor, either of which would probably give me access to IPublishedContent.

However, it seems that both of those will not work properly when used on application boot in an IComponent and I was wondering if this is a bug or expected behavior. If this is expected behavior and I should not be using an IComponent like that, then please guide me in how I should be doing this.

I was checking Umbraco.Web.Scheduling.SchedulerComponent for inspiration but it uses the IContentService instead which is not what I would prefer. Surely the PublishedContentCache should also be available outside of a request in one way or another?

Especially since Umbraco now contains the HybridUmbracoContextAccessor class which is referenced in the WebRuntimeComposer with the explicit comment mentioning situations without HttpContext I was hoping it would "just" work now:

// register the http context and umbraco context accessors
// we should use the HttpContextUmbracoContextAccessor, however there are cases when
// we have no http context, eg when booting Umbraco or in background threads, so instead
// let's use an hybrid accessor that can fall back to a ThreadStatic context.
composition.RegisterUnique();

Bug summary

UmbracoContext / IPublishedSnapshot are both null in an implementation of IComponent at its Initialize() method.

Steps to reproduce

Tested in version 8.0.0-alpha.58.2091.

Execute this code. If I should not be using Composer / Component in this way please forgive me and tell me how it should be done instead :)

using System;
using Umbraco.Core.Components;
using Umbraco.Web;
using Umbraco.Web.PublishedCache;

namespace UmbracoV8.Features.BackgroundJobs
{
    public class PerplexBackgroundJobsComposer 
        : ComponentComposer<PerplexBackgroundJobsComponent>, IUserComposer
    {
    }

    public class PerplexBackgroundJobsComponent : IComponent
    {
        private readonly IUmbracoContextAccessor _contextAccessor;
        private readonly IPublishedSnapshotAccessor _snapshotAccessor;

        public PerplexBackgroundJobsComponent(
            IUmbracoContextAccessor contextAccessor, 
            IPublishedSnapshotAccessor snapshotAccessor)
        {
            _contextAccessor = contextAccessor;
            _snapshotAccessor = snapshotAccessor;
        }

        public void Initialize()
        {
            if (_contextAccessor.UmbracoContext is UmbracoContext umbCtx)
            {
                // Do something with umbCtx ...
            }
            else if (_snapshotAccessor.PublishedSnapshot is IPublishedSnapshot snapshot)
            {
                // Do something with snapshot ...
            }
            else
            {
                // :-(
                throw new Exception("Cannot get IPublishedContent in any way!?");
            }
        }

        public void Terminate()
        {
        }
    }
}

Expected result

_contextAccessor.UmbracoContext and _snapshotAccessor.PublishedSnapshot are both available and grant access to IPublishedContent. No exception is thrown.

Actual result

_contextAccessor.UmbracoContext and _snapshotAccessor.PublishedSnapshot are both null, exception is thrown.

It would be great if there would be a single unified way to obtain IPublishedContent, with or without HttpContext / active request / etc. In v7 this was always painful and hacky to make work (EnsureContext shenanigans), I hope v8 can streamline this.

Most helpful comment

So... UmbracoContext is created as part of a front-end request, and creating a context creates an IPublishedSnapshot which represents a snapshot of the whole content cache - and that snapshot is attached to the UmbracoContext.

Outside of a request, you have to manage a snapshot by yourself.

using (var snapshot = publishedSnapshotService.CreatePublishedSnapshot(previewToken: null))
{
  var document = snapshot.Content.GetById(preview: false, contentId: 1234);

  // use the document - it's an `IPublishedContent` or a strongly-typed model
}

Here, publishedSnapshotService is an IPublishedSnapshotService which you can inject.

Making sense?

All 7 comments

UmbracoContext is a web based lifetime, it is created based on an HttpContext, just like v7. It is not a singleton but there is a new singleton accessor. If you are using that, and you are not in a web context, you will get a null because it cannot exist there. Just like v7.

As for accessing the content cache outside of a web based lifetime, I'm sure that's possible but AFAIK it's simply down to current time constraints that it's not available. I could be wrong but i think that is the case. In the meantime you might have to resort to v7 tactics and fake a context, etc...

@zpqrtbnk will know more on the subject and i'll chat to him later to see what the status is about accessing the content cache outside of a web request.

That said, the umbraco context accessor will never return an umbraco context that's not in a web request because that is it's lifetime.

Thanks for the very swift reply Shannon. It makes sense there is no UmbracoContext outside of a web request indeed, I can totally understand that. I'd rather use a more focused dependency like IPublishedSnapshot or just the IPublishedContentCache to query some IPublishedContent anyway. If that would somehow be made available that would be awesome. I hope @zpqrtbnk could perhaps comment a bit on how developers should go about obtaining IPublishedContent outside of a web request.

I'm closing the issue for now.
@zpqrtbnk if you have time to comment on it, feel free to 馃槂

So... UmbracoContext is created as part of a front-end request, and creating a context creates an IPublishedSnapshot which represents a snapshot of the whole content cache - and that snapshot is attached to the UmbracoContext.

Outside of a request, you have to manage a snapshot by yourself.

using (var snapshot = publishedSnapshotService.CreatePublishedSnapshot(previewToken: null))
{
  var document = snapshot.Content.GetById(preview: false, contentId: 1234);

  // use the document - it's an `IPublishedContent` or a strongly-typed model
}

Here, publishedSnapshotService is an IPublishedSnapshotService which you can inject.

Making sense?

Excellent, thank you Stephan (@zpqrtbnk) for your reply. Will look into your suggested approach. Something like this is what I was hoping for would be possible in v8.

For future readers, core team is discussing a similar issue and possible solutions in #4572

Also for future readers: the code in the referenced issue works more reliably and is as follows:

// _umbracoContextFactory is an injected IUmbracoContextFactory
using (var reference = _umbracoContextFactory.EnsureUmbracoContext())
{
    var content = reference.UmbracoContext.ContentCache.GetById(...)    
}
Was this page helpful?
0 / 5 - 0 ratings