Skip to content

Commit 11784bb

Browse files
committed
Fixed route and url redirections inside route groups
1 parent 831cdb1 commit 11784bb

17 files changed

+218
-285
lines changed

src/Framework/Framework/Configuration/FreezableDictionary.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ public static void Freeze<K, V>([AllowNull] ref IDictionary<K, V> dict)
2323
}
2424
}
2525
}
26-
sealed class FreezableDictionary<K, V> : IDictionary<K, V>, IReadOnlyCollection<KeyValuePair<K, V>>
26+
sealed class FreezableDictionary<K, V> : IDictionary<K, V>, IReadOnlyCollection<KeyValuePair<K, V>>, IReadOnlyDictionary<K, V>
2727
where K : notnull
2828
{
2929
private readonly Dictionary<K, V> dict;
@@ -102,5 +102,9 @@ public V this[K index]
102102
public ICollection<K> Keys => ((IDictionary<K, V>)dict).Keys.ToArray();
103103

104104
public ICollection<V> Values => ((IDictionary<K, V>)dict).Values.ToArray();
105+
106+
IEnumerable<K> IReadOnlyDictionary<K, V>.Keys => Keys;
107+
108+
IEnumerable<V> IReadOnlyDictionary<K, V>.Values => Values;
105109
}
106110
}

src/Framework/Framework/Controls/RouteLinkHelpers.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ private static void EnsureValidBindingType(IBinding binding)
194194

195195
private static Dictionary<string, object?> ComposeNewRouteParameters(RouteLink control, IDotvvmRequestContext context, RouteBase route)
196196
{
197-
var parameters = new Dictionary<string, object?>(route.DefaultValues, StringComparer.OrdinalIgnoreCase);
197+
var parameters = route.CloneDefaultValues();
198198
foreach (var param in context.Parameters!)
199199
{
200200
parameters[param.Key] = param.Value;

src/Framework/Framework/ResourceManagement/LocalResourceUrlManager.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ public LocalResourceUrlManager(DotvvmConfiguration configuration, IResourceHashS
2828
this.resourceRoute = new DotvvmRoute(
2929
url: $"{HostingConstants.ResourceRouteName}/{{{HashParameterName}}}/{{{NameParameterName}:regex(.*)}}",
3030
virtualPath: "",
31+
name: $"_dotvvm_{nameof(LocalResourceUrlManager)}",
3132
defaultValues: null,
3233
presenterFactory: _ => throw new NotSupportedException(),
3334
configuration: configuration);

src/Framework/Framework/Routing/DefaultRouteStrategy.cs

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -107,10 +107,7 @@ protected virtual RouteBase BuildRoute(RouteStrategyMarkupFileInfo file)
107107
var defaultParameters = GetRouteDefaultParameters(file);
108108
var presenterFactory = GetRoutePresenterFactory(file);
109109

110-
return new DotvvmRoute(url, file.AppRelativePath, defaultParameters, presenterFactory, configuration)
111-
{
112-
RouteName = routeName
113-
};
110+
return new DotvvmRoute(url, file.AppRelativePath, routeName, defaultParameters, presenterFactory, configuration);
114111
}
115112

116113
protected virtual string GetRouteName(RouteStrategyMarkupFileInfo file)
@@ -162,7 +159,7 @@ private static IEnumerable<string> GetRoutesForFile(string fileName)
162159
protected override IEnumerable<RouteBase> BuildRoutes(RouteStrategyMarkupFileInfo file)
163160
{
164161
return getRouteList(file.AppRelativePath)
165-
.Select(url => new DotvvmRoute(url, file.AppRelativePath, GetRouteDefaultParameters(file), GetRoutePresenterFactory(file), this.configuration) { RouteName = url });
162+
.Select(url => new DotvvmRoute(url, file.AppRelativePath, url, GetRouteDefaultParameters(file), GetRoutePresenterFactory(file), this.configuration));
166163
}
167164
}
168165
}

src/Framework/Framework/Routing/DotvvmRoute.cs

