Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -175,11 +175,70 @@
_publishedContentTypeFactory.NotifyDataTypeChanges();
_publishedModelFactory.WithSafeLiveFactoryReset(() =>
{
IEnumerable<int> documentTypeIds = payloads.Where(x => x.ItemType == nameof(IContentType)).Select(x => x.Id);
IEnumerable<int> mediaTypeIds = payloads.Where(x => x.ItemType == nameof(IMediaType)).Select(x => x.Id);

_documentCacheService.RebuildMemoryCacheByContentTypeAsync(documentTypeIds).GetAwaiter().GetResult();
_mediaCacheService.RebuildMemoryCacheByContentTypeAsync(mediaTypeIds).GetAwaiter().GetResult();
// Separate structural changes (RefreshMain) from non-structural changes (RefreshOther).
// Structural changes require a full memory cache rebuild, while non-structural changes
// only need the converted content cache cleared since ContentCacheNode only stores ContentTypeId.
var structuralDocumentTypeIds = payloads
.Where(x => x.ItemType == nameof(IContentType) && x.ChangeTypes.IsStructuralChange())
.Select(x => x.Id)
.ToArray();

var nonStructuralDocumentTypeIds = payloads
.Where(x => x.ItemType == nameof(IContentType) && x.ChangeTypes.IsNonStructuralChange())
.Select(x => x.Id)
.ToArray();

var structuralMediaTypeIds = payloads
.Where(x => x.ItemType == nameof(IMediaType) && x.ChangeTypes.IsStructuralChange())
.Select(x => x.Id)
.ToArray();

var nonStructuralMediaTypeIds = payloads
.Where(x => x.ItemType == nameof(IMediaType) && x.ChangeTypes.IsNonStructuralChange())
.Select(x => x.Id)
.ToArray();

// Full memory cache rebuild only for structural changes
if (structuralDocumentTypeIds.Length > 0)
{
_documentCacheService.RebuildMemoryCacheByContentTypeAsync(structuralDocumentTypeIds).GetAwaiter().GetResult();
}

if (structuralMediaTypeIds.Length > 0)
{
_mediaCacheService.RebuildMemoryCacheByContentTypeAsync(structuralMediaTypeIds).GetAwaiter().GetResult();
}

// Clear the converted content cache for non-structural changes (HybridCache entries remain valid).
// In auto models builder mode (InMemoryAuto), the factory reset above invalidates ALL compiled
// model types, so we must clear all entries to prevent stale instances of other types
// (e.g. Model.Parent<T>()) from being returned. In non-auto modes, only affected types need clearing.
var isAutoFactory = _publishedModelFactory is IAutoPublishedModelFactory;

if (isAutoFactory)
{
if (structuralDocumentTypeIds.Length > 0 || nonStructuralDocumentTypeIds.Length > 0)
{
_documentCacheService.ClearConvertedContentCache();
}

if (structuralMediaTypeIds.Length > 0 || nonStructuralMediaTypeIds.Length > 0)
{
_mediaCacheService.ClearConvertedContentCache();
}
}
else
{
if (nonStructuralDocumentTypeIds.Length > 0)
{
_documentCacheService.ClearConvertedContentCache(nonStructuralDocumentTypeIds);
}

if (nonStructuralMediaTypeIds.Length > 0)
{
_mediaCacheService.ClearConvertedContentCache(nonStructuralMediaTypeIds);
}
}

Check warning on line 241 in src/Umbraco.Core/Cache/Refreshers/Implement/ContentTypeCacheRefresher.cs

View check run for this annotation

CodeScene Delta Analysis / CodeScene Code Health Review (main)

❌ New issue: Complex Method

Refresh has a cyclomatic complexity of 14, threshold = 9. This function has many conditional statements (e.g. if, for, while), leading to lower code health. Avoid adding more conditionals and code to it without refactoring.

Check warning on line 241 in src/Umbraco.Core/Cache/Refreshers/Implement/ContentTypeCacheRefresher.cs

View check run for this annotation

CodeScene Delta Analysis / CodeScene Code Health Review (main)

❌ New issue: Bumpy Road Ahead

Refresh has 2 blocks with nested conditional logic. Any nesting of 2 or deeper is considered. Threshold is 2 blocks per function. The Bumpy Road code smell is a function that contains multiple chunks of nested conditional logic. The deeper the nesting and the more bumps, the lower the code health.
});

