[Blazor] Fixes issues with route precedence (#27907)
Description
In 5.0 we introduced two features on Blazor routing that enable users to write routing templates that match paths with variable length segments. These two features are optional parameters {parameter?} and catch all parameters {*catchall}.
Our routing system ordered the routes based on precedence and the (now false) assumption that route templates would only match paths with an equal number of segments.
The implementation that we have worked for naïve scenarios but breaks on more real world scenarios. The change here includes fixes to the way we order the routes in the route table to match the expectations as well as fixes on the route matching algorithm to ensure we match routes with variable number of segments correctly.
Customer Impact
This was reported by customers on #27250
The impact is that a route with {*catchall} will prevent more specific routes like /page/{parameter} from being accessible.
There are no workarounds since precedence is a fundamental behavior of the routing system.
Regression?
No, these Blazor features were initially added in 5.0.
Risk
Low. These two features were just introduced in 5.0 and their usage is not as prevalent as in asp.net core routing. That said, it's important to fix them as otherwise we run the risk of diverting in behavior from asp.net core routing and Blazor routing, which is not something we want to do.
We have functional tests covering the area and we've added a significant amount of unit tests to validate the changes.
This commit is contained in:
parent
d14b644036
commit
6bb4a3f370
|
|
@ -1 +1,3 @@
|
|||
#nullable enable
|
||||
Microsoft.AspNetCore.Components.Routing.Router.PreferExactMatches.get -> bool
|
||||
Microsoft.AspNetCore.Components.Routing.Router.PreferExactMatches.set -> void
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.Routing
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides an abstraction over <see cref="RouteTable"/> and <see cref="LegacyRouteMatching.LegacyRouteTable"/>.
|
||||
/// This is only an internal implementation detail of <see cref="Router"/> and can be removed once
|
||||
/// the legacy route matching logic is removed.
|
||||
/// </summary>
|
||||
internal interface IRouteTable
|
||||
{
|
||||
void Route(RouteContext routeContext);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,20 +1,20 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.Routing
|
||||
namespace Microsoft.AspNetCore.Components.LegacyRouteMatching
|
||||
{
|
||||
/// <summary>
|
||||
/// A route constraint that allows the value to be null or parseable as the specified
|
||||
/// type.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type to which the value must be parseable.</typeparam>
|
||||
internal class OptionalTypeRouteConstraint<T> : RouteConstraint
|
||||
internal class LegacyOptionalTypeRouteConstraint<T> : LegacyRouteConstraint
|
||||
{
|
||||
public delegate bool TryParseDelegate(string str, out T result);
|
||||
public delegate bool LegacyTryParseDelegate(string str, out T result);
|
||||
|
||||
private readonly TryParseDelegate _parser;
|
||||
private readonly LegacyTryParseDelegate _parser;
|
||||
|
||||
public OptionalTypeRouteConstraint(TryParseDelegate parser)
|
||||
public LegacyOptionalTypeRouteConstraint(LegacyTryParseDelegate parser)
|
||||
{
|
||||
_parser = parser;
|
||||
}
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Globalization;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.LegacyRouteMatching
|
||||
{
|
||||
internal abstract class LegacyRouteConstraint
|
||||
{
|
||||
// note: the things that prevent this cache from growing unbounded is that
|
||||
// we're the only caller to this code path, and the fact that there are only
|
||||
// 8 possible instances that we create.
|
||||
//
|
||||
// The values passed in here for parsing are always static text defined in route attributes.
|
||||
private static readonly ConcurrentDictionary<string, LegacyRouteConstraint> _cachedConstraints
|
||||
= new ConcurrentDictionary<string, LegacyRouteConstraint>();
|
||||
|
||||
public abstract bool Match(string pathSegment, out object? convertedValue);
|
||||
|
||||
public static LegacyRouteConstraint Parse(string template, string segment, string constraint)
|
||||
{
|
||||
if (string.IsNullOrEmpty(constraint))
|
||||
{
|
||||
throw new ArgumentException($"Malformed segment '{segment}' in route '{template}' contains an empty constraint.");
|
||||
}
|
||||
|
||||
if (_cachedConstraints.TryGetValue(constraint, out var cachedInstance))
|
||||
{
|
||||
return cachedInstance;
|
||||
}
|
||||
else
|
||||
{
|
||||
var newInstance = CreateRouteConstraint(constraint);
|
||||
if (newInstance != null)
|
||||
{
|
||||
// We've done to the work to create the constraint now, but it's possible
|
||||
// we're competing with another thread. GetOrAdd can ensure only a single
|
||||
// instance is returned so that any extra ones can be GC'ed.
|
||||
return _cachedConstraints.GetOrAdd(constraint, newInstance);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ArgumentException($"Unsupported constraint '{constraint}' in route '{template}'.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a structured RouteConstraint object given a string that contains
|
||||
/// the route constraint. A constraint is the place after the colon in a
|
||||
/// parameter definition, for example `{age:int?}`.
|
||||
///
|
||||
/// If the constraint denotes an optional, this method will return an
|
||||
/// <see cref="LegacyOptionalTypeRouteConstraint{T}" /> which handles the appropriate checks.
|
||||
/// </summary>
|
||||
/// <param name="constraint">String representation of the constraint</param>
|
||||
/// <returns>Type-specific RouteConstraint object</returns>
|
||||
private static LegacyRouteConstraint? CreateRouteConstraint(string constraint)
|
||||
{
|
||||
switch (constraint)
|
||||
{
|
||||
case "bool":
|
||||
return new LegacyTypeRouteConstraint<bool>(bool.TryParse);
|
||||
case "bool?":
|
||||
return new LegacyOptionalTypeRouteConstraint<bool>(bool.TryParse);
|
||||
case "datetime":
|
||||
return new LegacyTypeRouteConstraint<DateTime>((string str, out DateTime result)
|
||||
=> DateTime.TryParse(str, CultureInfo.InvariantCulture, DateTimeStyles.None, out result));
|
||||
case "datetime?":
|
||||
return new LegacyOptionalTypeRouteConstraint<DateTime>((string str, out DateTime result)
|
||||
=> DateTime.TryParse(str, CultureInfo.InvariantCulture, DateTimeStyles.None, out result));
|
||||
case "decimal":
|
||||
return new LegacyTypeRouteConstraint<decimal>((string str, out decimal result)
|
||||
=> decimal.TryParse(str, NumberStyles.Number, CultureInfo.InvariantCulture, out result));
|
||||
case "decimal?":
|
||||
return new LegacyOptionalTypeRouteConstraint<decimal>((string str, out decimal result)
|
||||
=> decimal.TryParse(str, NumberStyles.Number, CultureInfo.InvariantCulture, out result));
|
||||
case "double":
|
||||
return new LegacyTypeRouteConstraint<double>((string str, out double result)
|
||||
=> double.TryParse(str, NumberStyles.Number, CultureInfo.InvariantCulture, out result));
|
||||
case "double?":
|
||||
return new LegacyOptionalTypeRouteConstraint<double>((string str, out double result)
|
||||
=> double.TryParse(str, NumberStyles.Number, CultureInfo.InvariantCulture, out result));
|
||||
case "float":
|
||||
return new LegacyTypeRouteConstraint<float>((string str, out float result)
|
||||
=> float.TryParse(str, NumberStyles.Number, CultureInfo.InvariantCulture, out result));
|
||||
case "float?":
|
||||
return new LegacyOptionalTypeRouteConstraint<float>((string str, out float result)
|
||||
=> float.TryParse(str, NumberStyles.Number, CultureInfo.InvariantCulture, out result));
|
||||
case "guid":
|
||||
return new LegacyTypeRouteConstraint<Guid>(Guid.TryParse);
|
||||
case "guid?":
|
||||
return new LegacyOptionalTypeRouteConstraint<Guid>(Guid.TryParse);
|
||||
case "int":
|
||||
return new LegacyTypeRouteConstraint<int>((string str, out int result)
|
||||
=> int.TryParse(str, NumberStyles.Integer, CultureInfo.InvariantCulture, out result));
|
||||
case "int?":
|
||||
return new LegacyOptionalTypeRouteConstraint<int>((string str, out int result)
|
||||
=> int.TryParse(str, NumberStyles.Integer, CultureInfo.InvariantCulture, out result));
|
||||
case "long":
|
||||
return new LegacyTypeRouteConstraint<long>((string str, out long result)
|
||||
=> long.TryParse(str, NumberStyles.Integer, CultureInfo.InvariantCulture, out result));
|
||||
case "long?":
|
||||
return new LegacyOptionalTypeRouteConstraint<long>((string str, out long result)
|
||||
=> long.TryParse(str, NumberStyles.Integer, CultureInfo.InvariantCulture, out result));
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,146 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
#nullable disable warnings
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
|
||||
// Avoid referencing the whole Microsoft.AspNetCore.Components.Routing namespace to
|
||||
// avoid the risk of accidentally relying on the non-legacy types in the legacy fork
|
||||
using RouteContext = Microsoft.AspNetCore.Components.Routing.RouteContext;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.LegacyRouteMatching
|
||||
{
|
||||
[DebuggerDisplay("Handler = {Handler}, Template = {Template}")]
|
||||
internal class LegacyRouteEntry
|
||||
{
|
||||
public LegacyRouteEntry(LegacyRouteTemplate template, Type handler, string[] unusedRouteParameterNames)
|
||||
{
|
||||
Template = template;
|
||||
UnusedRouteParameterNames = unusedRouteParameterNames;
|
||||
Handler = handler;
|
||||
}
|
||||
|
||||
public LegacyRouteTemplate Template { get; }
|
||||
|
||||
public string[] UnusedRouteParameterNames { get; }
|
||||
|
||||
public Type Handler { get; }
|
||||
|
||||
internal void Match(RouteContext context)
|
||||
{
|
||||
string? catchAllValue = null;
|
||||
|
||||
// If this template contains a catch-all parameter, we can concatenate the pathSegments
|
||||
// at and beyond the catch-all segment's position. For example:
|
||||
// Template: /foo/bar/{*catchAll}
|
||||
// PathSegments: /foo/bar/one/two/three
|
||||
if (Template.ContainsCatchAllSegment && context.Segments.Length >= Template.Segments.Length)
|
||||
{
|
||||
catchAllValue = string.Join('/', context.Segments[Range.StartAt(Template.Segments.Length - 1)]);
|
||||
}
|
||||
// If there are no optional segments on the route and the length of the route
|
||||
// and the template do not match, then there is no chance of this matching and
|
||||
// we can bail early.
|
||||
else if (Template.OptionalSegmentsCount == 0 && Template.Segments.Length != context.Segments.Length)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Parameters will be lazily initialized.
|
||||
Dictionary<string, object> parameters = null;
|
||||
var numMatchingSegments = 0;
|
||||
for (var i = 0; i < Template.Segments.Length; i++)
|
||||
{
|
||||
var segment = Template.Segments[i];
|
||||
|
||||
if (segment.IsCatchAll)
|
||||
{
|
||||
numMatchingSegments += 1;
|
||||
parameters ??= new Dictionary<string, object>(StringComparer.Ordinal);
|
||||
parameters[segment.Value] = catchAllValue;
|
||||
break;
|
||||
}
|
||||
|
||||
// If the template contains more segments than the path, then
|
||||
// we may need to break out of this for-loop. This can happen
|
||||
// in one of two cases:
|
||||
//
|
||||
// (1) If we are comparing a literal route with a literal template
|
||||
// and the route is shorter than the template.
|
||||
// (2) If we are comparing a template where the last value is an optional
|
||||
// parameter that the route does not provide.
|
||||
if (i >= context.Segments.Length)
|
||||
{
|
||||
// If we are under condition (1) above then we can stop evaluating
|
||||
// matches on the rest of this template.
|
||||
if (!segment.IsParameter && !segment.IsOptional)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
string pathSegment = null;
|
||||
if (i < context.Segments.Length)
|
||||
{
|
||||
pathSegment = context.Segments[i];
|
||||
}
|
||||
|
||||
if (!segment.Match(pathSegment, out var matchedParameterValue))
|
||||
{
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
numMatchingSegments++;
|
||||
if (segment.IsParameter)
|
||||
{
|
||||
parameters ??= new Dictionary<string, object>(StringComparer.Ordinal);
|
||||
parameters[segment.Value] = matchedParameterValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// In addition to extracting parameter values from the URL, each route entry
|
||||
// also knows which other parameters should be supplied with null values. These
|
||||
// are parameters supplied by other route entries matching the same handler.
|
||||
if (!Template.ContainsCatchAllSegment && UnusedRouteParameterNames.Length > 0)
|
||||
{
|
||||
parameters ??= new Dictionary<string, object>(StringComparer.Ordinal);
|
||||
for (var i = 0; i < UnusedRouteParameterNames.Length; i++)
|
||||
{
|
||||
parameters[UnusedRouteParameterNames[i]] = null;
|
||||
}
|
||||
}
|
||||
|
||||
// We track the number of segments in the template that matched
|
||||
// against this particular route then only select the route that
|
||||
// matches the most number of segments on the route that was passed.
|
||||
// This check is an exactness check that favors the more precise of
|
||||
// two templates in the event that the following route table exists.
|
||||
// Route 1: /{anythingGoes}
|
||||
// Route 2: /users/{id:int}
|
||||
// And the provided route is `/users/1`. We want to choose Route 2
|
||||
// over Route 1.
|
||||
// Furthermore, literal routes are preferred over parameterized routes.
|
||||
// If the two routes below are registered in the route table.
|
||||
// Route 1: /users/1
|
||||
// Route 2: /users/{id:int}
|
||||
// And the provided route is `/users/1`. We want to choose Route 1 over
|
||||
// Route 2.
|
||||
var allRouteSegmentsMatch = numMatchingSegments >= context.Segments.Length;
|
||||
// Checking that all route segments have been matches does not suffice if we are
|
||||
// comparing literal templates with literal routes. For example, the template
|
||||
// `/this/is/a/template` and the route `/this/`. In that case, we want to ensure
|
||||
// that all non-optional segments have matched as well.
|
||||
var allNonOptionalSegmentsMatch = numMatchingSegments >= (Template.Segments.Length - Template.OptionalSegmentsCount);
|
||||
if (Template.ContainsCatchAllSegment || (allRouteSegmentsMatch && allNonOptionalSegmentsMatch))
|
||||
{
|
||||
context.Parameters = parameters;
|
||||
context.Handler = Handler;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
// Avoid referencing the whole Microsoft.AspNetCore.Components.Routing namespace to
|
||||
// avoid the risk of accidentally relying on the non-legacy types in the legacy fork
|
||||
using RouteContext = Microsoft.AspNetCore.Components.Routing.RouteContext;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.LegacyRouteMatching
|
||||
{
|
||||
internal class LegacyRouteTable : Routing.IRouteTable
|
||||
{
|
||||
public LegacyRouteTable(LegacyRouteEntry[] routes)
|
||||
{
|
||||
Routes = routes;
|
||||
}
|
||||
|
||||
public LegacyRouteEntry[] Routes { get; }
|
||||
|
||||
public void Route(RouteContext routeContext)
|
||||
{
|
||||
for (var i = 0; i < Routes.Length; i++)
|
||||
{
|
||||
Routes[i].Match(routeContext);
|
||||
if (routeContext.Handler != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,236 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.LegacyRouteMatching
|
||||
{
|
||||
/// <summary>
|
||||
/// Resolves components for an application.
|
||||
/// </summary>
|
||||
internal static class LegacyRouteTableFactory
|
||||
{
|
||||
private static readonly ConcurrentDictionary<Key, LegacyRouteTable> Cache =
|
||||
new ConcurrentDictionary<Key, LegacyRouteTable>();
|
||||
public static readonly IComparer<LegacyRouteEntry> RoutePrecedence = Comparer<LegacyRouteEntry>.Create(RouteComparison);
|
||||
|
||||
public static LegacyRouteTable Create(IEnumerable<Assembly> assemblies)
|
||||
{
|
||||
var key = new Key(assemblies.OrderBy(a => a.FullName).ToArray());
|
||||
if (Cache.TryGetValue(key, out var resolvedComponents))
|
||||
{
|
||||
return resolvedComponents;
|
||||
}
|
||||
|
||||
var componentTypes = key.Assemblies.SelectMany(a => a.ExportedTypes.Where(t => typeof(IComponent).IsAssignableFrom(t)));
|
||||
var routeTable = Create(componentTypes);
|
||||
Cache.TryAdd(key, routeTable);
|
||||
return routeTable;
|
||||
}
|
||||
|
||||
internal static LegacyRouteTable Create(IEnumerable<Type> componentTypes)
|
||||
{
|
||||
var templatesByHandler = new Dictionary<Type, string[]>();
|
||||
foreach (var componentType in componentTypes)
|
||||
{
|
||||
// We're deliberately using inherit = false here.
|
||||
//
|
||||
// RouteAttribute is defined as non-inherited, because inheriting a route attribute always causes an
|
||||
// ambiguity. You end up with two components (base class and derived class) with the same route.
|
||||
var routeAttributes = componentType.GetCustomAttributes<RouteAttribute>(inherit: false);
|
||||
|
||||
var templates = routeAttributes.Select(t => t.Template).ToArray();
|
||||
templatesByHandler.Add(componentType, templates);
|
||||
}
|
||||
return Create(templatesByHandler);
|
||||
}
|
||||
|
||||
internal static LegacyRouteTable Create(Dictionary<Type, string[]> templatesByHandler)
|
||||
{
|
||||
var routes = new List<LegacyRouteEntry>();
|
||||
foreach (var keyValuePair in templatesByHandler)
|
||||
{
|
||||
var parsedTemplates = keyValuePair.Value.Select(v => LegacyTemplateParser.ParseTemplate(v)).ToArray();
|
||||
var allRouteParameterNames = parsedTemplates
|
||||
.SelectMany(GetParameterNames)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
|
||||
foreach (var parsedTemplate in parsedTemplates)
|
||||
{
|
||||
var unusedRouteParameterNames = allRouteParameterNames
|
||||
.Except(GetParameterNames(parsedTemplate), StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
var entry = new LegacyRouteEntry(parsedTemplate, keyValuePair.Key, unusedRouteParameterNames);
|
||||
routes.Add(entry);
|
||||
}
|
||||
}
|
||||
|
||||
return new LegacyRouteTable(routes.OrderBy(id => id, RoutePrecedence).ToArray());
|
||||
}
|
||||
|
||||
private static string[] GetParameterNames(LegacyRouteTemplate routeTemplate)
|
||||
{
|
||||
return routeTemplate.Segments
|
||||
.Where(s => s.IsParameter)
|
||||
.Select(s => s.Value)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Route precedence algorithm.
|
||||
/// We collect all the routes and sort them from most specific to
|
||||
/// less specific. The specificity of a route is given by the specificity
|
||||
/// of its segments and the position of those segments in the route.
|
||||
/// * A literal segment is more specific than a parameter segment.
|
||||
/// * A parameter segment with more constraints is more specific than one with fewer constraints
|
||||
/// * Segment earlier in the route are evaluated before segments later in the route.
|
||||
/// For example:
|
||||
/// /Literal is more specific than /Parameter
|
||||
/// /Route/With/{parameter} is more specific than /{multiple}/With/{parameters}
|
||||
/// /Product/{id:int} is more specific than /Product/{id}
|
||||
///
|
||||
/// Routes can be ambiguous if:
|
||||
/// They are composed of literals and those literals have the same values (case insensitive)
|
||||
/// They are composed of a mix of literals and parameters, in the same relative order and the
|
||||
/// literals have the same values.
|
||||
/// For example:
|
||||
/// * /literal and /Literal
|
||||
/// /{parameter}/literal and /{something}/literal
|
||||
/// /{parameter:constraint}/literal and /{something:constraint}/literal
|
||||
///
|
||||
/// To calculate the precedence we sort the list of routes as follows:
|
||||
/// * Shorter routes go first.
|
||||
/// * A literal wins over a parameter in precedence.
|
||||
/// * For literals with different values (case insensitive) we choose the lexical order
|
||||
/// * For parameters with different numbers of constraints, the one with more wins
|
||||
/// If we get to the end of the comparison routing we've detected an ambiguous pair of routes.
|
||||
/// </summary>
|
||||
internal static int RouteComparison(LegacyRouteEntry x, LegacyRouteEntry y)
|
||||
{
|
||||
if (ReferenceEquals(x, y))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var xTemplate = x.Template;
|
||||
var yTemplate = y.Template;
|
||||
if (xTemplate.Segments.Length != y.Template.Segments.Length)
|
||||
{
|
||||
return xTemplate.Segments.Length < y.Template.Segments.Length ? -1 : 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
for (var i = 0; i < xTemplate.Segments.Length; i++)
|
||||
{
|
||||
var xSegment = xTemplate.Segments[i];
|
||||
var ySegment = yTemplate.Segments[i];
|
||||
if (!xSegment.IsParameter && ySegment.IsParameter)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
if (xSegment.IsParameter && !ySegment.IsParameter)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (xSegment.IsParameter)
|
||||
{
|
||||
// Always favor non-optional parameters over optional ones
|
||||
if (!xSegment.IsOptional && ySegment.IsOptional)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (xSegment.IsOptional && !ySegment.IsOptional)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (xSegment.Constraints.Length > ySegment.Constraints.Length)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
else if (xSegment.Constraints.Length < ySegment.Constraints.Length)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var comparison = string.Compare(xSegment.Value, ySegment.Value, StringComparison.OrdinalIgnoreCase);
|
||||
if (comparison != 0)
|
||||
{
|
||||
return comparison;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($@"The following routes are ambiguous:
|
||||
'{x.Template.TemplateText}' in '{x.Handler.FullName}'
|
||||
'{y.Template.TemplateText}' in '{y.Handler.FullName}'
|
||||
");
|
||||
}
|
||||
}
|
||||
|
||||
private readonly struct Key : IEquatable<Key>
|
||||
{
|
||||
public readonly Assembly[] Assemblies;
|
||||
|
||||
public Key(Assembly[] assemblies)
|
||||
{
|
||||
Assemblies = assemblies;
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return obj is Key other ? base.Equals(other) : false;
|
||||
}
|
||||
|
||||
public bool Equals(Key other)
|
||||
{
|
||||
if (Assemblies == null && other.Assemblies == null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else if ((Assemblies == null) || (other.Assemblies == null))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
else if (Assemblies.Length != other.Assemblies.Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (var i = 0; i < Assemblies.Length; i++)
|
||||
{
|
||||
if (!Assemblies[i].Equals(other.Assemblies[i]))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
var hash = new HashCode();
|
||||
|
||||
if (Assemblies != null)
|
||||
{
|
||||
for (var i = 0; i < Assemblies.Length; i++)
|
||||
{
|
||||
hash.Add(Assemblies[i]);
|
||||
}
|
||||
}
|
||||
|
||||
return hash.ToHashCode();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.LegacyRouteMatching
|
||||
{
|
||||
[DebuggerDisplay("{TemplateText}")]
|
||||
internal class LegacyRouteTemplate
|
||||
{
|
||||
public LegacyRouteTemplate(string templateText, LegacyTemplateSegment[] segments)
|
||||
{
|
||||
TemplateText = templateText;
|
||||
Segments = segments;
|
||||
OptionalSegmentsCount = segments.Count(template => template.IsOptional);
|
||||
ContainsCatchAllSegment = segments.Any(template => template.IsCatchAll);
|
||||
}
|
||||
|
||||
public string TemplateText { get; }
|
||||
|
||||
public LegacyTemplateSegment[] Segments { get; }
|
||||
|
||||
public int OptionalSegmentsCount { get; }
|
||||
|
||||
public bool ContainsCatchAllSegment { get; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,115 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.LegacyRouteMatching
|
||||
{
|
||||
// This implementation is temporary, in the future we'll want to have
|
||||
// a more performant/properly designed routing set of abstractions.
|
||||
// To be more precise these are some things we are scoping out:
|
||||
// * We are not doing link generation.
|
||||
// * We are not supporting all the route constraint formats supported by ASP.NET server-side routing.
|
||||
// The class in here just takes care of parsing a route and extracting
|
||||
// simple parameters from it.
|
||||
// Some differences with ASP.NET Core routes are:
|
||||
// * We don't support complex segments.
|
||||
// The things that we support are:
|
||||
// * Literal path segments. (Like /Path/To/Some/Page)
|
||||
// * Parameter path segments (Like /Customer/{Id}/Orders/{OrderId})
|
||||
// * Catch-all parameters (Like /blog/{*slug})
|
||||
internal class LegacyTemplateParser
|
||||
{
|
||||
public static readonly char[] InvalidParameterNameCharacters =
|
||||
new char[] { '{', '}', '=', '.' };
|
||||
|
||||
internal static LegacyRouteTemplate ParseTemplate(string template)
|
||||
{
|
||||
var originalTemplate = template;
|
||||
template = template.Trim('/');
|
||||
if (template == string.Empty)
|
||||
{
|
||||
// Special case "/";
|
||||
return new LegacyRouteTemplate("/", Array.Empty<LegacyTemplateSegment>());
|
||||
}
|
||||
|
||||
var segments = template.Split('/');
|
||||
var templateSegments = new LegacyTemplateSegment[segments.Length];
|
||||
for (int i = 0; i < segments.Length; i++)
|
||||
{
|
||||
var segment = segments[i];
|
||||
if (string.IsNullOrEmpty(segment))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Invalid template '{template}'. Empty segments are not allowed.");
|
||||
}
|
||||
|
||||
if (segment[0] != '{')
|
||||
{
|
||||
if (segment[segment.Length - 1] == '}')
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Invalid template '{template}'. Missing '{{' in parameter segment '{segment}'.");
|
||||
}
|
||||
templateSegments[i] = new LegacyTemplateSegment(originalTemplate, segment, isParameter: false);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (segment[segment.Length - 1] != '}')
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Invalid template '{template}'. Missing '}}' in parameter segment '{segment}'.");
|
||||
}
|
||||
|
||||
if (segment.Length < 3)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Invalid template '{template}'. Empty parameter name in segment '{segment}' is not allowed.");
|
||||
}
|
||||
|
||||
var invalidCharacter = segment.IndexOfAny(InvalidParameterNameCharacters, 1, segment.Length - 2);
|
||||
if (invalidCharacter != -1)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Invalid template '{template}'. The character '{segment[invalidCharacter]}' in parameter segment '{segment}' is not allowed.");
|
||||
}
|
||||
|
||||
templateSegments[i] = new LegacyTemplateSegment(originalTemplate, segment.Substring(1, segment.Length - 2), isParameter: true);
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < templateSegments.Length; i++)
|
||||
{
|
||||
var currentSegment = templateSegments[i];
|
||||
|
||||
if (currentSegment.IsCatchAll && i != templateSegments.Length - 1)
|
||||
{
|
||||
throw new InvalidOperationException($"Invalid template '{template}'. A catch-all parameter can only appear as the last segment of the route template.");
|
||||
}
|
||||
|
||||
if (!currentSegment.IsParameter)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
for (int j = i + 1; j < templateSegments.Length; j++)
|
||||
{
|
||||
var nextSegment = templateSegments[j];
|
||||
|
||||
if (currentSegment.IsOptional && !nextSegment.IsOptional)
|
||||
{
|
||||
throw new InvalidOperationException($"Invalid template '{template}'. Non-optional parameters or literal routes cannot appear after optional parameters.");
|
||||
}
|
||||
|
||||
if (string.Equals(currentSegment.Value, nextSegment.Value, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Invalid template '{template}'. The parameter '{currentSegment}' appears multiple times.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new LegacyRouteTemplate(template, templateSegments);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,123 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.LegacyRouteMatching
|
||||
{
|
||||
internal class LegacyTemplateSegment
|
||||
{
|
||||
public LegacyTemplateSegment(string template, string segment, bool isParameter)
|
||||
{
|
||||
IsParameter = isParameter;
|
||||
|
||||
IsCatchAll = segment.StartsWith('*');
|
||||
|
||||
if (IsCatchAll)
|
||||
{
|
||||
// Only one '*' currently allowed
|
||||
Value = segment.Substring(1);
|
||||
|
||||
var invalidCharacter = Value.IndexOf('*');
|
||||
if (Value.IndexOf('*') != -1)
|
||||
{
|
||||
throw new InvalidOperationException($"Invalid template '{template}'. A catch-all parameter may only have one '*' at the beginning of the segment.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Value = segment;
|
||||
}
|
||||
|
||||
// Process segments that are not parameters or do not contain
|
||||
// a token separating a type constraint.
|
||||
if (!isParameter || Value.IndexOf(':') < 0)
|
||||
{
|
||||
// Set the IsOptional flag to true for segments that contain
|
||||
// a parameter with no type constraints but optionality set
|
||||
// via the '?' token.
|
||||
if (Value.IndexOf('?') == Value.Length - 1)
|
||||
{
|
||||
IsOptional = true;
|
||||
Value = Value.Substring(0, Value.Length - 1);
|
||||
}
|
||||
// If the `?` optional marker shows up in the segment but not at the very end,
|
||||
// then throw an error.
|
||||
else if (Value.IndexOf('?') >= 0 && Value.IndexOf('?') != Value.Length - 1)
|
||||
{
|
||||
throw new ArgumentException($"Malformed parameter '{segment}' in route '{template}'. '?' character can only appear at the end of parameter name.");
|
||||
}
|
||||
|
||||
Constraints = Array.Empty<LegacyRouteConstraint>();
|
||||
}
|
||||
else
|
||||
{
|
||||
var tokens = Value.Split(':');
|
||||
if (tokens[0].Length == 0)
|
||||
{
|
||||
throw new ArgumentException($"Malformed parameter '{segment}' in route '{template}' has no name before the constraints list.");
|
||||
}
|
||||
|
||||
// Set the IsOptional flag to true if any type constraints
|
||||
// for this parameter are designated as optional.
|
||||
IsOptional = tokens.Skip(1).Any(token => token.EndsWith("?"));
|
||||
|
||||
Value = tokens[0];
|
||||
Constraints = tokens.Skip(1)
|
||||
.Select(token => LegacyRouteConstraint.Parse(template, segment, token))
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
if (IsParameter)
|
||||
{
|
||||
if (IsOptional && IsCatchAll)
|
||||
{
|
||||
throw new InvalidOperationException($"Invalid segment '{segment}' in route '{template}'. A catch-all parameter cannot be marked optional.");
|
||||
}
|
||||
|
||||
// Moving the check for this here instead of TemplateParser so we can allow catch-all.
|
||||
// We checked for '*' up above specifically for catch-all segments, this one checks for all others
|
||||
if (Value.IndexOf('*') != -1)
|
||||
{
|
||||
throw new InvalidOperationException($"Invalid template '{template}'. The character '*' in parameter segment '{{{segment}}}' is not allowed.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The value of the segment. The exact text to match when is a literal.
|
||||
// The parameter name when its a segment
|
||||
public string Value { get; }
|
||||
|
||||
public bool IsParameter { get; }
|
||||
|
||||
public bool IsOptional { get; }
|
||||
|
||||
public bool IsCatchAll { get; }
|
||||
|
||||
public LegacyRouteConstraint[] Constraints { get; }
|
||||
|
||||
public bool Match(string pathSegment, out object? matchedParameterValue)
|
||||
{
|
||||
if (IsParameter)
|
||||
{
|
||||
matchedParameterValue = pathSegment;
|
||||
|
||||
foreach (var constraint in Constraints)
|
||||
{
|
||||
if (!constraint.Match(pathSegment, out matchedParameterValue))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
matchedParameterValue = null;
|
||||
return string.Equals(Value, pathSegment, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.LegacyRouteMatching
|
||||
{
|
||||
/// <summary>
|
||||
/// A route constraint that requires the value to be parseable as a specified type.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type to which the value must be parseable.</typeparam>
|
||||
internal class LegacyTypeRouteConstraint<T> : LegacyRouteConstraint
|
||||
{
|
||||
public delegate bool LegacyTryParseDelegate(string str, [MaybeNullWhen(false)] out T result);
|
||||
|
||||
private readonly LegacyTryParseDelegate _parser;
|
||||
|
||||
public LegacyTypeRouteConstraint(LegacyTryParseDelegate parser)
|
||||
{
|
||||
_parser = parser;
|
||||
}
|
||||
|
||||
public override bool Match(string pathSegment, out object? convertedValue)
|
||||
{
|
||||
if (_parser(pathSegment, out var result))
|
||||
{
|
||||
convertedValue = result;
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
convertedValue = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -51,9 +51,6 @@ namespace Microsoft.AspNetCore.Components.Routing
|
|||
/// Creates a structured RouteConstraint object given a string that contains
|
||||
/// the route constraint. A constraint is the place after the colon in a
|
||||
/// parameter definition, for example `{age:int?}`.
|
||||
///
|
||||
/// If the constraint denotes an optional, this method will return an
|
||||
/// <see cref="OptionalTypeRouteConstraint{T}" /> which handles the appropriate checks.
|
||||
/// </summary>
|
||||
/// <param name="constraint">String representation of the constraint</param>
|
||||
/// <returns>Type-specific RouteConstraint object</returns>
|
||||
|
|
@ -63,48 +60,26 @@ namespace Microsoft.AspNetCore.Components.Routing
|
|||
{
|
||||
case "bool":
|
||||
return new TypeRouteConstraint<bool>(bool.TryParse);
|
||||
case "bool?":
|
||||
return new OptionalTypeRouteConstraint<bool>(bool.TryParse);
|
||||
case "datetime":
|
||||
return new TypeRouteConstraint<DateTime>((string str, out DateTime result)
|
||||
=> DateTime.TryParse(str, CultureInfo.InvariantCulture, DateTimeStyles.None, out result));
|
||||
case "datetime?":
|
||||
return new OptionalTypeRouteConstraint<DateTime>((string str, out DateTime result)
|
||||
=> DateTime.TryParse(str, CultureInfo.InvariantCulture, DateTimeStyles.None, out result));
|
||||
case "decimal":
|
||||
return new TypeRouteConstraint<decimal>((string str, out decimal result)
|
||||
=> decimal.TryParse(str, NumberStyles.Number, CultureInfo.InvariantCulture, out result));
|
||||
case "decimal?":
|
||||
return new OptionalTypeRouteConstraint<decimal>((string str, out decimal result)
|
||||
=> decimal.TryParse(str, NumberStyles.Number, CultureInfo.InvariantCulture, out result));
|
||||
case "double":
|
||||
return new TypeRouteConstraint<double>((string str, out double result)
|
||||
=> double.TryParse(str, NumberStyles.Number, CultureInfo.InvariantCulture, out result));
|
||||
case "double?":
|
||||
return new OptionalTypeRouteConstraint<double>((string str, out double result)
|
||||
=> double.TryParse(str, NumberStyles.Number, CultureInfo.InvariantCulture, out result));
|
||||
case "float":
|
||||
return new TypeRouteConstraint<float>((string str, out float result)
|
||||
=> float.TryParse(str, NumberStyles.Number, CultureInfo.InvariantCulture, out result));
|
||||
case "float?":
|
||||
return new OptionalTypeRouteConstraint<float>((string str, out float result)
|
||||
=> float.TryParse(str, NumberStyles.Number, CultureInfo.InvariantCulture, out result));
|
||||
case "guid":
|
||||
return new TypeRouteConstraint<Guid>(Guid.TryParse);
|
||||
case "guid?":
|
||||
return new OptionalTypeRouteConstraint<Guid>(Guid.TryParse);
|
||||
case "int":
|
||||
return new TypeRouteConstraint<int>((string str, out int result)
|
||||
=> int.TryParse(str, NumberStyles.Integer, CultureInfo.InvariantCulture, out result));
|
||||
case "int?":
|
||||
return new OptionalTypeRouteConstraint<int>((string str, out int result)
|
||||
=> int.TryParse(str, NumberStyles.Integer, CultureInfo.InvariantCulture, out result));
|
||||
case "long":
|
||||
return new TypeRouteConstraint<long>((string str, out long result)
|
||||
=> long.TryParse(str, NumberStyles.Integer, CultureInfo.InvariantCulture, out result));
|
||||
case "long?":
|
||||
return new OptionalTypeRouteConstraint<long>((string str, out long result)
|
||||
=> long.TryParse(str, NumberStyles.Integer, CultureInfo.InvariantCulture, out result));
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,116 +29,122 @@ namespace Microsoft.AspNetCore.Components.Routing
|
|||
|
||||
internal void Match(RouteContext context)
|
||||
{
|
||||
string? catchAllValue = null;
|
||||
|
||||
// If this template contains a catch-all parameter, we can concatenate the pathSegments
|
||||
// at and beyond the catch-all segment's position. For example:
|
||||
// Template: /foo/bar/{*catchAll}
|
||||
// PathSegments: /foo/bar/one/two/three
|
||||
if (Template.ContainsCatchAllSegment && context.Segments.Length >= Template.Segments.Length)
|
||||
{
|
||||
catchAllValue = string.Join('/', context.Segments[Range.StartAt(Template.Segments.Length - 1)]);
|
||||
}
|
||||
// If there are no optional segments on the route and the length of the route
|
||||
// and the template do not match, then there is no chance of this matching and
|
||||
// we can bail early.
|
||||
else if (Template.OptionalSegmentsCount == 0 && Template.Segments.Length != context.Segments.Length)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Parameters will be lazily initialized.
|
||||
var pathIndex = 0;
|
||||
var templateIndex = 0;
|
||||
Dictionary<string, object> parameters = null;
|
||||
var numMatchingSegments = 0;
|
||||
for (var i = 0; i < Template.Segments.Length; i++)
|
||||
// We will iterate over the path segments and the template segments until we have consumed
|
||||
// one of them.
|
||||
// There are three cases we need to account here for:
|
||||
// * Path is shorter than template ->
|
||||
// * This can match only if we have t-p optional parameters at the end.
|
||||
// * Path and template have the same number of segments
|
||||
// * This can happen when the catch-all segment matches 1 segment
|
||||
// * This can happen when an optional parameter has been specified.
|
||||
// * This can happen when the route only contains literals and parameters.
|
||||
// * Path is longer than template -> This can only match if the parameter has a catch-all at the end.
|
||||
// * We still need to iterate over all the path segments if the catch-all is constrained.
|
||||
// * We still need to iterate over all the template/path segments before the catch-all
|
||||
while (pathIndex < context.Segments.Length && templateIndex < Template.Segments.Length)
|
||||
{
|
||||
var segment = Template.Segments[i];
|
||||
var pathSegment = context.Segments[pathIndex];
|
||||
var templateSegment = Template.Segments[templateIndex];
|
||||
|
||||
if (segment.IsCatchAll)
|
||||
{
|
||||
numMatchingSegments += 1;
|
||||
parameters ??= new Dictionary<string, object>(StringComparer.Ordinal);
|
||||
parameters[segment.Value] = catchAllValue;
|
||||
break;
|
||||
}
|
||||
|
||||
// If the template contains more segments than the path, then
|
||||
// we may need to break out of this for-loop. This can happen
|
||||
// in one of two cases:
|
||||
//
|
||||
// (1) If we are comparing a literal route with a literal template
|
||||
// and the route is shorter than the template.
|
||||
// (2) If we are comparing a template where the last value is an optional
|
||||
// parameter that the route does not provide.
|
||||
if (i >= context.Segments.Length)
|
||||
{
|
||||
// If we are under condition (1) above then we can stop evaluating
|
||||
// matches on the rest of this template.
|
||||
if (!segment.IsParameter && !segment.IsOptional)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
string pathSegment = null;
|
||||
if (i < context.Segments.Length)
|
||||
{
|
||||
pathSegment = context.Segments[i];
|
||||
}
|
||||
|
||||
if (!segment.Match(pathSegment, out var matchedParameterValue))
|
||||
var matches = templateSegment.Match(pathSegment, out var match);
|
||||
if (!matches)
|
||||
{
|
||||
// A constraint or literal didn't match
|
||||
return;
|
||||
}
|
||||
|
||||
if (!templateSegment.IsCatchAll)
|
||||
{
|
||||
// We were dealing with a literal or a parameter, so just advance both cursors.
|
||||
pathIndex++;
|
||||
templateIndex++;
|
||||
|
||||
if (templateSegment.IsParameter)
|
||||
{
|
||||
parameters ??= new(StringComparer.OrdinalIgnoreCase);
|
||||
parameters[templateSegment.Value] = match;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
numMatchingSegments++;
|
||||
if (segment.IsParameter)
|
||||
if (templateSegment.Constraints.Length == 0)
|
||||
{
|
||||
parameters ??= new Dictionary<string, object>(StringComparer.Ordinal);
|
||||
parameters[segment.Value] = matchedParameterValue;
|
||||
|
||||
// Unconstrained catch all, we can stop early
|
||||
parameters ??= new(StringComparer.OrdinalIgnoreCase);
|
||||
parameters[templateSegment.Value] = string.Join('/', context.Segments, pathIndex, context.Segments.Length - pathIndex);
|
||||
|
||||
// Mark the remaining segments as consumed.
|
||||
pathIndex = context.Segments.Length;
|
||||
|
||||
// Catch-alls are always last.
|
||||
templateIndex++;
|
||||
|
||||
// We are done, so break out of the loop.
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
// For constrained catch-alls, we advance the path index but keep the template index on the catch-all.
|
||||
pathIndex++;
|
||||
if (pathIndex == context.Segments.Length)
|
||||
{
|
||||
parameters ??= new(StringComparer.OrdinalIgnoreCase);
|
||||
parameters[templateSegment.Value] = string.Join('/', context.Segments, templateIndex, context.Segments.Length - templateIndex);
|
||||
|
||||
// This is important to signal that we consumed the entire template.
|
||||
templateIndex++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// In addition to extracting parameter values from the URL, each route entry
|
||||
// also knows which other parameters should be supplied with null values. These
|
||||
// are parameters supplied by other route entries matching the same handler.
|
||||
if (!Template.ContainsCatchAllSegment && UnusedRouteParameterNames.Length > 0)
|
||||
var hasRemainingOptionalSegments = templateIndex < Template.Segments.Length &&
|
||||
RemainingSegmentsAreOptional(pathIndex, Template.Segments);
|
||||
|
||||
if ((pathIndex == context.Segments.Length && templateIndex == Template.Segments.Length) || hasRemainingOptionalSegments)
|
||||
{
|
||||
parameters ??= new Dictionary<string, object>(StringComparer.Ordinal);
|
||||
for (var i = 0; i < UnusedRouteParameterNames.Length; i++)
|
||||
if (hasRemainingOptionalSegments)
|
||||
{
|
||||
parameters[UnusedRouteParameterNames[i]] = null;
|
||||
parameters ??= new Dictionary<string, object>(StringComparer.Ordinal);
|
||||
AddDefaultValues(parameters, templateIndex, Template.Segments);
|
||||
}
|
||||
if (UnusedRouteParameterNames?.Length > 0)
|
||||
{
|
||||
parameters ??= new Dictionary<string, object>(StringComparer.Ordinal);
|
||||
for (var i = 0; i < UnusedRouteParameterNames.Length; i++)
|
||||
{
|
||||
parameters[UnusedRouteParameterNames[i]] = null;
|
||||
}
|
||||
}
|
||||
context.Handler = Handler;
|
||||
context.Parameters = parameters;
|
||||
}
|
||||
}
|
||||
|
||||
private void AddDefaultValues(Dictionary<string, object> parameters, int templateIndex, TemplateSegment[] segments)
|
||||
{
|
||||
for (var i = templateIndex; i < segments.Length; i++)
|
||||
{
|
||||
var currentSegment = segments[i];
|
||||
parameters[currentSegment.Value] = null;
|
||||
}
|
||||
}
|
||||
|
||||
private bool RemainingSegmentsAreOptional(int index, TemplateSegment[] segments)
|
||||
{
|
||||
for (var i = index; index < segments.Length - 1; index++)
|
||||
{
|
||||
if (!segments[i].IsOptional)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// We track the number of segments in the template that matched
|
||||
// against this particular route then only select the route that
|
||||
// matches the most number of segments on the route that was passed.
|
||||
// This check is an exactness check that favors the more precise of
|
||||
// two templates in the event that the following route table exists.
|
||||
// Route 1: /{anythingGoes}
|
||||
// Route 2: /users/{id:int}
|
||||
// And the provided route is `/users/1`. We want to choose Route 2
|
||||
// over Route 1.
|
||||
// Furthermore, literal routes are preferred over parameterized routes.
|
||||
// If the two routes below are registered in the route table.
|
||||
// Route 1: /users/1
|
||||
// Route 2: /users/{id:int}
|
||||
// And the provided route is `/users/1`. We want to choose Route 1 over
|
||||
// Route 2.
|
||||
var allRouteSegmentsMatch = numMatchingSegments >= context.Segments.Length;
|
||||
// Checking that all route segments have been matches does not suffice if we are
|
||||
// comparing literal templates with literal routes. For example, the template
|
||||
// `/this/is/a/template` and the route `/this/`. In that case, we want to ensure
|
||||
// that all non-optional segments have matched as well.
|
||||
var allNonOptionalSegmentsMatch = numMatchingSegments >= (Template.Segments.Length - Template.OptionalSegmentsCount);
|
||||
if (Template.ContainsCatchAllSegment || (allRouteSegmentsMatch && allNonOptionalSegmentsMatch))
|
||||
{
|
||||
context.Parameters = parameters;
|
||||
context.Handler = Handler;
|
||||
}
|
||||
return segments[^1].IsOptional || segments[^1].IsCatchAll;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
namespace Microsoft.AspNetCore.Components.Routing
|
||||
{
|
||||
internal class RouteTable
|
||||
internal class RouteTable : IRouteTable
|
||||
{
|
||||
public RouteTable(RouteEntry[] routes)
|
||||
{
|
||||
|
|
@ -12,7 +12,7 @@ namespace Microsoft.AspNetCore.Components.Routing
|
|||
|
||||
public RouteEntry[] Routes { get; }
|
||||
|
||||
internal void Route(RouteContext routeContext)
|
||||
public void Route(RouteContext routeContext)
|
||||
{
|
||||
for (var i = 0; i < Routes.Length; i++)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ using System.Collections.Generic;
|
|||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Microsoft.AspNetCore.Components.Routing;
|
||||
using Microsoft.Extensions.Internal;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components
|
||||
{
|
||||
|
|
@ -121,62 +120,63 @@ namespace Microsoft.AspNetCore.Components
|
|||
|
||||
var xTemplate = x.Template;
|
||||
var yTemplate = y.Template;
|
||||
if (xTemplate.Segments.Length != y.Template.Segments.Length)
|
||||
var minSegments = Math.Min(xTemplate.Segments.Length, yTemplate.Segments.Length);
|
||||
var currentResult = 0;
|
||||
for (var i = 0; i < minSegments; i++)
|
||||
{
|
||||
return xTemplate.Segments.Length < y.Template.Segments.Length ? -1 : 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
for (var i = 0; i < xTemplate.Segments.Length; i++)
|
||||
var xSegment = xTemplate.Segments[i];
|
||||
var ySegment = yTemplate.Segments[i];
|
||||
|
||||
var xRank = GetRank(xSegment);
|
||||
var yRank = GetRank(ySegment);
|
||||
|
||||
currentResult = xRank.CompareTo(yRank);
|
||||
|
||||
// If they are both literals we can disambiguate
|
||||
if ((xRank, yRank) == (0, 0))
|
||||
{
|
||||
var xSegment = xTemplate.Segments[i];
|
||||
var ySegment = yTemplate.Segments[i];
|
||||
if (!xSegment.IsParameter && ySegment.IsParameter)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
if (xSegment.IsParameter && !ySegment.IsParameter)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (xSegment.IsParameter)
|
||||
{
|
||||
// Always favor non-optional parameters over optional ones
|
||||
if (!xSegment.IsOptional && ySegment.IsOptional)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (xSegment.IsOptional && !ySegment.IsOptional)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (xSegment.Constraints.Length > ySegment.Constraints.Length)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
else if (xSegment.Constraints.Length < ySegment.Constraints.Length)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var comparison = string.Compare(xSegment.Value, ySegment.Value, StringComparison.OrdinalIgnoreCase);
|
||||
if (comparison != 0)
|
||||
{
|
||||
return comparison;
|
||||
}
|
||||
}
|
||||
currentResult = StringComparer.OrdinalIgnoreCase.Compare(xSegment.Value, ySegment.Value);
|
||||
}
|
||||
|
||||
if (currentResult != 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (currentResult == 0)
|
||||
{
|
||||
currentResult = xTemplate.Segments.Length.CompareTo(yTemplate.Segments.Length);
|
||||
}
|
||||
|
||||
if (currentResult == 0)
|
||||
{
|
||||
throw new InvalidOperationException($@"The following routes are ambiguous:
|
||||
'{x.Template.TemplateText}' in '{x.Handler.FullName}'
|
||||
'{y.Template.TemplateText}' in '{y.Handler.FullName}'
|
||||
");
|
||||
}
|
||||
|
||||
return currentResult;
|
||||
}
|
||||
|
||||
private static int GetRank(TemplateSegment xSegment)
|
||||
{
|
||||
return xSegment switch
|
||||
{
|
||||
// Literal
|
||||
{ IsParameter: false } => 0,
|
||||
// Parameter with constraints
|
||||
{ IsParameter: true, IsCatchAll: false, Constraints: { Length: > 0 } } => 1,
|
||||
// Parameter without constraints
|
||||
{ IsParameter: true, IsCatchAll: false, Constraints: { Length: 0 } } => 2,
|
||||
// Catch all parameter with constraints
|
||||
{ IsParameter: true, IsCatchAll: true, Constraints: { Length: > 0 } } => 3,
|
||||
// Catch all parameter without constraints
|
||||
{ IsParameter: true, IsCatchAll: true, Constraints: { Length: 0 } } => 4,
|
||||
// The segment is not correct
|
||||
_ => throw new InvalidOperationException($"Unknown segment definition '{xSegment}.")
|
||||
};
|
||||
}
|
||||
|
||||
private readonly struct Key : IEquatable<Key>
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ using System.Reflection;
|
|||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.AspNetCore.Components.LegacyRouteMatching;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.Routing
|
||||
{
|
||||
|
|
@ -75,7 +76,20 @@ namespace Microsoft.AspNetCore.Components.Routing
|
|||
/// </summary>
|
||||
[Parameter] public EventCallback<NavigationContext> OnNavigateAsync { get; set; }
|
||||
|
||||
private RouteTable Routes { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets a flag to indicate whether route matching should prefer exact matches
|
||||
/// over wildcards.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Important: all applications should explicitly set this to true. The option to set it to false
|
||||
/// (or leave unset, which defaults to false) is only provided for backward compatibility.
|
||||
/// In .NET 6, this option will be removed and the router will always prefer exact matches.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[Parameter] public bool PreferExactMatches { get; set; }
|
||||
|
||||
private IRouteTable Routes { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Attach(RenderHandle renderHandle)
|
||||
|
|
@ -142,7 +156,9 @@ namespace Microsoft.AspNetCore.Components.Routing
|
|||
|
||||
if (!_assemblies.SetEquals(assembliesSet))
|
||||
{
|
||||
Routes = RouteTableFactory.Create(assemblies);
|
||||
Routes = PreferExactMatches
|
||||
? RouteTableFactory.Create(assemblies)
|
||||
: LegacyRouteTableFactory.Create(assemblies);
|
||||
_assemblies.Clear();
|
||||
_assemblies.UnionWith(assembliesSet);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,6 +50,11 @@ namespace Microsoft.AspNetCore.Components.Routing
|
|||
throw new InvalidOperationException(
|
||||
$"Invalid template '{template}'. Missing '{{' in parameter segment '{segment}'.");
|
||||
}
|
||||
if (segment[^1] == '?')
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Invalid template '{template}'. '?' is not allowed in literal segment '{segment}'.");
|
||||
}
|
||||
templateSegments[i] = new TemplateSegment(originalTemplate, segment, isParameter: false);
|
||||
}
|
||||
else
|
||||
|
|
@ -95,7 +100,7 @@ namespace Microsoft.AspNetCore.Components.Routing
|
|||
{
|
||||
var nextSegment = templateSegments[j];
|
||||
|
||||
if (currentSegment.IsOptional && !nextSegment.IsOptional)
|
||||
if (currentSegment.IsOptional && !nextSegment.IsOptional && !nextSegment.IsCatchAll)
|
||||
{
|
||||
throw new InvalidOperationException($"Invalid template '{template}'. Non-optional parameters or literal routes cannot appear after optional parameters.");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,15 +12,15 @@ namespace Microsoft.AspNetCore.Components.Routing
|
|||
{
|
||||
IsParameter = isParameter;
|
||||
|
||||
IsCatchAll = segment.StartsWith('*');
|
||||
IsCatchAll = isParameter && segment.StartsWith('*');
|
||||
|
||||
if (IsCatchAll)
|
||||
{
|
||||
// Only one '*' currently allowed
|
||||
Value = segment.Substring(1);
|
||||
Value = segment[1..];
|
||||
|
||||
var invalidCharacter = Value.IndexOf('*');
|
||||
if (Value.IndexOf('*') != -1)
|
||||
var invalidCharacterIndex = Value.IndexOf('*');
|
||||
if (invalidCharacterIndex != -1)
|
||||
{
|
||||
throw new InvalidOperationException($"Invalid template '{template}'. A catch-all parameter may only have one '*' at the beginning of the segment.");
|
||||
}
|
||||
|
|
@ -30,43 +30,55 @@ namespace Microsoft.AspNetCore.Components.Routing
|
|||
Value = segment;
|
||||
}
|
||||
|
||||
// Process segments that are not parameters or do not contain
|
||||
// a token separating a type constraint.
|
||||
if (!isParameter || Value.IndexOf(':') < 0)
|
||||
// Process segments that parameters that do not contain a token separating a type constraint.
|
||||
if (IsParameter)
|
||||
{
|
||||
// Set the IsOptional flag to true for segments that contain
|
||||
// a parameter with no type constraints but optionality set
|
||||
// via the '?' token.
|
||||
if (Value.IndexOf('?') == Value.Length - 1)
|
||||
if (Value.IndexOf(':') < 0)
|
||||
{
|
||||
IsOptional = true;
|
||||
Value = Value.Substring(0, Value.Length - 1);
|
||||
}
|
||||
// If the `?` optional marker shows up in the segment but not at the very end,
|
||||
// then throw an error.
|
||||
else if (Value.IndexOf('?') >= 0 && Value.IndexOf('?') != Value.Length - 1)
|
||||
{
|
||||
throw new ArgumentException($"Malformed parameter '{segment}' in route '{template}'. '?' character can only appear at the end of parameter name.");
|
||||
}
|
||||
|
||||
Constraints = Array.Empty<RouteConstraint>();
|
||||
// Set the IsOptional flag to true for segments that contain
|
||||
// a parameter with no type constraints but optionality set
|
||||
// via the '?' token.
|
||||
var questionMarkIndex = Value.IndexOf('?');
|
||||
if (questionMarkIndex == Value.Length - 1)
|
||||
{
|
||||
IsOptional = true;
|
||||
Value = Value[0..^1];
|
||||
}
|
||||
// If the `?` optional marker shows up in the segment but not at the very end,
|
||||
// then throw an error.
|
||||
else if (questionMarkIndex >= 0)
|
||||
{
|
||||
throw new ArgumentException($"Malformed parameter '{segment}' in route '{template}'. '?' character can only appear at the end of parameter name.");
|
||||
}
|
||||
|
||||
Constraints = Array.Empty<RouteConstraint>();
|
||||
}
|
||||
else
|
||||
{
|
||||
var tokens = Value.Split(':');
|
||||
if (tokens[0].Length == 0)
|
||||
{
|
||||
throw new ArgumentException($"Malformed parameter '{segment}' in route '{template}' has no name before the constraints list.");
|
||||
}
|
||||
|
||||
Value = tokens[0];
|
||||
IsOptional = tokens[^1].EndsWith("?");
|
||||
if (IsOptional)
|
||||
{
|
||||
tokens[^1] = tokens[^1][0..^1];
|
||||
}
|
||||
|
||||
Constraints = new RouteConstraint[tokens.Length - 1];
|
||||
for (var i = 1; i < tokens.Length; i++)
|
||||
{
|
||||
Constraints[i - 1] = RouteConstraint.Parse(template, segment, tokens[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var tokens = Value.Split(':');
|
||||
if (tokens[0].Length == 0)
|
||||
{
|
||||
throw new ArgumentException($"Malformed parameter '{segment}' in route '{template}' has no name before the constraints list.");
|
||||
}
|
||||
|
||||
// Set the IsOptional flag to true if any type constraints
|
||||
// for this parameter are designated as optional.
|
||||
IsOptional = tokens.Skip(1).Any(token => token.EndsWith("?"));
|
||||
|
||||
Value = tokens[0];
|
||||
Constraints = tokens.Skip(1)
|
||||
.Select(token => RouteConstraint.Parse(template, segment, token))
|
||||
.ToArray();
|
||||
Constraints = Array.Empty<RouteConstraint>();
|
||||
}
|
||||
|
||||
if (IsParameter)
|
||||
|
|
@ -91,7 +103,7 @@ namespace Microsoft.AspNetCore.Components.Routing
|
|||
|
||||
public bool IsParameter { get; }
|
||||
|
||||
public bool IsOptional { get; }
|
||||
public bool IsOptional { get; }
|
||||
|
||||
public bool IsCatchAll { get; }
|
||||
|
||||
|
|
@ -119,5 +131,17 @@ namespace Microsoft.AspNetCore.Components.Routing
|
|||
return string.Equals(Value, pathSegment, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString() => this switch
|
||||
{
|
||||
{ IsParameter: true, IsOptional: false, IsCatchAll: false, Constraints: { Length: 0 } } => $"{{{Value}}}",
|
||||
{ IsParameter: true, IsOptional: false, IsCatchAll: false, Constraints: { Length: > 0 } } => $"{{{Value}:{string.Join(':', Constraints.Select(c => c.ToString()))}}}",
|
||||
{ IsParameter: true, IsOptional: true, Constraints: { Length: 0 } } => $"{{{Value}?}}",
|
||||
{ IsParameter: true, IsOptional: true, Constraints: { Length: > 0 } } => $"{{{Value}:{string.Join(':', Constraints.Select(c => c.ToString()))}?}}",
|
||||
{ IsParameter: true, IsCatchAll: true, Constraints: { Length: 0 } } => $"{{*{Value}}}",
|
||||
{ IsParameter: true, IsCatchAll: true, Constraints: { Length: > 0 } } => $"{{*{Value}:{string.Join(':', Constraints.Select(c => c.ToString()))}?}}",
|
||||
{ IsParameter: false } => Value,
|
||||
_ => throw new InvalidOperationException("Invalid template segment.")
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.Routing
|
||||
|
|
@ -33,5 +34,18 @@ namespace Microsoft.AspNetCore.Components.Routing
|
|||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString() => typeof(T) switch
|
||||
{
|
||||
var x when x == typeof(bool) => "bool",
|
||||
var x when x == typeof(DateTime) => "datetime",
|
||||
var x when x == typeof(decimal) => "decimal",
|
||||
var x when x == typeof(double) => "double",
|
||||
var x when x == typeof(float) => "float",
|
||||
var x when x == typeof(Guid) => "guid",
|
||||
var x when x == typeof(int) => "int",
|
||||
var x when x == typeof(long) => "long",
|
||||
var x => x.Name.ToLowerInvariant()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,46 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.LegacyRouteMatching
|
||||
{
|
||||
public class LegacyRouteConstraintTest
|
||||
{
|
||||
[Fact]
|
||||
public void Parse_CreatesDifferentConstraints_ForDifferentKinds()
|
||||
{
|
||||
// Arrange
|
||||
var original = LegacyRouteConstraint.Parse("ignore", "ignore", "int");
|
||||
|
||||
// Act
|
||||
var another = LegacyRouteConstraint.Parse("ignore", "ignore", "guid");
|
||||
|
||||
// Assert
|
||||
Assert.NotSame(original, another);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_CachesCreatedConstraint_ForSameKind()
|
||||
{
|
||||
// Arrange
|
||||
var original = LegacyRouteConstraint.Parse("ignore", "ignore", "int");
|
||||
|
||||
// Act
|
||||
var another = LegacyRouteConstraint.Parse("ignore", "ignore", "int");
|
||||
|
||||
// Assert
|
||||
Assert.Same(original, another);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_DoesNotThrowIfOptionalConstraint()
|
||||
{
|
||||
// Act
|
||||
var exceptions = Record.Exception(() => LegacyRouteConstraint.Parse("ignore", "ignore", "int?"));
|
||||
|
||||
// Assert
|
||||
Assert.Null(exceptions);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,740 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Xunit;
|
||||
|
||||
// Avoid referencing the whole Microsoft.AspNetCore.Components.Routing namespace to
|
||||
// avoid the risk of accidentally relying on the non-legacy types in the legacy fork
|
||||
using RouteContext = Microsoft.AspNetCore.Components.Routing.RouteContext;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.LegacyRouteMatching
|
||||
{
|
||||
public class LegacyRouteTableFactoryTests
|
||||
{
|
||||
[Fact]
|
||||
public void CanCacheRouteTable()
|
||||
{
|
||||
// Arrange
|
||||
var routes1 = LegacyRouteTableFactory.Create(new[] { GetType().Assembly, });
|
||||
|
||||
// Act
|
||||
var routes2 = LegacyRouteTableFactory.Create(new[] { GetType().Assembly, });
|
||||
|
||||
// Assert
|
||||
Assert.Same(routes1, routes2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanCacheRouteTableWithDifferentAssembliesAndOrder()
|
||||
{
|
||||
// Arrange
|
||||
var routes1 = LegacyRouteTableFactory.Create(new[] { typeof(object).Assembly, GetType().Assembly, });
|
||||
|
||||
// Act
|
||||
var routes2 = LegacyRouteTableFactory.Create(new[] { GetType().Assembly, typeof(object).Assembly, });
|
||||
|
||||
// Assert
|
||||
Assert.Same(routes1, routes2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DoesNotCacheRouteTableForDifferentAssemblies()
|
||||
{
|
||||
// Arrange
|
||||
var routes1 = LegacyRouteTableFactory.Create(new[] { GetType().Assembly, });
|
||||
|
||||
// Act
|
||||
var routes2 = LegacyRouteTableFactory.Create(new[] { GetType().Assembly, typeof(object).Assembly, });
|
||||
|
||||
// Assert
|
||||
Assert.NotSame(routes1, routes2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanDiscoverRoute()
|
||||
{
|
||||
// Arrange & Act
|
||||
var routes = LegacyRouteTableFactory.Create(new[] { typeof(MyComponent), });
|
||||
|
||||
// Assert
|
||||
Assert.Equal("Test1", Assert.Single(routes.Routes).Template.TemplateText);
|
||||
}
|
||||
|
||||
[Route("Test1")]
|
||||
private class MyComponent : ComponentBase
|
||||
{
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanDiscoverRoutes_WithInheritance()
|
||||
{
|
||||
// Arrange & Act
|
||||
var routes = LegacyRouteTableFactory.Create(new[] { typeof(MyComponent), typeof(MyInheritedComponent), });
|
||||
|
||||
// Assert
|
||||
Assert.Collection(
|
||||
routes.Routes.OrderBy(r => r.Template.TemplateText),
|
||||
r => Assert.Equal("Test1", r.Template.TemplateText),
|
||||
r => Assert.Equal("Test2", r.Template.TemplateText));
|
||||
}
|
||||
|
||||
[Route("Test2")]
|
||||
private class MyInheritedComponent : MyComponent
|
||||
{
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanMatchRootTemplate()
|
||||
{
|
||||
// Arrange
|
||||
var routeTable = new TestRouteTableBuilder().AddRoute("/").Build();
|
||||
var context = new RouteContext("/");
|
||||
|
||||
// Act
|
||||
routeTable.Route(context);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(context.Handler);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanMatchLiteralTemplate()
|
||||
{
|
||||
// Arrange
|
||||
var routeTable = new TestRouteTableBuilder().AddRoute("/literal").Build();
|
||||
var context = new RouteContext("/literal/");
|
||||
|
||||
// Act
|
||||
routeTable.Route(context);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(context.Handler);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanMatchTemplateWithMultipleLiterals()
|
||||
{
|
||||
// Arrange
|
||||
var routeTable = new TestRouteTableBuilder().AddRoute("/some/awesome/route/").Build();
|
||||
var context = new RouteContext("/some/awesome/route");
|
||||
|
||||
// Act
|
||||
routeTable.Route(context);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(context.Handler);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RouteMatchingIsCaseInsensitive()
|
||||
{
|
||||
// Arrange
|
||||
var routeTable = new TestRouteTableBuilder().AddRoute("/some/AWESOME/route/").Build();
|
||||
var context = new RouteContext("/Some/awesome/RouTe");
|
||||
|
||||
// Act
|
||||
routeTable.Route(context);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(context.Handler);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanMatchEncodedSegments()
|
||||
{
|
||||
// Arrange
|
||||
var routeTable = new TestRouteTableBuilder().AddRoute("/some/ünicõdē/🛣/").Build();
|
||||
var context = new RouteContext("/some/%C3%BCnic%C3%B5d%C4%93/%F0%9F%9B%A3");
|
||||
|
||||
// Act
|
||||
routeTable.Route(context);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(context.Handler);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DoesNotMatchIfSegmentsDontMatch()
|
||||
{
|
||||
// Arrange
|
||||
var routeTable = new TestRouteTableBuilder().AddRoute("/some/AWESOME/route/").Build();
|
||||
var context = new RouteContext("/some/brilliant/route");
|
||||
|
||||
// Act
|
||||
routeTable.Route(context);
|
||||
|
||||
// Assert
|
||||
Assert.Null(context.Handler);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("/{value:bool}", "/maybe")]
|
||||
[InlineData("/{value:datetime}", "/1955-01-32")]
|
||||
[InlineData("/{value:decimal}", "/hello")]
|
||||
[InlineData("/{value:double}", "/0.1.2")]
|
||||
[InlineData("/{value:float}", "/0.1.2")]
|
||||
[InlineData("/{value:guid}", "/not-a-guid")]
|
||||
[InlineData("/{value:int}", "/3.141")]
|
||||
[InlineData("/{value:long}", "/3.141")]
|
||||
public void DoesNotMatchIfConstraintDoesNotMatch(string template, string contextUrl)
|
||||
{
|
||||
// Arrange
|
||||
var routeTable = new TestRouteTableBuilder().AddRoute(template).Build();
|
||||
var context = new RouteContext(contextUrl);
|
||||
|
||||
// Act
|
||||
routeTable.Route(context);
|
||||
|
||||
// Assert
|
||||
Assert.Null(context.Handler);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("/some")]
|
||||
[InlineData("/some/awesome/route/with/extra/segments")]
|
||||
public void DoesNotMatchIfDifferentNumberOfSegments(string path)
|
||||
{
|
||||
// Arrange
|
||||
var routeTable = new TestRouteTableBuilder().AddRoute("/some/awesome/route/").Build();
|
||||
var context = new RouteContext(path);
|
||||
|
||||
// Act
|
||||
routeTable.Route(context);
|
||||
|
||||
// Assert
|
||||
Assert.Null(context.Handler);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("/value1", "value1")]
|
||||
[InlineData("/value2/", "value2")]
|
||||
[InlineData("/d%C3%A9j%C3%A0%20vu", "déjà vu")]
|
||||
[InlineData("/d%C3%A9j%C3%A0%20vu/", "déjà vu")]
|
||||
[InlineData("/d%C3%A9j%C3%A0+vu", "déjà+vu")]
|
||||
public void CanMatchParameterTemplate(string path, string expectedValue)
|
||||
{
|
||||
// Arrange
|
||||
var routeTable = new TestRouteTableBuilder().AddRoute("/{parameter}").Build();
|
||||
var context = new RouteContext(path);
|
||||
|
||||
// Act
|
||||
routeTable.Route(context);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(context.Handler);
|
||||
Assert.Single(context.Parameters, p => p.Key == "parameter" && (string)p.Value == expectedValue);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("/blog/value1", "value1")]
|
||||
[InlineData("/blog/value1/foo%20bar", "value1/foo bar")]
|
||||
public void CanMatchCatchAllParameterTemplate(string path, string expectedValue)
|
||||
{
|
||||
// Arrange
|
||||
var routeTable = new TestRouteTableBuilder().AddRoute("/blog/{*parameter}").Build();
|
||||
var context = new RouteContext(path);
|
||||
|
||||
// Act
|
||||
routeTable.Route(context);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(context.Handler);
|
||||
Assert.Single(context.Parameters, p => p.Key == "parameter" && (string)p.Value == expectedValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanMatchTemplateWithMultipleParameters()
|
||||
{
|
||||
// Arrange
|
||||
var routeTable = new TestRouteTableBuilder().AddRoute("/{some}/awesome/{route}/").Build();
|
||||
var context = new RouteContext("/an/awesome/path");
|
||||
|
||||
var expectedParameters = new Dictionary<string, object>
|
||||
{
|
||||
["some"] = "an",
|
||||
["route"] = "path"
|
||||
};
|
||||
|
||||
// Act
|
||||
routeTable.Route(context);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(context.Handler);
|
||||
Assert.Equal(expectedParameters, context.Parameters);
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
public void CanMatchTemplateWithMultipleParametersAndCatchAllParameter()
|
||||
{
|
||||
// Arrange
|
||||
var routeTable = new TestRouteTableBuilder().AddRoute("/{some}/awesome/{route}/with/{*catchAll}").Build();
|
||||
var context = new RouteContext("/an/awesome/path/with/some/catch/all/stuff");
|
||||
|
||||
var expectedParameters = new Dictionary<string, object>
|
||||
{
|
||||
["some"] = "an",
|
||||
["route"] = "path",
|
||||
["catchAll"] = "some/catch/all/stuff"
|
||||
};
|
||||
|
||||
// Act
|
||||
routeTable.Route(context);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(context.Handler);
|
||||
Assert.Equal(expectedParameters, context.Parameters);
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> CanMatchParameterWithConstraintCases() => new object[][]
|
||||
{
|
||||
new object[] { "/{value:bool}", "/true", true },
|
||||
new object[] { "/{value:bool}", "/false", false },
|
||||
new object[] { "/{value:datetime}", "/1955-01-30", new DateTime(1955, 1, 30) },
|
||||
new object[] { "/{value:decimal}", "/5.3", 5.3m },
|
||||
new object[] { "/{value:double}", "/0.1", 0.1d },
|
||||
new object[] { "/{value:float}", "/0.1", 0.1f },
|
||||
new object[] { "/{value:guid}", "/1FCEF085-884F-416E-B0A1-71B15F3E206B", Guid.Parse("1FCEF085-884F-416E-B0A1-71B15F3E206B") },
|
||||
new object[] { "/{value:int}", "/123", 123 },
|
||||
new object[] { "/{value:int}", "/-123", -123},
|
||||
new object[] { "/{value:long}", "/9223372036854775807", long.MaxValue },
|
||||
new object[] { "/{value:long}", $"/-9223372036854775808", long.MinValue },
|
||||
};
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(CanMatchParameterWithConstraintCases))]
|
||||
public void CanMatchParameterWithConstraint(string template, string contextUrl, object convertedValue)
|
||||
{
|
||||
// Arrange
|
||||
var routeTable = new TestRouteTableBuilder().AddRoute(template).Build();
|
||||
var context = new RouteContext(contextUrl);
|
||||
|
||||
// Act
|
||||
routeTable.Route(context);
|
||||
|
||||
// Assert
|
||||
if (context.Handler == null)
|
||||
{
|
||||
// Make it easier to track down failing tests when using MemberData
|
||||
throw new InvalidOperationException($"Failed to match template '{template}'.");
|
||||
}
|
||||
Assert.Equal(new Dictionary<string, object>
|
||||
{
|
||||
{ "value", convertedValue }
|
||||
}, context.Parameters);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanMatchOptionalParameterWithoutConstraints()
|
||||
{
|
||||
// Arrange
|
||||
var template = "/optional/{value?}";
|
||||
var contextUrl = "/optional/";
|
||||
string convertedValue = null;
|
||||
|
||||
var routeTable = new TestRouteTableBuilder().AddRoute(template).Build();
|
||||
var context = new RouteContext(contextUrl);
|
||||
|
||||
// Act
|
||||
routeTable.Route(context);
|
||||
|
||||
// Assert
|
||||
if (context.Handler == null)
|
||||
{
|
||||
// Make it easier to track down failing tests when using MemberData
|
||||
throw new InvalidOperationException($"Failed to match template '{template}'.");
|
||||
}
|
||||
Assert.Equal(new Dictionary<string, object>
|
||||
{
|
||||
{ "value", convertedValue }
|
||||
}, context.Parameters);
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> CanMatchOptionalParameterWithConstraintCases() => new object[][]
|
||||
{
|
||||
new object[] { "/optional/{value:bool?}", "/optional/", null },
|
||||
new object[] { "/optional/{value:datetime?}", "/optional/", null },
|
||||
new object[] { "/optional/{value:decimal?}", "/optional/", null },
|
||||
};
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(CanMatchOptionalParameterWithConstraintCases))]
|
||||
public void CanMatchOptionalParameterWithConstraint(string template, string contextUrl, object convertedValue)
|
||||
{
|
||||
// Arrange
|
||||
var routeTable = new TestRouteTableBuilder().AddRoute(template).Build();
|
||||
var context = new RouteContext(contextUrl);
|
||||
|
||||
// Act
|
||||
routeTable.Route(context);
|
||||
|
||||
// Assert
|
||||
if (context.Handler == null)
|
||||
{
|
||||
// Make it easier to track down failing tests when using MemberData
|
||||
throw new InvalidOperationException($"Failed to match template '{template}'.");
|
||||
}
|
||||
Assert.Equal(new Dictionary<string, object>
|
||||
{
|
||||
{ "value", convertedValue }
|
||||
}, context.Parameters);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanMatchMultipleOptionalParameterWithConstraint()
|
||||
{
|
||||
// Arrange
|
||||
var template = "/optional/{value:datetime?}/{value2:datetime?}";
|
||||
var contextUrl = "/optional//";
|
||||
object convertedValue = null;
|
||||
|
||||
var routeTable = new TestRouteTableBuilder().AddRoute(template).Build();
|
||||
var context = new RouteContext(contextUrl);
|
||||
|
||||
// Act
|
||||
routeTable.Route(context);
|
||||
|
||||
// Assert
|
||||
if (context.Handler == null)
|
||||
{
|
||||
// Make it easier to track down failing tests when using MemberData
|
||||
throw new InvalidOperationException($"Failed to match template '{template}'.");
|
||||
}
|
||||
Assert.Equal(new Dictionary<string, object>
|
||||
{
|
||||
{ "value", convertedValue },
|
||||
{ "value2", convertedValue }
|
||||
}, context.Parameters);
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> CanMatchSegmentWithMultipleConstraintsCases() => new object[][]
|
||||
{
|
||||
new object[] { "/{value:double:int}/", "/15", 15 },
|
||||
new object[] { "/{value:double?:int?}/", "/", null },
|
||||
};
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(CanMatchSegmentWithMultipleConstraintsCases))]
|
||||
public void CanMatchSegmentWithMultipleConstraints(string template, string contextUrl, object convertedValue)
|
||||
{
|
||||
// Arrange
|
||||
var routeTable = new TestRouteTableBuilder().AddRoute(template).Build();
|
||||
var context = new RouteContext(contextUrl);
|
||||
|
||||
// Act
|
||||
routeTable.Route(context);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(new Dictionary<string, object>
|
||||
{
|
||||
{ "value", convertedValue }
|
||||
}, context.Parameters);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PrefersLiteralTemplateOverTemplateWithParameters()
|
||||
{
|
||||
// Arrange
|
||||
var routeTable = new TestRouteTableBuilder()
|
||||
.AddRoute("/an/awesome/path", typeof(TestHandler1))
|
||||
.AddRoute("/{some}/awesome/{route}/", typeof(TestHandler2))
|
||||
.Build();
|
||||
var context = new RouteContext("/an/awesome/path");
|
||||
|
||||
// Act
|
||||
routeTable.Route(context);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(context.Handler);
|
||||
Assert.Null(context.Parameters);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PrefersLiteralTemplateOverTemplateWithOptionalParameters()
|
||||
{
|
||||
// Arrange
|
||||
var routeTable = new TestRouteTableBuilder()
|
||||
.AddRoute("/users/1", typeof(TestHandler1))
|
||||
.AddRoute("/users/{id?}", typeof(TestHandler2))
|
||||
.Build();
|
||||
var context = new RouteContext("/users/1");
|
||||
|
||||
// Act
|
||||
routeTable.Route(context);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(context.Handler);
|
||||
Assert.Null(context.Parameters);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PrefersOptionalParamsOverNonOptionalParams()
|
||||
{
|
||||
// Arrange
|
||||
var routeTable = new TestRouteTableBuilder()
|
||||
.AddRoute("/users/{id}", typeof(TestHandler1))
|
||||
.AddRoute("/users/{id?}", typeof(TestHandler2))
|
||||
.Build();
|
||||
var contextWithParam = new RouteContext("/users/1");
|
||||
var contextWithoutParam = new RouteContext("/users/");
|
||||
|
||||
// Act
|
||||
routeTable.Route(contextWithParam);
|
||||
routeTable.Route(contextWithoutParam);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(contextWithParam.Handler);
|
||||
Assert.Equal(typeof(TestHandler1), contextWithParam.Handler);
|
||||
|
||||
Assert.NotNull(contextWithoutParam.Handler);
|
||||
Assert.Equal(typeof(TestHandler2), contextWithoutParam.Handler);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PrefersOptionalParamsOverNonOptionalParamsReverseOrder()
|
||||
{
|
||||
// Arrange
|
||||
var routeTable = new TestRouteTableBuilder()
|
||||
.AddRoute("/users/{id}", typeof(TestHandler1))
|
||||
.AddRoute("/users/{id?}", typeof(TestHandler2))
|
||||
.Build();
|
||||
var contextWithParam = new RouteContext("/users/1");
|
||||
var contextWithoutParam = new RouteContext("/users/");
|
||||
|
||||
// Act
|
||||
routeTable.Route(contextWithParam);
|
||||
routeTable.Route(contextWithoutParam);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(contextWithParam.Handler);
|
||||
Assert.Equal(typeof(TestHandler1), contextWithParam.Handler);
|
||||
|
||||
Assert.NotNull(contextWithoutParam.Handler);
|
||||
Assert.Equal(typeof(TestHandler2), contextWithoutParam.Handler);
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
public void PrefersLiteralTemplateOverParameterizedTemplates()
|
||||
{
|
||||
// Arrange
|
||||
var routeTable = new TestRouteTableBuilder()
|
||||
.AddRoute("/users/1/friends", typeof(TestHandler1))
|
||||
.AddRoute("/users/{id}/{location}", typeof(TestHandler2))
|
||||
.AddRoute("/users/1/{location}", typeof(TestHandler2))
|
||||
.Build();
|
||||
var context = new RouteContext("/users/1/friends");
|
||||
|
||||
// Act
|
||||
routeTable.Route(context);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(context.Handler);
|
||||
Assert.Equal(typeof(TestHandler1), context.Handler);
|
||||
Assert.Null(context.Parameters);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PrefersShorterRoutesOverLongerRoutes()
|
||||
{
|
||||
// Arrange & Act
|
||||
var handler = typeof(int);
|
||||
var routeTable = new TestRouteTableBuilder()
|
||||
.AddRoute("/an/awesome/path")
|
||||
.AddRoute("/an/awesome/", handler).Build();
|
||||
|
||||
// Act
|
||||
Assert.Equal("an/awesome", routeTable.Routes[0].Template.TemplateText);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PrefersMoreConstraintsOverFewer()
|
||||
{
|
||||
// Arrange
|
||||
var routeTable = new TestRouteTableBuilder()
|
||||
.AddRoute("/products/{id}")
|
||||
.AddRoute("/products/{id:int}").Build();
|
||||
var context = new RouteContext("/products/456");
|
||||
|
||||
// Act
|
||||
routeTable.Route(context);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(context.Handler);
|
||||
Assert.Equal(context.Parameters, new Dictionary<string, object>
|
||||
{
|
||||
{ "id", 456 }
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PrefersRoutesThatMatchMoreSegments()
|
||||
{
|
||||
// Arrange
|
||||
var routeTable = new TestRouteTableBuilder()
|
||||
.AddRoute("/{anythingGoes}", typeof(TestHandler1))
|
||||
.AddRoute("/users/{id?}", typeof(TestHandler2))
|
||||
.Build();
|
||||
var context = new RouteContext("/users/1");
|
||||
|
||||
// Act
|
||||
routeTable.Route(context);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(context.Handler);
|
||||
Assert.Equal(typeof(TestHandler2), context.Handler);
|
||||
Assert.NotNull(context.Parameters);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProducesAStableOrderForNonAmbiguousRoutes()
|
||||
{
|
||||
// Arrange & Act
|
||||
var handler = typeof(int);
|
||||
var routeTable = new TestRouteTableBuilder()
|
||||
.AddRoute("/an/awesome/", handler)
|
||||
.AddRoute("/a/brilliant/").Build();
|
||||
|
||||
// Act
|
||||
Assert.Equal("a/brilliant", routeTable.Routes[0].Template.TemplateText);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DoesNotThrowIfStableSortComparesRouteWithItself()
|
||||
{
|
||||
// Test for https://github.com/dotnet/aspnetcore/issues/13313
|
||||
// Arrange & Act
|
||||
var builder = new TestRouteTableBuilder();
|
||||
builder.AddRoute("r16");
|
||||
builder.AddRoute("r05");
|
||||
builder.AddRoute("r09");
|
||||
builder.AddRoute("r00");
|
||||
builder.AddRoute("r13");
|
||||
builder.AddRoute("r02");
|
||||
builder.AddRoute("r03");
|
||||
builder.AddRoute("r10");
|
||||
builder.AddRoute("r15");
|
||||
builder.AddRoute("r14");
|
||||
builder.AddRoute("r12");
|
||||
builder.AddRoute("r07");
|
||||
builder.AddRoute("r11");
|
||||
builder.AddRoute("r08");
|
||||
builder.AddRoute("r06");
|
||||
builder.AddRoute("r04");
|
||||
builder.AddRoute("r01");
|
||||
|
||||
var routeTable = builder.Build();
|
||||
|
||||
// Act
|
||||
Assert.Equal(17, routeTable.Routes.Length);
|
||||
for (var i = 0; i < 17; i++)
|
||||
{
|
||||
var templateText = "r" + i.ToString().PadLeft(2, '0');
|
||||
Assert.Equal(templateText, routeTable.Routes[i].Template.TemplateText);
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("/literal", "/Literal/")]
|
||||
[InlineData("/{parameter}", "/{parameter}/")]
|
||||
[InlineData("/literal/{parameter}", "/Literal/{something}")]
|
||||
[InlineData("/{parameter}/literal/{something}", "{param}/Literal/{else}")]
|
||||
public void DetectsAmbiguousRoutes(string left, string right)
|
||||
{
|
||||
// Arrange
|
||||
var expectedMessage = $@"The following routes are ambiguous:
|
||||
'{left.Trim('/')}' in '{typeof(object).FullName}'
|
||||
'{right.Trim('/')}' in '{typeof(object).FullName}'
|
||||
";
|
||||
// Act
|
||||
var exception = Assert.Throws<InvalidOperationException>(() => new TestRouteTableBuilder()
|
||||
.AddRoute(left)
|
||||
.AddRoute(right).Build());
|
||||
|
||||
Assert.Equal(expectedMessage, exception.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SuppliesNullForUnusedHandlerParameters()
|
||||
{
|
||||
// Arrange
|
||||
var routeTable = new TestRouteTableBuilder()
|
||||
.AddRoute("/", typeof(TestHandler1))
|
||||
.AddRoute("/products/{param1:int}", typeof(TestHandler1))
|
||||
.AddRoute("/products/{param2}/{PaRam1}", typeof(TestHandler1))
|
||||
.AddRoute("/{unrelated}", typeof(TestHandler2))
|
||||
.Build();
|
||||
var context = new RouteContext("/products/456");
|
||||
|
||||
// Act
|
||||
routeTable.Route(context);
|
||||
|
||||
// Assert
|
||||
Assert.Collection(routeTable.Routes,
|
||||
route =>
|
||||
{
|
||||
Assert.Same(typeof(TestHandler1), route.Handler);
|
||||
Assert.Equal("/", route.Template.TemplateText);
|
||||
Assert.Equal(new[] { "param1", "param2" }, route.UnusedRouteParameterNames);
|
||||
},
|
||||
route =>
|
||||
{
|
||||
Assert.Same(typeof(TestHandler2), route.Handler);
|
||||
Assert.Equal("{unrelated}", route.Template.TemplateText);
|
||||
Assert.Equal(Array.Empty<string>(), route.UnusedRouteParameterNames);
|
||||
},
|
||||
route =>
|
||||
{
|
||||
Assert.Same(typeof(TestHandler1), route.Handler);
|
||||
Assert.Equal("products/{param1:int}", route.Template.TemplateText);
|
||||
Assert.Equal(new[] { "param2" }, route.UnusedRouteParameterNames);
|
||||
},
|
||||
route =>
|
||||
{
|
||||
Assert.Same(typeof(TestHandler1), route.Handler);
|
||||
Assert.Equal("products/{param2}/{PaRam1}", route.Template.TemplateText);
|
||||
Assert.Equal(Array.Empty<string>(), route.UnusedRouteParameterNames);
|
||||
});
|
||||
Assert.Same(typeof(TestHandler1), context.Handler);
|
||||
Assert.Equal(new Dictionary<string, object>
|
||||
{
|
||||
{ "param1", 456 },
|
||||
{ "param2", null },
|
||||
}, context.Parameters);
|
||||
}
|
||||
|
||||
private class TestRouteTableBuilder
|
||||
{
|
||||
IList<(string Template, Type Handler)> _routeTemplates = new List<(string, Type)>();
|
||||
Type _handler = typeof(object);
|
||||
|
||||
public TestRouteTableBuilder AddRoute(string template, Type handler = null)
|
||||
{
|
||||
_routeTemplates.Add((template, handler ?? _handler));
|
||||
return this;
|
||||
}
|
||||
|
||||
public LegacyRouteTable Build()
|
||||
{
|
||||
try
|
||||
{
|
||||
var templatesByHandler = _routeTemplates
|
||||
.GroupBy(rt => rt.Handler)
|
||||
.ToDictionary(group => group.Key, group => group.Select(g => g.Template).ToArray());
|
||||
return LegacyRouteTableFactory.Create(templatesByHandler);
|
||||
}
|
||||
catch (InvalidOperationException ex) when (ex.InnerException is InvalidOperationException)
|
||||
{
|
||||
// ToArray() will wrap our exception in its own.
|
||||
throw ex.InnerException;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class TestHandler1 { }
|
||||
class TestHandler2 { }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,295 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.LegacyRouteMatching
|
||||
{
|
||||
public class LegacyTemplateParserTests
|
||||
{
|
||||
[Fact]
|
||||
public void Parse_SingleLiteral()
|
||||
{
|
||||
// Arrange
|
||||
var expected = new ExpectedTemplateBuilder().Literal("awesome");
|
||||
|
||||
// Act
|
||||
var actual = LegacyTemplateParser.ParseTemplate("awesome");
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, actual, LegacyRouteTemplateTestComparer.Instance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_SingleParameter()
|
||||
{
|
||||
// Arrange
|
||||
var template = "{p}";
|
||||
|
||||
var expected = new ExpectedTemplateBuilder().Parameter("p");
|
||||
|
||||
// Act
|
||||
var actual = LegacyTemplateParser.ParseTemplate(template);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, actual, LegacyRouteTemplateTestComparer.Instance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_MultipleLiterals()
|
||||
{
|
||||
// Arrange
|
||||
var template = "awesome/cool/super";
|
||||
|
||||
var expected = new ExpectedTemplateBuilder().Literal("awesome").Literal("cool").Literal("super");
|
||||
|
||||
// Act
|
||||
var actual = LegacyTemplateParser.ParseTemplate(template);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, actual, LegacyRouteTemplateTestComparer.Instance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_MultipleParameters()
|
||||
{
|
||||
// Arrange
|
||||
var template = "{p1}/{p2}/{p3}";
|
||||
|
||||
var expected = new ExpectedTemplateBuilder().Parameter("p1").Parameter("p2").Parameter("p3");
|
||||
|
||||
// Act
|
||||
var actual = LegacyTemplateParser.ParseTemplate(template);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, actual, LegacyRouteTemplateTestComparer.Instance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_MultipleOptionalParameters()
|
||||
{
|
||||
// Arrange
|
||||
var template = "{p1?}/{p2?}/{p3?}";
|
||||
|
||||
var expected = new ExpectedTemplateBuilder().Parameter("p1?").Parameter("p2?").Parameter("p3?");
|
||||
|
||||
// Act
|
||||
var actual = LegacyTemplateParser.ParseTemplate(template);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, actual, LegacyRouteTemplateTestComparer.Instance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_SingleCatchAllParameter()
|
||||
{
|
||||
// Arrange
|
||||
var expected = new ExpectedTemplateBuilder().Parameter("p");
|
||||
|
||||
// Act
|
||||
var actual = LegacyTemplateParser.ParseTemplate("{*p}");
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, actual, LegacyRouteTemplateTestComparer.Instance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_MixedLiteralAndCatchAllParameter()
|
||||
{
|
||||
// Arrange
|
||||
var expected = new ExpectedTemplateBuilder().Literal("awesome").Literal("wow").Parameter("p");
|
||||
|
||||
// Act
|
||||
var actual = LegacyTemplateParser.ParseTemplate("awesome/wow/{*p}");
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, actual, LegacyRouteTemplateTestComparer.Instance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_MixedLiteralParameterAndCatchAllParameter()
|
||||
{
|
||||
// Arrange
|
||||
var expected = new ExpectedTemplateBuilder().Literal("awesome").Parameter("p1").Parameter("p2");
|
||||
|
||||
// Act
|
||||
var actual = LegacyTemplateParser.ParseTemplate("awesome/{p1}/{*p2}");
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, actual, LegacyRouteTemplateTestComparer.Instance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvalidTemplate_WithRepeatedParameter()
|
||||
{
|
||||
var ex = Assert.Throws<InvalidOperationException>(
|
||||
() => LegacyTemplateParser.ParseTemplate("{p1}/literal/{p1}"));
|
||||
|
||||
var expectedMessage = "Invalid template '{p1}/literal/{p1}'. The parameter 'Microsoft.AspNetCore.Components.LegacyRouteMatching.LegacyTemplateSegment' appears multiple times.";
|
||||
|
||||
Assert.Equal(expectedMessage, ex.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("p}", "Invalid template 'p}'. Missing '{' in parameter segment 'p}'.")]
|
||||
[InlineData("{p", "Invalid template '{p'. Missing '}' in parameter segment '{p'.")]
|
||||
[InlineData("Literal/p}", "Invalid template 'Literal/p}'. Missing '{' in parameter segment 'p}'.")]
|
||||
[InlineData("Literal/{p", "Invalid template 'Literal/{p'. Missing '}' in parameter segment '{p'.")]
|
||||
[InlineData("p}/Literal", "Invalid template 'p}/Literal'. Missing '{' in parameter segment 'p}'.")]
|
||||
[InlineData("{p/Literal", "Invalid template '{p/Literal'. Missing '}' in parameter segment '{p'.")]
|
||||
[InlineData("Another/p}/Literal", "Invalid template 'Another/p}/Literal'. Missing '{' in parameter segment 'p}'.")]
|
||||
[InlineData("Another/{p/Literal", "Invalid template 'Another/{p/Literal'. Missing '}' in parameter segment '{p'.")]
|
||||
|
||||
public void InvalidTemplate_WithMismatchedBraces(string template, string expectedMessage)
|
||||
{
|
||||
var ex = Assert.Throws<InvalidOperationException>(
|
||||
() => LegacyTemplateParser.ParseTemplate(template));
|
||||
|
||||
Assert.Equal(expectedMessage, ex.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
// * is only allowed at beginning for catch-all parameters
|
||||
[InlineData("{p*}", "Invalid template '{p*}'. The character '*' in parameter segment '{p*}' is not allowed.")]
|
||||
[InlineData("{{}", "Invalid template '{{}'. The character '{' in parameter segment '{{}' is not allowed.")]
|
||||
[InlineData("{}}", "Invalid template '{}}'. The character '}' in parameter segment '{}}' is not allowed.")]
|
||||
[InlineData("{=}", "Invalid template '{=}'. The character '=' in parameter segment '{=}' is not allowed.")]
|
||||
[InlineData("{.}", "Invalid template '{.}'. The character '.' in parameter segment '{.}' is not allowed.")]
|
||||
public void ParseRouteParameter_ThrowsIf_ParameterContainsSpecialCharacters(string template, string expectedMessage)
|
||||
{
|
||||
// Act & Assert
|
||||
var ex = Assert.Throws<InvalidOperationException>(() => LegacyTemplateParser.ParseTemplate(template));
|
||||
|
||||
Assert.Equal(expectedMessage, ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvalidTemplate_InvalidParameterNameWithEmptyNameThrows()
|
||||
{
|
||||
var ex = Assert.Throws<InvalidOperationException>(() => LegacyTemplateParser.ParseTemplate("{a}/{}/{z}"));
|
||||
|
||||
var expectedMessage = "Invalid template '{a}/{}/{z}'. Empty parameter name in segment '{}' is not allowed.";
|
||||
|
||||
Assert.Equal(expectedMessage, ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvalidTemplate_ConsecutiveSeparatorsSlashSlashThrows()
|
||||
{
|
||||
var ex = Assert.Throws<InvalidOperationException>(() => LegacyTemplateParser.ParseTemplate("{a}//{z}"));
|
||||
|
||||
var expectedMessage = "Invalid template '{a}//{z}'. Empty segments are not allowed.";
|
||||
|
||||
Assert.Equal(expectedMessage, ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvalidTemplate_LiteralAfterOptionalParam()
|
||||
{
|
||||
var ex = Assert.Throws<InvalidOperationException>(() => LegacyTemplateParser.ParseTemplate("/test/{a?}/test"));
|
||||
|
||||
var expectedMessage = "Invalid template 'test/{a?}/test'. Non-optional parameters or literal routes cannot appear after optional parameters.";
|
||||
|
||||
Assert.Equal(expectedMessage, ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvalidTemplate_NonOptionalParamAfterOptionalParam()
|
||||
{
|
||||
var ex = Assert.Throws<InvalidOperationException>(() => LegacyTemplateParser.ParseTemplate("/test/{a?}/{b}"));
|
||||
|
||||
var expectedMessage = "Invalid template 'test/{a?}/{b}'. Non-optional parameters or literal routes cannot appear after optional parameters.";
|
||||
|
||||
Assert.Equal(expectedMessage, ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvalidTemplate_CatchAllParamWithMultipleAsterisks()
|
||||
{
|
||||
var ex = Assert.Throws<InvalidOperationException>(() => LegacyTemplateParser.ParseTemplate("/test/{a}/{**b}"));
|
||||
|
||||
var expectedMessage = "Invalid template '/test/{a}/{**b}'. A catch-all parameter may only have one '*' at the beginning of the segment.";
|
||||
|
||||
Assert.Equal(expectedMessage, ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvalidTemplate_CatchAllParamNotLast()
|
||||
{
|
||||
var ex = Assert.Throws<InvalidOperationException>(() => LegacyTemplateParser.ParseTemplate("/test/{*a}/{b}"));
|
||||
|
||||
var expectedMessage = "Invalid template 'test/{*a}/{b}'. A catch-all parameter can only appear as the last segment of the route template.";
|
||||
|
||||
Assert.Equal(expectedMessage, ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvalidTemplate_BadOptionalCharacterPosition()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() => LegacyTemplateParser.ParseTemplate("/test/{a?bc}/{b}"));
|
||||
|
||||
var expectedMessage = "Malformed parameter 'a?bc' in route '/test/{a?bc}/{b}'. '?' character can only appear at the end of parameter name.";
|
||||
|
||||
Assert.Equal(expectedMessage, ex.Message);
|
||||
}
|
||||
|
||||
private class ExpectedTemplateBuilder
|
||||
{
|
||||
public IList<LegacyTemplateSegment> Segments { get; set; } = new List<LegacyTemplateSegment>();
|
||||
|
||||
public ExpectedTemplateBuilder Literal(string value)
|
||||
{
|
||||
Segments.Add(new LegacyTemplateSegment("testtemplate", value, isParameter: false));
|
||||
return this;
|
||||
}
|
||||
|
||||
public ExpectedTemplateBuilder Parameter(string value)
|
||||
{
|
||||
Segments.Add(new LegacyTemplateSegment("testtemplate", value, isParameter: true));
|
||||
return this;
|
||||
}
|
||||
|
||||
public LegacyRouteTemplate Build() => new LegacyRouteTemplate(string.Join('/', Segments), Segments.ToArray());
|
||||
|
||||
public static implicit operator LegacyRouteTemplate(ExpectedTemplateBuilder builder) => builder.Build();
|
||||
}
|
||||
|
||||
private class LegacyRouteTemplateTestComparer : IEqualityComparer<LegacyRouteTemplate>
|
||||
{
|
||||
public static LegacyRouteTemplateTestComparer Instance { get; } = new LegacyRouteTemplateTestComparer();
|
||||
|
||||
public bool Equals(LegacyRouteTemplate x, LegacyRouteTemplate y)
|
||||
{
|
||||
if (x.Segments.Length != y.Segments.Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (var i = 0; i < x.Segments.Length; i++)
|
||||
{
|
||||
var xSegment = x.Segments[i];
|
||||
var ySegment = y.Segments[i];
|
||||
if (xSegment.IsParameter != ySegment.IsParameter)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (xSegment.IsOptional != ySegment.IsOptional)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (!string.Equals(xSegment.Value, ySegment.Value, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public int GetHashCode(LegacyRouteTemplate obj) => 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -32,15 +32,5 @@ namespace Microsoft.AspNetCore.Components.Routing
|
|||
// Assert
|
||||
Assert.Same(original, another);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_DoesNotThrowIfOptionalConstraint()
|
||||
{
|
||||
// Act
|
||||
var exceptions = Record.Exception(() => RouteConstraint.Parse("ignore", "ignore", "int?"));
|
||||
|
||||
// Assert
|
||||
Assert.Null(exceptions);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -325,6 +325,253 @@ namespace Microsoft.AspNetCore.Components.Test.Routing
|
|||
}, context.Parameters);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MoreSpecificRoutesPrecedeMoreGeneralRoutes()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
// Routes are added in reverse precedence order
|
||||
var builder = new TestRouteTableBuilder()
|
||||
.AddRoute("/{*last}")
|
||||
.AddRoute("/{*last:int}")
|
||||
.AddRoute("/{last}")
|
||||
.AddRoute("/{last:int}")
|
||||
.AddRoute("/literal")
|
||||
.AddRoute("/literal/{*last}")
|
||||
.AddRoute("/literal/{*last:int}")
|
||||
.AddRoute("/literal/{last}")
|
||||
.AddRoute("/literal/{last:int}")
|
||||
.AddRoute("/literal/literal");
|
||||
|
||||
var expectedOrder = new[]
|
||||
{
|
||||
"literal",
|
||||
"literal/literal",
|
||||
"literal/{last:int}",
|
||||
"literal/{last}",
|
||||
"literal/{*last:int}",
|
||||
"literal/{*last}",
|
||||
"{last:int}",
|
||||
"{last}",
|
||||
"{*last:int}",
|
||||
"{*last}",
|
||||
};
|
||||
|
||||
// Act
|
||||
var table = builder.Build();
|
||||
|
||||
// Assert
|
||||
var tableTemplates = table.Routes.Select(p => p.Template.TemplateText).ToArray();
|
||||
Assert.Equal(expectedOrder, tableTemplates);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("literal", null, "literal", "literal/{parameter?}", typeof(TestHandler1))]
|
||||
[InlineData("literal/value", "value", "literal", "literal/{parameter?}", typeof(TestHandler2))]
|
||||
[InlineData("literal", null, "literal/{parameter?}", "literal/{*parameter}", typeof(TestHandler1))]
|
||||
[InlineData("literal/value", "value", "literal/{parameter?}", "literal/{*parameter}", typeof(TestHandler1))]
|
||||
[InlineData("literal/value/other", "value/other", "literal /{parameter?}", "literal/{*parameter}", typeof(TestHandler2))]
|
||||
public void CorrectlyMatchesVariableLengthSegments(string path, string expectedValue, string first, string second, Type handler)
|
||||
{
|
||||
// Arrange
|
||||
|
||||
// Routes are added in reverse precedence order
|
||||
var table = new TestRouteTableBuilder()
|
||||
.AddRoute(first, typeof(TestHandler1))
|
||||
.AddRoute(second, typeof(TestHandler2))
|
||||
.Build();
|
||||
|
||||
var context = new RouteContext(path);
|
||||
|
||||
// Act
|
||||
table.Route(context);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(handler, context.Handler);
|
||||
var value = expectedValue != null ? Assert.Single(context.Parameters, p => p.Key == "parameter").Value : null;
|
||||
Assert.Equal(expectedValue, value?.ToString());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("/values/{*values:int}", "values/1/2/3/4/5")]
|
||||
[InlineData("/{*values:int}", "1/2/3/4/5")]
|
||||
public void CanMatchCatchAllParametersWithConstraints(string template, string path)
|
||||
{
|
||||
// Arrange
|
||||
|
||||
// Routes are added in reverse precedence order
|
||||
var table = new TestRouteTableBuilder()
|
||||
.AddRoute(template)
|
||||
.Build();
|
||||
|
||||
var context = new RouteContext(path);
|
||||
|
||||
// Act
|
||||
table.Route(context);
|
||||
|
||||
// Assert
|
||||
Assert.True(context.Parameters.TryGetValue("values", out var values));
|
||||
Assert.Equal("1/2/3/4/5", values);
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
public void CatchAllEmpty()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
// Routes are added in reverse precedence order
|
||||
var table = new TestRouteTableBuilder()
|
||||
.AddRoute("{*catchall}")
|
||||
.Build();
|
||||
|
||||
var context = new RouteContext("/");
|
||||
|
||||
// Act
|
||||
table.Route(context);
|
||||
|
||||
// Assert
|
||||
Assert.True(context.Parameters.TryGetValue("catchall", out var values));
|
||||
Assert.Null(values);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OptionalParameterEmpty()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
// Routes are added in reverse precedence order
|
||||
var table = new TestRouteTableBuilder()
|
||||
.AddRoute("{parameter?}")
|
||||
.Build();
|
||||
|
||||
var context = new RouteContext("/");
|
||||
|
||||
// Act
|
||||
table.Route(context);
|
||||
|
||||
// Assert
|
||||
Assert.True(context.Parameters.TryGetValue("parameter", out var values));
|
||||
Assert.Null(values);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("", 0)]
|
||||
[InlineData("1", 1)]
|
||||
[InlineData("1/2", 2)]
|
||||
[InlineData("1/2/3", 3)]
|
||||
public void MultipleOptionalParameters(string path, int segments)
|
||||
{
|
||||
// Arrange
|
||||
|
||||
// Routes are added in reverse precedence order
|
||||
var table = new TestRouteTableBuilder()
|
||||
.AddRoute("{param1?}/{param2?}/{param3?}")
|
||||
.Build();
|
||||
|
||||
var context = new RouteContext(path);
|
||||
|
||||
// Act
|
||||
table.Route(context);
|
||||
|
||||
// Assert
|
||||
for (int i = 1; i <= segments; i++)
|
||||
{
|
||||
// Segments present in the path have the corresponding value.
|
||||
Assert.True(context.Parameters.TryGetValue($"param{i}", out var value));
|
||||
Assert.Equal(i.ToString(), value);
|
||||
}
|
||||
for (int i = segments + 1; i <= 3; i++)
|
||||
{
|
||||
// Segments omitted in the path have the default null value.
|
||||
Assert.True(context.Parameters.TryGetValue($"param{i}", out var value));
|
||||
Assert.Null(value);
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("prefix/", 0)]
|
||||
[InlineData("prefix/1", 1)]
|
||||
[InlineData("prefix/1/2", 2)]
|
||||
[InlineData("prefix/1/2/3", 3)]
|
||||
public void MultipleOptionalParametersWithPrefix(string path, int segments)
|
||||
{
|
||||
// Arrange
|
||||
|
||||
// Routes are added in reverse precedence order
|
||||
var table = new TestRouteTableBuilder()
|
||||
.AddRoute("prefix/{param1?}/{param2?}/{param3?}")
|
||||
.Build();
|
||||
|
||||
var context = new RouteContext(path);
|
||||
|
||||
// Act
|
||||
table.Route(context);
|
||||
|
||||
// Assert
|
||||
for (int i = 1; i <= segments; i++)
|
||||
{
|
||||
// Segments present in the path have the corresponding value.
|
||||
Assert.True(context.Parameters.TryGetValue($"param{i}", out var value));
|
||||
Assert.Equal(i.ToString(), value);
|
||||
}
|
||||
for (int i = segments + 1; i <= 3; i++)
|
||||
{
|
||||
// Segments omitted in the path have the default null value.
|
||||
Assert.True(context.Parameters.TryGetValue($"param{i}", out var value));
|
||||
Assert.Null(value);
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("/{parameter?}/{*catchAll}", "/", null, null)]
|
||||
[InlineData("/{parameter?}/{*catchAll}", "/parameter", "parameter", null)]
|
||||
[InlineData("/{parameter?}/{*catchAll}", "/value/1", "value", "1")]
|
||||
[InlineData("/{parameter?}/{*catchAll}", "/value/1/2/3/4/5", "value", "1/2/3/4/5")]
|
||||
[InlineData("prefix/{parameter?}/{*catchAll}", "/prefix/", null, null)]
|
||||
[InlineData("prefix/{parameter?}/{*catchAll}", "/prefix/parameter", "parameter", null)]
|
||||
[InlineData("prefix/{parameter?}/{*catchAll}", "/prefix/value/1", "value", "1")]
|
||||
[InlineData("prefix/{parameter?}/{*catchAll}", "/prefix/value/1/2/3/4/5", "value", "1/2/3/4/5")]
|
||||
public void OptionalParameterPlusCatchAllRoute(string template, string path, string parameterValue, string catchAllValue)
|
||||
{
|
||||
// Arrange
|
||||
|
||||
// Routes are added in reverse precedence order
|
||||
var table = new TestRouteTableBuilder()
|
||||
.AddRoute(template)
|
||||
.Build();
|
||||
|
||||
var context = new RouteContext(path);
|
||||
|
||||
// Act
|
||||
table.Route(context);
|
||||
|
||||
// Assert
|
||||
Assert.True(context.Parameters.TryGetValue("parameter", out var parameter));
|
||||
Assert.True(context.Parameters.TryGetValue("catchAll", out var catchAll));
|
||||
Assert.Equal(parameterValue, parameter);
|
||||
Assert.Equal(catchAllValue, catchAll);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanMatchCatchAllParametersWithConstraints_NotMatchingRoute()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
// Routes are added in reverse precedence order
|
||||
var table = new TestRouteTableBuilder()
|
||||
.AddRoute("/values/{*values:int}")
|
||||
.Build();
|
||||
|
||||
var context = new RouteContext("values/1/2/3/4/5/A");
|
||||
|
||||
// Act
|
||||
table.Route(context);
|
||||
|
||||
// Assert
|
||||
Assert.Null(context.Handler);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanMatchOptionalParameterWithoutConstraints()
|
||||
{
|
||||
|
|
@ -411,7 +658,7 @@ namespace Microsoft.AspNetCore.Components.Test.Routing
|
|||
public static IEnumerable<object[]> CanMatchSegmentWithMultipleConstraintsCases() => new object[][]
|
||||
{
|
||||
new object[] { "/{value:double:int}/", "/15", 15 },
|
||||
new object[] { "/{value:double?:int?}/", "/", null },
|
||||
new object[] { "/{value:double:int?}/", "/", null },
|
||||
};
|
||||
|
||||
[Theory]
|
||||
|
|
@ -469,52 +716,111 @@ namespace Microsoft.AspNetCore.Components.Test.Routing
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void PrefersOptionalParamsOverNonOptionalParams()
|
||||
public void ThrowsForOptionalParametersAndNonOptionalParameters()
|
||||
{
|
||||
// Arrange
|
||||
var routeTable = new TestRouteTableBuilder()
|
||||
// Arrange, act & assert
|
||||
Assert.Throws<InvalidOperationException>(() => new TestRouteTableBuilder()
|
||||
.AddRoute("/users/{id}", typeof(TestHandler1))
|
||||
.AddRoute("/users/{id?}", typeof(TestHandler2))
|
||||
.Build();
|
||||
var contextWithParam = new RouteContext("/users/1");
|
||||
var contextWithoutParam = new RouteContext("/users/");
|
||||
.Build());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("{*catchall}/literal")]
|
||||
[InlineData("{*catchall}/{parameter}")]
|
||||
[InlineData("{*catchall}/{parameter?}")]
|
||||
[InlineData("{*catchall}/{*other}")]
|
||||
[InlineData("prefix/{*catchall}/literal")]
|
||||
[InlineData("prefix/{*catchall}/{parameter}")]
|
||||
[InlineData("prefix/{*catchall}/{parameter?}")]
|
||||
[InlineData("prefix/{*catchall}/{*other}")]
|
||||
public void ThrowsWhenCatchAllIsNotTheLastSegment(string template)
|
||||
{
|
||||
// Arrange, act & assert
|
||||
Assert.Throws<InvalidOperationException>(() => new TestRouteTableBuilder()
|
||||
.AddRoute(template)
|
||||
.Build());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("{optional?}/literal")]
|
||||
[InlineData("{optional?}/{parameter}")]
|
||||
[InlineData("{optional?}/{parameter:int}")]
|
||||
[InlineData("prefix/{optional?}/literal")]
|
||||
[InlineData("prefix/{optional?}/{parameter}")]
|
||||
[InlineData("prefix/{optional?}/{parameter:int}")]
|
||||
public void ThrowsForOptionalParametersFollowedByNonOptionalParameters(string template)
|
||||
{
|
||||
// Arrange, act & assert
|
||||
Assert.Throws<InvalidOperationException>(() => new TestRouteTableBuilder()
|
||||
.AddRoute(template)
|
||||
.Build());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("{parameter}", "{parameter?}")]
|
||||
[InlineData("{parameter:int}", "{parameter:bool?}")]
|
||||
public void ThrowsForAmbiguousRoutes(string first, string second)
|
||||
{
|
||||
// Arrange, act & assert
|
||||
var exception = Assert.Throws<InvalidOperationException>(() => new TestRouteTableBuilder()
|
||||
.AddRoute(first, typeof(TestHandler1))
|
||||
.AddRoute(second, typeof(TestHandler2))
|
||||
.Build());
|
||||
|
||||
exception.Message.Contains("The following routes are ambiguous");
|
||||
}
|
||||
|
||||
// It's important the precedence is inverted here to also validate that
|
||||
// the precedence is correct in these cases
|
||||
[Theory]
|
||||
[InlineData("{optional?}", "/")]
|
||||
[InlineData("{optional?}", "literal")]
|
||||
[InlineData("{optional?}", "{optional:int?}")]
|
||||
[InlineData("{*catchAll:int}", "{optional?}")]
|
||||
[InlineData("{*catchAll}", "{optional?}")]
|
||||
[InlineData("literal/{optional?}", "/")]
|
||||
[InlineData("literal/{optional?}", "literal")]
|
||||
[InlineData("literal/{optional?}", "literal/{optional:int?}")]
|
||||
[InlineData("literal/{*catchAll:int}", "literal/{optional?}")]
|
||||
[InlineData("literal/{*catchAll}", "literal/{optional?}")]
|
||||
[InlineData("{param}/{optional?}", "/")]
|
||||
[InlineData("{param}/{optional?}", "{param}")]
|
||||
[InlineData("{param}/{optional?}", "{param}/{optional:int?}")]
|
||||
[InlineData("{param}/{*catchAll:int}", "{param}/{optional?}")]
|
||||
[InlineData("{param}/{*catchAll}", "{param}/{optional?}")]
|
||||
[InlineData("{param1?}/{param2?}/{param3?}/{optional?}", "/")]
|
||||
[InlineData("{param1?}/{param2?}/{param3?}/{optional?}", "{param1?}/{param2?}/{param3?}/{optional:int?}")]
|
||||
[InlineData("{param1?}/{param2?}/{param3?}/{optional?}", "{param1?}/{param2?}/{param3:int?}/{optional?}")]
|
||||
[InlineData("{param1?}/{param2?}/{param3:int?}/{optional?}", "{param1?}/{param2?}")]
|
||||
[InlineData("{param1?}/{param2?}/{param3?}/{*catchAll:int}", "{param1?}/{param2?}/{param3?}/{optional?}")]
|
||||
[InlineData("{param1?}/{param2?}/{param3?}/{*catchAll}", "{param1?}/{param2?}/{param3?}/{optional?}")]
|
||||
public void DoesNotThrowForNonAmbiguousRoutes(string first, string second)
|
||||
{
|
||||
// Arrange
|
||||
var builder = new TestRouteTableBuilder()
|
||||
.AddRoute(first, typeof(TestHandler1))
|
||||
.AddRoute(second, typeof(TestHandler2));
|
||||
|
||||
var expectedOrder = new[] { second, first };
|
||||
|
||||
// Act
|
||||
routeTable.Route(contextWithParam);
|
||||
routeTable.Route(contextWithoutParam);
|
||||
var table = builder.Build();
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(contextWithParam.Handler);
|
||||
Assert.Equal(typeof(TestHandler1), contextWithParam.Handler);
|
||||
|
||||
Assert.NotNull(contextWithoutParam.Handler);
|
||||
Assert.Equal(typeof(TestHandler2), contextWithoutParam.Handler);
|
||||
var tableTemplates = table.Routes.Select(p => p.Template.TemplateText).ToArray();
|
||||
Assert.Equal(expectedOrder, tableTemplates);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PrefersOptionalParamsOverNonOptionalParamsReverseOrder()
|
||||
public void ThrowsForLiteralWithQuestionMark()
|
||||
{
|
||||
// Arrange
|
||||
var routeTable = new TestRouteTableBuilder()
|
||||
.AddRoute("/users/{id}", typeof(TestHandler1))
|
||||
.AddRoute("/users/{id?}", typeof(TestHandler2))
|
||||
.Build();
|
||||
var contextWithParam = new RouteContext("/users/1");
|
||||
var contextWithoutParam = new RouteContext("/users/");
|
||||
|
||||
// Act
|
||||
routeTable.Route(contextWithParam);
|
||||
routeTable.Route(contextWithoutParam);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(contextWithParam.Handler);
|
||||
Assert.Equal(typeof(TestHandler1), contextWithParam.Handler);
|
||||
|
||||
Assert.NotNull(contextWithoutParam.Handler);
|
||||
Assert.Equal(typeof(TestHandler2), contextWithoutParam.Handler);
|
||||
// Arrange, act & assert
|
||||
Assert.Throws<InvalidOperationException>(() => new TestRouteTableBuilder()
|
||||
.AddRoute("literal?")
|
||||
.Build());
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
public void PrefersLiteralTemplateOverParameterizedTemplates()
|
||||
{
|
||||
|
|
@ -660,10 +966,10 @@ namespace Microsoft.AspNetCore.Components.Test.Routing
|
|||
{
|
||||
// Arrange
|
||||
var routeTable = new TestRouteTableBuilder()
|
||||
.AddRoute("/", typeof(TestHandler1))
|
||||
.AddRoute("/products/{param1:int}", typeof(TestHandler1))
|
||||
.AddRoute("/products/{param2}/{PaRam1}", typeof(TestHandler1))
|
||||
.AddRoute("/{unrelated}", typeof(TestHandler2))
|
||||
.AddRoute("/products/{param2}/{PaRam1}", typeof(TestHandler1))
|
||||
.AddRoute("/products/{param1:int}", typeof(TestHandler1))
|
||||
.AddRoute("/", typeof(TestHandler1))
|
||||
.Build();
|
||||
var context = new RouteContext("/products/456");
|
||||
|
||||
|
|
@ -676,26 +982,27 @@ namespace Microsoft.AspNetCore.Components.Test.Routing
|
|||
{
|
||||
Assert.Same(typeof(TestHandler1), route.Handler);
|
||||
Assert.Equal("/", route.Template.TemplateText);
|
||||
Assert.Equal(new[] { "param1", "param2" }, route.UnusedRouteParameterNames);
|
||||
},
|
||||
route =>
|
||||
{
|
||||
Assert.Same(typeof(TestHandler2), route.Handler);
|
||||
Assert.Equal("{unrelated}", route.Template.TemplateText);
|
||||
Assert.Equal(Array.Empty<string>(), route.UnusedRouteParameterNames);
|
||||
Assert.Equal(new[] { "PaRam1", "param2" }, route.UnusedRouteParameterNames.OrderBy(id => id).ToArray());
|
||||
},
|
||||
route =>
|
||||
{
|
||||
Assert.Same(typeof(TestHandler1), route.Handler);
|
||||
Assert.Equal("products/{param1:int}", route.Template.TemplateText);
|
||||
Assert.Equal(new[] { "param2" }, route.UnusedRouteParameterNames);
|
||||
Assert.Equal(new[] { "param2" }, route.UnusedRouteParameterNames.OrderBy(id => id).ToArray());
|
||||
},
|
||||
route =>
|
||||
{
|
||||
Assert.Same(typeof(TestHandler1), route.Handler);
|
||||
Assert.Equal("products/{param2}/{PaRam1}", route.Template.TemplateText);
|
||||
Assert.Equal(Array.Empty<string>(), route.UnusedRouteParameterNames);
|
||||
Assert.Equal(Array.Empty<string>(), route.UnusedRouteParameterNames.OrderBy(id => id).ToArray());
|
||||
},
|
||||
route =>
|
||||
{
|
||||
Assert.Same(typeof(TestHandler2), route.Handler);
|
||||
Assert.Equal("{unrelated}", route.Template.TemplateText);
|
||||
Assert.Equal(Array.Empty<string>(), route.UnusedRouteParameterNames.OrderBy(id => id).ToArray());
|
||||
});
|
||||
|
||||
Assert.Same(typeof(TestHandler1), context.Handler);
|
||||
Assert.Equal(new Dictionary<string, object>
|
||||
{
|
||||
|
|
|
|||
|
|
@ -2,9 +2,12 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Components.RenderTree;
|
||||
using Microsoft.AspNetCore.Components.Routing;
|
||||
using Microsoft.AspNetCore.Components.Test.Helpers;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
|
@ -17,13 +20,15 @@ namespace Microsoft.AspNetCore.Components.Test.Routing
|
|||
public class RouterTest
|
||||
{
|
||||
private readonly Router _router;
|
||||
private readonly TestNavigationManager _navigationManager;
|
||||
private readonly TestRenderer _renderer;
|
||||
|
||||
public RouterTest()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
_navigationManager = new TestNavigationManager();
|
||||
services.AddSingleton<ILoggerFactory>(NullLoggerFactory.Instance);
|
||||
services.AddSingleton<NavigationManager, TestNavigationManager>();
|
||||
services.AddSingleton<NavigationManager>(_navigationManager);
|
||||
services.AddSingleton<INavigationInterception, TestNavigationInterception>();
|
||||
var serviceProvider = services.BuildServiceProvider();
|
||||
|
||||
|
|
@ -31,7 +36,7 @@ namespace Microsoft.AspNetCore.Components.Test.Routing
|
|||
_renderer.ShouldHandleExceptions = true;
|
||||
_router = (Router)_renderer.InstantiateComponent<Router>();
|
||||
_router.AppAssembly = Assembly.GetExecutingAssembly();
|
||||
_router.Found = routeData => (builder) => builder.AddContent(0, "Rendering route...");
|
||||
_router.Found = routeData => (builder) => builder.AddContent(0, $"Rendering route matching {routeData.PageType}");
|
||||
_renderer.AssignRootComponentId(_router);
|
||||
}
|
||||
|
||||
|
|
@ -177,12 +182,65 @@ namespace Microsoft.AspNetCore.Components.Test.Routing
|
|||
await feb;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UsesLegacyRouteMatchingByDefault()
|
||||
{
|
||||
// Arrange
|
||||
// Legacy routing prefers {*someWildcard} over any other pattern than has more segments,
|
||||
// even if the other pattern is an exact match
|
||||
_navigationManager.NotifyLocationChanged("https://www.example.com/subdir/a/b", false);
|
||||
var parameters = new Dictionary<string, object>
|
||||
{
|
||||
{ nameof(Router.AppAssembly), typeof(RouterTest).Assembly },
|
||||
{ nameof(Router.NotFound), (RenderFragment)(builder => { }) },
|
||||
};
|
||||
|
||||
// Act
|
||||
await _renderer.Dispatcher.InvokeAsync(() =>
|
||||
_router.SetParametersAsync(ParameterView.FromDictionary(parameters)));
|
||||
|
||||
// Assert
|
||||
var renderedFrame = _renderer.Batches.First().ReferenceFrames.First();
|
||||
Assert.Equal(RenderTreeFrameType.Text, renderedFrame.FrameType);
|
||||
Assert.Equal($"Rendering route matching {typeof(MatchAnythingComponent)}", renderedFrame.TextContent);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UsesCurrentRouteMatchingIfSpecified()
|
||||
{
|
||||
// Arrange
|
||||
// Current routing prefers exactly-matched patterns over {*someWildcard}, no matter
|
||||
// how many segments are in the exact match
|
||||
_navigationManager.NotifyLocationChanged("https://www.example.com/subdir/a/b", false);
|
||||
var parameters = new Dictionary<string, object>
|
||||
{
|
||||
{ nameof(Router.AppAssembly), typeof(RouterTest).Assembly },
|
||||
{ nameof(Router.NotFound), (RenderFragment)(builder => { }) },
|
||||
{ nameof(Router.PreferExactMatches), true },
|
||||
};
|
||||
|
||||
// Act
|
||||
await _renderer.Dispatcher.InvokeAsync(() =>
|
||||
_router.SetParametersAsync(ParameterView.FromDictionary(parameters)));
|
||||
|
||||
// Assert
|
||||
var renderedFrame = _renderer.Batches.First().ReferenceFrames.First();
|
||||
Assert.Equal(RenderTreeFrameType.Text, renderedFrame.FrameType);
|
||||
Assert.Equal($"Rendering route matching {typeof(MultiSegmentRouteComponent)}", renderedFrame.TextContent);
|
||||
}
|
||||
|
||||
internal class TestNavigationManager : NavigationManager
|
||||
{
|
||||
public TestNavigationManager() =>
|
||||
Initialize("https://www.example.com/subdir/", "https://www.example.com/subdir/jan");
|
||||
|
||||
protected override void NavigateToCore(string uri, bool forceLoad) => throw new NotImplementedException();
|
||||
|
||||
public void NotifyLocationChanged(string uri, bool intercepted)
|
||||
{
|
||||
Uri = uri;
|
||||
NotifyLocationChanged(intercepted);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class TestNavigationInterception : INavigationInterception
|
||||
|
|
@ -200,5 +258,11 @@ namespace Microsoft.AspNetCore.Components.Test.Routing
|
|||
|
||||
[Route("jan")]
|
||||
public class JanComponent : ComponentBase { }
|
||||
|
||||
[Route("{*matchAnything}")]
|
||||
public class MatchAnythingComponent : ComponentBase { }
|
||||
|
||||
[Route("a/b")]
|
||||
public class MultiSegmentRouteComponent : ComponentBase { }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -128,7 +128,7 @@ namespace Microsoft.AspNetCore.Components.Routing
|
|||
var ex = Assert.Throws<InvalidOperationException>(
|
||||
() => TemplateParser.ParseTemplate("{p1}/literal/{p1}"));
|
||||
|
||||
var expectedMessage = "Invalid template '{p1}/literal/{p1}'. The parameter 'Microsoft.AspNetCore.Components.Routing.TemplateSegment' appears multiple times.";
|
||||
var expectedMessage = "Invalid template '{p1}/literal/{p1}'. The parameter '{p1}' appears multiple times.";
|
||||
|
||||
Assert.Equal(expectedMessage, ex.Message);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<Router AppAssembly="@typeof(Program).Assembly">
|
||||
<Router AppAssembly="@typeof(Program).Assembly" PreferExactMatches="true">
|
||||
<Found Context="routeData">
|
||||
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
|
||||
</Found>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<Router AppAssembly="@typeof(Program).Assembly">
|
||||
<Router AppAssembly="@typeof(Program).Assembly" PreferExactMatches="true">
|
||||
<Found Context="routeData">
|
||||
<RouteView RouteData="@routeData"/>
|
||||
</Found>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<Router AppAssembly="@typeof(Program).Assembly">
|
||||
<Router AppAssembly="@typeof(Program).Assembly" PreferExactMatches="true">
|
||||
<Found Context="routeData">
|
||||
<RouteView RouteData="@routeData"/>
|
||||
</Found>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<Router AppAssembly=typeof(StandaloneApp.Program).Assembly>
|
||||
<Router AppAssembly="@typeof(StandaloneApp.Program).Assembly" PreferExactMatches="true">
|
||||
<Found Context="routeData">
|
||||
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
|
||||
</Found>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<CascadingAuthenticationState>
|
||||
<Router AppAssembly="@typeof(Program).Assembly">
|
||||
<Router AppAssembly="@typeof(Program).Assembly" PreferExactMatches="true">
|
||||
<Found Context="routeData">
|
||||
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
|
||||
<NotAuthorized>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<Router AppAssembly="typeof(Program).Assembly">
|
||||
<Router AppAssembly="typeof(Program).Assembly" PreferExactMatches="true">
|
||||
<NotFound>Page not found</NotFound>
|
||||
<Found Context="routeData">
|
||||
<RouteView RouteData="routeData" DefaultLayout="typeof(MainLayout)"></RouteView>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<Router AppAssembly=typeof(Program).Assembly>
|
||||
<Router AppAssembly="@typeof(Program).Assembly" PreferExactMatches="true">
|
||||
<Found Context="routeData">
|
||||
<RouteView RouteData="@routeData" />
|
||||
</Found>
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
and @page authorization rules.
|
||||
*@
|
||||
|
||||
<Router AppAssembly="@typeof(BasicTestApp.Program).Assembly">
|
||||
<Router AppAssembly="@typeof(BasicTestApp.Program).Assembly" PreferExactMatches="true">
|
||||
<Found Context="routeData">
|
||||
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(AuthRouterLayout)">
|
||||
<Authorizing>Authorizing...</Authorizing>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
@using Microsoft.AspNetCore.Components.Routing
|
||||
<Router AppAssembly="@typeof(BasicTestApp.Program).Assembly">
|
||||
<Router AppAssembly="@typeof(BasicTestApp.Program).Assembly" PreferExactMatches="true">
|
||||
<Found Context="routeData">
|
||||
<RouteView RouteData="@routeData" />
|
||||
</Found>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
@using Microsoft.AspNetCore.Components.Routing
|
||||
<Router AppAssembly="@typeof(BasicTestApp.Program).Assembly" AdditionalAssemblies="@(new[] { typeof(TestContentPackage.RouteableComponentFromPackage).Assembly, })">
|
||||
<Router AppAssembly="@typeof(BasicTestApp.Program).Assembly" AdditionalAssemblies="@(new[] { typeof(TestContentPackage.RouteableComponentFromPackage).Assembly, })" PreferExactMatches="true">
|
||||
<Found Context="routeData">
|
||||
<RouteView RouteData="@routeData" />
|
||||
</Found>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
@inject LazyAssemblyLoader lazyLoader
|
||||
|
||||
<Router AppAssembly="@typeof(BasicTestApp.Program).Assembly" AdditionalAssemblies="@lazyLoadedAssemblies" OnNavigateAsync="@OnNavigateAsync">
|
||||
<Router AppAssembly="@typeof(BasicTestApp.Program).Assembly" AdditionalAssemblies="@lazyLoadedAssemblies" OnNavigateAsync="@OnNavigateAsync" PreferExactMatches="true">
|
||||
<Navigating>
|
||||
<div style="padding: 20px;background-color:blue;color:white;" id="loading-banner">
|
||||
<p>Loading the requested page...</p>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
<button @onclick="TriggerRerender" id="trigger-rerender">Trigger Rerender</button>
|
||||
|
||||
<Router AppAssembly="@typeof(BasicTestApp.Program).Assembly" OnNavigateAsync="@OnNavigateAsync">
|
||||
<Router AppAssembly="@typeof(BasicTestApp.Program).Assembly" OnNavigateAsync="@OnNavigateAsync" PreferExactMatches="true">
|
||||
<Navigating>
|
||||
<div style="padding: 20px;background-color:blue;color:white;" id="loading-banner">
|
||||
<p>Loading the requested page...</p>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
@using Microsoft.AspNetCore.Components;
|
||||
<CascadingValue Value="Name" Name="Name" IsFixed=true>
|
||||
<Router AppAssembly="@typeof(ComponentsApp.App.App).Assembly">
|
||||
<Router AppAssembly="@typeof(ComponentsApp.App.App).Assembly" PreferExactMatches="true">
|
||||
<Found Context="routeData">
|
||||
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
|
||||
</Found>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
@*#if (NoAuth)
|
||||
<Router AppAssembly="@typeof(Program).Assembly">
|
||||
<Router AppAssembly="@typeof(Program).Assembly" PreferExactMatches="true">
|
||||
<Found Context="routeData">
|
||||
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
|
||||
</Found>
|
||||
|
|
@ -11,7 +11,7 @@
|
|||
</Router>
|
||||
#else
|
||||
<CascadingAuthenticationState>
|
||||
<Router AppAssembly="@typeof(Program).Assembly">
|
||||
<Router AppAssembly="@typeof(Program).Assembly" PreferExactMatches="true">
|
||||
<Found Context="routeData">
|
||||
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
|
||||
</Found>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
@*#if (NoAuth)
|
||||
<Router AppAssembly="@typeof(Program).Assembly">
|
||||
<Router AppAssembly="@typeof(Program).Assembly" PreferExactMatches="true">
|
||||
<Found Context="routeData">
|
||||
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
|
||||
</Found>
|
||||
|
|
@ -11,7 +11,7 @@
|
|||
</Router>
|
||||
#else
|
||||
<CascadingAuthenticationState>
|
||||
<Router AppAssembly="@typeof(Program).Assembly">
|
||||
<Router AppAssembly="@typeof(Program).Assembly" PreferExactMatches="true">
|
||||
<Found Context="routeData">
|
||||
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
|
||||
<NotAuthorized>
|
||||
|
|
|
|||
Loading…
Reference in New Issue