Umbraco-cms: V8: Can't Save and Publish Content during startup due to notifications issue

Created on 16 Apr 2019  路  11Comments  路  Source: umbraco/Umbraco-CMS

I'm attempting to create content, then save and publish using the content service but I'm running in to a null reference exception.

I'm not sure why this is happening, my hunch at the moment it is that I'm trying to create it from a composer and wondering if maybe something isn't initialised for me at that point.

Anyway, here's the code I'm using (or at least, the important parts):

using System.Linq;
using Umbraco.Core;
using Umbraco.Core.Composing;
using Umbraco.Core.Models;

namespace Test
{
    [RuntimeLevel(MinLevel = RuntimeLevel.Run)]
    public class ContentComposer : IUserComposer
    {
        public void Compose(Composition composition)
        {
            composition.Components().Append<ContentComponent>();
        }
    }

    public class ContentComponent : IComponent
    {
        public void Initialize()
        {
            IContent homepageNode = Current.Services.ContentService.GetRootContent().FirstOrDefault(x => x.ContentType.Alias == "homepage");

            if (homepageNode == null)
            {
                IContent homepage = Current.Services.ContentService.Create("Homepage", -1, "homepage");

                Current.Services.ContentService.SaveAndPublish(homepage);
            }
        }

        public void Terminate() { }
    }
}

When I attempt to do this, I get the following error:

Object reference not set to an instance of an object.

And the stack trace:

   at Umbraco.Web.Routing.RedirectTrackingComponent.get_Moving()
   at Umbraco.Web.Routing.RedirectTrackingComponent.get_LockedEvents()
   at Umbraco.Web.Routing.RedirectTrackingComponent.ContentService_Publishing(IContentService sender, PublishEventArgs`1 args)
   at Umbraco.Core.Events.QueuingEventDispatcherBase.DispatchCancelable[TSender,TArgs](TypedEventHandler`2 eventHandler, TSender sender, TArgs args, String eventName)
   at Umbraco.Core.Services.Implement.ContentService.StrategyCanPublish(IScope scope, IContent content, Boolean checkPath, IReadOnlyList`1 culturesPublishing, IReadOnlyCollection`1 culturesUnpublishing, EventMessages evtMsgs, ContentSavingEventArgs savingEventArgs)
   at Umbraco.Core.Services.Implement.ContentService.CommitDocumentChangesInternal(IScope scope, IContent content, ContentSavingEventArgs saveEventArgs, Int32 userId, Boolean raiseEvents, Boolean branchOne, Boolean branchRoot)
   at Umbraco.Core.Services.Implement.ContentService.SaveAndPublish(IContent content, String culture, Int32 userId, Boolean raiseEvents)
   at Test.ContentComponent.Initialize() in C:\Users\benpa\Desktop\DELETE ME\Test\Test\ContentComponent.cs:line 27
   at Umbraco.Core.Composing.ComponentCollection.Initialize()
   at Umbraco.Core.Runtime.CoreRuntime.Boot(IRegister register, DisposableTimer timer)

Reproduction

Specifics

Umbraco Version: 8.0.1

Steps to reproduce

  • Set up fresh U8 install
  • Login, create new doc type with alias of "homepage" (allowed at root)
  • Set up component (code posted above)
  • Build, run (after site restart to ensure component code runs)

Expected result

Homepage node created at root of content site.

Actual result

Null reference exception thrown

Most helpful comment

What happens is this: the RedirectTrackingComponent uses the Current.UmbracoContext.HttpContext to store some state items that need to be carried over the different ContentService event handlers. In a IComponent when running Initialize, since there is no current request, there is no UmbracoContext, and it all ends in a NullReferenceException.

Turns out there is a way to ensure there is a context. Can you try with the following code?
~~~
public class ContentComponent : IComponent
{
private readonly IContentService _contentService;
private readonly IUmbracoContextFactory _contextFactory;

public ContentComponent(IContentService contentService, IUmbracoContextFactory contextFactory)
{
    _contentService = contentService;
    _contextFactory = contextFactory;
}

public void Initialize()
{
    IContent homepageNode = _contentService.GetRootContent().FirstOrDefault(x => x.ContentType.Alias == "homepage");

    if (homepageNode == null)
    {
        IContent homepage = _contentService.Create("Homepage", -1, "homepage");

        using (_contextFactory.EnsureUmbracoContext)
        {
            _contentService.SaveAndPublish(homepage);
        }
    }
}

public void Terminate() { }

}
~~~