// now we can trigger the event
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -203,15 +203,43 @@ public override void Refresh(JsonPayload[] payloads)

_publishedModelFactory.WithSafeLiveFactoryReset(() =>
{
IEnumerable<int> documentTypeIds = removedContentTypes
var documentTypeIds = removedContentTypes
.Where(x => x.ItemType == PublishedItemType.Content)
.Select(x => x.Id);
_documentCacheService.RebuildMemoryCacheByContentTypeAsync(documentTypeIds).GetAwaiter().GetResult();
.Select(x => x.Id)
.ToArray();

IEnumerable<int> mediaTypeIds = removedContentTypes
var mediaTypeIds = removedContentTypes
.Where(x => x.ItemType == PublishedItemType.Media)
.Select(x => x.Id);
_mediaCacheService.RebuildMemoryCacheByContentTypeAsync(mediaTypeIds).GetAwaiter().GetResult();
.Select(x => x.Id)
.ToArray();

if (documentTypeIds.Length > 0)
{
_documentCacheService.RebuildMemoryCacheByContentTypeAsync(documentTypeIds).GetAwaiter().GetResult();
}

if (mediaTypeIds.Length > 0)
{
_mediaCacheService.RebuildMemoryCacheByContentTypeAsync(mediaTypeIds).GetAwaiter().GetResult();
}

// In auto models builder mode (InMemoryAuto), the factory reset above invalidates ALL compiled
// model types, so we must clear all converted content entries — not just the rebuilt types —
// to prevent stale instances of other types (e.g. Model.Parent<T>()) from being returned.
// RebuildMemoryCacheByContentTypeAsync already clears selectively for the affected types,
// but the full clear is needed for all other types whose models were also invalidated.
if (_publishedModelFactory is IAutoPublishedModelFactory)
{
if (documentTypeIds.Length > 0)
{
_documentCacheService.ClearConvertedContentCache();
}

if (mediaTypeIds.Length > 0)
{
_mediaCacheService.ClearConvertedContentCache();
}
}
});
base.Refresh(payloads);
}
Expand Down
23 changes: 23 additions & 0 deletions src/Umbraco.Core/PublishedCache/IDocumentCacheService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -97,4 +97,27 @@ public interface IDocumentCacheService
/// <param name="contentTypeIds">The collection of content type identifiers to rebuild in memory cache.</param>
/// <returns>A task representing the asynchronous operation.</returns>
Task RebuildMemoryCacheByContentTypeAsync(IEnumerable<int> contentTypeIds);

/// <summary>
/// Clears all converted IPublishedContent entries from the in-memory cache,
/// without rebuilding the underlying database cache or HybridCache entries.
/// </summary>
/// <remarks>
/// Use this when the published model factory is reset (e.g. InMemoryAuto mode), which
/// invalidates all compiled model types and makes cached instances of any type stale.
/// </remarks>
// TODO (V18): Remove default implementation.
void ClearConvertedContentCache() { }

/// <summary>
/// Clears converted IPublishedContent entries for the specified content types from the in-memory cache,
/// without rebuilding the underlying database cache or HybridCache entries.
/// </summary>
/// <remarks>
/// Use this when the published model factory is NOT reset (e.g. SourceCodeAuto/SourceCodeManual modes),
/// so only the affected content types need their converted cache cleared.
/// </remarks>
/// <param name="contentTypeIds">The IDs of the content types whose converted entries should be cleared.</param>
// TODO (V18): Remove default implementation.
void ClearConvertedContentCache(IReadOnlyCollection<int> contentTypeIds) { }
}
23 changes: 23 additions & 0 deletions src/Umbraco.Core/PublishedCache/IMediaCacheService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -94,4 +94,27 @@ public interface IMediaCacheService
/// <param name="contentType">The published content type to filter by.</param>
/// <returns>A collection of published media items of the specified type.</returns>
IEnumerable<IPublishedContent> GetByContentType(IPublishedContentType contentType);

