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
16 changes: 14 additions & 2 deletions src/Umbraco.Core/Cache/DistributedCacheExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -374,13 +374,25 @@ public static void RefreshMediaCache(this DistributedCache dc, IEnumerable<TreeC

#region ElementCacheRefresher

/// <summary>
/// Refreshes all elements in the distributed cache.
/// </summary>
/// <param name="dc">The distributed cache.</param>
public static void RefreshAllElementCache(this DistributedCache dc)
// note: refresh all element cache does refresh content types too
=> dc.RefreshByPayload(ElementCacheRefresher.UniqueId, new ElementCacheRefresher.JsonPayload(0, Guid.Empty, TreeChangeTypes.RefreshAll).Yield());


/// <summary>
/// Refreshes the element cache for the specified element changes.
/// </summary>
/// <param name="dc">The distributed cache.</param>
/// <param name="changes">The element changes to refresh.</param>
public static void RefreshElementCache(this DistributedCache dc, IEnumerable<TreeChange<IElement>> changes)
=> dc.RefreshByPayload(ElementCacheRefresher.UniqueId, changes.DistinctBy(x => (x.Item.Id, x.Item.Key, x.ChangeTypes)).Select(x => new ElementCacheRefresher.JsonPayload(x.Item.Id, x.Item.Key, x.ChangeTypes)));
=> dc.RefreshByPayload(ElementCacheRefresher.UniqueId, changes.DistinctBy(x => (x.Item.Id, x.Item.Key, x.ChangeTypes)).Select(x => new ElementCacheRefresher.JsonPayload(x.Item.Id, x.Item.Key, x.ChangeTypes)
{
PublishedCultures = x.PublishedCultures?.ToArray(),
UnpublishedCultures = x.UnpublishedCultures?.ToArray(),
}));

#endregion

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Microsoft.Extensions.DependencyInjection;

Check notice on line 1 in src/Umbraco.Core/Cache/Refreshers/Implement/ContentCacheRefresher.cs

View check run for this annotation

CodeScene Delta Analysis / CodeScene Code Health Review (v18/dev)

✅ Getting better: Overall Code Complexity

The mean cyclomatic complexity decreases from 4.41 to 4.22, threshold = 4. This file has many conditional statements (e.g. if, for, while) across its implementation, leading to lower code health. Avoid adding more conditionals.
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Models;
Expand Down Expand Up @@ -223,27 +223,30 @@
base.Refresh(payloads);
}