Two things to note:

  • This is one of the few places where we are still using HttpContext to hold some context... and we are slowly getting rid of them as they create an unwanted dependency to System.Web.
  • We are working on making these "context" concepts generally easier to understand and use. It is not always exactly pretty at the moment.

All 11 comments

What happens is this: the RedirectTrackingComponent uses the Current.UmbracoContext.HttpContext to store some state items that need to be carried over the different ContentService event handlers. In a IComponent when running Initialize, since there is no current request, there is no UmbracoContext, and it all ends in a NullReferenceException.

Turns out there is a way to ensure there is a context. Can you try with the following code?
~~~
public class ContentComponent : IComponent
{
private readonly IContentService _contentService;
private readonly IUmbracoContextFactory _contextFactory;

public ContentComponent(IContentService contentService, IUmbracoContextFactory contextFactory)
{
    _contentService = contentService;
    _contextFactory = contextFactory;
}

public void Initialize()
{
    IContent homepageNode = _contentService.GetRootContent().FirstOrDefault(x => x.ContentType.Alias == "homepage");

    if (homepageNode == null)
    {
        IContent homepage = _contentService.Create("Homepage", -1, "homepage");

        using (_contextFactory.EnsureUmbracoContext)
        {
            _contentService.SaveAndPublish(homepage);
        }
    }
}

public void Terminate() { }

}
~~~

Two things to note:

  • This is one of the few places where we are still using HttpContext to hold some context... and we are slowly getting rid of them as they create an unwanted dependency to System.Web.
  • We are working on making these "context" concepts generally easier to understand and use. It is not always exactly pretty at the moment.

Thanks @zpqrtbnk - worked an absolute treat! Thanks for the super speedy reply, really good information.

Actually, may have spoken too soon. I'm now getting the following error:

Value cannot be null.
Parameter name: siteUri

Stack trace:

   at Umbraco.Web.Compose.NotificationsComponent.Notifier.SendNotification(IUser sender, IEnumerable`1 entities, IAction action, Uri siteUri)
   at Umbraco.Web.Compose.NotificationsComponent.Notifier.Notify(IAction action, IContent[] entities)
   at Umbraco.Web.Compose.NotificationsComponent.ContentServiceSaved(Notifier notifier, IContentService sender, SaveEventArgs`1 args, ActionCollection actions)
   at Umbraco.Web.Compose.NotificationsComponent.<Initialize>b__3_3(IContentService sender, ContentSavedEventArgs args)
   at Umbraco.Core.Events.TypedEventHandler`2.Invoke(TSender sender, TEventArgs e)
   at Umbraco.Core.Events.EventDefinition`2.RaiseEvent()
   at Umbraco.Core.Events.QueuingEventDispatcher.ScopeExitCompleted()
   at Umbraco.Core.Events.QueuingEventDispatcherBase.ScopeExit(Boolean completed)
   at Umbraco.Core.Scoping.Scope.<>c__DisplayClass72_0.<RobustExit>b__1()
   at Umbraco.Core.Scoping.Scope.TryFinally(Int32 index, Action[] actions)
   at Umbraco.Core.Scoping.Scope.TryFinally(Int32 index, Action[] actions)
   at Umbraco.Core.Scoping.Scope.RobustExit(Boolean completed, Boolean onException)
   at Umbraco.Core.Scoping.Scope.DisposeLastScope()
   at Umbraco.Core.Scoping.Scope.Dispose()
   at Umbraco.Core.Services.Implement.ContentService.SaveAndPublish(IContent content, String culture, Int32 userId, Boolean raiseEvents)
   at Test.ContentComponent.Initialize() in C:\Users\benpa\Desktop\DELETE ME\Test\Test\ContentComponent.cs:line 43
   at Umbraco.Core.Composing.ComponentCollection.Initialize()
   at Umbraco.Core.Runtime.CoreRuntime.Boot(IRegister register, DisposableTimer timer)

Just to note, I think this worked because my homepage had half been created but when I deleted the content and tried again, I didn't get the original error but the one above.

It looks like the content node is created and published just fine.

Also, this part of your code:

using (_contextFactory.EnsureUmbracoContext)

Throws the following error:

'method group': type used in a using statement must be implicitly convertible to 'System.IDisposable' or implement a suitable 'Dispose' method.

I had originally changed this out for a method call like this:

using (_contextFactory.EnsureUmbracoContext())

Not 100% sure if that's correct or not.

Hope that all makes sense 馃槗

Oops, yes, it's a method call - thanks for correcting ;-)

Now about the other error - going to look into it.

