Some more details which might help :
My notification handler :
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Notifications;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Scoping;
using Umbraco.Cms.Web.Common.PublishedModels;
using Umbraco.Extensions;
using OwainCodes.Core.Models;
using Umbraco.Cms.Core;
namespace OwainCodes.Core.NotificationHandler
{
public class CreateDateFolderNotificationHandler : INotificationAsyncHandler<ContentSavingNotification>
{
private readonly IContentService _contentService;
private readonly ICoreScopeProvider _scopeProvider;
private readonly IPublishedContentTypeCache _publishedContentTypeCache;
public CreateDateFolderNotificationHandler(IContentService contentService, ICoreScopeProvider scopeProvider, IPublishedContentTypeCache publishedContentTypeCache)
{
_contentService = contentService ?? throw new ArgumentNullException(nameof(contentService));
_scopeProvider = scopeProvider ?? throw new ArgumentNullException(nameof(scopeProvider));
_publishedContentTypeCache = publishedContentTypeCache ?? throw new ArgumentNullException(nameof(publishedContentTypeCache));
}
public async Task HandleAsync(ContentSavingNotification notification, CancellationToken cancellationToken)
{
using ICoreScope scope = _scopeProvider.CreateCoreScope();
foreach (var node in notification.SavedEntities)
{
var parentNodeId = node.ParentId;
if (node.ContentType.Alias == BlogPage.ModelTypeAlias)
{
string parentModelTypeAlias = string.Empty;
string modelTypeAlias = string.Empty;
string datePropertyTypeAlias = string.Empty;
switch (node.ContentType.Alias)
{
case BlogPage.ModelTypeAlias:
modelTypeAlias = BlogPage.ModelTypeAlias;
parentModelTypeAlias = Blog.ModelTypeAlias;
datePropertyTypeAlias = BlogPage.GetModelPropertyType(_publishedContentTypeCache, selector: r => r.PublishDate).Alias;
break;
default:
throw new Exception("Error setting Model/Prop alias");
}
IContent monthFolder;
if (parentNodeId <= 0)
{
notification.CancelOperation(new EventMessage("Error", "Something went wrong with saving the resource", EventMessageType.Error));
continue;
}
var currentParent = GetCurrentParent(parentNodeId);
var date = node.GetValue<DateTime>(datePropertyTypeAlias);
if (date == DateTime.MinValue)
{
date = DateTime.Today;
node.SetValue(datePropertyTypeAlias, date);
}
var dateYear = date.ToString("yyyy");
var dateMonth = date.ToString("MMMM");
if (currentParent.ContentType.Alias == DateFolder.ModelTypeAlias)
{
bool moveNode = VerifyOrMoveToCorrectLocation(scope, currentParent, dateYear, dateMonth, out monthFolder);
if (moveNode)
node.SetParent(monthFolder);
}
else if (currentParent.ContentType.Alias == parentModelTypeAlias)
{
monthFolder = MoveToCorrectLocation(scope, currentParent, dateYear, dateMonth);
node.SetParent(monthFolder);
}
}
}
scope.Complete();
}
private IContent MoveToCorrectLocation(ICoreScope scope, IContent currentParent, string dateYear, string dateMonth)
{
IContent monthFolder;
var isNew = GetDateFolder(currentParent, dateYear, scope, out var folder);
if (isNew)
{
monthFolder = CreateFolder(folder, dateMonth);
}
else
{
GetDateFolder(folder, dateMonth, scope, out monthFolder);
}
return monthFolder;
}
private bool VerifyOrMoveToCorrectLocation(ICoreScope scope, IContent currentParent, string dateYear, string dateMonth, out IContent monthFolder)
{
var moveNode = true;
var currentMonthFolder = currentParent;
var currentMonthFoldersParent = GetCurrentParent(currentParent.ParentId);
if (currentMonthFolder.Name.InvariantEquals(dateMonth) && currentMonthFoldersParent.Name.InvariantEquals(dateYear))
{
monthFolder = null;
moveNode = false;
}
//Wrong month right year
else if (!currentMonthFolder.Name.InvariantEquals(dateMonth) && currentMonthFoldersParent.Name.InvariantEquals(dateYear))
{
GetDateFolder(currentMonthFoldersParent, dateMonth, scope, out monthFolder);
}
else
{
var parentOfParent = GetCurrentParent(currentMonthFoldersParent.ParentId);
GetDateFolder(parentOfParent, dateYear, scope, out var yearFolder);
GetDateFolder(yearFolder, dateMonth, scope, out monthFolder);
}
return moveNode;
}
private bool GetDateFolder(IContent parentNode, string nodeName, ICoreScope scope, out IContent folder)
{
var dateFolderTypeId = DateFolder.GetModelContentType(_publishedContentTypeCache).Id;
var matchingChild = _contentService.GetPagedChildren(parentNode.Id, 0, int.MaxValue, out _)
.Where(c => c.Name.InvariantEquals(nodeName) && c.Trashed == false && c.ContentTypeId == dateFolderTypeId);
if (matchingChild == null || !matchingChild.Any())
{
folder = CreateFolder(parentNode, nodeName);
return true;
}
folder = matchingChild.FirstOrDefault();
return false;
}
private IContent CreateFolder(IContent parentNode, string name)
{
IContent dateFolder = _contentService.CreateAndSave(name, parentNode.Id, "DateFolder");
_contentService.PublishBranch(dateFolder, PublishBranchFilter.Default, ["en-us"]);
return dateFolder;
}
private IContent GetCurrentParent(int parentId)
{
return _contentService.GetById(parentId);
}
}
}
Registered the handler within Progam.cs
builder.CreateUmbracoBuilder()
.AddBackOffice()
.AddWebsite()
.AddComposers()
.AddNotificationAsyncHandler<ContentSavingNotification, CreateDateFolderNotificationHandler>()
.Build();
Then in the backoffice, I have a Blog Node which allows children of Blog. I also have a doctype called DateFolder which is added via the Notification Handler so that I get the hierarchy for the blogs e.g. Year/Month
I’ve tested this within the backoffice and everything gets created as expected, I’ve also ran my external api calls on non-blog pages e.g. a generic page which doesn’t use the DateFolder structure, just a simple creation on the root or under a home node and it gets created without any issues.
I use the API endpoints :
ValidateEndpoint - /umbraco/management/api/v1/document/validate
await CallUmbracoApi(validateEndpoint, token, 'POST', body);
CreateEndpoint - umbraco/management/api/v1/document
const createResponse = await CallUmbracoApi(createEndpoint, token, 'POST', body);
The body is :
const body = {
"documentType": {
"id": nodeId
},
"template": null,
"values": [
{
"alias": this.settings.titleAlias,
"culture": null,
"segment": null,
"value": pageTitle || ""
},
{
"alias": this.settings.blogContentAlias,
"culture": null,
"segment": null,
"value": pageContent?.content || ""
}
],
"variants": [
{
"name": pageTitle,
"culture": null,
"segment": null
}
],
"id": await GenerateGuid(),
"parent": (this.settings.blogParentNodeId && this.settings.blogParentNodeId.trim() !== '' && this.settings.blogParentNodeId !== 'null')
? { "id": this.settings.blogParentNodeId }
: null
};
And this body is used for Validate AND Document
The validate endpoint returns a 200 repsonse, the Document endpoint returns a 404.
Hope that helps.