private static bool ShouldClearPartialViewCache(JsonPayload[] payloads)
{
return payloads.Any(x =>
internal static bool ShouldClearPartialViewCache(IEnumerable<(TreeChangeTypes ChangeTypes, string[]? PublishedCultures, string[]? UnpublishedCultures)> changes)
=> changes.Any(change =>
{
// Check for relelvant change type
var isRelevantChangeType = x.ChangeTypes.HasType(TreeChangeTypes.RefreshAll) ||
x.ChangeTypes.HasType(TreeChangeTypes.Remove) ||
x.ChangeTypes.HasType(TreeChangeTypes.RefreshNode) ||
x.ChangeTypes.HasType(TreeChangeTypes.RefreshBranch);
// Check for relevant change type
var isRelevantChangeType = change.ChangeTypes.HasType(TreeChangeTypes.RefreshAll) ||
change.ChangeTypes.HasType(TreeChangeTypes.Remove) ||
change.ChangeTypes.HasType(TreeChangeTypes.RefreshNode) ||
change.ChangeTypes.HasType(TreeChangeTypes.RefreshBranch);

// Check for published/unpublished changes
var hasChanges = x.PublishedCultures?.Length > 0 ||
x.UnpublishedCultures?.Length > 0;
var hasChanges = change.PublishedCultures?.Length > 0 ||
change.UnpublishedCultures?.Length > 0;

// There's no other way to detect trashed content as the change type is only Remove when deleted permanently
var isTrashed = x.ChangeTypes.HasType(TreeChangeTypes.RefreshBranch) && x.PublishedCultures is null && x.UnpublishedCultures is null;
// There's no other way to detect trashed state as the change type is only Remove when deleted permanently
var isTrashed = change.ChangeTypes.HasType(TreeChangeTypes.RefreshBranch) && change.PublishedCultures is null && change.UnpublishedCultures is null;

// Skip blueprints and only clear the partial cache for removals or refreshes with changes
return x.Blueprint == false && (isTrashed || (isRelevantChangeType && hasChanges));
// Only clear the partial cache for removals or refreshes with changes
return isTrashed || (isRelevantChangeType && hasChanges);

Check notice on line 243 in src/Umbraco.Core/Cache/Refreshers/Implement/ContentCacheRefresher.cs

View check run for this annotation

CodeScene Delta Analysis / CodeScene Code Health Review (v18/dev)

✅ Getting better: Complex Method

ShouldClearPartialViewCache decreases in cyclomatic complexity from 10 to 9, 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.
});
}

private static bool ShouldClearPartialViewCache(JsonPayload[] payloads)
=> ShouldClearPartialViewCache(payloads
.Where(payload => payload.Blueprint is false)
.Select(payload => (payload.ChangeTypes, payload.PublishedCultures, payload.UnpublishedCultures)));

private void HandleMemoryCache(JsonPayload payload)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,25 @@

namespace Umbraco.Cms.Core.Cache;

/// <summary>
/// Provides cache refresh functionality for element items, ensuring that element-related caches are updated or
/// invalidated in response to element changes.
/// </summary>
/// <remarks>
/// The ElementCacheRefresher coordinates cache invalidation for elements, including memory caches and
/// isolated caches. It responds to element change notifications and ensures that all relevant caches reflect
/// the current state of published and unpublished elements. This refresher is used internally to maintain
/// cache consistency after element operations such as publish, unpublish, or delete.
/// </remarks>
public sealed class ElementCacheRefresher : PayloadCacheRefresherBase<ElementCacheRefresherNotification, ElementCacheRefresher.JsonPayload>
{
private readonly IIdKeyMap _idKeyMap;
private readonly IElementCacheService _elementCacheService;
private readonly ICacheManager _cacheManager;

/// <summary>
/// Initializes a new instance of the <see cref="ElementCacheRefresher"/> class.
/// </summary>
public ElementCacheRefresher(
AppCaches appCaches,
IJsonSerializer serializer,
Expand All @@ -35,6 +48,10 @@ public ElementCacheRefresher(

#region Json

/// <summary>
/// Represents a JSON-serializable payload containing information about an element change event, including
/// identifiers, change types, and culture-specific publishing details.
/// </summary>
public class JsonPayload
{
public JsonPayload(int id, Guid key, TreeChangeTypes changeTypes)
Expand All @@ -44,23 +61,45 @@ public JsonPayload(int id, Guid key, TreeChangeTypes changeTypes)
ChangeTypes = changeTypes;
}

/// <summary>
/// Gets the unique integer identifier for the entity.
/// </summary>
public int Id { get; }

/// <summary>
/// Gets the unique GUID key associated with the entity.
/// </summary>
public Guid Key { get; }

/// <summary>
/// Gets the types of changes that have occurred in the tree.
/// </summary>
public TreeChangeTypes ChangeTypes { get; }

// TODO ELEMENTS: should we support (un)published cultures in this payload? see ContentCacheRefresher.JsonPayload
/// <summary>
/// Gets the collection of culture codes in which the element is published.
/// </summary>
public string[]? PublishedCultures { get; init; }

/// <summary>
/// Gets the collection of culture codes for which the element has been unpublished.
/// </summary>
public string[]? UnpublishedCultures { get; init; }
}

#endregion

#region Define

/// <summary>
/// Represents a unique identifier for the cache refresher.
/// </summary>
public static readonly Guid UniqueId = Guid.Parse("EE5BB23A-A656-4F7E-A234-16F21AAABFD1");

/// <inheritdoc/>
public override Guid RefresherUniqueId => UniqueId;

/// <inheritdoc/>
public override string Name => "Element Cache Refresher";

#endregion
Expand Down Expand Up @@ -93,17 +132,25 @@ public override void Refresh(JsonPayload[] payloads)
// TODO ELEMENTS: if we need published status caching for elements (e.g. for seeding purposes), make sure
// it is kept in sync here (see ContentCacheRefresher)

if (payload.ChangeTypes == TreeChangeTypes.Remove)
if (payload.ChangeTypes.HasType(TreeChangeTypes.Remove))
{
_idKeyMap.ClearCache(payload.Id);
}
}

AppCaches.ClearPartialViewCache();
if (ShouldClearPartialViewCache(payloads))
{
AppCaches.ClearPartialViewCache();
}

base.Refresh(payloads);
}

private static bool ShouldClearPartialViewCache(JsonPayload[] payloads)
// Reuse the "should clear partial view cache" logic from the content cache refresher.
=> ContentCacheRefresher.ShouldClearPartialViewCache(payloads
.Select(payload => (payload.ChangeTypes, payload.PublishedCultures, payload.UnpublishedCultures)));

private void HandleMemoryCache(JsonPayload payload)
{
if (payload.ChangeTypes.HasType(TreeChangeTypes.RefreshAll))
Expand All @@ -122,14 +169,18 @@ private void HandleMemoryCache(JsonPayload payload)
}
}

