diff --git a/src/Umbraco.PublishedCache.HybridCache/Persistence/DatabaseCacheRepository.cs b/src/Umbraco.PublishedCache.HybridCache/Persistence/DatabaseCacheRepository.cs
index fbdfe3375876..be192b24d0a2 100644
--- a/src/Umbraco.PublishedCache.HybridCache/Persistence/DatabaseCacheRepository.cs
+++ b/src/Umbraco.PublishedCache.HybridCache/Persistence/DatabaseCacheRepository.cs
@@ -143,14 +143,9 @@ private void TruncateContent()
///
public async Task GetContentSourceAsync(Guid key, bool preview = false)
{
- Sql? sql = SqlContentSourcesSelect()
- .Append(SqlObjectTypeNotTrashed(SqlContext, Constants.ObjectTypes.Document))
- .Append(SqlWhereNodeKey(SqlContext, key))
- .Append(SqlOrderByLevelIdSortOrder(SqlContext));
+ ContentSourceDto? dto = await GetContentSourceDto(key);
- ContentSourceDto? dto = await Database.FirstOrDefaultAsync(sql);
-
- if (dto == null)
+ if (dto is null)
{
return null;
}
@@ -160,6 +155,39 @@ private void TruncateContent()
return null;
}
+ return CreateContentNodeKit(preview, dto);
+ }
+
+ ///
+ public async Task<(ContentCacheNode? Draft, ContentCacheNode? Published)> GetContentSourceForPublishStatesAsync(Guid key)
+ {
+ ContentSourceDto? dto = await GetContentSourceDto(key);
+
+ if (dto is null)
+ {
+ return (null, null);
+ }
+
+ ContentCacheNode draftNode = CreateContentNodeKit(true, dto);
+ ContentCacheNode? publishedNode = dto.PubDataRaw is null && dto.PubData is null
+ ? null
+ : CreateContentNodeKit(false, dto);
+
+ return (draftNode, publishedNode);
+ }
+
+ private async Task GetContentSourceDto(Guid key)
+ {
+ Sql? sql = SqlContentSourcesSelect()
+ .Append(SqlObjectTypeNotTrashed(SqlContext, Constants.ObjectTypes.Document))
+ .Append(SqlWhereNodeKey(SqlContext, key))
+ .Append(SqlOrderByLevelIdSortOrder(SqlContext));
+
+ return await Database.FirstOrDefaultAsync(sql);
+ }
+
+ private ContentCacheNode CreateContentNodeKit(bool preview, ContentSourceDto dto)
+ {
IContentCacheDataSerializer serializer =
_contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Document);
return CreateContentNodeKit(dto, serializer, preview);
diff --git a/src/Umbraco.PublishedCache.HybridCache/Persistence/IDatabaseCacheRepository.cs b/src/Umbraco.PublishedCache.HybridCache/Persistence/IDatabaseCacheRepository.cs
index ede7c426d168..be7b19ceeddd 100644
--- a/src/Umbraco.PublishedCache.HybridCache/Persistence/IDatabaseCacheRepository.cs
+++ b/src/Umbraco.PublishedCache.HybridCache/Persistence/IDatabaseCacheRepository.cs
@@ -16,11 +16,28 @@ internal interface IDatabaseCacheRepository
///
/// Gets a single cache node for a document key.
///
+ /// The document key.
+ /// A flag indicating whether to get the draft (preview) version or the published version.
Task GetContentSourceAsync(Guid key, bool preview = false);
+ ///
+ /// Gets both draft and published cache nodes for a document key in a single query.
+ ///
+ /// The document key.
+ /// A tuple containing the draft and published cache nodes (either may be null).
+ // TODO (V18): Remove the default implementation on this method.
+ async Task<(ContentCacheNode? Draft, ContentCacheNode? Published)> GetContentSourceForPublishStatesAsync(Guid key)
+ {
+ ContentCacheNode? draftNode = await GetContentSourceAsync(key, preview: true);
+ ContentCacheNode? publishedNode = await GetContentSourceAsync(key, preview: false);
+ return (draftNode, publishedNode);
+ }
+
///
/// Gets a collection of cache nodes for a collection of document keys.
///
+ /// The document keys.
+ /// A flag indicating whether to get the draft (preview) version or the published version.
// TODO (V18): Remove the default implementation on this method.
async Task> GetContentSourcesAsync(IEnumerable keys, bool preview = false)
{
diff --git a/src/Umbraco.PublishedCache.HybridCache/Services/DocumentCacheService.cs b/src/Umbraco.PublishedCache.HybridCache/Services/DocumentCacheService.cs
index b47e6c58c821..7a0e0f9e622c 100644
--- a/src/Umbraco.PublishedCache.HybridCache/Services/DocumentCacheService.cs
+++ b/src/Umbraco.PublishedCache.HybridCache/Services/DocumentCacheService.cs
@@ -180,13 +180,13 @@ public async Task RefreshMemoryCacheAsync(Guid key)
using ICoreScope scope = _scopeProvider.CreateCoreScope();
scope.ReadLock(Constants.Locks.ContentTree);
- ContentCacheNode? draftNode = await _databaseCacheRepository.GetContentSourceAsync(key, true);
+ (ContentCacheNode? draftNode, ContentCacheNode? publishedNode) = await _databaseCacheRepository.GetContentSourceForPublishStatesAsync(key);
+
if (draftNode is not null)
{
await _hybridCache.SetAsync(GetCacheKey(draftNode.Key, true), draftNode, GetEntryOptions(draftNode.Key, true), GenerateTags(key));
}
- ContentCacheNode? publishedNode = await _databaseCacheRepository.GetContentSourceAsync(key, false);
if (publishedNode is not null && _publishStatusQueryService.HasPublishedAncestorPath(publishedNode.Key))
{
var cacheKey = GetCacheKey(publishedNode.Key, false);
diff --git a/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheMockTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheMockTests.cs
index 1b683013802d..1f0831795fca 100644
--- a/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheMockTests.cs
+++ b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheMockTests.cs
@@ -87,6 +87,9 @@ public void SetUp()
_mockDatabaseCacheRepository.Setup(r => r.GetContentSourcesAsync(It.IsAny>(), false))
.ReturnsAsync([publishedTestCacheNode]);
+ _mockDatabaseCacheRepository.Setup(r => r.GetContentSourceForPublishStatesAsync(It.IsAny()))
+ .ReturnsAsync((draftTestCacheNode, publishedTestCacheNode));
+
_mockDatabaseCacheRepository.Setup(r => r.GetContentByContentTypeKey(It.IsAny>(), ContentCacheDataSerializerEntityType.Document)).Returns(
new List()
{
@@ -97,6 +100,7 @@ public void SetUp()
var mockedPublishedStatusService = new Mock();
mockedPublishedStatusService.Setup(x => x.IsDocumentPublishedInAnyCulture(It.IsAny())).Returns(true);
+ mockedPublishedStatusService.Setup(x => x.HasPublishedAncestorPath(It.IsAny())).Returns(true);
_documentCacheService = new DocumentCacheService(
_mockDatabaseCacheRepository.Object,
@@ -235,6 +239,47 @@ public async Task Content_Is_Not_Seeded_If_Unpublished_By_Key()
_mockDatabaseCacheRepository.Verify(x => x.GetContentSourceAsync(It.IsAny(), It.IsAny()), Times.Exactly(1));
}
+ [Test]
+ public async Task RefreshMemoryCache_Fetches_Draft_And_Published()
+ {
+ // Arrange
+ var hybridCache = GetRequiredService();
+
+ // Clear both draft and published cache entries.
+ await hybridCache.RemoveAsync($"{Textpage.Key}+draft");
+ await hybridCache.RemoveAsync($"{Textpage.Key}");
+
+ // Act
+ await _documentCacheService.RefreshMemoryCacheAsync(Textpage.Key);
+
+ // Assert - verify only a single call was made to the combined method for retrieving both states.
+ _mockDatabaseCacheRepository.Verify(
+ x => x.GetContentSourceForPublishStatesAsync(Textpage.Key),
+ Times.Exactly(1));
+
+ // Verify individual GetContentSourceAsync was NOT called
+ _mockDatabaseCacheRepository.Verify(
+ x => x.GetContentSourceAsync(It.IsAny(), It.IsAny()),
+ Times.Never);
+
+ // Verify content is now cached - fetching should not hit the repository again.
+ var draftPage = await _mockedCache.GetByIdAsync(Textpage.Key, true);
+ var publishedPage = await _mockedCache.GetByIdAsync(Textpage.Key, false);
+
+ Assert.IsNotNull(draftPage);
+ Assert.IsNotNull(publishedPage);
+ Assert.AreEqual(Textpage.Name, draftPage.Name);
+ Assert.AreEqual(Textpage.Name, publishedPage.Name);
+
+ // Verify no additional repository calls were made (content served from cache).
+ _mockDatabaseCacheRepository.Verify(
+ x => x.GetContentSourceAsync(It.IsAny(), It.IsAny()),
+ Times.Never);
+ _mockDatabaseCacheRepository.Verify(
+ x => x.GetContentSourceForPublishStatesAsync(It.IsAny()),
+ Times.Exactly(1));
+ }
+
private void AssertTextPage(IPublishedContent textPage)
{
Assert.Multiple(() =>