Lines changed: 6 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,15 @@
11
using System;
22
using System.Collections.Generic;
33
using System.Linq;
4-
using System.Text;
5-
using System.Threading.Tasks;
64
using DotVVM.Framework.Hosting;
75
using System.Text.RegularExpressions;
86
using DotVVM.Framework.Configuration;
97
using System.Diagnostics.CodeAnalysis;
10-
using System.Globalization;
11-
using DotVVM.Framework.Utils;
128

139
namespace DotVVM.Framework.Routing
1410
{
1511
public sealed class DotvvmRoute : RouteBase
1612
{
17-
private Func<IServiceProvider,IDotvvmPresenter> presenterFactory;
18-
1913
private Regex routeRegex;
2014
private List<Func<Dictionary<string, string?>, string>> urlBuilders;
2115
private List<KeyValuePair<string, Func<string, ParameterParseResult>?>> parameters;
@@ -35,42 +29,26 @@ public sealed class DotvvmRoute : RouteBase
3529
/// <summary>
3630
/// Initializes a new instance of the <see cref="DotvvmRoute"/> class.
3731
/// </summary>
38-
#pragma warning disable CS8618
39-
public DotvvmRoute(string url, string virtualPath, object? defaultValues, Func<IServiceProvider, IDotvvmPresenter> presenterFactory, DotvvmConfiguration configuration)
40-
: base(url, virtualPath, defaultValues)
41-
{
42-
this.presenterFactory = presenterFactory;
43-
44-
ParseRouteUrl(configuration);
45-
}
46-
47-
/// <summary>
48-
/// Initializes a new instance of the <see cref="DotvvmRoute"/> class.
49-
/// </summary>
50-
public DotvvmRoute(string url, string virtualPath, IDictionary<string, object?>? defaultValues, Func<IServiceProvider, IDotvvmPresenter> presenterFactory, DotvvmConfiguration configuration)
51-
: base(url, virtualPath, defaultValues)
32+
public DotvvmRoute(string url, string? virtualPath, string name, object? defaultValues, Func<IServiceProvider, IDotvvmPresenter> presenterFactory, DotvvmConfiguration configuration)
33+
: base(url, virtualPath, name, defaultValues, presenterFactory)
5234
{
53-
this.presenterFactory = presenterFactory;
54-
5535
ParseRouteUrl(configuration);
5636
}
5737

5838
/// <summary>
5939
/// Initializes a new instance of the <see cref="DotvvmRoute"/> class.
6040
/// </summary>
61-
public DotvvmRoute(string url, string virtualPath, string name, IDictionary<string, object?>? defaultValues, Func<IServiceProvider, IDotvvmPresenter> presenterFactory, DotvvmConfiguration configuration)
62-
: base(url, virtualPath, name, defaultValues)
41+
public DotvvmRoute(string url, string? virtualPath, string name, IDictionary<string, object?>? defaultValues, Func<IServiceProvider, IDotvvmPresenter> presenterFactory, DotvvmConfiguration configuration)
42+
: base(url, virtualPath, name, defaultValues, presenterFactory)
6343
{
64-
this.presenterFactory = presenterFactory;
65-
6644
ParseRouteUrl(configuration);
6745
}
68-
#pragma warning restore CS8618
6946

7047

7148
/// <summary>
7249
/// Parses the route URL and extracts the components.
7350
/// </summary>
51+
[MemberNotNull(nameof(routeRegex), nameof(urlBuilders), nameof(parameters), nameof(parameterMetadata), nameof(urlWithoutTypes))]
7452
private void ParseRouteUrl(DotvvmConfiguration configuration)
7553
{
7654
var parser = new DotvvmRouteParser(configuration.RouteConstraints);
@@ -99,8 +77,7 @@ public override bool IsMatch(string url, [MaybeNullWhen(false)] out IDictionary<
9977
return false;
10078
}
10179

102-
values = new Dictionary<string, object?>(DefaultValues, StringComparer.OrdinalIgnoreCase);
103-
80+
values = CloneDefaultValues();
10481
foreach (var parameter in parameters)
10582
{
10683
var g = match.Groups["param" + parameter.Key];
@@ -155,10 +132,6 @@ protected internal override string BuildUrlCore(Dictionary<string, object?> valu
155132
}
156133
}
157134

158-
/// <summary>
159-
/// Processes the request.
160-
/// </summary>
161-
public override IDotvvmPresenter GetPresenter(IServiceProvider provider) => presenterFactory(provider);
162135
protected override void Freeze2()
163136
{
164137
// there is no property that would have to be frozen

src/Framework/Framework/Routing/DotvvmRouteParser.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ public DotvvmRouteParser(IDictionary<string, IRouteParameterConstraint> routeCon
1616
this.routeConstraints = routeConstrains;
1717
}
1818

19-
public UrlParserResult ParseRouteUrl(string url, IDictionary<string, object?> defaultValues)
19+
public UrlParserResult ParseRouteUrl(string url, IReadOnlyDictionary<string, object?> defaultValues)
2020
{
2121
if (url.StartsWith("/", StringComparison.Ordinal))
2222
throw new ArgumentException("The route URL must not start with '/'!");
@@ -85,7 +85,7 @@ void AppendParameterParserResult(UrlParameterParserResult result)
8585
};
8686
}
8787

88-
private UrlParameterParserResult ParseParameter(string url, string prefix, ref int index, IDictionary<string, object?> defaultValues)
88+
private UrlParameterParserResult ParseParameter(string url, string prefix, ref int index, IReadOnlyDictionary<string, object?> defaultValues)
8989
{
9090
// find the end of the route parameter name
9191
var startIndex = index;

src/Framework/Framework/Routing/DotvvmRouteTable.cs

Lines changed: 67 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -117,11 +117,8 @@ public IDotvvmPresenter GetDefaultPresenter(IServiceProvider provider)
117117
/// <param name="virtualPath">The virtual path of the Dothtml file.</param>
118118
/// <param name="defaultValues">The default values.</param>
119119
/// <param name="presenterFactory">Delegate creating the presenter handling this route</param>
120-
public void Add(string routeName, string? url, string virtualPath, object? defaultValues = null, Func<IServiceProvider, IDotvvmPresenter>? presenterFactory = null, LocalizedRouteUrl[]? localizedUrls = null)
120+
public void Add(string routeName, string url, string? virtualPath, object? defaultValues = null, Func<IServiceProvider, IDotvvmPresenter>? presenterFactory = null, LocalizedRouteUrl[]? localizedUrls = null)
121121
{
122-
ThrowIfFrozen();
123-
124-
virtualPath = CombinePath(group?.VirtualPathPrefix, virtualPath);
125122
AddCore(routeName, url, virtualPath, defaultValues, presenterFactory, localizedUrls);
126123
}
127124

@@ -132,26 +129,74 @@ public void Add(string routeName, string? url, string virtualPath, object? defau
132129
/// <param name="url">The URL.</param>
133130
/// <param name="defaultValues">The default values.</param>
134131
/// <param name="presenterFactory">The presenter factory.</param>
135-
public void Add(string routeName, string? url, Func<IServiceProvider, IDotvvmPresenter>? presenterFactory = null, object? defaultValues = null, LocalizedRouteUrl[]? localizedUrls = null)
132+
public void Add(string routeName, string url, Func<IServiceProvider, IDotvvmPresenter> presenterFactory, object? defaultValues = null, LocalizedRouteUrl[]? localizedUrls = null)
133+
{
134+
AddCore(routeName, url, virtualPath: null, defaultValues, presenterFactory, localizedUrls);
135+
}
136+
137+
/// <summary>
138+
/// Adds the specified route name.
139+
/// </summary>
140+
/// <param name="routeName">Name of the route.</param>
141+
/// <param name="url">The URL.</param>
142+
/// <param name="presenterType">The presenter factory.</param>
143+
/// <param name="defaultValues">The default values.</param>
144+
public void Add(string routeName, string url, Type presenterType, object? defaultValues = null, LocalizedRouteUrl[]? localizedUrls = null)
145+
{
146+
if (!typeof(IDotvvmPresenter).IsAssignableFrom(presenterType))
147+
{
148+
throw new ArgumentException($@"{nameof(presenterType)} has to inherit from DotVVM.Framework.Hosting.IDotvvmPresenter.", nameof(presenterType));
149+
}
150+
Func<IServiceProvider, IDotvvmPresenter> presenterFactory = provider => (IDotvvmPresenter)provider.GetRequiredService(presenterType);
151+
AddCore(routeName, url, virtualPath: null, defaultValues, presenterFactory, localizedUrls);
152+
}
153+
154+
/// <summary>
155+
/// Adds the specified name.
156+
/// </summary>
157+
public void Add(RouteBase route)
136158
{
137159
ThrowIfFrozen();
160+
if (dictionary.ContainsKey(route.RouteName))
161+
{
162+
throw new InvalidOperationException($"The route with name '{route.RouteName}' has already been registered!");
163+
}
138164

139-
var virtualPath = group?.VirtualPathPrefix ?? "";
140-
AddCore(routeName, url, virtualPath, defaultValues, presenterFactory, localizedUrls);
165+
group?.AddToParentRouteTable?.Invoke(route);
166+
167+
// The list is used for finding the routes because it keeps the ordering, the dictionary is for checking duplicates
168+
list.Add(new KeyValuePair<string, RouteBase>(route.RouteName, route));
169+
dictionary.Add(route.RouteName, route);
170+
171+
if (route is IPartialMatchRoute partialMatchRoute)
172+
{
173+
partialMatchRoutes.Add(partialMatchRoute);
174+
}
141175
}
142176

143-
private void AddCore(string routeName, string? url, string virtualPath, object? defaultValues, Func<IServiceProvider, IDotvvmPresenter>? presenterFactory, LocalizedRouteUrl[]? localizedUrls)
177+
private void AddCore(string routeName, string url, string? virtualPath, object? defaultValues, Func<IServiceProvider, IDotvvmPresenter>? presenterFactory, LocalizedRouteUrl[]? localizedUrls = null)
144178
{
179+
ThrowIfFrozen();
180+
181+
if (url == null)
182+
throw new ArgumentNullException(nameof(url));
145183
url = CombinePath(group?.UrlPrefix, url);
184+
185+
virtualPath = CombinePath(group?.VirtualPathPrefix, virtualPath);
186+
if (virtualPath == null && presenterFactory == null)
187+
{
188+
throw new ArgumentNullException(nameof(presenterFactory), "The presenterFactory argument must be set when virtualPath is null!");
189+
}
190+
146191
presenterFactory ??= GetDefaultPresenter;
147192
routeName = group?.RouteNamePrefix + routeName;
148193

149194
RouteBase route = localizedUrls == null
150-
? new DotvvmRoute(url, virtualPath, defaultValues, presenterFactory, configuration)
195+
? new DotvvmRoute(url, virtualPath, routeName, defaultValues, presenterFactory, configuration)
151196
: new LocalizedDotvvmRoute(url,
152197
localizedUrls.Select(l => new LocalizedRouteUrl(l.CultureIdentifier, CombinePath(group?.UrlPrefix, l.RouteUrl))).ToArray(),
153-
virtualPath, defaultValues, presenterFactory, configuration);
154-
Add(routeName, route);
198+
virtualPath, routeName, defaultValues, presenterFactory, configuration);
199+
Add(route);
155200
}
156201

157202
/// <summary>
@@ -171,7 +216,6 @@ public void AddUrlRedirection(string routeName, string urlPattern, string target
171216
/// <param name="targetUrlProvider">URL provider to obtain context-based redirection targets.</param>
172217
public void AddUrlRedirection(string routeName, string urlPattern, Func<IDotvvmRequestContext, string> targetUrlProvider, object? defaultValues = null, bool permanent = false)
173218
{
174-
ThrowIfFrozen();
175219
IDotvvmPresenter presenterFactory(IServiceProvider serviceProvider) => new DelegatePresenter(context =>
176220
{
177221
var targetUrl = targetUrlProvider(context);
@@ -181,7 +225,7 @@ public void AddUrlRedirection(string routeName, string urlPattern, Func<IDotvvmR
181225
else
182226
context.RedirectToUrl(targetUrl);
183227
});
184-
Add(routeName, new DotvvmRoute(urlPattern, string.Empty, defaultValues, presenterFactory, configuration));
228+
AddCore(routeName, urlPattern, virtualPath: null, defaultValues, presenterFactory);
185229
}
186230

187231
/// <summary>
@@ -207,7 +251,6 @@ public void AddRouteRedirection(string routeName, string urlPattern, Func<IDotvv
207251
object? defaultValues = null, bool permanent = false, Func<IDotvvmRequestContext, Dictionary<string, object?>>? parametersProvider = null,
208252
Func<IDotvvmRequestContext, string>? urlSuffixProvider = null)
209253
{
210-
ThrowIfFrozen();
211254
IDotvvmPresenter presenterFactory(IServiceProvider serviceProvider) => new DelegatePresenter(context =>
212255
{
213256
var targetRouteName = targetRouteNameProvider(context);
@@ -219,50 +262,7 @@ public void AddRouteRedirection(string routeName, string urlPattern, Func<IDotvv
219262
else
220263
context.RedirectToRoute(targetRouteName, newParameterValues, urlSuffix: urlSuffix);
221264
});
222-
Add(routeName, new DotvvmRoute(urlPattern, string.Empty, defaultValues, presenterFactory, configuration));
223-
}
224-
225-
/// <summary>
226-
/// Adds the specified route name.
227-
/// </summary>
228-
/// <param name="routeName">Name of the route.</param>
229-
/// <param name="url">The URL.</param>
230-
/// <param name="presenterType">The presenter factory.</param>
231-
/// <param name="defaultValues">The default values.</param>
232-
public void Add(string routeName, string? url, Type presenterType, object? defaultValues = null, LocalizedRouteUrl[]? localizedUrls = null)
233-
{
234-
ThrowIfFrozen();
235-
if (!typeof(IDotvvmPresenter).IsAssignableFrom(presenterType))
236-
{
237-
throw new ArgumentException($@"{nameof(presenterType)} has to inherit from DotVVM.Framework.Hosting.IDotvvmPresenter.", nameof(presenterType));
238-
}
239-
Func<IServiceProvider, IDotvvmPresenter> presenterFactory = provider => (IDotvvmPresenter)provider.GetRequiredService(presenterType);
240-
Add(routeName, url, presenterFactory, defaultValues, localizedUrls);
241-
}
242-
243-
/// <summary>
244-
/// Adds the specified name.
245-
/// </summary>
246-
public void Add(string routeName, RouteBase route)
247-
{
248-
ThrowIfFrozen();
249-
if (dictionary.ContainsKey(routeName))
250-
{
251-
throw new InvalidOperationException($"The route with name '{routeName}' has already been registered!");
252-
}
253-
// internal assign routename
254-
route.RouteName = routeName;
255-
256-
group?.AddToParentRouteTable?.Invoke(routeName, route);
257-
258-
// The list is used for finding the routes because it keeps the ordering, the dictionary is for checking duplicates
259-
list.Add(new KeyValuePair<string, RouteBase>(routeName, route));
260-
dictionary.Add(routeName, route);
261-
262-
if (route is IPartialMatchRoute partialMatchRoute)
263-
{
264-
partialMatchRoutes.Add(partialMatchRoute);
265-
}
265+
AddCore(routeName, urlPattern, virtualPath: null, defaultValues, presenterFactory);
266266
}
267267

268268
public void AddPartialMatchHandler(IPartialMatchRouteHandler handler)
@@ -309,16 +309,21 @@ IEnumerator IEnumerable.GetEnumerator()
309309
return GetEnumerator();
310310
}
311311

312-
private string CombinePath(string? prefix, string? appendedPath)
312+
[return: NotNullIfNotNull(nameof(appendedPath))]
313+
private string? CombinePath(string? prefix, string? appendedPath)
313314
{
314315
if (string.IsNullOrEmpty(prefix))
315316
{
316-
return appendedPath ?? "";
317+
return appendedPath;
317318
}
318319

319-
if (string.IsNullOrEmpty(appendedPath))
320+
if (appendedPath == null)
321+
{
322+
return null;
323+
}
324+
else if (appendedPath == string.Empty)
320325
{
321-
return prefix!;
326+
return prefix ?? string.Empty;
322327
}
323328

324329
return $"{prefix}/{appendedPath}";

0 commit comments

Comments
 (0)