// these events should never trigger
// everything should be JSON
// These events should never trigger. Everything should be PAYLOAD/JSON.

/// <inheritdoc/>
public override void RefreshAll() => throw new NotSupportedException();

/// <inheritdoc/>
public override void Refresh(int id) => throw new NotSupportedException();

/// <inheritdoc/>
public override void Refresh(Guid id) => throw new NotSupportedException();

/// <inheritdoc/>
public override void Remove(int id) => throw new NotSupportedException();

#endregion
Expand Down
34 changes: 34 additions & 0 deletions tests/Umbraco.Tests.UnitTests/Umbraco.Core/Cache/RefresherTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,40 @@
Assert.AreEqual(1234, payload[0].Id);
Assert.AreEqual(key, payload[0].Key);
Assert.AreEqual(changeTypes, payload[0].ChangeTypes);
Assert.IsNull(payload[0].PublishedCultures);
Assert.IsNull(payload[0].UnpublishedCultures);
}

Check warning on line 115 in tests/Umbraco.Tests.UnitTests/Umbraco.Core/Cache/RefresherTests.cs

View check run for this annotation

CodeScene Delta Analysis / CodeScene Code Health Review (v18/dev)

❌ New issue: Large Assertion Blocks

The test suite contains 4 assertion blocks with at least 4 assertions, threshold = 4. This test file has several blocks of large, consecutive assert statements. Avoid adding more.

[Test]
public void ElementCacheRefresherCanDeserializeJsonPayloadWithCultures()
{
var key = Guid.NewGuid();
ElementCacheRefresher.JsonPayload[] source =
{
new(1234, key, TreeChangeTypes.RefreshNode)
{
PublishedCultures = ["en-US", "da-DK"],
UnpublishedCultures = ["de-DE"]
}
};

var json = JsonSerializer.Serialize(source);
var payload = JsonSerializer.Deserialize<ElementCacheRefresher.JsonPayload[]>(json);

Assert.IsNotNull(payload[0].PublishedCultures);
Assert.Multiple(() =>
{
Assert.AreEqual(2, payload[0].PublishedCultures.Length);
Assert.AreEqual("en-US", payload[0].PublishedCultures.First());
Assert.AreEqual("da-DK", payload[0].PublishedCultures.Last());
});

Assert.IsNotNull(payload[0].UnpublishedCultures);
Assert.Multiple(() =>
{
Assert.AreEqual(1, payload[0].UnpublishedCultures.Length);
Assert.AreEqual("de-DE", payload[0].UnpublishedCultures.First());
});
}

[Test]
Expand Down
Loading