/// <summary>
/// Clears all converted IPublishedContent entries from the in-memory cache,
/// without rebuilding the underlying database cache or HybridCache entries.
/// </summary>
/// <remarks>
/// Use this when the published model factory is reset (e.g. InMemoryAuto mode), which
/// invalidates all compiled model types and makes cached instances of any type stale.
/// </remarks>
// TODO (V18): Remove default implementation.
void ClearConvertedContentCache() { }

/// <summary>
/// Clears converted IPublishedContent entries for the specified media types from the in-memory cache,
/// without rebuilding the underlying database cache or HybridCache entries.
/// </summary>
/// <remarks>
/// Use this when the published model factory is NOT reset (e.g. SourceCodeAuto/SourceCodeManual modes),
/// so only the affected media types need their converted cache cleared.
/// </remarks>
/// <param name="mediaTypeIds">The IDs of the media types whose converted entries should be cleared.</param>
// TODO (V18): Remove default implementation.
void ClearConvertedContentCache(IReadOnlyCollection<int> mediaTypeIds) { }
}
16 changes: 16 additions & 0 deletions src/Umbraco.Core/Services/Changes/ContentTypeChangeExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,20 @@ public static bool HasTypesAny(this ContentTypeChangeTypes change, ContentTypeCh
/// <returns><c>true</c> if the change includes none of the types; otherwise, <c>false</c>.</returns>
public static bool HasTypesNone(this ContentTypeChangeTypes change, ContentTypeChangeTypes types) =>
(change & types) == ContentTypeChangeTypes.None;

/// <summary>
/// Determines whether the change has structural change impact.
/// </summary>
/// <param name="change">The change to check.</param>
/// <returns><c>true</c> if the change has structural impact; otherwise, <c>false</c>.</returns>
public static bool IsStructuralChange(this ContentTypeChangeTypes change) =>
change.HasType(ContentTypeChangeTypes.RefreshMain);

/// <summary>
/// Determines whether the change has non-structural change impact.
/// </summary>
/// <param name="change">The change to check.</param>
/// <returns><c>true</c> if the change has non-structural impact; otherwise, <c>false</c>.</returns>
public static bool IsNonStructuralChange(this ContentTypeChangeTypes change) =>
change.HasType(ContentTypeChangeTypes.RefreshOther) && !change.HasType(ContentTypeChangeTypes.RefreshMain);
}
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@
// To 17.3.0
To<V_17_3_0.IncreaseSizeOfLongRunningOperationTypeColumn>("{B2F4A1C3-8D5E-4F6A-9B7C-3E1D2A4F5B6C}");
To<V_17_3_0.RetrustForeignKeyAndCheckConstraints>("{0638E0E0-D914-4ACA-8A4B-9551A3AAB91F}");
To<V_17_3_0.RebuildHybridCache>("{E4A7C2D1-5F38-4B96-A1D3-8E2F6C9B0A74}");

Check warning on line 157 in src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs

View check run for this annotation

CodeScene Delta Analysis / CodeScene Code Health Review (main)

❌ Getting worse: Large Method

UmbracoPlan increases from 74 to 75 lines of code, threshold = 70. Large functions with many lines of code are generally harder to understand and lower the code health. Avoid adding more lines to this function.

// To 18.0.0
// TODO (V18): Enable on 18 branch
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using Umbraco.Cms.Core.PublishedCache;

namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_17_3_0;

