Skip to content

Commit 779b2f9

Browse files
authored
Merge pull request #1922 from riganti/fix/localizable-routes
Enhancements of localizable routes
2 parents d1141d5 + af544de commit 779b2f9

File tree

7 files changed

+167
-17
lines changed

7 files changed

+167
-17
lines changed

src/Framework/Framework/Hosting/Middlewares/DotvvmRoutingMiddleware.cs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System;
44
using System.Collections.Generic;
55
using System.Linq;
6+
using System.Net.Http;
67
using System.Reflection;
78
using System.Threading.Tasks;
89
using DotVVM.Framework.Runtime.Tracing;
@@ -40,9 +41,14 @@ private static bool TryParseGooglebotHashbangEscapedFragment(string queryString,
4041

4142
public static string GetRouteMatchUrl(IDotvvmRequestContext context)
4243
{
43-
if (!TryParseGooglebotHashbangEscapedFragment(context.HttpContext.Request.Url.Query, out var url))
44+
return GetRouteMatchUrl(context.HttpContext.Request.Path.Value!, context.HttpContext.Request.Url.Query);
45+
}
46+
47+
public static string GetRouteMatchUrl(string requestPath, string queryString)
48+
{
49+
if (!TryParseGooglebotHashbangEscapedFragment(queryString, out var url))
4450
{
45-
url = context.HttpContext.Request.Path.Value;
51+
url = requestPath;
4652
}
4753
url = url?.Trim('/') ?? "";
4854

src/Framework/Framework/Routing/DotvvmRouteTable.cs

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,8 @@ public void AddGroup(string groupName,
6464
string urlPrefix,
6565
string virtualPathPrefix,
6666
Action<DotvvmRouteTable> content,
67-
Func<IServiceProvider, IDotvvmPresenter>? presenterFactory = null)
67+
Func<IServiceProvider, IDotvvmPresenter>? presenterFactory = null,
68+
LocalizedRouteUrl[]? localizedUrls = null)
6869
{
6970
ThrowIfFrozen();
7071
if (string.IsNullOrEmpty(groupName))
@@ -77,6 +78,7 @@ public void AddGroup(string groupName,
7778
}
7879
urlPrefix = CombinePath(group?.UrlPrefix, urlPrefix);
7980
virtualPathPrefix = CombinePath(group?.VirtualPathPrefix, virtualPathPrefix);
81+
localizedUrls = CombineLocalizedUrls(group, urlPrefix, localizedUrls);
8082

8183
var newGroup = new DotvvmRouteTable(configuration);
8284
newGroup.group = new RouteTableGroup(
@@ -85,7 +87,8 @@ public void AddGroup(string groupName,
8587
urlPrefix,
8688
virtualPathPrefix,
8789
addToParentRouteTable: Add,
88-
presenterFactory);
90+
presenterFactory,
91+
localizedUrls);
8992

9093
content(newGroup);
9194
routeTableGroups.Add(groupName, newGroup);
@@ -180,6 +183,8 @@ private void AddCore(string routeName, string url, string? virtualPath, object?
180183

181184
if (url == null)
182185
throw new ArgumentNullException(nameof(url));
186+
187+
localizedUrls = CombineLocalizedUrls(group, url, localizedUrls);
183188
url = CombinePath(group?.UrlPrefix, url);
184189

185190
virtualPath = CombinePath(group?.VirtualPathPrefix, virtualPath);
@@ -194,7 +199,7 @@ private void AddCore(string routeName, string url, string? virtualPath, object?
194199
RouteBase route = localizedUrls == null
195200
? new DotvvmRoute(url, virtualPath, routeName, defaultValues, presenterFactory, configuration)
196201
: new LocalizedDotvvmRoute(url,
197-
localizedUrls.Select(l => new LocalizedRouteUrl(l.CultureIdentifier, CombinePath(group?.UrlPrefix, l.RouteUrl))).ToArray(),
202+
localizedUrls.Select(l => new LocalizedRouteUrl(l.CultureIdentifier, l.RouteUrl)).ToArray(),
198203
virtualPath, routeName, defaultValues, presenterFactory, configuration);
199204
Add(route);
200205
}
@@ -330,6 +335,29 @@ IEnumerator IEnumerable.GetEnumerator()
330335
return $"{prefix}/{appendedPath}";
331336
}
332337

338+
private LocalizedRouteUrl[]? CombineLocalizedUrls(RouteTableGroup? group, string routeUrl, LocalizedRouteUrl[]? localizedUrls)
339+
{
340+
if (group == null)
341+
{
342+
return localizedUrls;
343+
}
344+
if (group.LocalizedUrls == null && localizedUrls == null)
345+
{
346+
return null;
347+
}
348+
349+
var groupCultures = group.LocalizedUrls?.ToDictionary(u => u.CultureIdentifier, u => u.RouteUrl) ?? new();
350+
var routeCultures = localizedUrls?.ToDictionary(u => u.CultureIdentifier, u => u.RouteUrl) ?? new();
351+
352+
return groupCultures.Keys.Union(routeCultures.Keys)
353+
.Select(c =>
354+
new LocalizedRouteUrl(c, CombinePath(
355+
groupCultures.TryGetValue(c, out var localizedGroupUrl) ? localizedGroupUrl : group.UrlPrefix,
356+
routeCultures.TryGetValue(c, out var localizedRouteUrl) ? localizedRouteUrl : routeUrl)
357+
))
358+
.ToArray();
359+
}
360+
333361
private bool isFrozen = false;
334362

335363
private void ThrowIfFrozen()

src/Framework/Framework/Routing/LocalizedDotvvmRoute.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,9 +98,15 @@ public static void ValidateCultureName(string cultureIdentifier)
9898
public override bool IsMatch(string url, [MaybeNullWhen(false)] out IDictionary<string, object?> values) => GetRouteForCulture(CultureInfo.CurrentCulture).IsMatch(url, out values);
9999

100100
public bool IsPartialMatch(string url, [MaybeNullWhen(false)] out RouteBase matchedRoute, [MaybeNullWhen(false)] out IDictionary<string, object?> values)
101+
{
102+
return IsPartialMatch(url, out matchedRoute, out values, out _);
103+
}
104+
105+
public bool IsPartialMatch(string url, [MaybeNullWhen(false)] out RouteBase matchedRoute, [MaybeNullWhen(false)] out IDictionary<string, object?> values, [MaybeNullWhen(false)] out string? matchedCulture)
101106
{
102107
RouteBase? twoLetterCultureMatch = null;
103108
IDictionary<string, object?>? twoLetterCultureMatchValues = null;
109+
matchedCulture = null;
104110

105111
foreach (var route in localizedRoutes)
106112
{
@@ -110,13 +116,15 @@ public bool IsPartialMatch(string url, [MaybeNullWhen(false)] out RouteBase matc
110116
{
111117
// exact culture match - return immediately
112118
matchedRoute = route.Value;
119+
matchedCulture = route.Key;
113120
return true;
114121
}
115122
else if (route.Key.Length > 0 && twoLetterCultureMatch == null)
116123
{
117124
// match for two-letter culture - continue searching if there is a better match
118125
twoLetterCultureMatch = route.Value;
119126
twoLetterCultureMatchValues = values;
127+
matchedCulture = route.Key;
120128
}
121129
else
122130
{

src/Framework/Framework/Routing/RouteTableGroup.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Collections.Immutable;
34
using System.Text;
45
using DotVVM.Framework.Hosting;
56

@@ -15,15 +16,17 @@ public class RouteTableGroup
1516
public string RouteNamePrefix { get; private set; }
1617
public string UrlPrefix { get; private set; }
1718
public string VirtualPathPrefix { get; private set; }
19+
public ImmutableArray<LocalizedRouteUrl>? LocalizedUrls { get; private set; }
1820

19-
public RouteTableGroup(string groupName, string routeNamePrefix, string urlPrefix, string virtualPathPrefix, Action<RouteBase> addToParentRouteTable, Func<IServiceProvider, IDotvvmPresenter>? presenterFactory)
21+
public RouteTableGroup(string groupName, string routeNamePrefix, string urlPrefix, string virtualPathPrefix, Action<RouteBase> addToParentRouteTable, Func<IServiceProvider, IDotvvmPresenter>? presenterFactory, LocalizedRouteUrl[]? localizedUrls = null)
2022
{
2123
GroupName = groupName;
2224
RouteNamePrefix = routeNamePrefix;
2325
UrlPrefix = urlPrefix;
2426
VirtualPathPrefix = virtualPathPrefix;
2527
AddToParentRouteTable = addToParentRouteTable;
2628
PresenterFactory = presenterFactory;
29+
LocalizedUrls = localizedUrls?.ToImmutableArray();
2730
}
2831
}
2932
}

src/Framework/Hosting.AspNetCore/DotVVM.Framework.Hosting.AspNetCore.csproj

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,4 @@
2121
<ProjectReference Include="../Framework/DotVVM.Framework.csproj" />
2222
<ProjectReference Include="../Core/DotVVM.Core.csproj" />
2323
</ItemGroup>
24-
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.1'">
25-
<PackageReference Include="Microsoft.AspNetCore.Authentication.Cookies" Version="2.2.0" />
26-
<PackageReference Include="Microsoft.AspNetCore.DataProtection" Version="2.2.0" />
27-
<PackageReference Include="Microsoft.AspNetCore.Http.Extensions" Version="2.2.0" />
28-
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="2.2.0" />
29-
<PackageReference Include="Microsoft.AspNetCore.Localization" Version="2.2.0" />
30-
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="2.2.0" />
31-
<PackageReference Include="Microsoft.Extensions.Localization" Version="2.2.0" />
32-
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.HealthChecks" Version="2.2.0" />
33-
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions" Version="2.2.0" />
34-
</ItemGroup>
3524
</Project>
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
#nullable enable
2+
using System;
3+
using System.Collections;
4+
using System.Collections.Generic;
5+
using System.Linq;
6+
using System.Threading.Tasks;
7+
using DotVVM.Framework.Configuration;
8+
using DotVVM.Framework.Hosting.Middlewares;
9+
using DotVVM.Framework.Routing;
10+
using Microsoft.AspNetCore.Http;
11+
using Microsoft.AspNetCore.Localization;
12+
using Microsoft.Extensions.DependencyInjection;
13+
14+
namespace DotVVM.Framework.Hosting.AspNetCore.Localization;
15+
16+
public class DotvvmRoutingRequestCultureProvider : IRequestCultureProvider
17+
{
18+
private static readonly object locker = new();
19+
private static IReadOnlyList<LocalizedDotvvmRoute>? cachedRoutes;
20+
21+
public Task<ProviderCultureResult?> DetermineProviderCultureResult(HttpContext httpContext)
22+
{
23+
EnsureCachedRoutes(httpContext);
24+
25+
// find matching localizable route and extract culture from it
26+
var url = DotvvmRoutingMiddleware.GetRouteMatchUrl(httpContext.Request.Path.Value!, httpContext.Request.QueryString.Value!);
27+
foreach (var route in cachedRoutes!)
28+
{
29+
if (route.IsPartialMatch(url, out _, out var values, out var matchedCulture))
30+
{
31+
return Task.FromResult<ProviderCultureResult?>(new ProviderCultureResult(matchedCulture));
32+
}
33+
}
34+
35+
return Task.FromResult<ProviderCultureResult?>(null);
36+
}
37+
38+
private void EnsureCachedRoutes(HttpContext httpContext)
39+
{
40+
if (cachedRoutes == null)
41+
{
42+
lock (locker)
43+
{
44+
if (cachedRoutes == null)
45+
{
46+
// try to obtain DotVVM configuration
47+
if (httpContext.RequestServices.GetService<DotvvmConfiguration>() is not { } config)
48+
{
49+
throw new InvalidOperationException("DotVVM configuration not found in the service provider.");
50+
}
51+
52+
// try to obtain DotVVM routes
53+
cachedRoutes = config.RouteTable
54+
.OfType<LocalizedDotvvmRoute>()
55+
.ToArray();
56+
}
57+
}
58+
}
59+
}
60+
}

src/Tests/Routing/RouteTableGroupTests.cs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,62 @@ public void RouteTableGroup_DefaultPresenterFactory()
186186
Assert.IsInstanceOfType(table.First().GetPresenter(configuration.ServiceProvider), typeof(TestPresenter));
187187
}
188188

189+
[TestMethod]
190+
public void RouteTableGroup_LocalizedUrls_RouteOnly()
191+
{
192+
var table = new DotvvmRouteTable(configuration);
193+
table.AddGroup("Group", "group", "", opt => {
194+
opt.Add("Route", "route", "route.dothtml", null, null, [
195+
new LocalizedRouteUrl("cs-CZ", "cesta")
196+
]);
197+
});
198+
199+
var route = table["Group_Route"];
200+
Assert.IsInstanceOfType(route, typeof(LocalizedDotvvmRoute));
201+
var csCzRoute = ((LocalizedDotvvmRoute)route).GetRouteForCulture("cs-CZ");
202+
203+
Assert.AreEqual("group/route", route.Url);
204+
Assert.AreEqual("group/cesta", csCzRoute.Url);
205+
}
206+
207+
[TestMethod]
208+
public void RouteTableGroup_LocalizedUrls_GroupOnly()
209+
{
210+
var table = new DotvvmRouteTable(configuration);
211+
table.AddGroup("Group", "group", "", opt => {
212+
opt.Add("Route", "route", "route.dothtml", null, null, null);
213+
}, localizedUrls: [
214+
new LocalizedRouteUrl("cs-CZ", "skupina")
215+
]);
216+
217+
var route = table["Group_Route"];
218+
Assert.IsInstanceOfType(route, typeof(LocalizedDotvvmRoute));
219+
var csCzRoute = ((LocalizedDotvvmRoute)route).GetRouteForCulture("cs-CZ");
220+
221+
Assert.AreEqual("group/route", route.Url);
222+
Assert.AreEqual("skupina/route", csCzRoute.Url);
223+
}
224+
225+
[TestMethod]
226+
public void RouteTableGroup_LocalizedUrls()
227+
{
228+
var table = new DotvvmRouteTable(configuration);
229+
table.AddGroup("Group", "group", "", opt => {
230+
opt.Add("Route", "route", "route.dothtml", null, null, [
231+
new LocalizedRouteUrl("cs-CZ", "cesta")
232+
]);
233+
}, localizedUrls: [
234+
new LocalizedRouteUrl("cs-CZ", "skupina")
235+
]);
236+
237+
var route = table["Group_Route"];
238+
Assert.IsInstanceOfType(route, typeof(LocalizedDotvvmRoute));
239+
var csCzRoute = ((LocalizedDotvvmRoute)route).GetRouteForCulture("cs-CZ");
240+
241+
Assert.AreEqual("group/route", route.Url);
242+
Assert.AreEqual("skupina/cesta", csCzRoute.Url);
243+
}
244+
189245
[TestMethod]
190246
public void RouteTableGroup_Redirections()
191247
{

0 commit comments

Comments
 (0)