Skip to content

Commit d7dbe39

Browse files
AndyButlandclaude
andauthored
Routing: Add DocumentUrlAliasService for optimized URL alias lookups (closes #21383) (#21396)
* Implement document alias cache and service to optimize content finder by alias. * Renamed to DocumentUrlAlias. Fixed issues on start-up. * Remove tracking of root ancestor. * Optimize cache key, tidy up tests, move domain matching to content finder. * Handle language and document deletes. * Align further with document URL service. * Code tidy. * Fixed comment. * Refactor scope handling to avoid nested scopes Extract CreateOrUpdateAliasesInternalAsync to process documents without creating their own scope. Both CreateOrUpdateAliasesAsync and CreateOrUpdateAliasesWithDescendantsAsync now create a single scope and call the internal method, avoiding unnecessary nested scope creation. Co-Authored-By: Claude Opus 4.5 <[email protected]> * Extract CreateOrUpdateAliasesInternalAsync to process documents without creating their own scope. * Only return a document for a match under a domain if the document is found under the domain of the current request. * Fix failing integration tests. * Apply suggestions from code review. * Ensured language to culture code map is updated when a language isn't found in the cached map. --------- Co-authored-by: Claude Opus 4.5 <[email protected]>
1 parent b2801e6 commit d7dbe39

24 files changed

Lines changed: 2357 additions & 109 deletions

src/Umbraco.Core/Cache/Refreshers/Implement/ContentCacheRefresher.cs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
using Microsoft.Extensions.DependencyInjection;
2+
using Umbraco.Cms.Core.DependencyInjection;
13
using Umbraco.Cms.Core.Events;
24
using Umbraco.Cms.Core.Models;
35
using Umbraco.Cms.Core.Notifications;
@@ -28,6 +30,7 @@ public sealed class ContentCacheRefresher : PayloadCacheRefresherBase<ContentCac
2830
private readonly IDomainService _domainService;
2931
private readonly IDomainCacheService _domainCacheService;
3032
private readonly IDocumentUrlService _documentUrlService;
33+
private readonly IDocumentUrlAliasService _documentUrlAliasService;
3134
private readonly IDocumentNavigationQueryService _documentNavigationQueryService;
3235
private readonly IDocumentNavigationManagementService _documentNavigationManagementService;
3336
private readonly IContentService _contentService;
@@ -39,6 +42,7 @@ public sealed class ContentCacheRefresher : PayloadCacheRefresherBase<ContentCac
3942
/// <summary>
4043
/// Initializes a new instance of the <see cref="ContentCacheRefresher"/> class.
4144
/// </summary>
45+
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 19.")]
4246
public ContentCacheRefresher(
4347
AppCaches appCaches,
4448
IJsonSerializer serializer,
@@ -54,12 +58,51 @@ public ContentCacheRefresher(
5458
IPublishStatusManagementService publishStatusManagementService,
5559
IDocumentCacheService documentCacheService,
5660
ICacheManager cacheManager)
61+
: this(
62+
appCaches,
63+
serializer,
64+
idKeyMap,
65+
domainService,
66+
eventAggregator,
67+
factory,
68+
documentUrlService,
69+
StaticServiceProvider.Instance.GetRequiredService<IDocumentUrlAliasService>(),
70+
domainCacheService,
71+
documentNavigationQueryService,
72+
documentNavigationManagementService,
73+
contentService,
74+
publishStatusManagementService,
75+
documentCacheService,
76+
cacheManager)
77+
{
78+
}
79+
80+
/// <summary>
81+
/// Initializes a new instance of the <see cref="ContentCacheRefresher"/> class.
82+
/// </summary>
83+
public ContentCacheRefresher(
84+
AppCaches appCaches,
85+
IJsonSerializer serializer,
86+
IIdKeyMap idKeyMap,
87+
IDomainService domainService,
88+
IEventAggregator eventAggregator,
89+
ICacheRefresherNotificationFactory factory,
90+
IDocumentUrlService documentUrlService,
91+
IDocumentUrlAliasService documentUrlAliasService,
92+
IDomainCacheService domainCacheService,
93+
IDocumentNavigationQueryService documentNavigationQueryService,
94+
IDocumentNavigationManagementService documentNavigationManagementService,
95+
IContentService contentService,
96+
IPublishStatusManagementService publishStatusManagementService,
97+
IDocumentCacheService documentCacheService,
98+
ICacheManager cacheManager)
5799
: base(appCaches, serializer, eventAggregator, factory)
58100
{
59101
_idKeyMap = idKeyMap;
60102
_domainService = domainService;
61103
_domainCacheService = domainCacheService;
62104
_documentUrlService = documentUrlService;
105+
_documentUrlAliasService = documentUrlAliasService;
63106
_documentNavigationQueryService = documentNavigationQueryService;
64107
_documentNavigationManagementService = documentNavigationManagementService;
65108
_contentService = contentService;
@@ -270,28 +313,33 @@ private void HandleRouting(JsonPayload payload)
270313
if (_documentNavigationQueryService.TryGetDescendantsKeysOrSelfKeys(key, out IEnumerable<Guid>? descendantsOrSelfKeys))
271314
{
272315
_documentUrlService.DeleteUrlsFromCacheAsync(descendantsOrSelfKeys).GetAwaiter().GetResult();
316+
_documentUrlAliasService.DeleteAliasesFromCacheAsync(descendantsOrSelfKeys).GetAwaiter().GetResult();
273317
}
274318
else if (_documentNavigationQueryService.TryGetDescendantsKeysOrSelfKeysInBin(key, out IEnumerable<Guid>? descendantsOrSelfKeysInBin))
275319
{
276320
_documentUrlService.DeleteUrlsFromCacheAsync(descendantsOrSelfKeysInBin).GetAwaiter().GetResult();
321+
_documentUrlAliasService.DeleteAliasesFromCacheAsync(descendantsOrSelfKeysInBin).GetAwaiter().GetResult();
277322
}
278323
}
279324

280325
if (payload.ChangeTypes.HasType(TreeChangeTypes.RefreshAll))
281326
{
282327
_documentUrlService.InitAsync(false, CancellationToken.None).GetAwaiter().GetResult(); // TODO: make async
328+
_documentUrlAliasService.InitAsync(false, CancellationToken.None).GetAwaiter().GetResult();
283329
}
284330

285331
if (payload.ChangeTypes.HasType(TreeChangeTypes.RefreshNode))
286332
{
287333
Guid key = payload.Key ?? _idKeyMap.GetKeyForId(payload.Id, UmbracoObjectTypes.Document).Result;
288334
_documentUrlService.CreateOrUpdateUrlSegmentsAsync(key).GetAwaiter().GetResult();
335+
_documentUrlAliasService.CreateOrUpdateAliasesAsync(key).GetAwaiter().GetResult();
289336
}
290337

291338
if (payload.ChangeTypes.HasType(TreeChangeTypes.RefreshBranch))
292339
{
293340
Guid key = payload.Key ?? _idKeyMap.GetKeyForId(payload.Id, UmbracoObjectTypes.Document).Result;
294341
_documentUrlService.CreateOrUpdateUrlSegmentsWithDescendantsAsync(key).GetAwaiter().GetResult();
342+
_documentUrlAliasService.CreateOrUpdateAliasesWithDescendantsAsync(key).GetAwaiter().GetResult();
295343
}
296344
}
297345

src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,8 @@ private void AddCoreServices()
439439
// Routing
440440
Services.AddUnique<IDocumentUrlService, DocumentUrlService>();
441441
Services.AddNotificationAsyncHandler<UmbracoApplicationStartingNotification, DocumentUrlServiceInitializerNotificationHandler>();
442+
Services.AddUnique<IDocumentUrlAliasService, DocumentUrlAliasService>();
443+
Services.AddNotificationAsyncHandler<UmbracoApplicationStartingNotification, DocumentUrlAliasServiceInitializerNotificationHandler>();
442444
}
443445
}
444446
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
using Umbraco.Cms.Core.Services;
2+
3+
namespace Umbraco.Cms.Core.Extensions;
4+
5+
/// <summary>
6+
/// Provides extension methods for <see cref="IDocumentUrlAliasService"/>.
7+
/// </summary>
8+
internal static class DocumentUrlAliasServiceExtensions
9+
{
10+
/// <summary>
11+
/// Normalizes a URL alias by trimming whitespace, removing leading/trailing slashes, and converting to lowercase.
12+
/// </summary>
13+
/// <param name="service">The <see cref="IDocumentUrlAliasService"/>.</param>
14+
/// <param name="alias">The alias to normalize.</param>
15+
/// <returns>The normalized alias.</returns>
16+
public static string NormalizeAlias(this IDocumentUrlAliasService service, string alias) =>
17+
alias
18+
.Trim()
19+
.TrimStart('/')
20+
.TrimEnd('/')
21+
.ToLowerInvariant();
22+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
namespace Umbraco.Cms.Core.Models;
2+
3+
/// <summary>
4+
/// Represents a URL alias for a published document.
5+
/// </summary>
6+
public class PublishedDocumentUrlAlias
7+
{
8+
/// <summary>
9+
/// Gets or sets the document key.
10+
/// </summary>
11+
public required Guid DocumentKey { get; set; }
12+
13+
/// <summary>
14+
/// Gets or sets the language Id.
15+
/// </summary>
16+
public required int LanguageId { get; set; }
17+
18+
/// <summary>
19+
/// Gets or sets the normalized URL alias (lowercase, no leading slash).
20+
/// </summary>
21+
public required string Alias { get; set; }
22+
}

src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ public static class Tables
5050
public const string DocumentCultureVariation = TableNamePrefix + "DocumentCultureVariation";
5151
public const string DocumentVersion = TableNamePrefix + "DocumentVersion";
5252
public const string DocumentUrl = TableNamePrefix + "DocumentUrl";
53+
public const string DocumentUrlAlias = TableNamePrefix + "DocumentUrlAlias";
5354
public const string MediaVersion = TableNamePrefix + "MediaVersion";
5455
public const string ContentSchedule = TableNamePrefix + "ContentSchedule";
5556

src/Umbraco.Core/Persistence/Constants-Locks.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,5 +100,10 @@ public static class Locks
100100
/// All distributed jobs.
101101
/// </summary>
102102
public const int DistributedJobs = -347;
103+
104+
/// <summary>
105+
/// All document URL aliases.
106+
/// </summary>
107+
public const int DocumentUrlAliases = -348;
103108
}
104109
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
using Umbraco.Cms.Core.Models;
2+
3+
namespace Umbraco.Cms.Core.Persistence.Repositories;
4+
5+
/// <summary>
6+
/// Repository for document URL aliases.
7+
/// </summary>
8+
public interface IDocumentUrlAliasRepository
9+
{
10+
/// <summary>
11+
/// Saves the specified aliases to the database.
12+
/// Handles insert/update/delete via diff - existing aliases not in the new set are deleted.
13+
/// </summary>
14+
/// <param name="aliases">The aliases to save.</param>
15+
void Save(IEnumerable<PublishedDocumentUrlAlias> aliases);
16+
17+
/// <summary>
18+
/// Gets all persisted aliases from the database.
19+
/// </summary>
20+
/// <returns>All persisted aliases.</returns>
21+
IEnumerable<PublishedDocumentUrlAlias> GetAll();
22+
23+
/// <summary>
24+
/// Deletes all aliases for the specified document keys.
25+
/// </summary>
26+
/// <param name="documentKeys">The document keys to delete aliases for.</param>
27+
void DeleteByDocumentKey(IEnumerable<Guid> documentKeys);
28+
29+
/// <summary>
30+
/// Gets all document aliases.
31+
/// </summary>
32+
/// <returns>Raw alias data from documents with umbracoUrlAlias property.</returns>
33+
IEnumerable<DocumentUrlAliasRaw> GetAllDocumentUrlAliases();
34+
}
35+
36+
/// <summary>
37+
/// Raw alias data from a direct SQL query.
38+
/// </summary>
39+
public class DocumentUrlAliasRaw
40+
{
41+
/// <summary>
42+
/// Gets or sets the document key.
43+
/// </summary>
44+
public Guid DocumentKey { get; set; }
45+
46+
/// <summary>
47+
/// Gets or sets the language ID (null for invariant).
48+
/// </summary>
49+
public int? LanguageId { get; set; }
50+
51+
/// <summary>
52+
/// Gets or sets the raw alias value (may be comma-separated).
53+
/// </summary>
54+
public string AliasValue { get; set; } = string.Empty;
55+
}

0 commit comments

Comments
 (0)