Ok, so when you save the content item, we want to send a notification, and we want to know the Url of the site - but, that Url is determined when the first request hits the site. During a component Initialize method, it therefore is... null.

If we initialize it with an "empty" (not null) value, the notification message will look weird. We could try to run a first detection... but then you would have to configure it in config files, we could not rely on guessing the Url from the first request.

Of course another way would be to... wait for the first request (I think we have an event for this) and do what you have to do, only at that time - but that is not entirely satisfying either.

Mmmm... tough one, not exactly sure. Thoughts?

@zpqrtbnk Do you know what the "first request" event is? I'm experiencing the same problem. I'm trying to install some default content when my package is installed. I might be going about it completely the wrong way though! :)

I'm facing the same issue, I'm trying to import some data from a API using a BackgroundTaskRunner. The weird part is the save and publish does work at my end it just throws an error.

at LightInject.Web.PerWebRequestScopeManager.GetOrAddScope() at LightInject.Web.PerWebRequestScopeManager.get_CurrentScope() at LightInject.ServiceContainer.GetInstance(Type serviceType) at Umbraco.Core.Composing.LightInject.LightInjectContainer.GetInstance(Type type) at Umbraco.Core.FactoryExtensions.GetInstance[T](IFactory factory) at Umbraco.Web.Runtime.WebInitialComposer.<>c.<Compose>b__0_6(IFactory factory) at Umbraco.Core.Composing.LightInject.LightInjectContainer.<>c__DisplayClass20_01.<Register>b__0(IServiceFactory f) at LightInject.ServiceContainer.GetInstance(Type serviceType) at Umbraco.Core.Composing.LightInject.LightInjectContainer.GetInstance(Type type) at Umbraco.Core.FactoryExtensions.GetInstance[T](IFactory factory) at Umbraco.Deploy.Cloud.LiveEditing.EventSubscriber.GetHubAndHelper(IHubContext& hub, UmbracoHelper& helper) at Umbraco.Deploy.Cloud.LiveEditing.EventSubscriber.ContentService_Published(IContentService sender, PublishEventArgs1 e)`

Ok, so when you save the content item, we want to send a notification, and we want to know the Url of the site - but, that Url is determined when the first request hits the site. During a component Initialize method, it therefore is... null.

If we initialize it with an "empty" (not null) value, the notification message will look weird. We could try to run a first detection... but then you would have to configure it in config files, we could not rely on guessing the Url from the first request.

Of course another way would be to... wait for the first request (I think we have an event for this) and do what you have to do, only at that time - but that is not entirely satisfying either.

Mmmm... tough one, not exactly sure. Thoughts?

Been a while since I picked this up, I'm still seeing the issue and not entirely sure on the solution. I'm happy with the solution to run this on the first request (which I think it better than changing config files) if that's a thing but can't see anything about it documented or in the code.

can confirm this issue still occurs in Umbraco 8.2.0

and can also confirm @zpqrtbnk suggested workaround/fix still works as well :)

tx!

I hadn't seen this issue yet, it is essentially a duplicate of this https://github.com/umbraco/Umbraco-CMS/issues/6464

On startup, or anytime you need to use an UmbracoContext when there isn't one, you will need to use IUmbracoContextFactory.

The issue now though is that on startup you cannot save/publish things because notifications want to be sent and there is no application URL so you will get a null ref.

as part of https://github.com/umbraco/Umbraco-CMS/issues/6464 @KevinJump may make a 'quick fix' for this, but longer term we will need to sort out how notifications are sent: on a background task runner with a queue, also enusring that if any static app url is specified that we don't need to wait until first request, also that the app url isn't overwritten on every request, etc... I will close this for now, please follow #6464

Ok, so when you save the content item, we want to send a notification, and we want to know the Url of the site - but, that Url is determined when the first request hits the site. During a component Initialize method, it therefore is... null.
If we initialize it with an "empty" (not null) value, the notification message will look weird. We could try to run a first detection... but then you would have to configure it in config files, we could not rely on guessing the Url from the first request.
Of course another way would be to... wait for the first request (I think we have an event for this) and do what you have to do, only at that time - but that is not entirely satisfying either.
Mmmm... tough one, not exactly sure. Thoughts?

Been a while since I picked this up, I'm still seeing the issue and not entirely sure on the solution. I'm happy with the solution to run this on the first request (which I think it better than changing config files) if that's a thing but can't see anything about it documented or in the code.

Which event are you using? Can you show sample code where you subscribe to the event?

Was this page helpful?
0 / 5 - 0 ratings