/// <summary>
/// Clears and re-seeds the HybridCache so that all entries are tagged with the content type ID.
/// This is required for the content type tag-based eviction introduced in 17.3.
/// </summary>
public class RebuildHybridCache : AsyncMigrationBase
{
private readonly IDocumentCacheService _documentCacheService;
private readonly IMediaCacheService _mediaCacheService;

/// <summary>
/// Initializes a new instance of the <see cref="RebuildHybridCache"/> class.
/// </summary>
public RebuildHybridCache(
IMigrationContext context,
IDocumentCacheService documentCacheService,
IMediaCacheService mediaCacheService)
: base(context)
{
_documentCacheService = documentCacheService;
_mediaCacheService = mediaCacheService;
}

protected override async Task MigrateAsync()
{
await _documentCacheService.ClearMemoryCacheAsync(CancellationToken.None);
await _mediaCacheService.ClearMemoryCacheAsync(CancellationToken.None);
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Notifications;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.Changes;
using Umbraco.Cms.Core.Services.Navigation;
using Umbraco.Cms.Core.Sync;
using Umbraco.Extensions;

Expand All @@ -16,21 +21,45 @@
private readonly IMediaService _mediaService;
private readonly IMemberService _memberService;
private readonly IMemberTypeService _memberTypeService;
private readonly IPublishStatusQueryService _publishStatusQueryService;
private readonly IUmbracoIndexingHandler _umbracoIndexingHandler;
private readonly IOptionsMonitor<IndexingSettings> _indexingSettings;

[Obsolete("Please use the non-obsolete constructor. Scheduled for removal in Umbraco 18.")]
public ContentTypeIndexingNotificationHandler(
IUmbracoIndexingHandler umbracoIndexingHandler,
IContentService contentService,
IMemberService memberService,
IMediaService mediaService,
IMemberTypeService memberTypeService)
: this(
umbracoIndexingHandler,
contentService,
memberService,
mediaService,
memberTypeService,
StaticServiceProvider.Instance.GetRequiredService<IPublishStatusQueryService>(),
StaticServiceProvider.Instance.GetRequiredService<IOptionsMonitor<IndexingSettings>>())
{
}

public ContentTypeIndexingNotificationHandler(
IUmbracoIndexingHandler umbracoIndexingHandler,
IContentService contentService,
IMemberService memberService,
IMediaService mediaService,
IMemberTypeService memberTypeService,
IPublishStatusQueryService publishStatusQueryService,
IOptionsMonitor<IndexingSettings> indexingSettings)
{
_umbracoIndexingHandler =
umbracoIndexingHandler ?? throw new ArgumentNullException(nameof(umbracoIndexingHandler));
_contentService = contentService ?? throw new ArgumentNullException(nameof(contentService));
_memberService = memberService ?? throw new ArgumentNullException(nameof(memberService));
_mediaService = mediaService ?? throw new ArgumentNullException(nameof(mediaService));
_memberTypeService = memberTypeService ?? throw new ArgumentNullException(nameof(memberTypeService));
_publishStatusQueryService = publishStatusQueryService;
_indexingSettings = indexingSettings;

Check warning on line 62 in src/Umbraco.Infrastructure/Search/ContentTypeIndexingNotificationHandler.cs

View check run for this annotation

CodeScene Delta Analysis / CodeScene Code Health Review (main)

❌ New issue: Constructor Over-Injection

ContentTypeIndexingNotificationHandler has 7 arguments, max arguments = 5. This constructor has too many arguments, indicating an object with low cohesion or missing function argument abstraction. Avoid adding more arguments.
}

/// <summary>
Expand All @@ -54,49 +83,45 @@
throw new NotSupportedException();
}

var changedIds = new Dictionary<string, (List<int> removedIds, List<int> refreshedIds, List<int> otherIds)>();
var changedIds = new Dictionary<string, (List<int> removedIds, List<int> refreshedIds)>();

foreach (ContentTypeCacheRefresher.JsonPayload payload in (ContentTypeCacheRefresher.JsonPayload[])args
.MessageObject)
{
if (!changedIds.TryGetValue(
payload.ItemType,
out (List<int> removedIds, List<int> refreshedIds, List<int> otherIds) idLists))
out (List<int> removedIds, List<int> refreshedIds) idLists))
{
idLists = (removedIds: new List<int>(), refreshedIds: new List<int>(), otherIds: new List<int>());
idLists = (removedIds: new List<int>(), refreshedIds: new List<int>());
changedIds.Add(payload.ItemType, idLists);
}

