diff --git a/Directory.Packages.props b/Directory.Packages.props
index 431bfc7ba7..e5ff6712ea 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -5,7 +5,7 @@
-
+
@@ -45,9 +45,9 @@
-
-
-
+
+
+
@@ -61,8 +61,8 @@
-
-
+
+
@@ -70,17 +70,17 @@
-
+
-
+
-
+
@@ -95,5 +95,7 @@
+
+
\ No newline at end of file
diff --git a/src/Umbraco.Cms.Api.Delivery/Controllers/Content/ByIdContentApiController.cs b/src/Umbraco.Cms.Api.Delivery/Controllers/Content/ByIdContentApiController.cs
index 4239b15f60..523c496956 100644
--- a/src/Umbraco.Cms.Api.Delivery/Controllers/Content/ByIdContentApiController.cs
+++ b/src/Umbraco.Cms.Api.Delivery/Controllers/Content/ByIdContentApiController.cs
@@ -1,3 +1,4 @@
+using System.Diagnostics;
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
@@ -46,19 +47,17 @@ public class ByIdContentApiController : ContentApiItemControllerBase
private async Task HandleRequest(Guid id)
{
- IPublishedContent? contentItem = ApiPublishedContentCache.GetById(id);
+ IPublishedContent? contentItem = await ApiPublishedContentCache.GetByIdAsync(id).ConfigureAwait(false);
if (contentItem is null)
{
return NotFound();
}
-
- IActionResult? deniedAccessResult = await HandleMemberAccessAsync(contentItem, _requestMemberAccessService);
+ IActionResult? deniedAccessResult = await HandleMemberAccessAsync(contentItem, _requestMemberAccessService).ConfigureAwait(false);
if (deniedAccessResult is not null)
{
return deniedAccessResult;
}
-
IApiContentResponse? apiContentResponse = ApiContentResponseBuilder.Build(contentItem);
if (apiContentResponse is null)
{
diff --git a/src/Umbraco.Cms.Api.Delivery/Controllers/Content/ByIdsContentApiController.cs b/src/Umbraco.Cms.Api.Delivery/Controllers/Content/ByIdsContentApiController.cs
index 61309079c6..0771c35758 100644
--- a/src/Umbraco.Cms.Api.Delivery/Controllers/Content/ByIdsContentApiController.cs
+++ b/src/Umbraco.Cms.Api.Delivery/Controllers/Content/ByIdsContentApiController.cs
@@ -45,7 +45,7 @@ public class ByIdsContentApiController : ContentApiItemControllerBase
private async Task HandleRequest(HashSet ids)
{
- IPublishedContent[] contentItems = ApiPublishedContentCache.GetByIds(ids).ToArray();
+ IPublishedContent[] contentItems = (await ApiPublishedContentCache.GetByIdsAsync(ids).ConfigureAwait(false)).ToArray();
IActionResult? deniedAccessResult = await HandleMemberAccessAsync(contentItems, _requestMemberAccessService);
if (deniedAccessResult is not null)
diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Content/ContentControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Content/ContentControllerBase.cs
index c008dad102..0b527a9b39 100644
--- a/src/Umbraco.Cms.Api.Management/Controllers/Content/ContentControllerBase.cs
+++ b/src/Umbraco.Cms.Api.Management/Controllers/Content/ContentControllerBase.cs
@@ -4,6 +4,7 @@ using Umbraco.Cms.Api.Management.ViewModels.Content;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models.ContentEditing;
using Umbraco.Cms.Core.Models.ContentEditing.Validation;
+using Umbraco.Cms.Core.PropertyEditors.Validation;
using Umbraco.Cms.Core.Services.OperationStatus;
using Umbraco.Extensions;
@@ -100,7 +101,7 @@ public abstract class ContentControllerBase : ManagementApiControllerBase
var errors = new SortedDictionary();
- var missingPropertyModels = new List();
+ var validationErrorExpressionRoot = $"$.{nameof(ContentModelBase.Values).ToFirstLowerInvariant()}";
foreach (PropertyValidationError validationError in validationResult.ValidationErrors)
{
TValueModel? requestValue = requestModel.Values.FirstOrDefault(value =>
@@ -109,13 +110,16 @@ public abstract class ContentControllerBase : ManagementApiControllerBase
&& value.Segment == validationError.Segment);
if (requestValue is null)
{
- missingPropertyModels.Add(MapMissingProperty(validationError));
+ errors.Add(
+ $"{validationErrorExpressionRoot}[{JsonPathExpression.MissingPropertyValue(validationError.Alias, validationError.Culture, validationError.Segment)}].{nameof(ValueModelBase.Value)}",
+ validationError.ErrorMessages);
continue;
}
var index = requestModel.Values.IndexOf(requestValue);
- var key = $"$.{nameof(ContentModelBase.Values).ToFirstLowerInvariant()}[{index}].{nameof(ValueModelBase.Value).ToFirstLowerInvariant()}{validationError.JsonPath}";
- errors.Add(key, validationError.ErrorMessages);
+ errors.Add(
+ $"$.{nameof(ContentModelBase.Values).ToFirstLowerInvariant()}[{index}].{nameof(ValueModelBase.Value).ToFirstLowerInvariant()}{validationError.JsonPath}",
+ validationError.ErrorMessages);
}
return OperationStatusResult(status, problemDetailsBuilder
@@ -123,16 +127,6 @@ public abstract class ContentControllerBase : ManagementApiControllerBase
.WithTitle("Validation failed")
.WithDetail("One or more properties did not pass validation")
.WithRequestModelErrors(errors)
- .WithExtension("missingValues", missingPropertyModels.ToArray())
.Build()));
}
-
- private PropertyValidationResponseModel MapMissingProperty(PropertyValidationError source) =>
- new()
- {
- Alias = source.Alias,
- Segment = source.Segment,
- Culture = source.Culture,
- Messages = source.ErrorMessages,
- };
}
diff --git a/src/Umbraco.Core/Cache/Refreshers/Implement/ContentCacheRefresher.cs b/src/Umbraco.Core/Cache/Refreshers/Implement/ContentCacheRefresher.cs
index 1ff1b21277..da70761466 100644
--- a/src/Umbraco.Core/Cache/Refreshers/Implement/ContentCacheRefresher.cs
+++ b/src/Umbraco.Core/Cache/Refreshers/Implement/ContentCacheRefresher.cs
@@ -20,6 +20,7 @@ public sealed class ContentCacheRefresher : PayloadCacheRefresherBase descendantsKeys))
+ {
+ var branchKeys = descendantsKeys.ToList();
+ branchKeys.Add(key);
+
+ foreach (Guid branchKey in branchKeys)
+ {
+ _documentCacheService.RefreshMemoryCacheAsync(branchKey).GetAwaiter().GetResult();
+ }
+ }
+ }
+
+ if (payload.ChangeTypes.HasType(TreeChangeTypes.RefreshAll))
+ {
+ _documentCacheService.ClearMemoryCacheAsync(CancellationToken.None).GetAwaiter().GetResult();
+ }
+
+ if (payload.ChangeTypes.HasType(TreeChangeTypes.Remove))
+ {
+ _documentCacheService.RemoveFromMemoryCacheAsync(key).GetAwaiter().GetResult();
+ }
+ }
+
private void HandleNavigation(JsonPayload payload)
{
diff --git a/src/Umbraco.Core/Cache/Refreshers/Implement/ContentTypeCacheRefresher.cs b/src/Umbraco.Core/Cache/Refreshers/Implement/ContentTypeCacheRefresher.cs
index f9936933cc..35320f47d3 100644
--- a/src/Umbraco.Core/Cache/Refreshers/Implement/ContentTypeCacheRefresher.cs
+++ b/src/Umbraco.Core/Cache/Refreshers/Implement/ContentTypeCacheRefresher.cs
@@ -3,6 +3,7 @@ using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.Notifications;
using Umbraco.Cms.Core.Persistence.Repositories;
+using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.Changes;
@@ -15,6 +16,9 @@ public sealed class ContentTypeCacheRefresher : PayloadCacheRefresherBase { });
-
+ _publishedContentTypeCache.ClearContentTypes(payloads.Select(x => x.Id));
_publishedContentTypeFactory.NotifyDataTypeChanges();
+ _publishedModelFactory.WithSafeLiveFactoryReset(() =>
+ {
+ IEnumerable documentTypeIds = payloads.Where(x => x.ItemType == nameof(IContentType)).Select(x => x.Id);
+ IEnumerable mediaTypeIds = payloads.Where(x => x.ItemType == nameof(IMediaType)).Select(x => x.Id);
+
+ _documentCacheService.RebuildMemoryCacheByContentTypeAsync(documentTypeIds);
+ _mediaCacheService.RebuildMemoryCacheByContentTypeAsync(mediaTypeIds);
+ });
// now we can trigger the event
base.Refresh(payloads);
diff --git a/src/Umbraco.Core/Cache/Refreshers/Implement/DataTypeCacheRefresher.cs b/src/Umbraco.Core/Cache/Refreshers/Implement/DataTypeCacheRefresher.cs
index de25660fa0..bf45161caa 100644
--- a/src/Umbraco.Core/Cache/Refreshers/Implement/DataTypeCacheRefresher.cs
+++ b/src/Umbraco.Core/Cache/Refreshers/Implement/DataTypeCacheRefresher.cs
@@ -3,6 +3,7 @@ using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.Notifications;
using Umbraco.Cms.Core.Persistence.Repositories;
+using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Services;
using Umbraco.Extensions;
@@ -14,6 +15,9 @@ public sealed class DataTypeCacheRefresher : PayloadCacheRefresherBase dataTypeCache = AppCaches.IsolatedCaches.Get();
+ List removedContentTypes = new();
foreach (JsonPayload payload in payloads)
{
_idKeyMap.ClearCache(payload.Id);
@@ -84,14 +95,25 @@ public sealed class DataTypeCacheRefresher : PayloadCacheRefresherBase(payload.Id));
}
- }
- // TODO: We need to clear the HybridCache of any content using the ContentType, but NOT the database cache here, and this should be done within the "WithSafeLiveFactoryReset" to ensure that the factory is locked in the meantime.
- _publishedModelFactory.WithSafeLiveFactoryReset(() => { });
+ removedContentTypes.AddRange(_publishedContentTypeCache.ClearByDataTypeId(payload.Id));
+ }
var changedIds = payloads.Select(x => x.Id).ToArray();
_publishedContentTypeFactory.NotifyDataTypeChanges(changedIds);
+ _publishedModelFactory.WithSafeLiveFactoryReset(() =>
+ {
+ IEnumerable documentTypeIds = removedContentTypes
+ .Where(x => x.ItemType == PublishedItemType.Content)
+ .Select(x => x.Id);
+ _documentCacheService.RebuildMemoryCacheByContentTypeAsync(documentTypeIds).GetAwaiter().GetResult();
+
+ IEnumerable mediaTypeIds = removedContentTypes
+ .Where(x => x.ItemType == PublishedItemType.Media)
+ .Select(x => x.Id);
+ _mediaCacheService.RebuildMemoryCacheByContentTypeAsync(mediaTypeIds);
+ });
base.Refresh(payloads);
}
diff --git a/src/Umbraco.Core/Cache/Refreshers/Implement/MediaCacheRefresher.cs b/src/Umbraco.Core/Cache/Refreshers/Implement/MediaCacheRefresher.cs
index 581e77c52a..4b4384a84d 100644
--- a/src/Umbraco.Core/Cache/Refreshers/Implement/MediaCacheRefresher.cs
+++ b/src/Umbraco.Core/Cache/Refreshers/Implement/MediaCacheRefresher.cs
@@ -2,6 +2,7 @@ using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Notifications;
using Umbraco.Cms.Core.Persistence.Repositories;
+using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.Changes;
@@ -16,6 +17,7 @@ public sealed class MediaCacheRefresher : PayloadCacheRefresherBase descendantsKeys))
+ {
+ var branchKeys = descendantsKeys.ToList();
+ branchKeys.Add(key);
+
+ foreach (Guid branchKey in branchKeys)
+ {
+ _mediaCacheService.RefreshMemoryCacheAsync(branchKey).GetAwaiter().GetResult();
+ }
+ }
+ }
+
+ if (payload.ChangeTypes.HasType(TreeChangeTypes.RefreshAll))
+ {
+ _mediaCacheService.ClearMemoryCacheAsync(CancellationToken.None).GetAwaiter().GetResult();
+ }
+
+ if (payload.ChangeTypes.HasType(TreeChangeTypes.Remove))
+ {
+ _mediaCacheService.RemoveFromMemoryCacheAsync(key).GetAwaiter().GetResult();
+ }
+ }
+
private void HandleNavigation(JsonPayload payload)
{
if (payload.Key is null)
diff --git a/src/Umbraco.Core/Configuration/Models/NuCacheSettings.cs b/src/Umbraco.Core/Configuration/Models/NuCacheSettings.cs
index 309c4560cf..9b1fde9826 100644
--- a/src/Umbraco.Core/Configuration/Models/NuCacheSettings.cs
+++ b/src/Umbraco.Core/Configuration/Models/NuCacheSettings.cs
@@ -19,6 +19,7 @@ public class NuCacheSettings
///
/// Gets or sets a value defining the BTree block size.
///
+ [Obsolete("This property is no longer used")]
public int? BTreeBlockSize { get; set; }
///
@@ -37,8 +38,10 @@ public class NuCacheSettings
/// The size to use for nucache Kit batches. Higher value means more content loaded into memory at a time.
///
[DefaultValue(StaticKitBatchSize)]
+ [Obsolete("This property is no longer used")]
public int KitBatchSize { get; set; } = StaticKitBatchSize;
+ [Obsolete("This property is no longer used")]
public bool UnPublishedContentCompression { get; set; } = false;
[DefaultValue(StaticUsePagedSqlQuery)]
diff --git a/src/Umbraco.Core/Constants-Configuration.cs b/src/Umbraco.Core/Constants-Configuration.cs
index 60b3397eeb..a569110f01 100644
--- a/src/Umbraco.Core/Constants-Configuration.cs
+++ b/src/Umbraco.Core/Constants-Configuration.cs
@@ -66,6 +66,7 @@ public static partial class Constants
public const string ConfigPackageManifests = ConfigPrefix + "PackageManifests";
public const string ConfigWebhook = ConfigPrefix + "Webhook";
public const string ConfigCache = ConfigPrefix + "Cache";
+ public const string ConfigCacheEntry = ConfigCache + ":Entry";
public static class NamedOptions
{
@@ -79,6 +80,13 @@ public static partial class Constants
public const string MemberTypes = "MemberTypes";
}
+
+ public static class CacheEntry
+ {
+ public const string Document = "Document";
+
+ public const string Media = "Media";
+ }
}
}
}
diff --git a/src/Umbraco.Core/DeliveryApi/ApiPublishedContentCache.cs b/src/Umbraco.Core/DeliveryApi/ApiPublishedContentCache.cs
index 8d4cf4026a..1c153b0143 100644
--- a/src/Umbraco.Core/DeliveryApi/ApiPublishedContentCache.cs
+++ b/src/Umbraco.Core/DeliveryApi/ApiPublishedContentCache.cs
@@ -32,6 +32,35 @@ public sealed class ApiPublishedContentCache : IApiPublishedContentCache
deliveryApiSettings.OnChange(settings => _deliveryApiSettings = settings);
}
+ public async Task GetByRouteAsync(string route)
+ {
+ var isPreviewMode = _requestPreviewService.IsPreview();
+
+ // Handle the nasty logic with domain document ids in front of paths.
+ int? documentStartNodeId = null;
+ if (route.StartsWith("/") is false)
+ {
+ var index = route.IndexOf('/');
+
+ if (index > -1 && int.TryParse(route.Substring(0, index), out var nodeId))
+ {
+ documentStartNodeId = nodeId;
+ route = route.Substring(index);
+ }
+ }
+
+ Guid? documentKey = _documentUrlService.GetDocumentKeyByRoute(
+ route,
+ _requestCultureService.GetRequestedCulture(),
+ documentStartNodeId,
+ _requestPreviewService.IsPreview()
+ );
+ IPublishedContent? content = documentKey.HasValue
+ ? await _publishedContentCache.GetByIdAsync(documentKey.Value, isPreviewMode)
+ : null;
+
+ return ContentOrNullIfDisallowed(content);
+ }
public IPublishedContent? GetByRoute(string route)
{
@@ -64,16 +93,37 @@ public sealed class ApiPublishedContentCache : IApiPublishedContentCache
return ContentOrNullIfDisallowed(content);
}
+ public async Task GetByIdAsync(Guid contentId)
+ {
+ IPublishedContent? content = await _publishedContentCache.GetByIdAsync(contentId, _requestPreviewService.IsPreview()).ConfigureAwait(false);
+ return ContentOrNullIfDisallowed(content);
+ }
+
public IPublishedContent? GetById(Guid contentId)
{
IPublishedContent? content = _publishedContentCache.GetById(_requestPreviewService.IsPreview(), contentId);
return ContentOrNullIfDisallowed(content);
}
+ public async Task> GetByIdsAsync(IEnumerable contentIds)
+ {
+ var isPreviewMode = _requestPreviewService.IsPreview();
+
+ IEnumerable> tasks = contentIds
+ .Select(contentId => _publishedContentCache.GetByIdAsync(contentId, isPreviewMode));
+
+ IPublishedContent?[] allContent = await Task.WhenAll(tasks);
+
+ return allContent
+ .WhereNotNull()
+ .Where(IsAllowedContentType)
+ .ToArray();
+ }
public IEnumerable GetByIds(IEnumerable contentIds)
{
+ var isPreviewMode = _requestPreviewService.IsPreview();
return contentIds
- .Select(contentId => _publishedContentCache.GetById(_requestPreviewService.IsPreview(), contentId))
+ .Select(contentId => _publishedContentCache.GetById(isPreviewMode, contentId))
.WhereNotNull()
.Where(IsAllowedContentType)
.ToArray();
diff --git a/src/Umbraco.Core/DeliveryApi/IApiPublishedContentCache.cs b/src/Umbraco.Core/DeliveryApi/IApiPublishedContentCache.cs
index e24e43474c..d72eacd1c1 100644
--- a/src/Umbraco.Core/DeliveryApi/IApiPublishedContentCache.cs
+++ b/src/Umbraco.Core/DeliveryApi/IApiPublishedContentCache.cs
@@ -9,4 +9,8 @@ public interface IApiPublishedContentCache
IPublishedContent? GetById(Guid contentId);
IEnumerable GetByIds(IEnumerable contentIds);
+
+ Task GetByIdAsync(Guid contentId) => Task.FromResult(GetById(contentId));
+ Task GetByRouteAsync(string route) => Task.FromResult(GetByRoute(route));
+ Task> GetByIdsAsync(IEnumerable contentIds) => Task.FromResult(GetByIds(contentIds));
}
diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs
index 6c771f5023..a3b3ed7631 100644
--- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs
+++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs
@@ -104,6 +104,10 @@ public static partial class UmbracoBuilderExtensions
builder.Services.Configure(
Constants.Configuration.NamedOptions.InstallDefaultData.MemberTypes,
builder.Config.GetSection($"{Constants.Configuration.ConfigInstallDefaultData}:{Constants.Configuration.NamedOptions.InstallDefaultData.MemberTypes}"));
+ builder.Services.Configure(Constants.Configuration.NamedOptions.CacheEntry.Media,
+ builder.Config.GetSection($"{Constants.Configuration.ConfigCacheEntry}:{Constants.Configuration.NamedOptions.CacheEntry.Media}"));
+ builder.Services.Configure(Constants.Configuration.NamedOptions.CacheEntry.Document,
+ builder.Config.GetSection($"{Constants.Configuration.ConfigCacheEntry}:{Constants.Configuration.NamedOptions.CacheEntry.Document}"));
// TODO: Remove this in V12
// This is to make the move of the AllowEditInvariantFromNonDefault setting from SecuritySettings to ContentSettings backwards compatible
diff --git a/src/Umbraco.Core/Extensions/ObjectExtensions.cs b/src/Umbraco.Core/Extensions/ObjectExtensions.cs
index 21fa8fa3b4..50fe788da0 100644
--- a/src/Umbraco.Core/Extensions/ObjectExtensions.cs
+++ b/src/Umbraco.Core/Extensions/ObjectExtensions.cs
@@ -48,7 +48,14 @@ public static class ObjectExtensions
{
if (input is IDisposable disposable)
{
- disposable.Dispose();
+ try
+ {
+ disposable.Dispose();
+ }
+ catch (ObjectDisposedException)
+ {
+ // ignore if it is already disposed
+ }
}
}
diff --git a/src/Umbraco.Core/Extensions/PublishedContentExtensions.cs b/src/Umbraco.Core/Extensions/PublishedContentExtensions.cs
index 7d9f66052f..2dd971114d 100644
--- a/src/Umbraco.Core/Extensions/PublishedContentExtensions.cs
+++ b/src/Umbraco.Core/Extensions/PublishedContentExtensions.cs
@@ -68,6 +68,7 @@ public static class PublishedContentExtensions
/// The specific culture to get the URL segment for. If null is used the current culture is used
/// (Default is null).
///
+ [Obsolete("Please use GetUrlSegment() on IDocumentUrlService instead. Scheduled for removal in V16.")]
public static string? UrlSegment(this IPublishedContent content, IVariationContextAccessor? variationContextAccessor, string? culture = null)
{
if (content == null)
diff --git a/src/Umbraco.Core/Models/CacheEntrySettings.cs b/src/Umbraco.Core/Models/CacheEntrySettings.cs
new file mode 100644
index 0000000000..21748b73f5
--- /dev/null
+++ b/src/Umbraco.Core/Models/CacheEntrySettings.cs
@@ -0,0 +1,19 @@
+using System.ComponentModel;
+
+namespace Umbraco.Cms.Core.Models;
+
+public class CacheEntrySettings
+{
+ internal const string StaticLocalCacheDuration = "1.00:00:00";
+ internal const string StaticRemoteCacheDuration = "365.00:00:00";
+ internal const string StaticSeedCacheDuration = "365.00:00:00";
+
+ [DefaultValue(StaticLocalCacheDuration)]
+ public TimeSpan LocalCacheDuration { get; set; } = TimeSpan.Parse(StaticLocalCacheDuration);
+
+ [DefaultValue(StaticRemoteCacheDuration)]
+ public TimeSpan RemoteCacheDuration { get; set; } = TimeSpan.Parse(StaticRemoteCacheDuration);
+
+ [DefaultValue(StaticSeedCacheDuration)]
+ public TimeSpan SeedCacheDuration { get; set; } = TimeSpan.Parse(StaticSeedCacheDuration);
+}
diff --git a/src/Umbraco.Core/Models/CacheSettings.cs b/src/Umbraco.Core/Models/CacheSettings.cs
index 2d4373a4da..f478756f0c 100644
--- a/src/Umbraco.Core/Models/CacheSettings.cs
+++ b/src/Umbraco.Core/Models/CacheSettings.cs
@@ -7,7 +7,6 @@ namespace Umbraco.Cms.Core.Models;
public class CacheSettings
{
internal const int StaticDocumentBreadthFirstSeedCount = 100;
-
internal const int StaticMediaBreadthFirstSeedCount = 100;
internal const string StaticSeedCacheDuration = "365.00:00:00";
@@ -20,10 +19,10 @@ public class CacheSettings
[DefaultValue(StaticDocumentBreadthFirstSeedCount)]
public int DocumentBreadthFirstSeedCount { get; set; } = StaticDocumentBreadthFirstSeedCount;
-
[DefaultValue(StaticMediaBreadthFirstSeedCount)]
public int MediaBreadthFirstSeedCount { get; set; } = StaticDocumentBreadthFirstSeedCount;
+ [Obsolete("Use Cache:Entry:Document:SeedCacheDuration instead")]
[DefaultValue(StaticSeedCacheDuration)]
public TimeSpan SeedCacheDuration { get; set; } = TimeSpan.Parse(StaticSeedCacheDuration);
}
diff --git a/src/Umbraco.Core/Models/IDataValueEditor.cs b/src/Umbraco.Core/Models/IDataValueEditor.cs
index 691ee70bf1..f1599677d5 100644
--- a/src/Umbraco.Core/Models/IDataValueEditor.cs
+++ b/src/Umbraco.Core/Models/IDataValueEditor.cs
@@ -1,6 +1,7 @@
using System.ComponentModel.DataAnnotations;
using System.Xml.Linq;
using Umbraco.Cms.Core.Models.Editors;
+using Umbraco.Cms.Core.Models.Validation;
using Umbraco.Cms.Core.PropertyEditors;
namespace Umbraco.Cms.Core.Models;
@@ -42,7 +43,8 @@ public interface IDataValueEditor
/// The property value.
/// A value indicating whether the property value is required.
/// A specific format (regex) that the property value must respect.
- IEnumerable Validate(object? value, bool required, string? format);
+ /// The context in which the property value is being validated.
+ IEnumerable Validate(object? value, bool required, string? format, PropertyValidationContext validationContext);
///
/// Converts a value posted by the editor to a property value.
diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentBase.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentBase.cs
index 56f7789578..bf41498c43 100644
--- a/src/Umbraco.Core/Models/PublishedContent/PublishedContentBase.cs
+++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentBase.cs
@@ -31,6 +31,7 @@ namespace Umbraco.Cms.Core.Models.PublishedContent
public virtual string Name => this.Name(_variationContextAccessor);
///
+ [Obsolete("Please use GetUrlSegment() on IDocumentUrlService instead. Scheduled for removal in V16.")]
public virtual string? UrlSegment => this.UrlSegment(_variationContextAccessor);
///
@@ -75,7 +76,6 @@ namespace Umbraco.Cms.Core.Models.PublishedContent
[Obsolete("Please use TryGetParentKey() on IDocumentNavigationQueryService or IMediaNavigationQueryService instead. Scheduled for removal in V16.")]
public abstract IPublishedContent? Parent { get; }
- // FIXME
///
[Obsolete("Please use TryGetChildrenKeys() on IDocumentNavigationQueryService or IMediaNavigationQueryService instead. Scheduled for removal in V16.")]
public virtual IEnumerable Children => GetChildren();
diff --git a/src/Umbraco.Core/Models/Validation/PropertyValidationContext.cs b/src/Umbraco.Core/Models/Validation/PropertyValidationContext.cs
new file mode 100644
index 0000000000..d10fa076b0
--- /dev/null
+++ b/src/Umbraco.Core/Models/Validation/PropertyValidationContext.cs
@@ -0,0 +1,22 @@
+namespace Umbraco.Cms.Core.Models.Validation;
+
+public sealed class PropertyValidationContext
+{
+ public required string? Culture { get; init; }
+
+ public required string? Segment { get; init; }
+
+ public required IEnumerable CulturesBeingValidated { get; init; }
+
+ public required IEnumerable SegmentsBeingValidated { get; init; }
+
+ public static PropertyValidationContext Empty() => new()
+ {
+ Culture = null, Segment = null, CulturesBeingValidated = [], SegmentsBeingValidated = []
+ };
+
+ public static PropertyValidationContext CultureAndSegment(string? culture, string? segment) => new()
+ {
+ Culture = culture, Segment = segment, CulturesBeingValidated = [], SegmentsBeingValidated = []
+ };
+}
diff --git a/src/Umbraco.Core/PropertyEditors/ConfigurationEditor.cs b/src/Umbraco.Core/PropertyEditors/ConfigurationEditor.cs
index 68ce85eac9..58412fa7a2 100644
--- a/src/Umbraco.Core/PropertyEditors/ConfigurationEditor.cs
+++ b/src/Umbraco.Core/PropertyEditors/ConfigurationEditor.cs
@@ -1,5 +1,6 @@
using System.ComponentModel.DataAnnotations;
using System.Runtime.Serialization;
+using Umbraco.Cms.Core.Models.Validation;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Extensions;
@@ -85,7 +86,7 @@ public class ConfigurationEditor : IConfigurationEditor
=> Fields
.SelectMany(field =>
configuration.TryGetValue(field.Key, out var value)
- ? field.Validators.SelectMany(validator => validator.Validate(value, null, null))
+ ? field.Validators.SelectMany(validator => validator.Validate(value, null, null, PropertyValidationContext.Empty()))
: Enumerable.Empty())
.ToArray();
diff --git a/src/Umbraco.Core/PropertyEditors/DataValueEditor.cs b/src/Umbraco.Core/PropertyEditors/DataValueEditor.cs
index e9d131c75a..5f06db2b3c 100644
--- a/src/Umbraco.Core/PropertyEditors/DataValueEditor.cs
+++ b/src/Umbraco.Core/PropertyEditors/DataValueEditor.cs
@@ -6,6 +6,7 @@ using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Editors;
+using Umbraco.Cms.Core.Models.Validation;
using Umbraco.Cms.Core.PropertyEditors.Validators;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Services;
@@ -105,10 +106,10 @@ public class DataValueEditor : IDataValueEditor
public List Validators { get; private set; } = new();
///
- public IEnumerable Validate(object? value, bool required, string? format)
+ public IEnumerable Validate(object? value, bool required, string? format, PropertyValidationContext validationContext)
{
List? results = null;
- var r = Validators.SelectMany(v => v.Validate(value, ValueType, ConfigurationObject)).ToList();
+ var r = Validators.SelectMany(v => v.Validate(value, ValueType, ConfigurationObject, validationContext)).ToList();
if (r.Any())
{
results = r;
diff --git a/src/Umbraco.Core/PropertyEditors/IValueValidator.cs b/src/Umbraco.Core/PropertyEditors/IValueValidator.cs
index 7d26f8a96c..338c2384cb 100644
--- a/src/Umbraco.Core/PropertyEditors/IValueValidator.cs
+++ b/src/Umbraco.Core/PropertyEditors/IValueValidator.cs
@@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations;
+using Umbraco.Cms.Core.Models.Validation;
namespace Umbraco.Cms.Core.PropertyEditors;
@@ -13,6 +14,7 @@ public interface IValueValidator
/// The value to validate.
/// The value type.
/// A datatype configuration.
+ /// The context in which the value is being validated.
/// Validation results.
///
///
@@ -20,5 +22,5 @@ public interface IValueValidator
/// editor.
///
///
- IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration);
+ IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration, PropertyValidationContext validationContext);
}
diff --git a/src/Umbraco.Core/PropertyEditors/Validation/JsonPathExpression.cs b/src/Umbraco.Core/PropertyEditors/Validation/JsonPathExpression.cs
new file mode 100644
index 0000000000..80582d50de
--- /dev/null
+++ b/src/Umbraco.Core/PropertyEditors/Validation/JsonPathExpression.cs
@@ -0,0 +1,9 @@
+using Umbraco.Extensions;
+
+namespace Umbraco.Cms.Core.PropertyEditors.Validation;
+
+public static class JsonPathExpression
+{
+ public static string MissingPropertyValue(string propertyAlias, string? culture, string? segment)
+ => $"?(@.alias == '{propertyAlias}' && @.culture == {(culture.IsNullOrWhiteSpace() ? "null" : $"'{culture}'")} && @.segment == {(segment.IsNullOrWhiteSpace() ? "null" : $"'{segment}'")})";
+}
diff --git a/src/Umbraco.Core/PropertyEditors/Validators/DateTimeValidator.cs b/src/Umbraco.Core/PropertyEditors/Validators/DateTimeValidator.cs
index 530935d276..1c8fcedab2 100644
--- a/src/Umbraco.Core/PropertyEditors/Validators/DateTimeValidator.cs
+++ b/src/Umbraco.Core/PropertyEditors/Validators/DateTimeValidator.cs
@@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations;
+using Umbraco.Cms.Core.Models.Validation;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.PropertyEditors.Validators;
@@ -8,7 +9,7 @@ namespace Umbraco.Cms.Core.PropertyEditors.Validators;
///
public class DateTimeValidator : IValueValidator
{
- public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration)
+ public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration, PropertyValidationContext validationContext)
{
// don't validate if empty
if (value == null || value.ToString().IsNullOrWhiteSpace())
diff --git a/src/Umbraco.Core/PropertyEditors/Validators/DecimalValidator.cs b/src/Umbraco.Core/PropertyEditors/Validators/DecimalValidator.cs
index 3dc3774d2f..ea04699aed 100644
--- a/src/Umbraco.Core/PropertyEditors/Validators/DecimalValidator.cs
+++ b/src/Umbraco.Core/PropertyEditors/Validators/DecimalValidator.cs
@@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations;
+using Umbraco.Cms.Core.Models.Validation;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.PropertyEditors.Validators;
@@ -9,7 +10,7 @@ namespace Umbraco.Cms.Core.PropertyEditors.Validators;
public sealed class DecimalValidator : IValueValidator
{
///
- public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration)
+ public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration, PropertyValidationContext validationContext)
{
if (value == null || value.ToString() == string.Empty)
{
diff --git a/src/Umbraco.Core/PropertyEditors/Validators/EmailValidator.cs b/src/Umbraco.Core/PropertyEditors/Validators/EmailValidator.cs
index 69b7dafff9..592b2dc2c7 100644
--- a/src/Umbraco.Core/PropertyEditors/Validators/EmailValidator.cs
+++ b/src/Umbraco.Core/PropertyEditors/Validators/EmailValidator.cs
@@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations;
+using Umbraco.Cms.Core.Models.Validation;
namespace Umbraco.Cms.Core.PropertyEditors.Validators;
@@ -8,7 +9,7 @@ namespace Umbraco.Cms.Core.PropertyEditors.Validators;
public sealed class EmailValidator : IValueValidator
{
///
- public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration)
+ public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration, PropertyValidationContext validationContext)
{
var asString = value == null ? string.Empty : value.ToString();
diff --git a/src/Umbraco.Core/PropertyEditors/Validators/IntegerValidator.cs b/src/Umbraco.Core/PropertyEditors/Validators/IntegerValidator.cs
index 4e344dffff..5027bd69ef 100644
--- a/src/Umbraco.Core/PropertyEditors/Validators/IntegerValidator.cs
+++ b/src/Umbraco.Core/PropertyEditors/Validators/IntegerValidator.cs
@@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations;
+using Umbraco.Cms.Core.Models.Validation;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.PropertyEditors.Validators;
@@ -9,7 +10,7 @@ namespace Umbraco.Cms.Core.PropertyEditors.Validators;
public sealed class IntegerValidator : IValueValidator
{
///
- public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration)
+ public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration, PropertyValidationContext validationContext)
{
if (value != null && value.ToString() != string.Empty)
{
diff --git a/src/Umbraco.Core/PropertyEditors/Validators/RegexValidator.cs b/src/Umbraco.Core/PropertyEditors/Validators/RegexValidator.cs
index 4e985937cc..f1fe0a199e 100644
--- a/src/Umbraco.Core/PropertyEditors/Validators/RegexValidator.cs
+++ b/src/Umbraco.Core/PropertyEditors/Validators/RegexValidator.cs
@@ -3,6 +3,7 @@
using System.ComponentModel.DataAnnotations;
using System.Text.RegularExpressions;
+using Umbraco.Cms.Core.Models.Validation;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Core.PropertyEditors.Validators;
@@ -49,7 +50,7 @@ public sealed class RegexValidator : IValueFormatValidator, IValueValidator
=> _regex = regex;
///
- public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration)
+ public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration, PropertyValidationContext validationContext)
{
if (_regex == null)
{
diff --git a/src/Umbraco.Core/PropertyEditors/Validators/RequiredValidator.cs b/src/Umbraco.Core/PropertyEditors/Validators/RequiredValidator.cs
index 92e3678e7d..45389caec4 100644
--- a/src/Umbraco.Core/PropertyEditors/Validators/RequiredValidator.cs
+++ b/src/Umbraco.Core/PropertyEditors/Validators/RequiredValidator.cs
@@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations;
+using Umbraco.Cms.Core.Models.Validation;
using Umbraco.Cms.Core.Services;
using Umbraco.Extensions;
@@ -20,7 +21,7 @@ public class RequiredValidator : IValueRequiredValidator, IValueValidator
}
///
- public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration) =>
+ public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration, PropertyValidationContext validationContext) =>
ValidateRequired(value, valueType);
///
diff --git a/src/Umbraco.PublishedCache.HybridCache/Services/IDocumentCacheService.cs b/src/Umbraco.Core/PublishedCache/IDocumentCacheService.cs
similarity index 59%
rename from src/Umbraco.PublishedCache.HybridCache/Services/IDocumentCacheService.cs
rename to src/Umbraco.Core/PublishedCache/IDocumentCacheService.cs
index 18a25496e2..d17c628063 100644
--- a/src/Umbraco.PublishedCache.HybridCache/Services/IDocumentCacheService.cs
+++ b/src/Umbraco.Core/PublishedCache/IDocumentCacheService.cs
@@ -1,7 +1,7 @@
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.PublishedContent;
-namespace Umbraco.Cms.Infrastructure.HybridCache.Services;
+namespace Umbraco.Cms.Core.PublishedCache;
public interface IDocumentCacheService
{
@@ -19,5 +19,13 @@ public interface IDocumentCacheService
void Rebuild(IReadOnlyCollection contentTypeIds);
- internal IEnumerable GetByContentType(IPublishedContentType contentType);
+ IEnumerable GetByContentType(IPublishedContentType contentType);
+
+ Task ClearMemoryCacheAsync(CancellationToken cancellationToken);
+
+ Task RefreshMemoryCacheAsync(Guid key);
+
+ Task RemoveFromMemoryCacheAsync(Guid key);
+
+ Task RebuildMemoryCacheByContentTypeAsync(IEnumerable contentTypeIds);
}
diff --git a/src/Umbraco.PublishedCache.HybridCache/Services/IMediaCacheService.cs b/src/Umbraco.Core/PublishedCache/IMediaCacheService.cs
similarity index 62%
rename from src/Umbraco.PublishedCache.HybridCache/Services/IMediaCacheService.cs
rename to src/Umbraco.Core/PublishedCache/IMediaCacheService.cs
index abaf0f28b5..13d654676c 100644
--- a/src/Umbraco.PublishedCache.HybridCache/Services/IMediaCacheService.cs
+++ b/src/Umbraco.Core/PublishedCache/IMediaCacheService.cs
@@ -1,7 +1,7 @@
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.PublishedContent;
-namespace Umbraco.Cms.Infrastructure.HybridCache.Services;
+namespace Umbraco.Cms.Core.PublishedCache;
public interface IMediaCacheService
{
@@ -13,6 +13,14 @@ public interface IMediaCacheService
Task RefreshMediaAsync(IMedia media);
+ Task RebuildMemoryCacheByContentTypeAsync(IEnumerable mediaTypeIds);
+
+ Task ClearMemoryCacheAsync(CancellationToken cancellationToken);
+
+ Task RefreshMemoryCacheAsync(Guid key);
+
+ Task RemoveFromMemoryCacheAsync(Guid key);
+
Task DeleteItemAsync(IContentBase media);
Task SeedAsync(CancellationToken cancellationToken);
diff --git a/src/Umbraco.Core/PublishedCache/IPublishedContentTypeCache.cs b/src/Umbraco.Core/PublishedCache/IPublishedContentTypeCache.cs
index 318e7046c1..39c02d9f2f 100644
--- a/src/Umbraco.Core/PublishedCache/IPublishedContentTypeCache.cs
+++ b/src/Umbraco.Core/PublishedCache/IPublishedContentTypeCache.cs
@@ -15,12 +15,35 @@ public interface IPublishedContentTypeCache
/// An identifier.
public void ClearContentType(int id);
+ ///
+ /// Clears cached content types.
+ ///
+ /// ContentType IDs to clear
+ public void ClearContentTypes(IEnumerable ids)
+ {
+ foreach (var id in ids)
+ {
+ ClearContentType(id);
+ }
+ }
+
///
/// Clears all cached content types referencing a data type.
///
/// A data type identifier.
public void ClearDataType(int id);
+ ///
+ /// Clears all cached content types referencing a data type.
+ ///
+ /// The data type id to remove content types by
+ /// The removed content types
+ public IEnumerable ClearByDataTypeId(int id)
+ {
+ ClearDataType(id);
+ return [];
+ }
+
///
/// Gets a published content type.
///
diff --git a/src/Umbraco.Core/Routing/NewDefaultUrlProvider.cs b/src/Umbraco.Core/Routing/NewDefaultUrlProvider.cs
index 4e636a3f9e..f7eeb1a1b4 100644
--- a/src/Umbraco.Core/Routing/NewDefaultUrlProvider.cs
+++ b/src/Umbraco.Core/Routing/NewDefaultUrlProvider.cs
@@ -177,7 +177,7 @@ public class NewDefaultUrlProvider : IUrlProvider
UrlMode mode,
string? culture)
{
- if (string.IsNullOrWhiteSpace(route))
+ if (string.IsNullOrWhiteSpace(route) || route.Equals("#"))
{
if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug))
{
diff --git a/src/Umbraco.Core/Services/ContentPublishingService.cs b/src/Umbraco.Core/Services/ContentPublishingService.cs
index 61592ee021..24c73271ee 100644
--- a/src/Umbraco.Core/Services/ContentPublishingService.cs
+++ b/src/Umbraco.Core/Services/ContentPublishingService.cs
@@ -77,17 +77,13 @@ internal sealed class ContentPublishingService : IContentPublishingService
}
ContentValidationResult validationResult = await ValidateCurrentContentAsync(content, cultures);
-
- var errors = validationResult.ValidationErrors.Where(err =>
- cultures.Contains(err.Culture ?? "*", StringComparer.InvariantCultureIgnoreCase));
- if (errors.Any())
+ if (validationResult.ValidationErrors.Any())
{
scope.Complete();
return Attempt.FailWithStatus(ContentPublishingOperationStatus.ContentInvalid, new ContentPublishingResult
{
Content = content,
- InvalidPropertyAliases = errors.Select(property => property.Alias).ToArray()
- ?? Enumerable.Empty()
+ InvalidPropertyAliases = validationResult.ValidationErrors.Select(property => property.Alias).ToArray()
});
}
@@ -131,11 +127,12 @@ internal sealed class ContentPublishingService : IContentPublishingService
var model = new ContentUpdateModel()
{
InvariantName = content.Name,
- InvariantProperties = cultures.Contains("*") ? content.Properties.Where(x=>x.PropertyType.VariesByCulture() is false).Select(x=> new PropertyValueModel()
+ // NOTE KJA: this needs redoing; we need to make an informed decision whether to include invariant properties, depending on if editing invariant properties is allowed on all variants, or if the default language is included in cultures
+ InvariantProperties = content.Properties.Where(x => x.PropertyType.VariesByCulture() is false).Select(x => new PropertyValueModel()
{
Alias = x.Alias,
Value = x.GetValue()
- }) : Array.Empty(),
+ }),
Variants = cultures.Select(culture => new VariantModel()
{
Name = content.GetPublishName(culture) ?? string.Empty,
@@ -149,7 +146,7 @@ internal sealed class ContentPublishingService : IContentPublishingService
})
};
IContentType? contentType = _contentTypeService.Get(content.ContentType.Key)!;
- ContentValidationResult validationResult = await _contentValidationService.ValidatePropertiesAsync(model, contentType);
+ ContentValidationResult validationResult = await _contentValidationService.ValidatePropertiesAsync(model, contentType, cultures);
return validationResult;
}
diff --git a/src/Umbraco.Core/Services/ContentValidationServiceBase.cs b/src/Umbraco.Core/Services/ContentValidationServiceBase.cs
index 63add24731..03d2590f65 100644
--- a/src/Umbraco.Core/Services/ContentValidationServiceBase.cs
+++ b/src/Umbraco.Core/Services/ContentValidationServiceBase.cs
@@ -2,6 +2,7 @@
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.ContentEditing;
using Umbraco.Cms.Core.Models.ContentEditing.Validation;
+using Umbraco.Cms.Core.Models.Validation;
using Umbraco.Cms.Core.PropertyEditors.Validation;
using Umbraco.Extensions;
@@ -34,9 +35,28 @@ internal abstract class ContentValidationServiceBase
.ToArray();
IPropertyType[] variantPropertyTypes = contentTypePropertyTypes.Except(invariantPropertyTypes).ToArray();
+ var cultures = culturesToValidate?.WhereNotNull().Except(["*"]).ToArray();
+ if (cultures?.Any() is not true)
+ {
+ cultures = await GetCultureCodes();
+ }
+
+ // we don't have any managed segments, so we have to make do with the ones passed in the model
+ var segments = contentEditingModelBase.Variants
+ .Where(variant => variant.Culture is null || cultures.Contains(variant.Culture))
+ .DistinctBy(variant => variant.Segment).Select(variant => variant.Segment)
+ .WhereNotNull()
+ .ToArray();
+
foreach (IPropertyType propertyType in invariantPropertyTypes)
{
- validationErrors.AddRange(ValidateProperty(contentEditingModelBase, propertyType, null, null));
+ var validationContext = new PropertyValidationContext
+ {
+ Culture = null, Segment = null, CulturesBeingValidated = cultures, SegmentsBeingValidated = segments
+ };
+
+ PropertyValueModel? propertyValueModel = contentEditingModelBase.InvariantProperties.FirstOrDefault(propertyValue => propertyValue.Alias == propertyType.Alias);
+ validationErrors.AddRange(ValidateProperty(propertyType, propertyValueModel, validationContext));
}
if (variantPropertyTypes.Any() is false)
@@ -44,17 +64,23 @@ internal abstract class ContentValidationServiceBase
return new ContentValidationResult { ValidationErrors = validationErrors };
}
- var cultures = culturesToValidate?.ToArray() ?? await GetCultureCodes();
- // we don't have any managed segments, so we have to make do with the ones passed in the model
- var segments = contentEditingModelBase.Variants.DistinctBy(variant => variant.Segment).Select(variant => variant.Segment).ToArray();
-
foreach (IPropertyType propertyType in variantPropertyTypes)
{
foreach (var culture in cultures)
{
- foreach (var segment in segments)
+ foreach (var segment in segments.DefaultIfEmpty(null))
{
- validationErrors.AddRange(ValidateProperty(contentEditingModelBase, propertyType, culture, segment));
+ var validationContext = new PropertyValidationContext
+ {
+ Culture = culture, Segment = segment, CulturesBeingValidated = cultures, SegmentsBeingValidated = segments
+ };
+
+ PropertyValueModel? propertyValueModel = contentEditingModelBase
+ .Variants
+ .FirstOrDefault(variant => string.Equals(variant.Culture, culture, StringComparison.InvariantCultureIgnoreCase) && string.Equals(segment, variant.Segment, StringComparison.InvariantCultureIgnoreCase))?
+ .Properties
+ .FirstOrDefault(propertyValue => propertyValue.Alias == propertyType.Alias);
+ validationErrors.AddRange(ValidateProperty(propertyType, propertyValueModel, validationContext));
}
}
}
@@ -75,19 +101,10 @@ internal abstract class ContentValidationServiceBase
private async Task GetCultureCodes() => (await _languageService.GetAllAsync()).Select(language => language.IsoCode).ToArray();
- private IEnumerable ValidateProperty(ContentEditingModelBase contentEditingModelBase, IPropertyType propertyType, string? culture, string? segment)
+ private IEnumerable ValidateProperty(IPropertyType propertyType, PropertyValueModel? propertyValueModel, PropertyValidationContext validationContext)
{
- IEnumerable? properties = culture is null && segment is null
- ? contentEditingModelBase.InvariantProperties
- : contentEditingModelBase
- .Variants
- .FirstOrDefault(variant => string.Equals(variant.Culture, culture, StringComparison.InvariantCultureIgnoreCase) && string.Equals(segment, variant.Segment, StringComparison.InvariantCultureIgnoreCase))?
- .Properties;
-
- PropertyValueModel? propertyValueModel = properties?.FirstOrDefault(p => p.Alias == propertyType.Alias);
-
ValidationResult[] validationResults = _propertyValidationService
- .ValidatePropertyValue(propertyType, propertyValueModel?.Value)
+ .ValidatePropertyValue(propertyType, propertyValueModel?.Value, validationContext)
.ToArray();
if (validationResults.Any() is false)
@@ -96,7 +113,7 @@ internal abstract class ContentValidationServiceBase
}
PropertyValidationError[] validationErrors = validationResults
- .SelectMany(validationResult => ExtractPropertyValidationResultJsonPath(validationResult, propertyType.Alias, culture, segment))
+ .SelectMany(validationResult => ExtractPropertyValidationResultJsonPath(validationResult, propertyType.Alias, validationContext.Culture, validationContext.Segment))
.ToArray();
if (validationErrors.Any() is false)
{
@@ -107,8 +124,8 @@ internal abstract class ContentValidationServiceBase
JsonPath = string.Empty,
ErrorMessages = validationResults.Select(v => v.ErrorMessage).WhereNotNull().ToArray(),
Alias = propertyType.Alias,
- Culture = culture,
- Segment = segment
+ Culture = validationContext.Culture,
+ Segment = validationContext.Segment
}
};
}
diff --git a/src/Umbraco.Core/Services/DocumentUrlService.cs b/src/Umbraco.Core/Services/DocumentUrlService.cs
index 16df42f894..7dc95deacc 100644
--- a/src/Umbraco.Core/Services/DocumentUrlService.cs
+++ b/src/Umbraco.Core/Services/DocumentUrlService.cs
@@ -6,6 +6,7 @@ using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Persistence.Repositories;
+using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Cms.Core.Routing;
using Umbraco.Cms.Core.Scoping;
using Umbraco.Cms.Core.Services.Navigation;
@@ -30,8 +31,8 @@ public class DocumentUrlService : IDocumentUrlService
private readonly IKeyValueService _keyValueService;
private readonly IIdKeyMap _idKeyMap;
private readonly IDocumentNavigationQueryService _documentNavigationQueryService;
- private readonly IDomainService _domainService;
private readonly IPublishStatusQueryService _publishStatusQueryService;
+ private readonly IDomainCacheService _domainCacheService;
private readonly ConcurrentDictionary _cache = new();
private bool _isInitialized;
@@ -49,8 +50,8 @@ public class DocumentUrlService : IDocumentUrlService
IKeyValueService keyValueService,
IIdKeyMap idKeyMap,
IDocumentNavigationQueryService documentNavigationQueryService,
- IDomainService domainService,
- IPublishStatusQueryService publishStatusQueryService)
+ IPublishStatusQueryService publishStatusQueryService,
+ IDomainCacheService domainCacheService)
{
_logger = logger;
_documentUrlRepository = documentUrlRepository;
@@ -64,8 +65,8 @@ public class DocumentUrlService : IDocumentUrlService
_keyValueService = keyValueService;
_idKeyMap = idKeyMap;
_documentNavigationQueryService = documentNavigationQueryService;
- _domainService = domainService;
_publishStatusQueryService = publishStatusQueryService;
+ _domainCacheService = domainCacheService;
}
public async Task InitAsync(bool forceEmpty, CancellationToken cancellationToken)
@@ -424,36 +425,48 @@ public class DocumentUrlService : IDocumentUrlService
private bool IsContentPublished(Guid contentKey, string culture) => _publishStatusQueryService.IsDocumentPublished(contentKey, culture);
- public string GetLegacyRouteFormat(Guid docuemntKey, string? culture, bool isDraft)
+ public string GetLegacyRouteFormat(Guid documentKey, string? culture, bool isDraft)
{
- Attempt documentIdAttempt = _idKeyMap.GetIdForKey(docuemntKey, UmbracoObjectTypes.Document);
+ Attempt documentIdAttempt = _idKeyMap.GetIdForKey(documentKey, UmbracoObjectTypes.Document);
if(documentIdAttempt.Success is false)
{
return "#";
}
- if (_documentNavigationQueryService.TryGetAncestorsOrSelfKeys(docuemntKey, out IEnumerable ancestorsOrSelfKeys) is false)
+ if (_documentNavigationQueryService.TryGetAncestorsOrSelfKeys(documentKey, out IEnumerable ancestorsOrSelfKeys) is false)
{
return "#";
}
- var cultureOrDefault = culture ?? _languageService.GetDefaultIsoCodeAsync().GetAwaiter().GetResult();
+ if(isDraft is false && culture != null && _publishStatusQueryService.IsDocumentPublished(documentKey, culture) is false)
+ {
+ return "#";
+ }
+
+ var cultureOrDefault = string.IsNullOrWhiteSpace(culture) is false ? culture : _languageService.GetDefaultIsoCodeAsync().GetAwaiter().GetResult();
Guid[] ancestorsOrSelfKeysArray = ancestorsOrSelfKeys as Guid[] ?? ancestorsOrSelfKeys.ToArray();
- IDictionary ancestorOrSelfKeyToDomains = ancestorsOrSelfKeysArray.ToDictionary(x => x, ancestorKey =>
+ IDictionary ancestorOrSelfKeyToDomains = ancestorsOrSelfKeysArray.ToDictionary(x => x, ancestorKey =>
{
- IEnumerable domains = _domainService.GetAssignedDomainsAsync(ancestorKey, false).GetAwaiter().GetResult();
- return domains.FirstOrDefault(x=>x.LanguageIsoCode == cultureOrDefault);
+ Attempt idAttempt = _idKeyMap.GetIdForKey(ancestorKey, UmbracoObjectTypes.Document);
+
+ if(idAttempt.Success is false)
+ {
+ return null;
+ }
+
+ IEnumerable domains = _domainCacheService.GetAssigned(idAttempt.Result, false);
+ return domains.FirstOrDefault(x=>x.Culture == cultureOrDefault);
});
var urlSegments = new List();
- IDomain? foundDomain = null;
+ Domain? foundDomain = null;
foreach (Guid ancestorOrSelfKey in ancestorsOrSelfKeysArray)
{
- if (ancestorOrSelfKeyToDomains.TryGetValue(ancestorOrSelfKey, out IDomain? domain))
+ if (ancestorOrSelfKeyToDomains.TryGetValue(ancestorOrSelfKey, out Domain? domain))
{
if (domain is not null)
{
@@ -476,7 +489,7 @@ public class DocumentUrlService : IDocumentUrlService
if (foundDomain is not null)
{
//we found a domain, and not to construct the route in the funny legacy way
- return foundDomain.RootContentId + "/" + string.Join("/", urlSegments);
+ return foundDomain.ContentId + "/" + string.Join("/", urlSegments);
}
var isRootFirstItem = GetTopMostRootKey(isDraft, cultureOrDefault) == ancestorsOrSelfKeysArray.Last();
@@ -508,24 +521,30 @@ public class DocumentUrlService : IDocumentUrlService
var cultures = languages.ToDictionary(x=>x.IsoCode);
Guid[] ancestorsOrSelfKeysArray = ancestorsOrSelfKeys as Guid[] ?? ancestorsOrSelfKeys.ToArray();
- Dictionary>> ancestorOrSelfKeyToDomains = ancestorsOrSelfKeysArray.ToDictionary(x => x, async ancestorKey =>
+ Dictionary>> ancestorOrSelfKeyToDomains = ancestorsOrSelfKeysArray.ToDictionary(x => x, async ancestorKey =>
{
- IEnumerable domains = await _domainService.GetAssignedDomainsAsync(ancestorKey, false);
- return domains.ToDictionary(x => x.LanguageIsoCode!);
- });
+ Attempt idAttempt = _idKeyMap.GetIdForKey(ancestorKey, UmbracoObjectTypes.Document);
+
+ if(idAttempt.Success is false)
+ {
+ return null;
+ }
+ IEnumerable domains = _domainCacheService.GetAssigned(idAttempt.Result, false);
+ return domains.ToDictionary(x => x.Culture!);
+ })!;
foreach ((string culture, ILanguage language) in cultures)
{
var urlSegments = new List();
- IDomain? foundDomain = null;
+ Domain? foundDomain = null;
var hasUrlInCulture = true;
foreach (Guid ancestorOrSelfKey in ancestorsOrSelfKeysArray)
{
- if (ancestorOrSelfKeyToDomains.TryGetValue(ancestorOrSelfKey, out Task>? domainDictionaryTask))
+ if (ancestorOrSelfKeyToDomains.TryGetValue(ancestorOrSelfKey, out Task>? domainDictionaryTask))
{
- Dictionary domainDictionary = await domainDictionaryTask;
- if (domainDictionary.TryGetValue(culture, out IDomain? domain))
+ Dictionary domainDictionary = await domainDictionaryTask;
+ if (domainDictionary.TryGetValue(culture, out Domain? domain))
{
foundDomain = domain;
break;
@@ -560,14 +579,14 @@ public class DocumentUrlService : IDocumentUrlService
return result;
}
- private string GetFullUrl(bool isRootFirstItem, List reversedUrlSegments, IDomain? foundDomain)
+ private string GetFullUrl(bool isRootFirstItem, List reversedUrlSegments, Domain? foundDomain)
{
var urlSegments = new List(reversedUrlSegments);
urlSegments.Reverse();
if (foundDomain is not null)
{
- return foundDomain.DomainName.EnsureEndsWith("/") + string.Join('/', urlSegments);
+ return foundDomain.Name.EnsureEndsWith("/") + string.Join('/', urlSegments);
}
return '/' + string.Join('/', urlSegments.Skip(_globalSettings.HideTopLevelNodeFromPath && isRootFirstItem ? 1 : 0));
diff --git a/src/Umbraco.Core/Services/IPropertyValidationService.cs b/src/Umbraco.Core/Services/IPropertyValidationService.cs
index e854d0f7f5..04ef31fb6a 100644
--- a/src/Umbraco.Core/Services/IPropertyValidationService.cs
+++ b/src/Umbraco.Core/Services/IPropertyValidationService.cs
@@ -1,5 +1,6 @@
using System.ComponentModel.DataAnnotations;
using Umbraco.Cms.Core.Models;
+using Umbraco.Cms.Core.Models.Validation;
using Umbraco.Cms.Core.PropertyEditors;
namespace Umbraco.Cms.Core.Services;
@@ -14,6 +15,9 @@ public interface IPropertyValidationService
///
/// Gets a value indicating whether the property has valid values.
///
+ bool IsPropertyValid(IProperty property, PropertyValidationContext validationContext);
+
+ [Obsolete("Please use the overload that accepts a PropertyValidationContext. Will be removed in V16.")]
bool IsPropertyValid(IProperty property, string culture = "*", string segment = "*");
///
@@ -26,11 +30,28 @@ public interface IPropertyValidationService
bool isRequired,
string? validationRegExp,
string? isRequiredMessage,
- string? validationRegExpMessage);
+ string? validationRegExpMessage,
+ PropertyValidationContext validationContext);
///
/// Validates a property value.
///
+ IEnumerable ValidatePropertyValue(
+ IPropertyType propertyType,
+ object? postedValue,
+ PropertyValidationContext validationContext);
+
+ [Obsolete("Please use the overload that accepts a PropertyValidationContext. Will be removed in V16.")]
+ IEnumerable ValidatePropertyValue(
+ IDataEditor editor,
+ IDataType dataType,
+ object? postedValue,
+ bool isRequired,
+ string? validationRegExp,
+ string? isRequiredMessage,
+ string? validationRegExpMessage);
+
+ [Obsolete("Please use the overload that accepts a PropertyValidationContext. Will be removed in V16.")]
IEnumerable ValidatePropertyValue(
IPropertyType propertyType,
object? postedValue);
diff --git a/src/Umbraco.Core/Services/PropertyValidationService.cs b/src/Umbraco.Core/Services/PropertyValidationService.cs
index 0f12e5cbdd..e56b68e86e 100644
--- a/src/Umbraco.Core/Services/PropertyValidationService.cs
+++ b/src/Umbraco.Core/Services/PropertyValidationService.cs
@@ -4,6 +4,7 @@ using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Dictionary;
using Umbraco.Cms.Core.Models;
+using Umbraco.Cms.Core.Models.Validation;
using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Extensions;
@@ -44,29 +45,36 @@ public class PropertyValidationService : IPropertyValidationService
///
public IEnumerable ValidatePropertyValue(
IPropertyType propertyType,
- object? postedValue)
+ object? postedValue,
+ PropertyValidationContext validationContext)
{
if (propertyType is null)
{
throw new ArgumentNullException(nameof(propertyType));
}
- IDataType? dataType = _dataTypeService.GetDataType(propertyType.DataTypeId);
+ IDataType? dataType = GetDataType(propertyType);
if (dataType == null)
{
throw new InvalidOperationException("No data type found by id " + propertyType.DataTypeId);
}
- IDataEditor? editor = _propertyEditors[propertyType.PropertyEditorAlias];
- if (editor == null)
+ IDataEditor? dataEditor = GetDataEditor(propertyType);
+ if (dataEditor == null)
{
throw new InvalidOperationException("No property editor found by alias " +
propertyType.PropertyEditorAlias);
}
- return ValidatePropertyValue(editor, dataType, postedValue, propertyType.Mandatory, propertyType.ValidationRegExp, propertyType.MandatoryMessage, propertyType.ValidationRegExpMessage);
+ return ValidatePropertyValue(dataEditor, dataType, postedValue, propertyType.Mandatory, propertyType.ValidationRegExp, propertyType.MandatoryMessage, propertyType.ValidationRegExpMessage, validationContext);
}
+ [Obsolete("Please use the overload that accepts a PropertyValidationContext. Will be removed in V16.")]
+ public IEnumerable ValidatePropertyValue(
+ IPropertyType propertyType,
+ object? postedValue)
+ => ValidatePropertyValue(propertyType, postedValue, PropertyValidationContext.Empty());
+
///
public IEnumerable ValidatePropertyValue(
IDataEditor editor,
@@ -75,7 +83,8 @@ public class PropertyValidationService : IPropertyValidationService
bool isRequired,
string? validationRegExp,
string? isRequiredMessage,
- string? validationRegExpMessage)
+ string? validationRegExpMessage,
+ PropertyValidationContext validationContext)
{
// Retrieve default messages used for required and regex validatation. We'll replace these
// if set with custom ones if they've been provided for a given property.
@@ -83,7 +92,7 @@ public class PropertyValidationService : IPropertyValidationService
var formatDefaultMessages = new[] { Constants.Validation.ErrorMessages.Properties.PatternMismatch };
IDataValueEditor valueEditor = _valueEditorCache.GetValueEditor(editor, dataType);
- foreach (ValidationResult validationResult in valueEditor.Validate(postedValue, isRequired, validationRegExp))
+ foreach (ValidationResult validationResult in valueEditor.Validate(postedValue, isRequired, validationRegExp, validationContext))
{
// If we've got custom error messages, we'll replace the default ones that will have been applied in the call to Validate().
if (isRequired && !string.IsNullOrWhiteSpace(isRequiredMessage) &&
@@ -102,6 +111,17 @@ public class PropertyValidationService : IPropertyValidationService
}
}
+ [Obsolete("Please use the overload that accepts a PropertyValidationContext. Will be removed in V16.")]
+ public IEnumerable ValidatePropertyValue(
+ IDataEditor editor,
+ IDataType dataType,
+ object? postedValue,
+ bool isRequired,
+ string? validationRegExp,
+ string? isRequiredMessage,
+ string? validationRegExpMessage)
+ => ValidatePropertyValue(editor, dataType, postedValue, isRequired, validationRegExp, isRequiredMessage, validationRegExpMessage, PropertyValidationContext.Empty());
+
///
public bool IsPropertyDataValid(IContent content, out IProperty[] invalidProperties, CultureImpact? impact)
{
@@ -118,19 +138,30 @@ public class PropertyValidationService : IPropertyValidationService
// impacts invariant = validate invariant property, invariant culture
if (impact.ImpactsOnlyInvariantCulture)
{
- return !(propertyTypeVaries || IsPropertyValid(x, null));
+ return !(propertyTypeVaries || IsPropertyValid(x, PropertyValidationContext.Empty()));
}
// impacts all = validate property, all cultures (incl. invariant)
if (impact.ImpactsAllCultures)
{
- return !IsPropertyValid(x);
+ return !IsPropertyValid(x, PropertyValidationContext.CultureAndSegment("*", null));
}
// impacts explicit culture = validate variant property, explicit culture
if (propertyTypeVaries)
{
- return !IsPropertyValid(x, impact.Culture);
+ return !IsPropertyValid(x, PropertyValidationContext.CultureAndSegment(impact.Culture, null));
+ }
+
+ if (impact.ImpactsExplicitCulture && GetDataEditor(x.PropertyType)?.CanMergePartialPropertyValues(x.PropertyType) is true)
+ {
+ return !IsPropertyValid(x, new PropertyValidationContext
+ {
+ Culture = null,
+ Segment = null,
+ CulturesBeingValidated = [impact.Culture!],
+ SegmentsBeingValidated = []
+ });
}
// and, for explicit culture, we may also have to validate invariant property, invariant culture
@@ -138,19 +169,31 @@ public class PropertyValidationService : IPropertyValidationService
// - it is impacted (default culture), or
// - there is no published version of the content - maybe non-default culture, but no published version
var alsoInvariant = impact.ImpactsAlsoInvariantProperties || !content.Published;
- return alsoInvariant && !IsPropertyValid(x, null);
+ return alsoInvariant && !IsPropertyValid(x, PropertyValidationContext.Empty());
}).ToArray();
return invalidProperties.Length == 0;
}
+ [Obsolete("Please use the overload that accepts a PropertyValidationContext. Will be removed in V16.")]
+ public bool IsPropertyValid(IProperty property, string culture = "*", string segment = "*")
+ => IsPropertyValid(property, PropertyValidationContext.CultureAndSegment(culture, segment));
+
///
- public bool IsPropertyValid(IProperty property, string? culture = "*", string? segment = "*")
+ public bool IsPropertyValid(IProperty property, PropertyValidationContext validationContext)
{
// NOTE - the pvalue and vvalues logic in here is borrowed directly from the Property.Values setter so if you are wondering what that's all about, look there.
// The underlying Property._pvalue and Property._vvalues are not exposed but we can re-create these values ourselves which is what it's doing.
- culture = culture?.NullOrWhiteSpaceAsNull();
- segment = segment?.NullOrWhiteSpaceAsNull();
+ validationContext = new PropertyValidationContext
+ {
+ Culture = validationContext.Culture?.NullOrWhiteSpaceAsNull(),
+ Segment = validationContext.Segment?.NullOrWhiteSpaceAsNull(),
+ CulturesBeingValidated = validationContext.CulturesBeingValidated,
+ SegmentsBeingValidated = validationContext.SegmentsBeingValidated
+ };
+
+ var culture = validationContext.Culture;
+ var segment = validationContext.Segment;
IPropertyValue? pvalue = null;
@@ -161,7 +204,7 @@ public class PropertyValidationService : IPropertyValidationService
{
// validate pvalue (which is the invariant value)
pvalue = property.Values.FirstOrDefault(x => x.Culture == null && x.Segment == null);
- if (!IsValidPropertyValue(property, pvalue?.EditedValue))
+ if (!IsValidPropertyValue(property, pvalue?.EditedValue, validationContext))
{
return false;
}
@@ -188,7 +231,7 @@ public class PropertyValidationService : IPropertyValidationService
// if we don't have vvalues (property.Values is empty or only contains pvalue), validate null
if (property.Values.Count == (pvalue == null ? 0 : 1))
{
- return culture == "*" || IsValidPropertyValue(property, null);
+ return culture == "*" || IsValidPropertyValue(property, null, validationContext);
}
// else validate vvalues (but don't revalidate pvalue)
@@ -202,10 +245,10 @@ public class PropertyValidationService : IPropertyValidationService
// if we do not have any vvalues at this point, validate null (no variant values present)
if (vvalues.Any() is false)
{
- return IsValidPropertyValue(property, null);
+ return IsValidPropertyValue(property, null, validationContext);
}
- return vvalues.All(x => IsValidPropertyValue(property, x.EditedValue));
+ return vvalues.All(x => IsValidPropertyValue(property, x.EditedValue, validationContext));
}
///
@@ -214,15 +257,15 @@ public class PropertyValidationService : IPropertyValidationService
///
///
/// True is property value is valid, otherwise false
- private bool IsValidPropertyValue(IProperty property, object? value) =>
- IsPropertyValueValid(property.PropertyType, value);
+ private bool IsValidPropertyValue(IProperty property, object? value, PropertyValidationContext validationContext) =>
+ IsPropertyValueValid(property.PropertyType, value, validationContext);
///
/// Determines whether a value is valid for this property type.
///
- private bool IsPropertyValueValid(IPropertyType propertyType, object? value)
+ private bool IsPropertyValueValid(IPropertyType propertyType, object? value, PropertyValidationContext validationContext)
{
- IDataEditor? editor = _propertyEditors[propertyType.PropertyEditorAlias];
+ IDataEditor? editor = GetDataEditor(propertyType);
if (editor == null)
{
// nothing much we can do validation wise if the property editor has been removed.
@@ -230,8 +273,15 @@ public class PropertyValidationService : IPropertyValidationService
return true;
}
- var configuration = _dataTypeService.GetDataType(propertyType.DataTypeId)?.ConfigurationObject;
+ var configuration = GetDataType(propertyType)?.ConfigurationObject;
IDataValueEditor valueEditor = editor.GetValueEditor(configuration);
- return !valueEditor.Validate(value, propertyType.Mandatory, propertyType.ValidationRegExp).Any();
+
+ return !valueEditor.Validate(value, propertyType.Mandatory, propertyType.ValidationRegExp, validationContext).Any();
}
+
+ private IDataType? GetDataType(IPropertyType propertyType)
+ => _dataTypeService.GetDataType(propertyType.DataTypeId);
+
+ private IDataEditor? GetDataEditor(IPropertyType propertyType)
+ => _propertyEditors[propertyType.PropertyEditorAlias];
}
diff --git a/src/Umbraco.Examine.Lucene/Umbraco.Examine.Lucene.csproj b/src/Umbraco.Examine.Lucene/Umbraco.Examine.Lucene.csproj
index e036605449..cb5bea7a3d 100644
--- a/src/Umbraco.Examine.Lucene/Umbraco.Examine.Lucene.csproj
+++ b/src/Umbraco.Examine.Lucene/Umbraco.Examine.Lucene.csproj
@@ -13,6 +13,8 @@
+
+
diff --git a/src/Umbraco.Infrastructure/Persistence/NPocoSqlExtensions.cs b/src/Umbraco.Infrastructure/Persistence/NPocoSqlExtensions.cs
index 8206570061..55f98f58e5 100644
--- a/src/Umbraco.Infrastructure/Persistence/NPocoSqlExtensions.cs
+++ b/src/Umbraco.Infrastructure/Persistence/NPocoSqlExtensions.cs
@@ -63,6 +63,24 @@ namespace Umbraco.Extensions
return sql.Where(s, a);
}
+ ///
+ /// Appends a WHERE clause to the Sql statement.
+ ///
+ /// The type of Dto 1.
+ /// The type of Dto 2.
+ /// The type of Dto 3.
+ /// The Sql statement.
+ /// A predicate to transform and append to the Sql statement.
+ /// An optional alias for Dto 1 table.
+ /// An optional alias for Dto 2 table.
+ /// An optional alias for Dto 3 table.
+ /// The Sql statement.
+ public static Sql Where(this Sql sql, Expression> predicate, string? alias1 = null, string? alias2 = null, string? alias3 = null)
+ {
+ var (s, a) = sql.SqlContext.VisitDto(predicate, alias1, alias2, alias3);
+ return sql.Where(s, a);
+ }
+
///
/// Appends a WHERE IN clause to the Sql statement.
///
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorMinMaxValidatorBase.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorMinMaxValidatorBase.cs
index f86b47b53d..f8f1dde8a1 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorMinMaxValidatorBase.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorMinMaxValidatorBase.cs
@@ -3,6 +3,7 @@
using System.ComponentModel.DataAnnotations;
using Umbraco.Cms.Core.Models.Blocks;
+using Umbraco.Cms.Core.Models.Validation;
using Umbraco.Cms.Core.Services;
using Umbraco.Extensions;
@@ -19,7 +20,7 @@ internal abstract class BlockEditorMinMaxValidatorBase : IValue
protected ILocalizedTextService TextService { get; }
- public abstract IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration);
+ public abstract IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration, PropertyValidationContext validationContext);
protected IEnumerable ValidateNumberOfBlocks(BlockEditorData? blockEditorData, int? min, int? max)
{
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValidator.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValidator.cs
index 332c414628..9c1041c143 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValidator.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValidator.cs
@@ -3,6 +3,7 @@
using Umbraco.Cms.Core.Cache.PropertyEditors;
using Umbraco.Cms.Core.Models.Blocks;
+using Umbraco.Cms.Core.Models.Validation;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Core.PropertyEditors;
@@ -20,11 +21,11 @@ public class BlockEditorValidator : BlockEditorValidatorBase _blockEditorValues = blockEditorValues;
- protected override IEnumerable GetElementTypeValidation(object? value)
+ protected override IEnumerable GetElementTypeValidation(object? value, PropertyValidationContext validationContext)
{
BlockEditorData? blockEditorData = _blockEditorValues.DeserializeAndClean(value);
return blockEditorData is not null
- ? GetBlockEditorDataValidation(blockEditorData)
+ ? GetBlockEditorDataValidation(blockEditorData, validationContext)
: Array.Empty();
}
}
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValidatorBase.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValidatorBase.cs
index 281b5151ed..243f130875 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValidatorBase.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValidatorBase.cs
@@ -1,6 +1,8 @@
using Umbraco.Cms.Core.Cache.PropertyEditors;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Blocks;
+using Umbraco.Cms.Core.Models.Validation;
+using Umbraco.Cms.Core.PropertyEditors.Validation;
using Umbraco.Cms.Core.Services;
using Umbraco.Extensions;
@@ -16,16 +18,55 @@ public abstract class BlockEditorValidatorBase : ComplexEditorV
: base(propertyValidationService)
=> _elementTypeCache = elementTypeCache;
- protected IEnumerable GetBlockEditorDataValidation(BlockEditorData blockEditorData)
+ protected IEnumerable GetBlockEditorDataValidation(BlockEditorData blockEditorData, PropertyValidationContext validationContext)
+ {
+ var elementTypeValidation = new List();
+ var isWildcardCulture = validationContext.Culture == "*";
+ var validationContextCulture = isWildcardCulture ? null : validationContext.Culture.NullOrWhiteSpaceAsNull();
+ elementTypeValidation.AddRange(GetBlockEditorDataValidation(blockEditorData, validationContextCulture, validationContext.Segment));
+
+ if (validationContextCulture is null)
+ {
+ IEnumerable validationContextCulturesBeingValidated = isWildcardCulture
+ ? blockEditorData.BlockValue.Expose.Select(e => e.Culture).WhereNotNull().Distinct()
+ : validationContext.CulturesBeingValidated;
+ foreach (var culture in validationContextCulturesBeingValidated)
+ {
+ foreach (var segment in validationContext.SegmentsBeingValidated.DefaultIfEmpty(null))
+ {
+ elementTypeValidation.AddRange(GetBlockEditorDataValidation(blockEditorData, culture, segment));
+ }
+ }
+ }
+
+ return elementTypeValidation;
+ }
+
+ private IEnumerable GetBlockEditorDataValidation(BlockEditorData blockEditorData, string? culture, string? segment)
{
// There is no guarantee that the client will post data for every property defined in the Element Type but we still
// need to validate that data for each property especially for things like 'required' data to work.
// Lookup all element types for all content/settings and then we can populate any empty properties.
+ if (blockEditorData.Layout is null)
+ {
+ yield break;
+ }
+
+ Guid[] exposedContentKeys = blockEditorData.BlockValue.Expose
+ .Where(expose => culture is null || expose.Culture == culture)
+ .Select(expose => expose.ContentKey)
+ .Distinct()
+ .ToArray();
+ Guid[] exposedSettingsKeys = blockEditorData.Layout
+ .Where(layout => layout.SettingsKey.HasValue && exposedContentKeys.Contains(layout.ContentKey))
+ .Select(layout => layout.SettingsKey!.Value)
+ .ToArray();
+
var itemDataGroups = new[]
{
- new { Path = nameof(BlockValue.ContentData).ToFirstLowerInvariant(), Items = blockEditorData.BlockValue.ContentData },
- new { Path = nameof(BlockValue.SettingsData).ToFirstLowerInvariant(), Items = blockEditorData.BlockValue.SettingsData }
+ new { Path = nameof(BlockValue.ContentData).ToFirstLowerInvariant(), Items = blockEditorData.BlockValue.ContentData.Where(cd => exposedContentKeys.Contains(cd.Key)).ToArray() },
+ new { Path = nameof(BlockValue.SettingsData).ToFirstLowerInvariant(), Items = blockEditorData.BlockValue.SettingsData.Where(sd => exposedSettingsKeys.Contains(sd.Key)).ToArray() }
};
var valuesJsonPathPart = nameof(BlockItemData.Values).ToFirstLowerInvariant();
@@ -34,7 +75,7 @@ public abstract class BlockEditorValidatorBase : ComplexEditorV
{
var allElementTypes = _elementTypeCache.GetMany(group.Items.Select(x => x.ContentTypeKey).ToArray()).ToDictionary(x => x.Key);
- for (var i = 0; i < group.Items.Count; i++)
+ for (var i = 0; i < group.Items.Length; i++)
{
BlockItemData item = group.Items[i];
if (!allElementTypes.TryGetValue(item.ContentTypeKey, out IContentType? elementType))
@@ -42,9 +83,6 @@ public abstract class BlockEditorValidatorBase : ComplexEditorV
throw new InvalidOperationException($"No element type found with key {item.ContentTypeKey}");
}
- // NOTE: for now this only validates the property data actually sent by the client, not all element properties.
- // we need to ensure that all properties for all languages have a matching "item" entry here, to handle validation of
- // required properties (see comment in the top of this method). a separate task has been created, get in touch with KJA.
var elementValidation = new ElementTypeValidationModel(item.ContentTypeAlias, item.Key);
for (var j = 0; j < item.Values.Count; j++)
{
@@ -55,10 +93,45 @@ public abstract class BlockEditorValidatorBase : ComplexEditorV
throw new ArgumentException("One or more block properties did not have a resolved property type. Block editor values must be resolved before attempting to validate them.", nameof(blockEditorData));
}
+ if (propertyType.VariesByCulture() != (culture is not null) || blockPropertyValue.Culture.InvariantEquals(culture) is false)
+ {
+ continue;
+ }
+
+ if (segment != "*")
+ {
+ if (propertyType.VariesBySegment() != (segment is not null) || blockPropertyValue.Segment.InvariantEquals(segment) is false)
+ {
+ continue;
+ }
+ }
+
elementValidation.AddPropertyTypeValidation(
new PropertyTypeValidationModel(propertyType, blockPropertyValue.Value, $"{group.Path}[{i}].{valuesJsonPathPart}[{j}].value"));
}
+ var handledPropertyTypeAliases = elementValidation.PropertyTypeValidation.Select(v => v.PropertyType.Alias).ToArray();
+ foreach (IPropertyType propertyType in elementType.CompositionPropertyTypes)
+ {
+ if (handledPropertyTypeAliases.Contains(propertyType.Alias))
+ {
+ continue;
+ }
+
+ if (propertyType.VariesByCulture() != (culture is not null))
+ {
+ continue;
+ }
+
+ if (segment == "*" || propertyType.VariesBySegment() != (segment is not null))
+ {
+ continue;
+ }
+
+ elementValidation.AddPropertyTypeValidation(
+ new PropertyTypeValidationModel(propertyType, null, $"{group.Path}[{i}].{valuesJsonPathPart}[{JsonPathExpression.MissingPropertyValue(propertyType.Alias, culture, segment)}].value"));
+ }
+
yield return elementValidation;
}
}
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockGridPropertyEditorBase.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockGridPropertyEditorBase.cs
index b0e8ac46fc..4e410841a1 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/BlockGridPropertyEditorBase.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockGridPropertyEditorBase.cs
@@ -7,6 +7,7 @@ using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Cache.PropertyEditors;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Blocks;
+using Umbraco.Cms.Core.Models.Validation;
using Umbraco.Cms.Core.PropertyEditors.ValueConverters;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Services;
@@ -69,7 +70,7 @@ public abstract class BlockGridPropertyEditorBase : DataEditor
: base(textService) =>
_blockEditorValues = blockEditorValues;
- public override IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration)
+ public override IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration, PropertyValidationContext validationContext)
{
if (dataTypeConfiguration is not BlockGridConfiguration blockConfig)
{
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditorBase.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditorBase.cs
index 2c194ba2c2..749937bd4d 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditorBase.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditorBase.cs
@@ -5,6 +5,7 @@ using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Cache.PropertyEditors;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Blocks;
+using Umbraco.Cms.Core.Models.Validation;
using Umbraco.Cms.Core.PropertyEditors.ValueConverters;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Services;
@@ -80,7 +81,7 @@ public abstract class BlockListPropertyEditorBase : DataEditor
: base(textService) =>
_blockEditorValues = blockEditorValues;
- public override IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration)
+ public override IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration, PropertyValidationContext validationContext)
{
var blockConfig = (BlockListConfiguration?)dataTypeConfiguration;
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ColorPickerConfigurationEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/ColorPickerConfigurationEditor.cs
index 6a3f73e9bb..0ce5f4bfdf 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/ColorPickerConfigurationEditor.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/ColorPickerConfigurationEditor.cs
@@ -4,6 +4,7 @@
using System.ComponentModel.DataAnnotations;
using System.Text.RegularExpressions;
using Umbraco.Cms.Core.IO;
+using Umbraco.Cms.Core.Models.Validation;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Extensions;
@@ -25,7 +26,7 @@ internal class ColorPickerConfigurationEditor : ConfigurationEditor _configurationEditorJsonSerializer = configurationEditorJsonSerializer;
- public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration)
+ public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration, PropertyValidationContext validationContext)
{
var stringValue = value?.ToString();
if (stringValue.IsNullOrWhiteSpace())
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ComplexEditorValidator.cs b/src/Umbraco.Infrastructure/PropertyEditors/ComplexEditorValidator.cs
index 9717eac14d..dea6c098fc 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/ComplexEditorValidator.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/ComplexEditorValidator.cs
@@ -3,6 +3,7 @@
using System.ComponentModel.DataAnnotations;
using Umbraco.Cms.Core.Models;
+using Umbraco.Cms.Core.Models.Validation;
using Umbraco.Cms.Core.PropertyEditors.Validation;
using Umbraco.Cms.Core.Services;
using Umbraco.Extensions;
@@ -19,18 +20,11 @@ public abstract class ComplexEditorValidator : IValueValidator
public ComplexEditorValidator(IPropertyValidationService propertyValidationService) =>
_propertyValidationService = propertyValidationService;
- ///
- /// Return a single for all sub nested validation results in the complex
- /// editor
- ///
- ///
- ///
- ///
- ///
- public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration)
+ ///
+ public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration, PropertyValidationContext validationContext)
{
- var elementTypeValues = GetElementTypeValidation(value).ToList();
- var rowResults = GetNestedValidationResults(elementTypeValues).ToList();
+ var elementTypeValues = GetElementTypeValidation(value, validationContext).ToList();
+ var rowResults = GetNestedValidationResults(elementTypeValues, validationContext).ToList();
if (rowResults.Count > 0)
{
@@ -46,13 +40,14 @@ public abstract class ComplexEditorValidator : IValueValidator
return Enumerable.Empty();
}
- protected abstract IEnumerable GetElementTypeValidation(object? value);
+ protected abstract IEnumerable GetElementTypeValidation(object? value, PropertyValidationContext validationContext);
///
/// Return a nested validation result per row (Element Type)
///
protected IEnumerable GetNestedValidationResults(
- IEnumerable elements)
+ IEnumerable elements,
+ PropertyValidationContext validationContext)
{
foreach (ElementTypeValidationModel row in elements)
{
@@ -63,7 +58,7 @@ public abstract class ComplexEditorValidator : IValueValidator
var propValidationResult = new NestedJsonPathValidationResults(prop.JsonPath);
foreach (ValidationResult validationResult in _propertyValidationService.ValidatePropertyValue(
- prop.PropertyType, prop.PostedValue))
+ prop.PropertyType, prop.PostedValue, validationContext))
{
// add the result to the property results
propValidationResult.ValidationResults.Add(validationResult);
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/RichTextEditorBlockValidator.cs b/src/Umbraco.Infrastructure/PropertyEditors/RichTextEditorBlockValidator.cs
index c3a2a57c13..74428dbd30 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/RichTextEditorBlockValidator.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/RichTextEditorBlockValidator.cs
@@ -1,6 +1,7 @@
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core.Cache.PropertyEditors;
using Umbraco.Cms.Core.Models.Blocks;
+using Umbraco.Cms.Core.Models.Validation;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Services;
@@ -25,7 +26,7 @@ internal class RichTextEditorBlockValidator: BlockEditorValidatorBase GetElementTypeValidation(object? value)
+ protected override IEnumerable GetElementTypeValidation(object? value, PropertyValidationContext validationContext)
{
RichTextPropertyEditorHelper.TryParseRichTextEditorValue(value, _jsonSerializer, _logger, out RichTextEditorValue? richTextEditorValue);
if (richTextEditorValue?.Blocks is null)
@@ -35,7 +36,7 @@ internal class RichTextEditorBlockValidator: BlockEditorValidatorBase? blockEditorData = _blockEditorValues.ConvertAndClean(richTextEditorValue.Blocks);
return blockEditorData is not null
- ? GetBlockEditorDataValidation(blockEditorData)
+ ? GetBlockEditorDataValidation(blockEditorData, validationContext)
: Array.Empty();
}
}
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/TemporaryFileUploadValidator.cs b/src/Umbraco.Infrastructure/PropertyEditors/TemporaryFileUploadValidator.cs
index d53fe22068..71aba644cd 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/TemporaryFileUploadValidator.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/TemporaryFileUploadValidator.cs
@@ -1,6 +1,7 @@
using System.ComponentModel.DataAnnotations;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Models.TemporaryFile;
+using Umbraco.Cms.Core.Models.Validation;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.PropertyEditors;
@@ -32,7 +33,7 @@ internal class TemporaryFileUploadValidator : IValueValidator
_validateFileType = validateFileType;
}
- public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration)
+ public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration, PropertyValidationContext validationContext)
{
Guid? temporaryFileKey = _parseTemporaryFileKey(value);
if (temporaryFileKey.HasValue == false)
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueListUniqueValueValidator.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueListUniqueValueValidator.cs
index b8fd53be8a..9edde833b1 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/ValueListUniqueValueValidator.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueListUniqueValueValidator.cs
@@ -2,6 +2,7 @@
// See LICENSE for more details.
using System.ComponentModel.DataAnnotations;
+using Umbraco.Cms.Core.Models.Validation;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Extensions;
@@ -17,7 +18,7 @@ public class ValueListUniqueValueValidator : IValueValidator
public ValueListUniqueValueValidator(IConfigurationEditorJsonSerializer configurationEditorJsonSerializer)
=> _configurationEditorJsonSerializer = configurationEditorJsonSerializer;
- public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration)
+ public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration, PropertyValidationContext validationContext)
{
if (value is null)
{
diff --git a/src/Umbraco.Infrastructure/PublishedCache/PublishedContentTypeCache.cs b/src/Umbraco.Infrastructure/PublishedCache/PublishedContentTypeCache.cs
index 204f7a910c..c101e2a0e9 100644
--- a/src/Umbraco.Infrastructure/PublishedCache/PublishedContentTypeCache.cs
+++ b/src/Umbraco.Infrastructure/PublishedCache/PublishedContentTypeCache.cs
@@ -120,11 +120,21 @@ public class PublishedContentTypeCache : IPublishedContentTypeCache
}
}
+ public void ClearContentTypes(IEnumerable ids)
+ {
+ foreach (var id in ids)
+ {
+ ClearContentType(id);
+ }
+ }
+
///
/// Clears all cached content types referencing a data type.
///
/// A data type identifier.
- public void ClearDataType(int id)
+ public void ClearDataType(int id) => ClearByDataTypeId(id);
+
+ public IEnumerable ClearByDataTypeId(int id)
{
if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug))
{
@@ -135,11 +145,12 @@ public class PublishedContentTypeCache : IPublishedContentTypeCache
// properties ie both its own properties and those that were inherited (it's based upon an
// IContentTypeComposition) and so every PublishedContentType having a property based upon
// the cleared data type, be it local or inherited, will be cleared.
+ IPublishedContentType[] toRemove;
try
{
_lock.EnterWriteLock();
- IPublishedContentType[] toRemove = _typesById.Values
+ toRemove = _typesById.Values
.Where(x => x.PropertyTypes.Any(xx => xx.DataType.Id == id)).ToArray();
foreach (IPublishedContentType type in toRemove)
{
@@ -154,6 +165,8 @@ public class PublishedContentTypeCache : IPublishedContentTypeCache
_lock.ExitWriteLock();
}
}
+
+ return toRemove;
}
///
diff --git a/src/Umbraco.Infrastructure/Routing/RedirectTracker.cs b/src/Umbraco.Infrastructure/Routing/RedirectTracker.cs
index 067fa91fa4..ee752af798 100644
--- a/src/Umbraco.Infrastructure/Routing/RedirectTracker.cs
+++ b/src/Umbraco.Infrastructure/Routing/RedirectTracker.cs
@@ -14,30 +14,30 @@ namespace Umbraco.Cms.Infrastructure.Routing
{
internal class RedirectTracker : IRedirectTracker
{
- private readonly IUmbracoContextFactory _umbracoContextFactory;
private readonly IVariationContextAccessor _variationContextAccessor;
private readonly ILocalizationService _localizationService;
private readonly IRedirectUrlService _redirectUrlService;
private readonly IPublishedContentCache _contentCache;
private readonly IDocumentNavigationQueryService _navigationQueryService;
private readonly ILogger _logger;
+ private readonly IDocumentUrlService _documentUrlService;
public RedirectTracker(
- IUmbracoContextFactory umbracoContextFactory,
IVariationContextAccessor variationContextAccessor,
ILocalizationService localizationService,
IRedirectUrlService redirectUrlService,
IPublishedContentCache contentCache,
IDocumentNavigationQueryService navigationQueryService,
- ILogger logger)
+ ILogger logger,
+ IDocumentUrlService documentUrlService)
{
- _umbracoContextFactory = umbracoContextFactory;
_variationContextAccessor = variationContextAccessor;
_localizationService = localizationService;
_redirectUrlService = redirectUrlService;
_contentCache = contentCache;
_navigationQueryService = navigationQueryService;
_logger = logger;
+ _documentUrlService = documentUrlService;
}
///
@@ -98,19 +98,11 @@ namespace Umbraco.Cms.Infrastructure.Routing
return;
}
- using UmbracoContextReference reference = _umbracoContextFactory.EnsureUmbracoContext();
- IPublishedContentCache? contentCache = reference.UmbracoContext.Content;
- if (contentCache == null)
- {
- _logger.LogWarning("Could not track redirects because there is no published content cache available on the current published snapshot.");
- return;
- }
-
foreach (((int contentId, string culture), (Guid contentKey, string oldRoute)) in oldRoutes)
{
try
{
- var newRoute = contentCache.GetRouteById(contentId, culture);
+ var newRoute = _documentUrlService.GetLegacyRouteFormat(contentKey, culture, false);
if (!IsValidRoute(newRoute) || oldRoute == newRoute)
{
continue;
diff --git a/src/Umbraco.PublishedCache.HybridCache/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.PublishedCache.HybridCache/DependencyInjection/UmbracoBuilderExtensions.cs
index c33db4c8e1..031ab0d0c5 100644
--- a/src/Umbraco.PublishedCache.HybridCache/DependencyInjection/UmbracoBuilderExtensions.cs
+++ b/src/Umbraco.PublishedCache.HybridCache/DependencyInjection/UmbracoBuilderExtensions.cs
@@ -30,7 +30,12 @@ public static class UmbracoBuilderExtensions
public static IUmbracoBuilder AddUmbracoHybridCache(this IUmbracoBuilder builder)
{
#pragma warning disable EXTEXP0018
- builder.Services.AddHybridCache();
+ builder.Services.AddHybridCache(options =>
+ {
+ // We'll be a bit friendlier and default this to a higher value, you quickly hit the 1MB limit with a few languages and especially blocks.
+ // This can be overwritten later if needed.
+ options.MaximumPayloadBytes = 1024 * 1024 * 100; // 100MB
+ });
#pragma warning restore EXTEXP0018
builder.Services.AddSingleton();
builder.Services.AddSingleton();
diff --git a/src/Umbraco.PublishedCache.HybridCache/DocumentCache.cs b/src/Umbraco.PublishedCache.HybridCache/DocumentCache.cs
index 9be78a9ea2..aab97f9b3c 100644
--- a/src/Umbraco.PublishedCache.HybridCache/DocumentCache.cs
+++ b/src/Umbraco.PublishedCache.HybridCache/DocumentCache.cs
@@ -3,9 +3,9 @@ using Umbraco.Cms.Core;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.PublishedCache;
+using Umbraco.Cms.Core.Routing;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.Navigation;
-using Umbraco.Cms.Infrastructure.HybridCache.Services;
using Umbraco.Extensions;
namespace Umbraco.Cms.Infrastructure.HybridCache;
@@ -95,7 +95,7 @@ public sealed class DocumentCache : IPublishedContentCache
public IEnumerable GetByContentType(IPublishedContentType contentType)
=> _documentCacheService.GetByContentType(contentType);
- [Obsolete("Use IDocumentUrlService.GetDocumentKeyByRoute instead, scheduled for removal in v17")]
+ [Obsolete("Use IPublishedUrlProvider.GetUrl instead, scheduled for removal in v17")]
public IPublishedContent? GetByRoute(bool preview, string route, bool? hideTopLevelNode = null, string? culture = null)
{
IDocumentUrlService documentUrlService = StaticServiceProvider.Instance.GetRequiredService();
@@ -103,7 +103,7 @@ public sealed class DocumentCache : IPublishedContentCache
return key is not null ? GetById(preview, key.Value) : null;
}
- [Obsolete("Use IDocumentUrlService.GetDocumentKeyByRoute instead, scheduled for removal in v17")]
+ [Obsolete("Use IPublishedUrlProvider.GetUrl instead, scheduled for removal in v17")]
public IPublishedContent? GetByRoute(string route, bool? hideTopLevelNode = null, string? culture = null)
{
IDocumentUrlService documentUrlService = StaticServiceProvider.Instance.GetRequiredService();
@@ -111,14 +111,15 @@ public sealed class DocumentCache : IPublishedContentCache
return key is not null ? GetById(key.Value) : null;
}
- [Obsolete("Use IDocumentUrlService.GetDocumentKeyByRoute instead, scheduled for removal in v17")]
+ [Obsolete("Use IPublishedUrlProvider.GetUrl instead, scheduled for removal in v17")]
public string? GetRouteById(bool preview, int contentId, string? culture = null)
{
- IDocumentUrlService documentUrlService = StaticServiceProvider.Instance.GetRequiredService();
+ IPublishedUrlProvider publishedUrlProvider = StaticServiceProvider.Instance.GetRequiredService();
IPublishedContent? content = GetById(preview, contentId);
- return content is not null ? documentUrlService.GetLegacyRouteFormat(content.Key, culture, preview) : null;
+
+ return content is not null ? publishedUrlProvider.GetUrl(content, UrlMode.Relative, culture) : null;
}
- [Obsolete("Use IDocumentUrlService.GetDocumentKeyByRoute instead, scheduled for removal in v17")]
+ [Obsolete("Use IPublishedUrlProvider.GetUrl instead, scheduled for removal in v17")]
public string? GetRouteById(int contentId, string? culture = null) => GetRouteById(false, contentId, culture);
}
diff --git a/src/Umbraco.PublishedCache.HybridCache/Factories/CacheNodeFactory.cs b/src/Umbraco.PublishedCache.HybridCache/Factories/CacheNodeFactory.cs
index 7fa38dd626..e2be9797a0 100644
--- a/src/Umbraco.PublishedCache.HybridCache/Factories/CacheNodeFactory.cs
+++ b/src/Umbraco.PublishedCache.HybridCache/Factories/CacheNodeFactory.cs
@@ -1,6 +1,5 @@
-using StackExchange.Profiling.Internal;
-using Umbraco.Cms.Core.Media.EmbedProviders;
-using Umbraco.Cms.Core.Models;
+using Umbraco.Cms.Core.Models;
+using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Strings;
using Umbraco.Extensions;
@@ -10,11 +9,13 @@ internal class CacheNodeFactory : ICacheNodeFactory
{
private readonly IShortStringHelper _shortStringHelper;
private readonly UrlSegmentProviderCollection _urlSegmentProviders;
+ private readonly IDocumentUrlService _documentUrlService;
- public CacheNodeFactory(IShortStringHelper shortStringHelper, UrlSegmentProviderCollection urlSegmentProviders)
+ public CacheNodeFactory(IShortStringHelper shortStringHelper, UrlSegmentProviderCollection urlSegmentProviders, IDocumentUrlService documentUrlService)
{
_shortStringHelper = shortStringHelper;
_urlSegmentProviders = urlSegmentProviders;
+ _documentUrlService = documentUrlService;
}
public ContentCacheNode ToContentCacheNode(IContent content, bool preview)
@@ -126,6 +127,7 @@ internal class CacheNodeFactory : ICacheNodeFactory
}
var cultureData = new Dictionary();
+ string? urlSegment = null;
// sanitize - names should be ok but ... never knows
if (content.ContentType.VariesByCulture())
@@ -153,10 +155,14 @@ internal class CacheNodeFactory : ICacheNodeFactory
}
}
}
+ else
+ {
+ urlSegment = content.GetUrlSegment(_shortStringHelper, _urlSegmentProviders);
+ }
return new ContentData(
content.Name,
- null,
+ urlSegment,
content.VersionId,
content.UpdateDate,
content.CreatorId,
diff --git a/src/Umbraco.PublishedCache.HybridCache/NotificationHandlers/SeedingNotificationHandler.cs b/src/Umbraco.PublishedCache.HybridCache/NotificationHandlers/SeedingNotificationHandler.cs
index 196dd3950e..47f590125f 100644
--- a/src/Umbraco.PublishedCache.HybridCache/NotificationHandlers/SeedingNotificationHandler.cs
+++ b/src/Umbraco.PublishedCache.HybridCache/NotificationHandlers/SeedingNotificationHandler.cs
@@ -3,6 +3,7 @@ using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Notifications;
+using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Infrastructure.HybridCache.Services;
diff --git a/src/Umbraco.PublishedCache.HybridCache/Persistence/DatabaseCacheRepository.cs b/src/Umbraco.PublishedCache.HybridCache/Persistence/DatabaseCacheRepository.cs
index 30d1358d64..df357c90c9 100644
--- a/src/Umbraco.PublishedCache.HybridCache/Persistence/DatabaseCacheRepository.cs
+++ b/src/Umbraco.PublishedCache.HybridCache/Persistence/DatabaseCacheRepository.cs
@@ -161,6 +161,16 @@ AND cmsContentNu.nodeId IS NULL
return count == 0;
}
+ public async Task> GetContentKeysAsync(Guid nodeObjectType)
+ {
+ Sql sql = Sql()
+ .Select(x => x.UniqueId)
+ .From()
+ .Where(x => x.NodeObjectType == nodeObjectType);
+
+ return await Database.FetchAsync(sql);
+ }
+
// assumes member tree lock
public bool VerifyMemberDbCache()
{
@@ -235,8 +245,13 @@ AND cmsContentNu.nodeId IS NULL
return [];
}
- Sql? sql = SqlContentSourcesSelect()
- .InnerJoin("n")
+ Sql sql = objectType == Constants.ObjectTypes.Document
+ ? SqlContentSourcesSelect()
+ : objectType == Constants.ObjectTypes.Media
+ ? SqlMediaSourcesSelect()
+ : throw new ArgumentOutOfRangeException(nameof(objectType), objectType, null);
+
+ sql.InnerJoin("n")
.On((n, c) => n.NodeId == c.ContentTypeId, "n", "umbracoContent")
.Append(SqlObjectTypeNotTrashed(SqlContext, objectType))
.WhereIn(x => x.UniqueId, keys,"n")
@@ -251,7 +266,6 @@ AND cmsContentNu.nodeId IS NULL
{
ContentCacheDataSerializerEntityType.Document => Constants.ObjectTypes.Document,
ContentCacheDataSerializerEntityType.Media => Constants.ObjectTypes.Media,
- ContentCacheDataSerializerEntityType.Member => Constants.ObjectTypes.Member,
_ => throw new ArgumentOutOfRangeException(nameof(entityType), entityType, null),
};
@@ -262,7 +276,15 @@ AND cmsContentNu.nodeId IS NULL
foreach (ContentSourceDto row in dtos)
{
- yield return CreateContentNodeKit(row, serializer, row.Published is false);
+ if (entityType == ContentCacheDataSerializerEntityType.Document)
+ {
+ yield return CreateContentNodeKit(row, serializer, row.Published is false);
+ }
+ else
+ {
+ yield return CreateMediaNodeKit(row, serializer);
+ }
+
}
}
@@ -574,8 +596,7 @@ WHERE cmsContentNu.nodeId IN (
cultureData[cultureInfo.Culture] = new CultureVariation
{
Name = cultureInfo.Name,
- UrlSegment =
- content.GetUrlSegment(_shortStringHelper, _urlSegmentProviders, cultureInfo.Culture),
+ UrlSegment = content.GetUrlSegment(_shortStringHelper, _urlSegmentProviders, cultureInfo.Culture),
Date = content.GetUpdateDate(cultureInfo.Culture) ?? DateTime.MinValue,
IsDraft = cultureIsDraft,
};
@@ -843,7 +864,7 @@ WHERE cmsContentNu.nodeId IN (
serializer.Deserialize(dto, dto.EditData, dto.EditDataRaw, published);
var draftContentData = new ContentData(
dto.EditName,
- null,
+ deserializedDraftContent?.UrlSegment,
dto.VersionId,
dto.EditVersionDate,
dto.CreatorId,
@@ -882,7 +903,7 @@ WHERE cmsContentNu.nodeId IN (
ContentCacheDataModel? deserializedContent = serializer.Deserialize(dto, dto.PubData, dto.PubDataRaw, true);
var publishedContentData = new ContentData(
dto.PubName,
- null,
+ deserializedContent?.UrlSegment,
dto.VersionId,
dto.PubVersionDate,
dto.CreatorId,
diff --git a/src/Umbraco.PublishedCache.HybridCache/Persistence/IDatabaseCacheRepository.cs b/src/Umbraco.PublishedCache.HybridCache/Persistence/IDatabaseCacheRepository.cs
index 0b5b739ac3..93a589d926 100644
--- a/src/Umbraco.PublishedCache.HybridCache/Persistence/IDatabaseCacheRepository.cs
+++ b/src/Umbraco.PublishedCache.HybridCache/Persistence/IDatabaseCacheRepository.cs
@@ -67,4 +67,6 @@ internal interface IDatabaseCacheRepository
/// Rebuilds the caches for content, media and/or members based on the content type ids specified
///
bool VerifyMediaDbCache();
+
+ Task> GetContentKeysAsync(Guid nodeObjectType);
}
diff --git a/src/Umbraco.PublishedCache.HybridCache/Services/DocumentCacheService.cs b/src/Umbraco.PublishedCache.HybridCache/Services/DocumentCacheService.cs
index 011f0cd24b..dc7885ee38 100644
--- a/src/Umbraco.PublishedCache.HybridCache/Services/DocumentCacheService.cs
+++ b/src/Umbraco.PublishedCache.HybridCache/Services/DocumentCacheService.cs
@@ -3,6 +3,7 @@ using Microsoft.Extensions.Options;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.PublishedContent;
+using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Cms.Core.Scoping;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Infrastructure.HybridCache.Factories;
@@ -23,8 +24,7 @@ internal sealed class DocumentCacheService : IDocumentCacheService
private readonly IEnumerable _seedKeyProviders;
private readonly IPublishedModelFactory _publishedModelFactory;
private readonly IPreviewService _previewService;
- private readonly CacheSettings _cacheSettings;
-
+ private readonly CacheEntrySettings _cacheEntrySettings;
private HashSet? _seedKeys;
private HashSet SeedKeys
{
@@ -54,7 +54,7 @@ internal sealed class DocumentCacheService : IDocumentCacheService
IPublishedContentFactory publishedContentFactory,
ICacheNodeFactory cacheNodeFactory,
IEnumerable seedKeyProviders,
- IOptions cacheSettings,
+ IOptionsMonitor cacheEntrySettings,
IPublishedModelFactory publishedModelFactory,
IPreviewService previewService)
{
@@ -67,7 +67,7 @@ internal sealed class DocumentCacheService : IDocumentCacheService
_seedKeyProviders = seedKeyProviders;
_publishedModelFactory = publishedModelFactory;
_previewService = previewService;
- _cacheSettings = cacheSettings.Value;
+ _cacheEntrySettings = cacheEntrySettings.Get(Constants.Configuration.NamedOptions.CacheEntry.Document);
}
public async Task GetByKeyAsync(Guid key, bool? preview = null)
@@ -82,7 +82,8 @@ internal sealed class DocumentCacheService : IDocumentCacheService
ContentCacheNode? contentCacheNode = await _databaseCacheRepository.GetContentSourceAsync(key, calculatedPreview);
scope.Complete();
return contentCacheNode;
- });
+ },
+ GetEntryOptions(key));
return contentCacheNode is null ? null : _publishedContentFactory.ToIPublishedContent(contentCacheNode, calculatedPreview).CreateModel(_publishedModelFactory);
}
@@ -101,6 +102,7 @@ internal sealed class DocumentCacheService : IDocumentCacheService
}
bool calculatedPreview = preview ?? GetPreview();
+ Guid key = keyAttempt.Result;
ContentCacheNode? contentCacheNode = await _hybridCache.GetOrCreateAsync(
GetCacheKey(keyAttempt.Result, calculatedPreview), // Unique key to the cache entry
@@ -110,7 +112,7 @@ internal sealed class DocumentCacheService : IDocumentCacheService
ContentCacheNode? contentCacheNode = await _databaseCacheRepository.GetContentSourceAsync(id, calculatedPreview);
scope.Complete();
return contentCacheNode;
- });
+ }, GetEntryOptions(key));
return contentCacheNode is null ? null : _publishedContentFactory.ToIPublishedContent(contentCacheNode, calculatedPreview).CreateModel(_publishedModelFactory);;
}
@@ -126,6 +128,57 @@ internal sealed class DocumentCacheService : IDocumentCacheService
.WhereNotNull();
}
+ public async Task ClearMemoryCacheAsync(CancellationToken cancellationToken)
+ {
+ // TODO: This should be done with tags, however this is not implemented yet, so for now we have to naively get all content keys and clear them all.
+ using ICoreScope scope = _scopeProvider.CreateCoreScope();
+
+ // We have to get ALL document keys in order to be able to remove them from the cache,
+ IEnumerable documentKeys = await _databaseCacheRepository.GetContentKeysAsync(Constants.ObjectTypes.Document);
+
+ foreach (Guid documentKey in documentKeys)
+ {
+ if (cancellationToken.IsCancellationRequested)
+ {
+ return;
+ }
+
+ // We'll remove both the draft and published cache
+ await _hybridCache.RemoveAsync(GetCacheKey(documentKey, false), cancellationToken);
+ await _hybridCache.RemoveAsync(GetCacheKey(documentKey, true), cancellationToken);
+ }
+
+ // We have to run seeding again after the cache is cleared
+ await SeedAsync(cancellationToken);
+
+ scope.Complete();
+ }
+
+ public async Task RefreshMemoryCacheAsync(Guid key)
+ {
+ using ICoreScope scope = _scopeProvider.CreateCoreScope();
+
+ ContentCacheNode? draftNode = await _databaseCacheRepository.GetContentSourceAsync(key, true);
+ if (draftNode is not null)
+ {
+ await _hybridCache.SetAsync(GetCacheKey(draftNode.Key, true), draftNode, GetEntryOptions(draftNode.Key));
+ }
+
+ ContentCacheNode? publishedNode = await _databaseCacheRepository.GetContentSourceAsync(key, false);
+ if (publishedNode is not null)
+ {
+ await _hybridCache.SetAsync(GetCacheKey(publishedNode.Key, false), publishedNode, GetEntryOptions(publishedNode.Key));
+ }
+
+ scope.Complete();
+ }
+
+ public async Task RemoveFromMemoryCacheAsync(Guid key)
+ {
+ await _hybridCache.RemoveAsync(GetCacheKey(key, true));
+ await _hybridCache.RemoveAsync(GetCacheKey(key, false));
+ }
+
public async Task SeedAsync(CancellationToken cancellationToken)
{
foreach (Guid key in SeedKeys)
@@ -155,7 +208,8 @@ internal sealed class DocumentCacheService : IDocumentCacheService
return cacheNode;
},
- GetSeedEntryOptions());
+ GetSeedEntryOptions(),
+ cancellationToken: cancellationToken);
// If the value is null, it's likely because
if (cachedValue is null)
@@ -167,10 +221,24 @@ internal sealed class DocumentCacheService : IDocumentCacheService
private HybridCacheEntryOptions GetSeedEntryOptions() => new()
{
- Expiration = _cacheSettings.SeedCacheDuration,
- LocalCacheExpiration = _cacheSettings.SeedCacheDuration
+ Expiration = _cacheEntrySettings.SeedCacheDuration,
+ LocalCacheExpiration = _cacheEntrySettings.SeedCacheDuration
};
+ private HybridCacheEntryOptions GetEntryOptions(Guid key)
+ {
+ if (SeedKeys.Contains(key))
+ {
+ return GetSeedEntryOptions();
+ }
+
+ return new HybridCacheEntryOptions
+ {
+ Expiration = _cacheEntrySettings.RemoteCacheDuration,
+ LocalCacheExpiration = _cacheEntrySettings.LocalCacheDuration,
+ };
+ }
+
public async Task HasContentByIdAsync(int id, bool preview = false)
{
Attempt keyAttempt = _idKeyMap.GetKeyForId(id, UmbracoObjectTypes.Document);
@@ -195,67 +263,35 @@ internal sealed class DocumentCacheService : IDocumentCacheService
{
using ICoreScope scope = _scopeProvider.CreateCoreScope();
- bool isSeeded = SeedKeys.Contains(content.Key);
-
// Always set draft node
// We have nodes seperate in the cache, cause 99% of the time, you are only using one
// and thus we won't get too much data when retrieving from the cache.
ContentCacheNode draftCacheNode = _cacheNodeFactory.ToContentCacheNode(content, true);
await _databaseCacheRepository.RefreshContentAsync(draftCacheNode, content.PublishedState);
- _scopeProvider.Context?.Enlist($"UpdateMemoryCache_Draft_{content.Key}", completed =>
- {
- if(completed is false)
- {
- return;
- }
- RefreshHybridCache(draftCacheNode, GetCacheKey(content.Key, true), isSeeded).GetAwaiter().GetResult();
- }, 1);
-
- if (content.PublishedState == PublishedState.Publishing)
+ if (content.PublishedState == PublishedState.Publishing || content.PublishedState == PublishedState.Unpublishing)
{
var publishedCacheNode = _cacheNodeFactory.ToContentCacheNode(content, false);
await _databaseCacheRepository.RefreshContentAsync(publishedCacheNode, content.PublishedState);
- _scopeProvider.Context?.Enlist($"UpdateMemoryCache_{content.Key}", completed =>
- {
- if(completed is false)
- {
- return;
- }
- RefreshHybridCache(publishedCacheNode, GetCacheKey(content.Key, false), isSeeded).GetAwaiter().GetResult();
- }, 1);
+ if (content.PublishedState == PublishedState.Unpublishing)
+ {
+ await _hybridCache.RemoveAsync(GetCacheKey(publishedCacheNode.Key, false));
+ }
+
}
scope.Complete();
}
- private async Task RefreshHybridCache(ContentCacheNode cacheNode, string cacheKey, bool isSeeded)
- {
- // If it's seeded we want it to stick around the cache for longer.
- if (isSeeded)
- {
- await _hybridCache.SetAsync(
- cacheKey,
- cacheNode,
- GetSeedEntryOptions());
- }
- else
- {
- await _hybridCache.SetAsync(cacheKey, cacheNode);
- }
- }
-
private string GetCacheKey(Guid key, bool preview) => preview ? $"{key}+draft" : $"{key}";
public async Task DeleteItemAsync(IContentBase content)
{
using ICoreScope scope = _scopeProvider.CreateCoreScope();
await _databaseCacheRepository.DeleteContentItemAsync(content.Id);
- await _hybridCache.RemoveAsync(GetCacheKey(content.Key, true));
- await _hybridCache.RemoveAsync(GetCacheKey(content.Key, false));
scope.Complete();
}
@@ -263,6 +299,14 @@ internal sealed class DocumentCacheService : IDocumentCacheService
{
using ICoreScope scope = _scopeProvider.CreateCoreScope();
_databaseCacheRepository.Rebuild(contentTypeIds.ToList());
+ RebuildMemoryCacheByContentTypeAsync(contentTypeIds).GetAwaiter().GetResult();
+ scope.Complete();
+ }
+
+ public async Task RebuildMemoryCacheByContentTypeAsync(IEnumerable contentTypeIds)
+ {
+ using ICoreScope scope = _scopeProvider.CreateCoreScope();
+
IEnumerable contentByContentTypeKey = _databaseCacheRepository.GetContentByContentTypeKey(contentTypeIds.Select(x => _idKeyMap.GetKeyForId(x, UmbracoObjectTypes.DocumentType).Result), ContentCacheDataSerializerEntityType.Document);
scope.Complete();
@@ -276,6 +320,5 @@ internal sealed class DocumentCacheService : IDocumentCacheService
}
}
-
}
}
diff --git a/src/Umbraco.PublishedCache.HybridCache/Services/MediaCacheService.cs b/src/Umbraco.PublishedCache.HybridCache/Services/MediaCacheService.cs
index 98ba341c19..77e2370dc3 100644
--- a/src/Umbraco.PublishedCache.HybridCache/Services/MediaCacheService.cs
+++ b/src/Umbraco.PublishedCache.HybridCache/Services/MediaCacheService.cs
@@ -3,6 +3,7 @@ using Microsoft.Extensions.Options;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.PublishedContent;
+using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Cms.Core.Scoping;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Infrastructure.HybridCache.Factories;
@@ -22,7 +23,7 @@ internal class MediaCacheService : IMediaCacheService
private readonly ICacheNodeFactory _cacheNodeFactory;
private readonly IEnumerable _seedKeyProviders;
private readonly IPublishedModelFactory _publishedModelFactory;
- private readonly CacheSettings _cacheSettings;
+ private readonly CacheEntrySettings _cacheEntrySettings;
private HashSet? _seedKeys;
private HashSet SeedKeys
@@ -54,7 +55,7 @@ internal class MediaCacheService : IMediaCacheService
ICacheNodeFactory cacheNodeFactory,
IEnumerable seedKeyProviders,
IPublishedModelFactory publishedModelFactory,
- IOptions cacheSettings)
+ IOptionsMonitor cacheEntrySettings)
{
_databaseCacheRepository = databaseCacheRepository;
_idKeyMap = idKeyMap;
@@ -64,7 +65,7 @@ internal class MediaCacheService : IMediaCacheService
_cacheNodeFactory = cacheNodeFactory;
_seedKeyProviders = seedKeyProviders;
_publishedModelFactory = publishedModelFactory;
- _cacheSettings = cacheSettings.Value;
+ _cacheEntrySettings = cacheEntrySettings.Get(Constants.Configuration.NamedOptions.CacheEntry.Media);
}
public async Task GetByKeyAsync(Guid key)
@@ -83,7 +84,7 @@ internal class MediaCacheService : IMediaCacheService
ContentCacheNode? mediaCacheNode = await _databaseCacheRepository.GetMediaSourceAsync(idAttempt.Result);
scope.Complete();
return mediaCacheNode;
- });
+ }, GetEntryOptions(key));
return contentCacheNode is null ? null : _publishedContentFactory.ToIPublishedMedia(contentCacheNode).CreateModel(_publishedModelFactory);
}
@@ -95,6 +96,7 @@ internal class MediaCacheService : IMediaCacheService
{
return null;
}
+ Guid key = keyAttempt.Result;
ContentCacheNode? contentCacheNode = await _hybridCache.GetOrCreateAsync(
$"{keyAttempt.Result}", // Unique key to the cache entry
@@ -104,7 +106,7 @@ internal class MediaCacheService : IMediaCacheService
ContentCacheNode? mediaCacheNode = await _databaseCacheRepository.GetMediaSourceAsync(id);
scope.Complete();
return mediaCacheNode;
- });
+ }, GetEntryOptions(key));
return contentCacheNode is null ? null : _publishedContentFactory.ToIPublishedMedia(contentCacheNode).CreateModel(_publishedModelFactory);
}
@@ -137,7 +139,6 @@ internal class MediaCacheService : IMediaCacheService
// We have nodes seperate in the cache, cause 99% of the time, you are only using one
// and thus we won't get too much data when retrieving from the cache.
var cacheNode = _cacheNodeFactory.ToContentCacheNode(media);
- await _hybridCache.SetAsync(GetCacheKey(media.Key, false), cacheNode);
await _databaseCacheRepository.RefreshMediaAsync(cacheNode);
scope.Complete();
}
@@ -146,7 +147,6 @@ internal class MediaCacheService : IMediaCacheService
{
using ICoreScope scope = _scopeProvider.CreateCoreScope();
await _databaseCacheRepository.DeleteContentItemAsync(media.Id);
- await _hybridCache.RemoveAsync(media.Key.ToString());
scope.Complete();
}
@@ -180,6 +180,65 @@ internal class MediaCacheService : IMediaCacheService
}
}
+ public async Task RefreshMemoryCacheAsync(Guid key)
+ {
+ using ICoreScope scope = _scopeProvider.CreateCoreScope();
+
+ ContentCacheNode? publishedNode = await _databaseCacheRepository.GetMediaSourceAsync(key);
+ if (publishedNode is not null)
+ {
+ await _hybridCache.SetAsync(GetCacheKey(publishedNode.Key, false), publishedNode, GetEntryOptions(publishedNode.Key));
+ }
+
+ scope.Complete();
+ }
+
+ public async Task ClearMemoryCacheAsync(CancellationToken cancellationToken)
+ {
+ // TODO: This should be done with tags, however this is not implemented yet, so for now we have to naively get all content keys and clear them all.
+ using ICoreScope scope = _scopeProvider.CreateCoreScope();
+
+ // We have to get ALL document keys in order to be able to remove them from the cache,
+ IEnumerable documentKeys = await _databaseCacheRepository.GetContentKeysAsync(Constants.ObjectTypes.Media);
+
+ foreach (Guid documentKey in documentKeys)
+ {
+ if (cancellationToken.IsCancellationRequested)
+ {
+ return;
+ }
+
+ // We'll remove both the draft and published cache
+ await _hybridCache.RemoveAsync(GetCacheKey(documentKey, false), cancellationToken);
+ }
+
+ // We have to run seeding again after the cache is cleared
+ await SeedAsync(cancellationToken);
+
+ scope.Complete();
+ }
+
+ public async Task RemoveFromMemoryCacheAsync(Guid key)
+ => await _hybridCache.RemoveAsync(GetCacheKey(key, false));
+
+ public async Task RebuildMemoryCacheByContentTypeAsync(IEnumerable mediaTypeIds)
+ {
+ using ICoreScope scope = _scopeProvider.CreateCoreScope();
+
+ IEnumerable contentByContentTypeKey = _databaseCacheRepository.GetContentByContentTypeKey(mediaTypeIds.Select(x => _idKeyMap.GetKeyForId(x, UmbracoObjectTypes.MediaType).Result), ContentCacheDataSerializerEntityType.Media);
+
+ foreach (ContentCacheNode content in contentByContentTypeKey)
+ {
+ _hybridCache.RemoveAsync(GetCacheKey(content.Key, true)).GetAwaiter().GetResult();
+
+ if (content.IsDraft is false)
+ {
+ _hybridCache.RemoveAsync(GetCacheKey(content.Key, false)).GetAwaiter().GetResult();
+ }
+ }
+ scope.Complete();
+ }
+
public void Rebuild(IReadOnlyCollection contentTypeIds)
{
using ICoreScope scope = _scopeProvider.CreateCoreScope();
@@ -200,10 +259,25 @@ internal class MediaCacheService : IMediaCacheService
scope.Complete();
}
+ private HybridCacheEntryOptions GetEntryOptions(Guid key)
+ {
+ if (SeedKeys.Contains(key))
+ {
+ return GetSeedEntryOptions();
+ }
+
+ return new HybridCacheEntryOptions
+ {
+ Expiration = _cacheEntrySettings.RemoteCacheDuration,
+ LocalCacheExpiration = _cacheEntrySettings.LocalCacheDuration,
+ };
+ }
+
+
private HybridCacheEntryOptions GetSeedEntryOptions() => new()
{
- Expiration = _cacheSettings.SeedCacheDuration,
- LocalCacheExpiration = _cacheSettings.SeedCacheDuration,
+ Expiration = _cacheEntrySettings.SeedCacheDuration,
+ LocalCacheExpiration = _cacheEntrySettings.SeedCacheDuration,
};
private string GetCacheKey(Guid key, bool preview) => preview ? $"{key}+draft" : $"{key}";
diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockEditorElementVariationTestBase.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockEditorElementVariationTestBase.cs
index daada604eb..74e7ea77a9 100644
--- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockEditorElementVariationTestBase.cs
+++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockEditorElementVariationTestBase.cs
@@ -7,18 +7,19 @@ using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.PublishedContent;
+using Umbraco.Cms.Core.Notifications;
using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Services;
-using Umbraco.Cms.Core.Services.Changes;
+using Umbraco.Cms.Core.Sync;
using Umbraco.Cms.Core.Web;
-using Umbraco.Cms.Infrastructure.HybridCache.Services;
using Umbraco.Cms.Tests.Common;
using Umbraco.Cms.Tests.Common.Builders;
using Umbraco.Cms.Tests.Common.Builders.Extensions;
using Umbraco.Cms.Tests.Common.Testing;
using Umbraco.Cms.Tests.Integration.Testing;
+using Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services;
namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.PropertyEditors;
@@ -63,6 +64,9 @@ public abstract class BlockEditorElementVariationTestBase : UmbracoIntegrationTe
builder.Services.Configure(config =>
config.AllowEditInvariantFromNonDefault = TestsRequiringAllowEditInvariantFromNonDefault.Contains(TestContext.CurrentContext.Test.Name));
+
+ builder.AddNotificationHandler();
+ builder.Services.AddUnique();
}
[SetUp]
diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListElementLevelVariationTests.Publishing.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListElementLevelVariationTests.Publishing.cs
index 87a16c170a..c48f4e588d 100644
--- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListElementLevelVariationTests.Publishing.cs
+++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListElementLevelVariationTests.Publishing.cs
@@ -1539,4 +1539,332 @@ public partial class BlockListElementLevelVariationTests
validateBlocks?.Invoke(value);
}
}
+
+ [Test]
+ public async Task Can_Publish_Valid_Properties()
+ {
+ var elementType = CreateElementTypeWithValidation();
+ var blockListDataType = await CreateBlockListDataType(elementType);
+ var contentType = CreateContentType(ContentVariation.Culture, blockListDataType);
+
+ var content = CreateContent(contentType, elementType, [], false);
+ var blockListValue = BlockListPropertyValue(
+ elementType,
+ [
+ (
+ Guid.NewGuid(),
+ Guid.NewGuid(),
+ new BlockProperty(
+ new List
+ {
+ new() { Alias = "invariantText", Value = "Valid invariant content value" },
+ new() { Alias = "variantText", Value = "Valid content value in English", Culture = "en-US" },
+ new() { Alias = "variantText", Value = "Valid content value in Danish", Culture = "da-DK" }
+ },
+ new List
+ {
+ new() { Alias = "invariantText", Value = "Valid invariant settings value" },
+ new() { Alias = "variantText", Value = "Valid value in English", Culture = "en-US" },
+ new() { Alias = "variantText", Value = "Valid value in Danish", Culture = "da-DK" },
+ },
+ null,
+ null
+ )
+ )
+ ]
+ );
+
+ content.Properties["blocks"]!.SetValue(JsonSerializer.Serialize(blockListValue));
+ ContentService.Save(content);
+
+ var publishResult = ContentService.Publish(content, ["en-US", "da-DK"]);
+ Assert.IsTrue(publishResult.Success);
+ Assert.IsTrue(publishResult.Content.PublishedCultures.Contains("en-US"));
+ Assert.IsTrue(publishResult.Content.PublishedCultures.Contains("da-DK"));
+ }
+
+ [Test]
+ public async Task Can_Publish_Valid_Properties_Specific_Culture_Only()
+ {
+ var elementType = CreateElementTypeWithValidation();
+ var blockListDataType = await CreateBlockListDataType(elementType);
+ var contentType = CreateContentType(ContentVariation.Culture, blockListDataType);
+
+ var content = CreateContent(contentType, elementType, [], false);
+ var blockListValue = BlockListPropertyValue(
+ elementType,
+ [
+ (
+ Guid.NewGuid(),
+ Guid.NewGuid(),
+ new BlockProperty(
+ new List
+ {
+ new() { Alias = "invariantText", Value = "Valid invariant content value" },
+ new() { Alias = "variantText", Value = "Valid content value in English", Culture = "en-US" },
+ new() { Alias = "variantText", Value = "Valid content value in Danish", Culture = "da-DK" }
+ },
+ new List
+ {
+ new() { Alias = "invariantText", Value = "Valid invariant settings value" },
+ new() { Alias = "variantText", Value = "Valid value in English", Culture = "en-US" },
+ new() { Alias = "variantText", Value = "Valid value in Danish", Culture = "da-DK" },
+ },
+ null,
+ null
+ )
+ )
+ ]
+ );
+
+ content.Properties["blocks"]!.SetValue(JsonSerializer.Serialize(blockListValue));
+ ContentService.Save(content);
+
+ var publishResult = ContentService.Publish(content, ["en-US"]);
+ Assert.IsTrue(publishResult.Success);
+ Assert.IsTrue(publishResult.Content.PublishedCultures.Contains("en-US"));
+ Assert.IsFalse(publishResult.Content.PublishedCultures.Contains("da-DK"));
+ }
+
+ [Test]
+ public async Task Can_Publish_Valid_Properties_With_Wildcard_Culture()
+ {
+ var elementType = CreateElementTypeWithValidation();
+ var blockListDataType = await CreateBlockListDataType(elementType);
+ var contentType = CreateContentType(ContentVariation.Culture, blockListDataType);
+
+ var content = CreateContent(contentType, elementType, [], false);
+ var blockListValue = BlockListPropertyValue(
+ elementType,
+ [
+ (
+ Guid.NewGuid(),
+ Guid.NewGuid(),
+ new BlockProperty(
+ new List
+ {
+ new() { Alias = "invariantText", Value = "Valid invariant content value" },
+ new() { Alias = "variantText", Value = "Valid content value in English", Culture = "en-US" },
+ new() { Alias = "variantText", Value = "Valid content value in Danish", Culture = "da-DK" }
+ },
+ new List
+ {
+ new() { Alias = "invariantText", Value = "Valid invariant settings value" },
+ new() { Alias = "variantText", Value = "Valid value in English", Culture = "en-US" },
+ new() { Alias = "variantText", Value = "Valid value in Danish", Culture = "da-DK" },
+ },
+ null,
+ null
+ )
+ )
+ ]
+ );
+
+ content.Properties["blocks"]!.SetValue(JsonSerializer.Serialize(blockListValue));
+ ContentService.Save(content);
+
+ var publishResult = ContentService.Publish(content, ["*"]);
+ Assert.IsTrue(publishResult.Success);
+ Assert.IsTrue(publishResult.Content.PublishedCultures.Contains("en-US"));
+ Assert.IsTrue(publishResult.Content.PublishedCultures.Contains("da-DK"));
+ }
+
+ [TestCase(true)]
+ [TestCase(false)]
+ public async Task Cannot_Publish_Invalid_Invariant_Properties(bool invalidSettingsValue)
+ {
+ var elementType = CreateElementTypeWithValidation();
+ var blockListDataType = await CreateBlockListDataType(elementType);
+ var contentType = CreateContentType(ContentVariation.Culture, blockListDataType);
+
+ var content = CreateContent(contentType, elementType, [], false);
+ var blockListValue = BlockListPropertyValue(
+ elementType,
+ [
+ (
+ Guid.NewGuid(),
+ Guid.NewGuid(),
+ new BlockProperty(
+ new List
+ {
+ new() { Alias = "invariantText", Value = $"{(invalidSettingsValue ? "Valid" : "Invalid")} invariant content value" },
+ new() { Alias = "variantText", Value = "Valid content value in English", Culture = "en-US" },
+ new() { Alias = "variantText", Value = "Valid content value in Danish", Culture = "da-DK" }
+ },
+ new List
+ {
+ new() { Alias = "invariantText", Value = $"{(invalidSettingsValue ? "Invalid" : "Valid")} invariant settings value" },
+ new() { Alias = "variantText", Value = "Valid value in English", Culture = "en-US" },
+ new() { Alias = "variantText", Value = "Valid value in Danish", Culture = "da-DK" },
+ },
+ null,
+ null
+ )
+ )
+ ]
+ );
+
+ content.Properties["blocks"]!.SetValue(JsonSerializer.Serialize(blockListValue));
+ ContentService.Save(content);
+
+ var publishResult = ContentService.Publish(content, ["en-US", "da-DK"]);
+ Assert.Multiple(() =>
+ {
+ Assert.IsFalse(publishResult.Success);
+ Assert.IsNotNull(publishResult.InvalidProperties);
+ Assert.AreEqual(1, publishResult.InvalidProperties.Count());
+ Assert.AreEqual("blocks", publishResult.InvalidProperties.First().Alias);
+ });
+ }
+
+ [Test]
+ public async Task Cannot_Publish_Missing_Invariant_Properties()
+ {
+ var elementType = CreateElementTypeWithValidation();
+ var blockListDataType = await CreateBlockListDataType(elementType);
+ var contentType = CreateContentType(ContentVariation.Culture, blockListDataType);
+
+ var content = CreateContent(contentType, elementType, [], false);
+ var blockListValue = BlockListPropertyValue(
+ elementType,
+ [
+ (
+ Guid.NewGuid(),
+ Guid.NewGuid(),
+ new BlockProperty(
+ new List
+ {
+ new() { Alias = "variantText", Value = "Valid content value in English", Culture = "en-US" },
+ new() { Alias = "variantText", Value = "Valid content value in Danish", Culture = "da-DK" }
+ },
+ new List
+ {
+ new() { Alias = "variantText", Value = "Valid value in English", Culture = "en-US" },
+ new() { Alias = "variantText", Value = "Valid value in Danish", Culture = "da-DK" },
+ },
+ null,
+ null
+ )
+ )
+ ]
+ );
+
+ content.Properties["blocks"]!.SetValue(JsonSerializer.Serialize(blockListValue));
+ ContentService.Save(content);
+
+ var publishResult = ContentService.Publish(content, ["en-US", "da-DK"]);
+ Assert.Multiple(() =>
+ {
+ Assert.IsFalse(publishResult.Success);
+ Assert.IsNotNull(publishResult.InvalidProperties);
+ Assert.AreEqual(1, publishResult.InvalidProperties.Count());
+ Assert.AreEqual("blocks", publishResult.InvalidProperties.First().Alias);
+ });
+ }
+
+ [TestCase(true)]
+ [TestCase(false)]
+ public async Task Cannot_Publish_Invalid_Variant_Properties(bool invalidSettingsValue)
+ {
+ var elementType = CreateElementTypeWithValidation();
+ var blockListDataType = await CreateBlockListDataType(elementType);
+ var contentType = CreateContentType(ContentVariation.Culture, blockListDataType);
+
+ var content = CreateContent(contentType, elementType, [], false);
+ var blockListValue = BlockListPropertyValue(
+ elementType,
+ [
+ (
+ Guid.NewGuid(),
+ Guid.NewGuid(),
+ new BlockProperty(
+ new List
+ {
+ new() { Alias = "invariantText", Value = "Valid invariant content value" },
+ new() { Alias = "variantText", Value = "Valid content value in English", Culture = "en-US" },
+ new() { Alias = "variantText", Value = $"{(invalidSettingsValue ? "Valid" : "Invalid")} content value in Danish", Culture = "da-DK" }
+ },
+ new List
+ {
+ new() { Alias = "invariantText", Value = "Valid invariant settings value" },
+ new() { Alias = "variantText", Value = "Valid settings value in English", Culture = "en-US" },
+ new() { Alias = "variantText", Value = $"{(invalidSettingsValue ? "Invalid" : "Valid")} settings value in Danish", Culture = "da-DK" },
+ },
+ null,
+ null
+ )
+ )
+ ]
+ );
+
+ content.Properties["blocks"]!.SetValue(JsonSerializer.Serialize(blockListValue));
+ ContentService.Save(content);
+
+ var publishResult = ContentService.Publish(content, ["en-US"]);
+ Assert.IsTrue(publishResult.Success);
+
+ publishResult = ContentService.Publish(content, ["da-DK"]);
+ Assert.Multiple(() =>
+ {
+ Assert.IsFalse(publishResult.Success);
+ Assert.IsNotNull(publishResult.InvalidProperties);
+ Assert.AreEqual(1, publishResult.InvalidProperties.Count());
+ Assert.AreEqual("blocks", publishResult.InvalidProperties.First().Alias);
+ });
+ }
+
+ [Test]
+ public async Task Cannot_Publish_Missing_Variant_Properties()
+ {
+ var elementType = CreateElementTypeWithValidation();
+ var blockListDataType = await CreateBlockListDataType(elementType);
+ var contentType = CreateContentType(ContentVariation.Culture, blockListDataType);
+
+ var content = CreateContent(contentType, elementType, [], false);
+ var blockListValue = BlockListPropertyValue(
+ elementType,
+ [
+ (
+ Guid.NewGuid(),
+ Guid.NewGuid(),
+ new BlockProperty(
+ new List
+ {
+ new() { Alias = "invariantText", Value = "Valid invariant content value" },
+ new() { Alias = "variantText", Value = "Valid content value in English", Culture = "en-US" },
+ },
+ new List
+ {
+ new() { Alias = "invariantText", Value = "Valid invariant settings value" },
+ new() { Alias = "variantText", Value = "Valid settings value in English", Culture = "en-US" },
+ },
+ null,
+ null
+ )
+ )
+ ]
+ );
+
+ // make sure all blocks are exposed
+ blockListValue.Expose =
+ [
+ new() { ContentKey = blockListValue.ContentData[0].Key, Culture = "en-US" },
+ new() { ContentKey = blockListValue.ContentData[0].Key, Culture = "da-DK" },
+ ];
+
+ content.Properties["blocks"]!.SetValue(JsonSerializer.Serialize(blockListValue));
+ ContentService.Save(content);
+
+ var publishResult = ContentService.Publish(content, ["en-US"]);
+ Assert.IsTrue(publishResult.Success);
+
+ publishResult = ContentService.Publish(content, ["da-DK"]);
+ Assert.Multiple(() =>
+ {
+ Assert.IsFalse(publishResult.Success);
+ Assert.IsNotNull(publishResult.InvalidProperties);
+ Assert.AreEqual(1, publishResult.InvalidProperties.Count());
+ Assert.AreEqual("blocks", publishResult.InvalidProperties.First().Alias);
+ });
+ }
}
diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListElementLevelVariationTests.Validation.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListElementLevelVariationTests.Validation.cs
new file mode 100644
index 0000000000..e2bd4b3ffe
--- /dev/null
+++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListElementLevelVariationTests.Validation.cs
@@ -0,0 +1,515 @@
+using NUnit.Framework;
+using Umbraco.Cms.Core.Models;
+using Umbraco.Cms.Core.Models.Blocks;
+using Umbraco.Cms.Core.Models.ContentEditing;
+using Umbraco.Cms.Core.Services;
+
+namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.PropertyEditors;
+
+public partial class BlockListElementLevelVariationTests
+{
+ private IContentValidationService ContentValidationService => GetRequiredService();
+
+ [Test]
+ public async Task Can_Validate_Invalid_Properties()
+ {
+ var elementType = CreateElementTypeWithValidation();
+ var blockListDataType = await CreateBlockListDataType(elementType);
+ var contentType = CreateContentType(ContentVariation.Culture, blockListDataType);
+ var blockListValue = BlockListPropertyValue(
+ elementType,
+ Guid.NewGuid(),
+ Guid.NewGuid(),
+ new BlockProperty(
+ new List
+ {
+ new() { Alias = "invariantText", Value = "Invalid invariant content value" },
+ new() { Alias = "variantText", Value = "Valid content value in English", Culture = "en-US" },
+ new() { Alias = "variantText", Value = "Invalid content value in Danish", Culture = "da-DK" },
+ },
+ new List
+ {
+ new() { Alias = "invariantText", Value = "Valid invariant settings value" },
+ new() { Alias = "variantText", Value = "Invalid settings value in English", Culture = "en-US" },
+ new() { Alias = "variantText", Value = "Valid settings value in Danish", Culture = "da-DK" },
+ },
+ null,
+ null));
+
+ var result = await ContentValidationService.ValidatePropertiesAsync(
+ new ContentCreateModel
+ {
+ ContentTypeKey = contentType.Key,
+ Variants =
+ [
+ new VariantModel { Name = "Name en-US", Properties = [], Culture = "en-US", Segment = null },
+ new VariantModel { Name = "Name da-DK", Properties = [], Culture = "da-DK", Segment = null }
+ ],
+ InvariantProperties =
+ [
+ new PropertyValueModel { Alias = "blocks", Value = JsonSerializer.Serialize(blockListValue) }
+ ]
+ },
+ contentType);
+
+ var errors = result.ValidationErrors.ToArray();
+ Assert.Multiple(() =>
+ {
+ Assert.AreEqual(3, errors.Length);
+ Assert.IsTrue(errors.All(error => error.Alias == "blocks" && error.Culture == null && error.Segment == null));
+ Assert.IsNotNull(errors.FirstOrDefault(error => error.JsonPath == ".contentData[0].values[0].value"));
+ Assert.IsNotNull(errors.FirstOrDefault(error => error.JsonPath == ".contentData[0].values[2].value"));
+ Assert.IsNotNull(errors.FirstOrDefault(error => error.JsonPath == ".settingsData[0].values[1].value"));
+ });
+ }
+
+ [Test]
+ public async Task Can_Validate_Invalid_Properties_Nested_Blocks()
+ {
+ var (rootElementType, nestedElementType) = await CreateElementTypeWithValidationAndNestedBlocksAsync();
+ var rootBlockListDataType = await CreateBlockListDataType(rootElementType);
+ var contentType = CreateContentType(ContentVariation.Culture, rootBlockListDataType);
+
+ var blockListValue = BlockListPropertyValue(
+ rootElementType,
+ Guid.NewGuid(),
+ Guid.NewGuid(),
+ new BlockProperty(
+ new List
+ {
+ new()
+ {
+ Alias = "nestedBlocks",
+ Value = BlockListPropertyValue(
+ nestedElementType,
+ Guid.NewGuid(),
+ Guid.NewGuid(),
+ new BlockProperty(
+ new List
+ {
+ new() { Alias = "invariantText", Value = "Invalid nested invariant content value" },
+ new() { Alias = "variantText", Value = "Valid nested content value in English", Culture = "en-US" },
+ new() { Alias = "variantText", Value = "Invalid nested content value in Danish", Culture = "da-DK" },
+ },
+ new List
+ {
+ new() { Alias = "invariantText", Value = "Valid nested invariant settings value" },
+ new() { Alias = "variantText", Value = "Invalid nested settings value in English", Culture = "en-US" },
+ new() { Alias = "variantText", Value = "Valid nested settings value in Danish", Culture = "da-DK" },
+ },
+ null,
+ null))
+ },
+ new() { Alias = "invariantText", Value = "Invalid invariant content value" },
+ new() { Alias = "variantText", Value = "Valid content value in English", Culture = "en-US" },
+ new() { Alias = "variantText", Value = "Invalid content value in Danish", Culture = "da-DK" },
+ },
+ new List
+ {
+ new()
+ {
+ Alias = "nestedBlocks",
+ Value = BlockListPropertyValue(
+ nestedElementType,
+ Guid.NewGuid(),
+ Guid.NewGuid(),
+ new BlockProperty(
+ new List
+ {
+ new() { Alias = "invariantText", Value = "Valid nested invariant content value" },
+ new() { Alias = "variantText", Value = "Invalid nested content value in English", Culture = "en-US" },
+ new() { Alias = "variantText", Value = "Valid nested content value in Danish", Culture = "da-DK" },
+ },
+ new List
+ {
+ new() { Alias = "invariantText", Value = "Invalid nested invariant settings value" },
+ new() { Alias = "variantText", Value = "Valid nested settings value in English", Culture = "en-US" },
+ new() { Alias = "variantText", Value = "Invalid nested settings value in Danish", Culture = "da-DK" },
+ },
+ null,
+ null))
+ },
+ new() { Alias = "invariantText", Value = "Valid invariant content value" },
+ new() { Alias = "variantText", Value = "Invalid content value in English", Culture = "en-US" },
+ new() { Alias = "variantText", Value = "Valid content value in Danish", Culture = "da-DK" },
+ },
+ null,
+ null));
+
+ var result = await ContentValidationService.ValidatePropertiesAsync(
+ new ContentCreateModel
+ {
+ ContentTypeKey = contentType.Key,
+ Variants =
+ [
+ new VariantModel { Name = "Name en-US", Properties = [], Culture = "en-US", Segment = null },
+ new VariantModel { Name = "Name da-DK", Properties = [], Culture = "da-DK", Segment = null }
+ ],
+ InvariantProperties =
+ [
+ new PropertyValueModel { Alias = "blocks", Value = JsonSerializer.Serialize(blockListValue) }
+ ]
+ },
+ contentType);
+
+ var errors = result.ValidationErrors.ToArray();
+ Assert.Multiple(() =>
+ {
+ Assert.AreEqual(9, errors.Length);
+ Assert.IsTrue(errors.All(error => error.Alias == "blocks" && error.Culture == null && error.Segment == null));
+
+ Assert.IsNotNull(errors.FirstOrDefault(error => error.JsonPath == ".contentData[0].values[0].value.contentData[0].values[0].value"));
+ Assert.IsNotNull(errors.FirstOrDefault(error => error.JsonPath == ".contentData[0].values[0].value.contentData[0].values[2].value"));
+ Assert.IsNotNull(errors.FirstOrDefault(error => error.JsonPath == ".contentData[0].values[0].value.settingsData[0].values[1].value"));
+ Assert.IsNotNull(errors.FirstOrDefault(error => error.JsonPath == ".contentData[0].values[1].value"));
+ Assert.IsNotNull(errors.FirstOrDefault(error => error.JsonPath == ".contentData[0].values[3].value"));
+
+ Assert.IsNotNull(errors.FirstOrDefault(error => error.JsonPath == ".settingsData[0].values[0].value.contentData[0].values[1].value"));
+ Assert.IsNotNull(errors.FirstOrDefault(error => error.JsonPath == ".settingsData[0].values[0].value.settingsData[0].values[0].value"));
+ Assert.IsNotNull(errors.FirstOrDefault(error => error.JsonPath == ".settingsData[0].values[0].value.settingsData[0].values[2].value"));
+ Assert.IsNotNull(errors.FirstOrDefault(error => error.JsonPath == ".settingsData[0].values[2].value"));
+ });
+ }
+
+ [Test]
+ public async Task Can_Validate_Invalid_Properties_Specific_Culture_Only()
+ {
+ var elementType = CreateElementTypeWithValidation();
+ var blockListDataType = await CreateBlockListDataType(elementType);
+ var contentType = CreateContentType(ContentVariation.Culture, blockListDataType);
+ var blockListValue = BlockListPropertyValue(
+ elementType,
+ Guid.NewGuid(),
+ Guid.NewGuid(),
+ new BlockProperty(
+ new List
+ {
+ new() { Alias = "invariantText", Value = "Invalid invariant content value" },
+ new() { Alias = "variantText", Value = "Valid content value in English", Culture = "en-US" },
+ new() { Alias = "variantText", Value = "Invalid content value in Danish", Culture = "da-DK" },
+ },
+ new List
+ {
+ new() { Alias = "invariantText", Value = "Valid invariant settings value" },
+ new() { Alias = "variantText", Value = "Invalid settings value in English", Culture = "en-US" },
+ new() { Alias = "variantText", Value = "Valid settings value in Danish", Culture = "da-DK" },
+ },
+ null,
+ null));
+
+ var result = await ContentValidationService.ValidatePropertiesAsync(
+ new ContentCreateModel
+ {
+ ContentTypeKey = contentType.Key,
+ Variants =
+ [
+ new VariantModel { Name = "Name en-US", Properties = [], Culture = "en-US", Segment = null },
+ new VariantModel { Name = "Name da-DK", Properties = [], Culture = "da-DK", Segment = null }
+ ],
+ InvariantProperties =
+ [
+ new PropertyValueModel { Alias = "blocks", Value = JsonSerializer.Serialize(blockListValue) }
+ ]
+ },
+ contentType,
+ new[] { "en-US" });
+
+ var errors = result.ValidationErrors.ToArray();
+ Assert.Multiple(() =>
+ {
+ Assert.AreEqual(2, errors.Length);
+ Assert.IsTrue(errors.All(error => error.Alias == "blocks" && error.Culture == null && error.Segment == null));
+ Assert.IsNotNull(errors.FirstOrDefault(error => error.JsonPath == ".contentData[0].values[0].value"));
+ Assert.IsNotNull(errors.FirstOrDefault(error => error.JsonPath == ".settingsData[0].values[1].value"));
+ });
+ }
+
+ [Test]
+ public async Task Can_Validate_Invalid_Properties_With_Wildcard_Culture()
+ {
+ var elementType = CreateElementTypeWithValidation();
+ var blockListDataType = await CreateBlockListDataType(elementType);
+ var contentType = CreateContentType(ContentVariation.Culture, blockListDataType);
+ var blockListValue = BlockListPropertyValue(
+ elementType,
+ Guid.NewGuid(),
+ Guid.NewGuid(),
+ new BlockProperty(
+ new List
+ {
+ new() { Alias = "invariantText", Value = "Invalid invariant content value" },
+ new() { Alias = "variantText", Value = "Valid content value in English", Culture = "en-US" },
+ new() { Alias = "variantText", Value = "Invalid content value in Danish", Culture = "da-DK" },
+ },
+ new List
+ {
+ new() { Alias = "invariantText", Value = "Valid invariant settings value" },
+ new() { Alias = "variantText", Value = "Invalid settings value in English", Culture = "en-US" },
+ new() { Alias = "variantText", Value = "Valid settings value in Danish", Culture = "da-DK" },
+ },
+ null,
+ null));
+
+ var result = await ContentValidationService.ValidatePropertiesAsync(
+ new ContentCreateModel
+ {
+ ContentTypeKey = contentType.Key,
+ Variants =
+ [
+ new VariantModel { Name = "Name en-US", Properties = [], Culture = "en-US", Segment = null },
+ new VariantModel { Name = "Name da-DK", Properties = [], Culture = "da-DK", Segment = null }
+ ],
+ InvariantProperties =
+ [
+ new PropertyValueModel { Alias = "blocks", Value = JsonSerializer.Serialize(blockListValue) }
+ ]
+ },
+ contentType,
+ ["*"]);
+
+ var errors = result.ValidationErrors.ToArray();
+ Assert.Multiple(() =>
+ {
+ Assert.AreEqual(3, errors.Length);
+ Assert.IsTrue(errors.All(error => error.Alias == "blocks" && error.Culture == null && error.Segment == null));
+ Assert.IsNotNull(errors.FirstOrDefault(error => error.JsonPath == ".contentData[0].values[0].value"));
+ Assert.IsNotNull(errors.FirstOrDefault(error => error.JsonPath == ".contentData[0].values[2].value"));
+ Assert.IsNotNull(errors.FirstOrDefault(error => error.JsonPath == ".settingsData[0].values[1].value"));
+ });
+ }
+
+ [Test]
+ public async Task Can_Validate_Missing_Properties()
+ {
+ var elementType = CreateElementTypeWithValidation();
+ var blockListDataType = await CreateBlockListDataType(elementType);
+ var contentType = CreateContentType(ContentVariation.Culture, blockListDataType);
+ var blockListValue = BlockListPropertyValue(
+ elementType,
+ Guid.NewGuid(),
+ Guid.NewGuid(),
+ new BlockProperty(
+ new List
+ {
+ // missing the mandatory "invariantText" (invariant) and "variantText" (in Danish)
+ new() { Alias = "variantText", Value = "Valid content value in English", Culture = "en-US" },
+ },
+ new List
+ {
+ // missing the mandatory "variantText" (in English)
+ new() { Alias = "invariantText", Value = "Valid invariant settings value" },
+ new() { Alias = "variantText", Value = "Valid settings value in Danish", Culture = "da-DK" },
+ },
+ null,
+ null));
+
+ // make sure all blocks are exposed
+ blockListValue.Expose =
+ [
+ new() { ContentKey = blockListValue.ContentData[0].Key, Culture = "en-US" },
+ new() { ContentKey = blockListValue.ContentData[0].Key, Culture = "da-DK" },
+ ];
+
+ var result = await ContentValidationService.ValidatePropertiesAsync(
+ new ContentCreateModel
+ {
+ ContentTypeKey = contentType.Key,
+ Variants =
+ [
+ new VariantModel { Name = "Name en-US", Properties = [], Culture = "en-US", Segment = null },
+ new VariantModel { Name = "Name da-DK", Properties = [], Culture = "da-DK", Segment = null }
+ ],
+ InvariantProperties =
+ [
+ new PropertyValueModel { Alias = "blocks", Value = JsonSerializer.Serialize(blockListValue) }
+ ]
+ },
+ contentType);
+
+ var errors = result.ValidationErrors.ToArray();
+ Assert.Multiple(() =>
+ {
+ Assert.AreEqual(3, errors.Length);
+ Assert.IsTrue(errors.All(error => error.Alias == "blocks" && error.Culture == null && error.Segment == null));
+ Assert.IsNotNull(errors.FirstOrDefault(error => error.JsonPath == ".contentData[0].values[?(@.alias == 'invariantText' && @.culture == null && @.segment == null)].value"));
+ Assert.IsNotNull(errors.FirstOrDefault(error => error.JsonPath == ".contentData[0].values[?(@.alias == 'variantText' && @.culture == 'da-DK' && @.segment == null)].value"));
+ Assert.IsNotNull(errors.FirstOrDefault(error => error.JsonPath == ".settingsData[0].values[?(@.alias == 'variantText' && @.culture == 'en-US' && @.segment == null)].value"));
+ });
+ }
+
+ [Test]
+ public async Task Can_Validate_Missing_Properties_Nested_Blocks_Specific_Culture_Only()
+ {
+ var (rootElementType, nestedElementType) = await CreateElementTypeWithValidationAndNestedBlocksAsync();
+ var rootBlockListDataType = await CreateBlockListDataType(rootElementType);
+ var contentType = CreateContentType(ContentVariation.Culture, rootBlockListDataType);
+
+ var nestedContentBlocks = BlockListPropertyValue(
+ nestedElementType,
+ Guid.NewGuid(),
+ Guid.NewGuid(),
+ new BlockProperty(
+ new List
+ {
+ // missing the mandatory "invariantText" (invariant) and "variantText" (in Danish)
+ new() { Alias = "variantText", Value = "Valid nested content value in English", Culture = "en-US" },
+ },
+ new List
+ {
+ // missing the mandatory "variantText" (in English)
+ new() { Alias = "invariantText", Value = "Valid nested invariant settings value" },
+ new() { Alias = "variantText", Value = "Valid nested settings value in Danish", Culture = "da-DK" },
+ },
+ null,
+ null));
+
+ var nestedSettingsBlocks = BlockListPropertyValue(
+ nestedElementType,
+ Guid.NewGuid(),
+ Guid.NewGuid(),
+ new BlockProperty(
+ new List
+ {
+ // missing the mandatory "variantText" (in English)
+ new() { Alias = "invariantText", Value = "Valid nested invariant content value" },
+ new() { Alias = "variantText", Value = "Valid nested content value in Danish", Culture = "da-DK" },
+ },
+ new List
+ {
+ // missing the mandatory "invariantText" (invariant) and "variantText" (in Danish)
+ new() { Alias = "variantText", Value = "Valid nested settings value in English", Culture = "en-US" },
+ },
+ null,
+ null));
+
+ // make sure all nested blocks are exposed
+ nestedContentBlocks.Expose =
+ [
+ new() { ContentKey = nestedContentBlocks.ContentData[0].Key, Culture = "en-US" },
+ new() { ContentKey = nestedContentBlocks.ContentData[0].Key, Culture = "da-DK" },
+ ];
+ nestedSettingsBlocks.Expose =
+ [
+ new() { ContentKey = nestedSettingsBlocks.ContentData[0].Key, Culture = "en-US" },
+ new() { ContentKey = nestedSettingsBlocks.ContentData[0].Key, Culture = "da-DK" },
+ ];
+
+ var blockListValue = BlockListPropertyValue(
+ rootElementType,
+ Guid.NewGuid(),
+ Guid.NewGuid(),
+ new BlockProperty(
+ new List
+ {
+ new()
+ {
+ Alias = "nestedBlocks",
+ Value = nestedContentBlocks
+ },
+ // missing the mandatory "variantText" (in both English and Danish)
+ new() { Alias = "invariantText", Value = "Valid root invariant content value" }
+ },
+ new List
+ {
+ new()
+ {
+ Alias = "nestedBlocks",
+ Value = nestedSettingsBlocks
+ },
+ // missing the mandatory "invariantText"
+ new() { Alias = "variantText", Value = "Valid root settings value in English", Culture = "en-US" },
+ new() { Alias = "variantText", Value = "Valid root settings value in Danish", Culture = "da-DK" }
+ },
+ null,
+ null));
+
+ // make sure all root blocks are exposed
+ blockListValue.Expose =
+ [
+ new() { ContentKey = blockListValue.ContentData[0].Key, Culture = "en-US" },
+ new() { ContentKey = blockListValue.ContentData[0].Key, Culture = "da-DK" },
+ ];
+
+ var result = await ContentValidationService.ValidatePropertiesAsync(
+ new ContentCreateModel
+ {
+ ContentTypeKey = contentType.Key,
+ Variants =
+ [
+ new VariantModel { Name = "Name en-US", Properties = [], Culture = "en-US", Segment = null },
+ new VariantModel { Name = "Name da-DK", Properties = [], Culture = "da-DK", Segment = null }
+ ],
+ InvariantProperties =
+ [
+ new PropertyValueModel { Alias = "blocks", Value = JsonSerializer.Serialize(blockListValue) }
+ ]
+ },
+ contentType,
+ new[] { "da-DK" });
+
+ var errors = result.ValidationErrors.ToArray();
+ Assert.Multiple(() =>
+ {
+ Assert.AreEqual(6, errors.Length);
+ Assert.IsTrue(errors.All(error => error.Alias == "blocks" && error.Culture == null && error.Segment == null));
+
+ Assert.IsNotNull(errors.FirstOrDefault(error => error.JsonPath == ".contentData[0].values[0].value.contentData[0].values[?(@.alias == 'invariantText' && @.culture == null && @.segment == null)].value"));
+ Assert.IsNotNull(errors.FirstOrDefault(error => error.JsonPath == ".contentData[0].values[0].value.contentData[0].values[?(@.alias == 'variantText' && @.culture == 'da-DK' && @.segment == null)].value"));
+ Assert.IsNotNull(errors.FirstOrDefault(error => error.JsonPath == ".contentData[0].values[?(@.alias == 'variantText' && @.culture == 'da-DK' && @.segment == null)].value"));
+
+ Assert.IsNotNull(errors.FirstOrDefault(error => error.JsonPath == ".settingsData[0].values[0].value.settingsData[0].values[?(@.alias == 'invariantText' && @.culture == null && @.segment == null)].value"));
+ Assert.IsNotNull(errors.FirstOrDefault(error => error.JsonPath == ".settingsData[0].values[0].value.settingsData[0].values[?(@.alias == 'variantText' && @.culture == 'da-DK' && @.segment == null)].value"));
+ Assert.IsNotNull(errors.FirstOrDefault(error => error.JsonPath == ".settingsData[0].values[?(@.alias == 'invariantText' && @.culture == null && @.segment == null)].value"));
+ });
+ }
+
+ [Test]
+ public async Task Does_Not_Validate_Unexposed_Blocks()
+ {
+ var elementType = CreateElementTypeWithValidation();
+ var blockListDataType = await CreateBlockListDataType(elementType);
+ var contentType = CreateContentType(ContentVariation.Culture, blockListDataType);
+ var blockListValue = BlockListPropertyValue(
+ elementType,
+ Guid.NewGuid(),
+ Guid.NewGuid(),
+ new BlockProperty(
+ new List
+ {
+ new() { Alias = "invariantText", Value = "Valid invariant content value" },
+ new() { Alias = "variantText", Value = "Valid content value in English", Culture = "en-US" },
+ },
+ new List
+ {
+ new() { Alias = "invariantText", Value = "Valid invariant settings value" },
+ new() { Alias = "variantText", Value = "Valid settings value in English", Culture = "en-US" },
+ },
+ null,
+ null));
+
+ // only expose the block in English
+ blockListValue.Expose =
+ [
+ new() { ContentKey = blockListValue.ContentData[0].Key, Culture = "en-US" },
+ ];
+
+ var result = await ContentValidationService.ValidatePropertiesAsync(
+ new ContentCreateModel
+ {
+ ContentTypeKey = contentType.Key,
+ Variants =
+ [
+ new VariantModel { Name = "Name en-US", Properties = [], Culture = "en-US", Segment = null },
+ new VariantModel { Name = "Name da-DK", Properties = [], Culture = "da-DK", Segment = null }
+ ],
+ InvariantProperties =
+ [
+ new PropertyValueModel { Alias = "blocks", Value = JsonSerializer.Serialize(blockListValue) }
+ ]
+ },
+ contentType,
+ ["da-DK"]);
+
+ Assert.IsEmpty(result.ValidationErrors);
+ }
+}
diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListElementLevelVariationTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListElementLevelVariationTests.cs
index 8a798a603c..53af4af967 100644
--- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListElementLevelVariationTests.cs
+++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListElementLevelVariationTests.cs
@@ -146,6 +146,63 @@ public partial class BlockListElementLevelVariationTests : BlockEditorElementVar
return GetPublishedContent(content.Key);
}
+ private IContentType CreateElementTypeWithValidation()
+ {
+ var elementType = CreateElementType(ContentVariation.Culture);
+ foreach (var propertyType in elementType.PropertyTypes)
+ {
+ propertyType.Mandatory = true;
+ propertyType.ValidationRegExp = "^Valid.*$";
+ }
+
+ ContentTypeService.Save(elementType);
+ return elementType;
+ }
+
+ private async Task<(IContentType RootElementType, IContentType NestedElementType)> CreateElementTypeWithValidationAndNestedBlocksAsync()
+ {
+ var nestedElementType = CreateElementTypeWithValidation();
+ var nestedBlockListDataType = await CreateBlockListDataType(nestedElementType);
+
+ var rootElementType = new ContentTypeBuilder()
+ .WithAlias("myRootElementType")
+ .WithName("My Root Element Type")
+ .WithIsElement(true)
+ .WithContentVariation(ContentVariation.Culture)
+ .AddPropertyType()
+ .WithAlias("invariantText")
+ .WithName("Invariant text")
+ .WithMandatory(true)
+ .WithValidationRegExp("^Valid.*$")
+ .WithDataTypeId(Constants.DataTypes.Textbox)
+ .WithPropertyEditorAlias(Constants.PropertyEditors.Aliases.TextBox)
+ .WithValueStorageType(ValueStorageType.Nvarchar)
+ .WithVariations(ContentVariation.Nothing)
+ .Done()
+ .AddPropertyType()
+ .WithAlias("variantText")
+ .WithName("Variant text")
+ .WithMandatory(true)
+ .WithValidationRegExp("^Valid.*$")
+ .WithDataTypeId(Constants.DataTypes.Textbox)
+ .WithPropertyEditorAlias(Constants.PropertyEditors.Aliases.TextBox)
+ .WithValueStorageType(ValueStorageType.Nvarchar)
+ .WithVariations(ContentVariation.Culture)
+ .Done()
+ .AddPropertyType()
+ .WithAlias("nestedBlocks")
+ .WithName("Nested blocks")
+ .WithDataTypeId(nestedBlockListDataType.Id)
+ .WithPropertyEditorAlias(Constants.PropertyEditors.Aliases.BlockList)
+ .WithValueStorageType(ValueStorageType.Ntext)
+ .WithVariations(ContentVariation.Nothing)
+ .Done()
+ .Build();
+ ContentTypeService.Save(rootElementType);
+
+ return (rootElementType, nestedElementType);
+ }
+
private class BlockProperty
{
public BlockProperty(IList blockContentValues, IList blockSettingsValues, string? culture, string? segment)
diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/RichTextElementLevelVariationTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/RichTextElementLevelVariationTests.cs
index 80d02caa15..dfc8ae6141 100644
--- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/RichTextElementLevelVariationTests.cs
+++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/RichTextElementLevelVariationTests.cs
@@ -482,7 +482,7 @@ public class RichTextElementLevelVariationTests : BlockEditorElementVariationTes
Assert.AreEqual(1, indexValue.Values.Count());
var indexedValue = indexValue.Values.First() as string;
Assert.IsNotNull(indexedValue);
- var values = indexedValue.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries).Select(s => s.Trim()).ToArray();
+ var values = indexedValue.Split(Environment.NewLine).Select(s => s.Trim()).Where(s => s.IsNullOrWhiteSpace() is false).ToArray();
Assert.AreEqual(expectedIndexedValues.Length, values.Length);
Assert.IsTrue(values.ContainsAll(expectedIndexedValues));
}
@@ -562,7 +562,7 @@ public class RichTextElementLevelVariationTests : BlockEditorElementVariationTes
Assert.AreEqual(1, indexValue.Values.Count());
var indexedValue = indexValue.Values.First() as string;
Assert.IsNotNull(indexedValue);
- var values = indexedValue.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries).Select(s => s.Trim()).ToArray();
+ var values = indexedValue.Split(Environment.NewLine).Select(s => s.Trim()).Where(s => s.IsNullOrWhiteSpace() is false).ToArray();
Assert.AreEqual(expectedIndexedValues.Length, values.Length);
Assert.IsTrue(values.ContainsAll(expectedIndexedValues));
}
@@ -624,7 +624,7 @@ public class RichTextElementLevelVariationTests : BlockEditorElementVariationTes
Assert.AreEqual(1, indexValue.Values.Count());
var indexedValue = indexValue.Values.First() as string;
Assert.IsNotNull(indexedValue);
- var values = indexedValue.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries).Select(s => s.Trim()).ToArray();
+ var values = indexedValue.Split(Environment.NewLine).Select(s => s.Trim()).Where(s => s.IsNullOrWhiteSpace() is false).ToArray();
Assert.AreEqual(expectedIndexedValues.Length, values.Length);
Assert.IsTrue(values.ContainsAll(expectedIndexedValues));
}
diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentValidationServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentValidationServiceTests.cs
index 00e8284135..5640afeae2 100644
--- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentValidationServiceTests.cs
+++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentValidationServiceTests.cs
@@ -102,6 +102,10 @@ public class ContentValidationServiceTests : UmbracoIntegrationTestWithContent
{ "alias": "text", "value": "Valid nested setting text)" }
]
}
+ ],
+ "expose": [
+ { "contentKey": "f36cebfa-d03b-4451-9e60-4bf32c5b1e2f", "culture": null, "segment": null },
+ { "contentKey": "b8173e4a-0618-475c-8277-c3c6af68bee6", "culture": null, "segment": null }
]
}
}
@@ -130,6 +134,10 @@ public class ContentValidationServiceTests : UmbracoIntegrationTestWithContent
{ "alias": "text", "value": "Invalid root setting text (ref #3)" }
]
}
+ ],
+ "expose": [
+ { "contentKey": "9addc377-c02c-4db0-88c2-73b933704f7b", "culture": null, "segment": null },
+ { "contentKey": "3af93b5b-5e40-4c64-b142-2564309fc4c7", "culture": null, "segment": null }
]
}
"""
diff --git a/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheDocumentTypeTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheDocumentTypeTests.cs
index c316b00ae6..17b55019db 100644
--- a/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheDocumentTypeTests.cs
+++ b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheDocumentTypeTests.cs
@@ -1,9 +1,13 @@
using NUnit.Framework;
using Umbraco.Cms.Core;
+using Umbraco.Cms.Core.Cache;
+using Umbraco.Cms.Core.Notifications;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Cms.Core.Services;
+using Umbraco.Cms.Core.Sync;
using Umbraco.Cms.Tests.Common.Testing;
using Umbraco.Cms.Tests.Integration.Testing;
+using Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services;
namespace Umbraco.Cms.Tests.Integration.Umbraco.PublishedCache.HybridCache;
@@ -11,7 +15,11 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.PublishedCache.HybridCache;
[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)]
public class DocumentHybridCacheDocumentTypeTests : UmbracoIntegrationTestWithContentEditing
{
- protected override void CustomTestSetup(IUmbracoBuilder builder) => builder.AddUmbracoHybridCache();
+ protected override void CustomTestSetup(IUmbracoBuilder builder)
+ {
+ builder.AddNotificationHandler();
+ builder.Services.AddUnique();
+ }
private IPublishedContentCache PublishedContentHybridCache => GetRequiredService();
diff --git a/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheMockTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheMockTests.cs
index 2e810114d0..5c38d4e4b0 100644
--- a/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheMockTests.cs
+++ b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheMockTests.cs
@@ -94,6 +94,8 @@ public class DocumentHybridCacheMockTests : UmbracoIntegrationTestWithContent
});
_mockedNucacheRepository.Setup(r => r.DeleteContentItemAsync(It.IsAny()));
+ var optionsMonitorMock = new Mock>();
+ optionsMonitorMock.Setup(x => x.Get(It.IsAny())).Returns(new CacheEntrySettings());
_mockDocumentCacheService = new DocumentCacheService(
_mockedNucacheRepository.Object,
@@ -103,7 +105,7 @@ public class DocumentHybridCacheMockTests : UmbracoIntegrationTestWithContent
GetRequiredService(),
GetRequiredService(),
GetSeedProviders(),
- Options.Create(new CacheSettings()),
+ optionsMonitorMock.Object,
GetRequiredService(),
GetRequiredService());
diff --git a/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCachePropertyTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCachePropertyTest.cs
index 1471234c8d..b5dff7085e 100644
--- a/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCachePropertyTest.cs
+++ b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCachePropertyTest.cs
@@ -1,16 +1,20 @@
using NUnit.Framework;
using Umbraco.Cms.Core;
+using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.ContentEditing;
using Umbraco.Cms.Core.Models.ContentPublishing;
using Umbraco.Cms.Core.Models.PublishedContent;
+using Umbraco.Cms.Core.Notifications;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.ContentTypeEditing;
using Umbraco.Cms.Core.Services.OperationStatus;
+using Umbraco.Cms.Core.Sync;
using Umbraco.Cms.Tests.Common.Builders;
using Umbraco.Cms.Tests.Common.Testing;
using Umbraco.Cms.Tests.Integration.Testing;
+using Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services;
namespace Umbraco.Cms.Tests.Integration.Umbraco.PublishedCache.HybridCache;
@@ -18,7 +22,11 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.PublishedCache.HybridCache;
[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)]
public class DocumentHybridCachePropertyTest : UmbracoIntegrationTest
{
- protected override void CustomTestSetup(IUmbracoBuilder builder) => builder.AddUmbracoHybridCache();
+ protected override void CustomTestSetup(IUmbracoBuilder builder)
+ {
+ builder.AddNotificationHandler();
+ builder.Services.AddUnique();
+ }
private ICacheManager CacheManager => GetRequiredService();
diff --git a/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheTemplateTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheTemplateTests.cs
index a0873bd983..c1291b1fff 100644
--- a/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheTemplateTests.cs
+++ b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheTemplateTests.cs
@@ -1,18 +1,27 @@
using NUnit.Framework;
using Umbraco.Cms.Core;
+using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Models.ContentEditing;
+using Umbraco.Cms.Core.Notifications;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;
+using Umbraco.Cms.Core.Sync;
using Umbraco.Cms.Tests.Common.Testing;
using Umbraco.Cms.Tests.Integration.Testing;
+using Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services;
+
namespace Umbraco.Cms.Tests.Integration.Umbraco.PublishedCache.HybridCache;
[TestFixture]
[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)]
public class DocumentHybridCacheTemplateTests : UmbracoIntegrationTestWithContentEditing
{
- protected override void CustomTestSetup(IUmbracoBuilder builder) => builder.AddUmbracoHybridCache();
+ protected override void CustomTestSetup(IUmbracoBuilder builder)
+ {
+ builder.AddNotificationHandler();
+ builder.Services.AddUnique();
+ }
private IPublishedContentCache PublishedContentHybridCache => GetRequiredService();
diff --git a/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheTests.cs
index 787fecaddf..c757001136 100644
--- a/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheTests.cs
+++ b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheTests.cs
@@ -1,11 +1,15 @@
using NUnit.Framework;
using Umbraco.Cms.Core;
+using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Models.ContentEditing;
using Umbraco.Cms.Core.Models.PublishedContent;
+using Umbraco.Cms.Core.Notifications;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Cms.Core.Services;
+using Umbraco.Cms.Core.Sync;
using Umbraco.Cms.Tests.Common.Testing;
using Umbraco.Cms.Tests.Integration.Testing;
+using Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services;
namespace Umbraco.Cms.Tests.Integration.Umbraco.PublishedCache.HybridCache;
@@ -13,7 +17,11 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.PublishedCache.HybridCache;
[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)]
public class DocumentHybridCacheTests : UmbracoIntegrationTestWithContentEditing
{
- protected override void CustomTestSetup(IUmbracoBuilder builder) => builder.AddUmbracoHybridCache();
+ protected override void CustomTestSetup(IUmbracoBuilder builder)
+ {
+ builder.AddNotificationHandler();
+ builder.Services.AddUnique();
+ }
private IPublishedContentCache PublishedContentHybridCache => GetRequiredService();
@@ -75,6 +83,19 @@ public class DocumentHybridCacheTests : UmbracoIntegrationTestWithContentEditing
Assert.IsFalse(textPage.IsPublished());
}
+ [Test]
+ public async Task Cannot_get_unpublished_content()
+ {
+ // Arrange
+ var unpublishAttempt = await ContentPublishingService.UnpublishAsync(PublishedTextPage.Key.Value, null, Constants.Security.SuperUserKey);
+
+ //Act
+ var textPage = await PublishedContentHybridCache.GetByIdAsync(PublishedTextPageId, false);
+
+ // Assert
+ Assert.IsNull(textPage);
+ }
+
[Test]
public async Task Can_Get_Draft_Of_Published_Content_By_Key()
{
diff --git a/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheVariantsTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheVariantsTests.cs
index 0094fbe526..a4572cf19c 100644
--- a/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheVariantsTests.cs
+++ b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheVariantsTests.cs
@@ -1,15 +1,19 @@
using NUnit.Framework;
using Umbraco.Cms.Core;
+using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.ContentEditing;
+using Umbraco.Cms.Core.Notifications;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.ContentTypeEditing;
+using Umbraco.Cms.Core.Sync;
using Umbraco.Cms.Core.Web;
using Umbraco.Cms.Tests.Common.Builders;
using Umbraco.Cms.Tests.Common.Builders.Extensions;
using Umbraco.Cms.Tests.Common.Testing;
using Umbraco.Cms.Tests.Integration.Testing;
+using Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services;
namespace Umbraco.Cms.Tests.Integration.Umbraco.PublishedCache.HybridCache;
@@ -36,7 +40,11 @@ public class DocumentHybridCacheVariantsTests : UmbracoIntegrationTest
private IContent VariantPage { get; set; }
- protected override void CustomTestSetup(IUmbracoBuilder builder) => builder.AddUmbracoHybridCache();
+ protected override void CustomTestSetup(IUmbracoBuilder builder)
+ {
+ builder.AddNotificationHandler();
+ builder.Services.AddUnique();
+ }
[SetUp]
public async Task Setup() => await CreateTestData();
diff --git a/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/MediaHybridCacheMediaTypeTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/MediaHybridCacheMediaTypeTests.cs
index 728f940169..60ac4157dd 100644
--- a/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/MediaHybridCacheMediaTypeTests.cs
+++ b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/MediaHybridCacheMediaTypeTests.cs
@@ -1,10 +1,14 @@
using NUnit.Framework;
using Umbraco.Cms.Core;
+using Umbraco.Cms.Core.Cache;
+using Umbraco.Cms.Core.Notifications;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Cms.Core.Services.ContentTypeEditing;
+using Umbraco.Cms.Core.Sync;
using Umbraco.Cms.Tests.Common.TestHelpers;
using Umbraco.Cms.Tests.Common.Testing;
using Umbraco.Cms.Tests.Integration.Testing;
+using Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services;
namespace Umbraco.Cms.Tests.Integration.Umbraco.PublishedCache.HybridCache;
@@ -16,7 +20,11 @@ public class MediaHybridCacheMediaTypeTests : UmbracoIntegrationTestWithMediaEdi
private new IMediaTypeEditingService MediaTypeEditingService => GetRequiredService();
- protected override void CustomTestSetup(IUmbracoBuilder builder) => builder.AddUmbracoHybridCache();
+ protected override void CustomTestSetup(IUmbracoBuilder builder)
+ {
+ builder.AddNotificationHandler();
+ builder.Services.AddUnique();
+ }
[Test]
public async Task Cannot_Get_Property_From_Media_After_It_Is_Removed_From_MediaType_By_Id()
diff --git a/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/MediaHybridCacheTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/MediaHybridCacheTests.cs
index f5edf65762..37263d913d 100644
--- a/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/MediaHybridCacheTests.cs
+++ b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/MediaHybridCacheTests.cs
@@ -1,9 +1,13 @@
using NUnit.Framework;
using Umbraco.Cms.Core;
+using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Models.ContentEditing;
+using Umbraco.Cms.Core.Notifications;
using Umbraco.Cms.Core.PublishedCache;
+using Umbraco.Cms.Core.Sync;
using Umbraco.Cms.Tests.Common.Testing;
using Umbraco.Cms.Tests.Integration.Testing;
+using Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services;
namespace Umbraco.Cms.Tests.Integration.Umbraco.PublishedCache.HybridCache;
@@ -13,7 +17,11 @@ public class MediaHybridCacheTests : UmbracoIntegrationTestWithMediaEditing
{
private IPublishedMediaCache PublishedMediaHybridCache => GetRequiredService();
- protected override void CustomTestSetup(IUmbracoBuilder builder) => builder.AddUmbracoHybridCache();
+ protected override void CustomTestSetup(IUmbracoBuilder builder)
+ {
+ builder.AddNotificationHandler();
+ builder.Services.AddUnique();
+ }
// Media with crops
[Test]
diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj b/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj
index 84b018c4ab..d174243522 100644
--- a/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj
+++ b/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj
@@ -173,6 +173,9 @@
BlockListElementLevelVariationTests.cs
+
+ BlockListElementLevelVariationTests.cs
+
DocumentNavigationServiceTests.cs
diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/ColorListValidatorTest.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/ColorListValidatorTest.cs
index c6e8fd5cf3..5ed3a5de3b 100644
--- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/ColorListValidatorTest.cs
+++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/ColorListValidatorTest.cs
@@ -3,6 +3,7 @@
using System.Text.Json.Nodes;
using NUnit.Framework;
+using Umbraco.Cms.Core.Models.Validation;
using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Infrastructure.Serialization;
@@ -23,7 +24,8 @@ public class ColorListValidatorTest
validator.Validate(
"hello",
null,
- null);
+ null,
+ PropertyValidationContext.Empty());
Assert.AreEqual(1, result.Count());
}
@@ -35,7 +37,8 @@ public class ColorListValidatorTest
validator.Validate(
new JsonArray("hello", "world"),
null,
- null);
+ null,
+ PropertyValidationContext.Empty());
Assert.AreEqual(1, result.Count());
}
@@ -51,7 +54,8 @@ public class ColorListValidatorTest
JsonNode.Parse("""{"value": "ABC", "label": "Three"}"""),
JsonNode.Parse("""{"value": "1234567", "label": "Four"}""")),
null,
- null);
+ null,
+ PropertyValidationContext.Empty());
Assert.AreEqual(2, result.Count());
}
}
diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/EnsureUniqueValuesValidatorTest.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/EnsureUniqueValuesValidatorTest.cs
index 8192afda99..efb61bb0f2 100644
--- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/EnsureUniqueValuesValidatorTest.cs
+++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/EnsureUniqueValuesValidatorTest.cs
@@ -3,6 +3,7 @@
using System.Text.Json.Nodes;
using NUnit.Framework;
+using Umbraco.Cms.Core.Models.Validation;
using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Infrastructure.Serialization;
@@ -22,7 +23,8 @@ public class EnsureUniqueValuesValidatorTest
var result = validator.Validate(
"hello",
null,
- null);
+ null,
+ PropertyValidationContext.Empty());
Assert.AreEqual(1, result.Count());
}
@@ -34,7 +36,8 @@ public class EnsureUniqueValuesValidatorTest
validator.Validate(
new JsonArray("hello", "world"),
null,
- null);
+ null,
+ PropertyValidationContext.Empty());
Assert.AreEqual(0, result.Count());
}
@@ -46,7 +49,8 @@ public class EnsureUniqueValuesValidatorTest
validator.Validate(
new JsonArray("one", "two", "three"),
null,
- null);
+ null,
+ PropertyValidationContext.Empty());
Assert.AreEqual(0, result.Count());
}
@@ -58,7 +62,8 @@ public class EnsureUniqueValuesValidatorTest
validator.Validate(
new JsonArray("one", "one"),
null,
- null);
+ null,
+ PropertyValidationContext.Empty());
Assert.AreEqual(1, result.Count());
}
@@ -70,7 +75,8 @@ public class EnsureUniqueValuesValidatorTest
validator.Validate(
new JsonArray("one", "two", "three", "one", "two"),
null,
- null);
+ null,
+ PropertyValidationContext.Empty());
Assert.AreEqual(2, result.Count());
}
@@ -82,7 +88,8 @@ public class EnsureUniqueValuesValidatorTest
validator.Validate(
null,
null,
- null);
+ null,
+ PropertyValidationContext.Empty());
Assert.AreEqual(0, result.Count());
}
@@ -95,7 +102,8 @@ public class EnsureUniqueValuesValidatorTest
validator.Validate(
value,
null,
- null);
+ null,
+ PropertyValidationContext.Empty());
Assert.AreEqual(0, result.Count());
}
@@ -108,7 +116,8 @@ public class EnsureUniqueValuesValidatorTest
validator.Validate(
value,
null,
- null);
+ null,
+ PropertyValidationContext.Empty());
Assert.AreEqual(0, result.Count());
}
@@ -121,7 +130,8 @@ public class EnsureUniqueValuesValidatorTest
validator.Validate(
value,
null,
- null);
+ null,
+ PropertyValidationContext.Empty());
Assert.AreEqual(0, result.Count());
}
}
diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.PublishedCache.HybridCache/DocumentBreadthFirstKeyProviderTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.PublishedCache.HybridCache/DocumentBreadthFirstKeyProviderTests.cs
index fef4486863..241ee77479 100644
--- a/tests/Umbraco.Tests.UnitTests/Umbraco.PublishedCache.HybridCache/DocumentBreadthFirstKeyProviderTests.cs
+++ b/tests/Umbraco.Tests.UnitTests/Umbraco.PublishedCache.HybridCache/DocumentBreadthFirstKeyProviderTests.cs
@@ -10,7 +10,6 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.PublishedCache.HybridCache;
[TestFixture]
public class DocumentBreadthFirstKeyProviderTests
{
-
[Test]
public void ZeroSeedCountReturnsZeroKeys()
{
@@ -22,7 +21,6 @@ public class DocumentBreadthFirstKeyProviderTests
navigationQueryService.Setup(x => x.TryGetRootKeys(out rootKeyList)).Returns(true);
navigationQueryService.Setup(x => x.TryGetChildrenKeys(It.IsAny(), out rootChildren)).Returns(true);
-
var cacheSettings = new CacheSettings { DocumentBreadthFirstSeedCount = 0 };
var sut = new DocumentBreadthFirstKeyProvider(navigationQueryService.Object, Options.Create(cacheSettings));