if (payload.ChangeTypes.HasType(ContentTypeChangeTypes.Remove))
{
idLists.removedIds.Add(payload.Id);
}
else if (payload.ChangeTypes.HasType(ContentTypeChangeTypes.RefreshMain))
{
idLists.refreshedIds.Add(payload.Id);
}
else if (payload.ChangeTypes.HasType(ContentTypeChangeTypes.RefreshOther))
{
idLists.otherIds.Add(payload.Id);
}
}

foreach (KeyValuePair<string, (List<int> removedIds, List<int> refreshedIds, List<int> otherIds)> ci in
foreach (KeyValuePair<string, (List<int> removedIds, List<int> refreshedIds)> ci in
changedIds)
{
if (ci.Value.refreshedIds.Count > 0 || ci.Value.otherIds.Count > 0)
if (ci.Value.refreshedIds.Count > 0)
{
switch (ci.Key)
{
case var itemType when itemType == typeof(IContentType).Name:
RefreshContentOfContentTypes(ci.Value.refreshedIds.Concat(ci.Value.otherIds).Distinct()
RefreshContentOfContentTypes(ci.Value.refreshedIds.Distinct()
.ToArray());
break;
case var itemType when itemType == typeof(IMediaType).Name:
RefreshMediaOfMediaTypes(ci.Value.refreshedIds.Concat(ci.Value.otherIds).Distinct().ToArray());
RefreshMediaOfMediaTypes(ci.Value.refreshedIds.Distinct().ToArray());
break;
case var itemType when itemType == typeof(IMemberType).Name:
RefreshMemberOfMemberTypes(ci.Value.refreshedIds.Concat(ci.Value.otherIds).Distinct()
RefreshMemberOfMemberTypes(ci.Value.refreshedIds.Distinct()

Check notice on line 124 in src/Umbraco.Infrastructure/Search/ContentTypeIndexingNotificationHandler.cs

View check run for this annotation

CodeScene Delta Analysis / CodeScene Code Health Review (main)

✅ Getting better: Complex Method

Handle decreases in cyclomatic complexity from 15 to 13, threshold = 9. This function has many conditional statements (e.g. if, for, while), leading to lower code health. Avoid adding more conditionals and code to it without refactoring.
.ToArray());
break;
}
Expand All @@ -109,7 +134,7 @@

private void RefreshMemberOfMemberTypes(int[] memberTypeIds)
{
const int pageSize = 500;
var pageSize = _indexingSettings.CurrentValue.BatchSize;

IEnumerable<IMemberType> memberTypes = _memberTypeService.GetMany(memberTypeIds);
foreach (IMemberType memberType in memberTypes)
Expand Down Expand Up @@ -138,7 +163,7 @@

private void RefreshMediaOfMediaTypes(int[] mediaTypeIds)
{
const int pageSize = 500;
var pageSize = _indexingSettings.CurrentValue.BatchSize;
var page = 0;
var total = long.MaxValue;
while (page * pageSize < total)
Expand All @@ -162,7 +187,7 @@

private void RefreshContentOfContentTypes(int[] contentTypeIds)
{
const int pageSize = 500;
var pageSize = _indexingSettings.CurrentValue.BatchSize;
var page = 0;
var total = long.MaxValue;
while (page * pageSize < total)
Expand Down Expand Up @@ -190,7 +215,7 @@
if (!publishChecked.TryGetValue(c.ParentId, out isPublished))
{
// nothing by parent id, so query the service and cache the result for the next child to check against
isPublished = _contentService.IsPathPublished(c);
isPublished = _publishStatusQueryService.HasPublishedAncestorPath(c.Key);
publishChecked[c.Id] = isPublished;
}
}
Expand Down
Loading
Loading