Support route constraints of the form ":type" (e.g, ":int", ":guid", etc.)
This commit is contained in:
parent
c58df0b739
commit
a88ab0db49
|
|
@ -26,7 +26,7 @@ namespace Microsoft.AspNetCore.Blazor.Layouts
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the parameters to pass to the page.
|
/// Gets or sets the parameters to pass to the page.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IDictionary<string, string> PageParameters { get; set; }
|
public IDictionary<string, object> PageParameters { get; set; }
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void Init(RenderHandle renderHandle)
|
public void Init(RenderHandle renderHandle)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
// 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.Globalization;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Blazor.Routing
|
||||||
|
{
|
||||||
|
internal abstract class RouteConstraint
|
||||||
|
{
|
||||||
|
private static readonly IDictionary<string, RouteConstraint> _cachedConstraints
|
||||||
|
= new Dictionary<string, RouteConstraint>();
|
||||||
|
|
||||||
|
public abstract bool Match(string pathSegment, out object convertedValue);
|
||||||
|
|
||||||
|
public static RouteConstraint 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)
|
||||||
|
{
|
||||||
|
_cachedConstraints[constraint] = newInstance;
|
||||||
|
return newInstance;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new ArgumentException($"Unsupported constraint '{constraint}' in route '{template}'.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RouteConstraint CreateRouteConstraint(string constraint)
|
||||||
|
{
|
||||||
|
switch (constraint)
|
||||||
|
{
|
||||||
|
case "bool":
|
||||||
|
return new TypeRouteConstraint<bool>(bool.TryParse);
|
||||||
|
case "datetime":
|
||||||
|
return new TypeRouteConstraint<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 "double":
|
||||||
|
return new TypeRouteConstraint<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 "guid":
|
||||||
|
return new TypeRouteConstraint<Guid>(Guid.TryParse);
|
||||||
|
case "int":
|
||||||
|
return new TypeRouteConstraint<int>((string str, out int result)
|
||||||
|
=> int.TryParse(str, NumberStyles.None, CultureInfo.InvariantCulture, out result));
|
||||||
|
case "long":
|
||||||
|
return new TypeRouteConstraint<long>((string str, out long result)
|
||||||
|
=> long.TryParse(str, NumberStyles.None, CultureInfo.InvariantCulture, out result));
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -21,6 +21,6 @@ namespace Microsoft.AspNetCore.Blazor.Routing
|
||||||
|
|
||||||
public Type Handler { get; set; }
|
public Type Handler { get; set; }
|
||||||
|
|
||||||
public IDictionary<string, string> Parameters { get; set; }
|
public IDictionary<string, object> Parameters { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -26,12 +26,12 @@ namespace Microsoft.AspNetCore.Blazor.Routing
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parameters will be lazily initialized.
|
// Parameters will be lazily initialized.
|
||||||
IDictionary<string, string> parameters = null;
|
IDictionary<string, object> parameters = null;
|
||||||
for (int i = 0; i < Template.Segments.Length; i++)
|
for (int i = 0; i < Template.Segments.Length; i++)
|
||||||
{
|
{
|
||||||
var segment = Template.Segments[i];
|
var segment = Template.Segments[i];
|
||||||
var pathSegment = context.Segments[i];
|
var pathSegment = context.Segments[i];
|
||||||
if (!segment.Match(pathSegment))
|
if (!segment.Match(pathSegment, out var matchedParameterValue))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -39,7 +39,7 @@ namespace Microsoft.AspNetCore.Blazor.Routing
|
||||||
{
|
{
|
||||||
if (segment.IsParameter)
|
if (segment.IsParameter)
|
||||||
{
|
{
|
||||||
GetParameters()[segment.Value] = pathSegment;
|
GetParameters()[segment.Value] = matchedParameterValue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -47,11 +47,11 @@ namespace Microsoft.AspNetCore.Blazor.Routing
|
||||||
context.Parameters = parameters;
|
context.Parameters = parameters;
|
||||||
context.Handler = Handler;
|
context.Handler = Handler;
|
||||||
|
|
||||||
IDictionary<string, string> GetParameters()
|
IDictionary<string, object> GetParameters()
|
||||||
{
|
{
|
||||||
if (parameters == null)
|
if (parameters == null)
|
||||||
{
|
{
|
||||||
parameters = new Dictionary<string, string>();
|
parameters = new Dictionary<string, object>();
|
||||||
}
|
}
|
||||||
|
|
||||||
return parameters;
|
return parameters;
|
||||||
|
|
|
||||||
|
|
@ -43,10 +43,12 @@ namespace Microsoft.AspNetCore.Blazor.Routing
|
||||||
/// less specific. The specificity of a route is given by the specificity
|
/// less specific. The specificity of a route is given by the specificity
|
||||||
/// of its segments and the position of those segments in the route.
|
/// of its segments and the position of those segments in the route.
|
||||||
/// * A literal segment is more specific than a parameter segment.
|
/// * 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.
|
/// * Segment earlier in the route are evaluated before segments later in the route.
|
||||||
/// For example:
|
/// For example:
|
||||||
/// /Literal is more specific than /Parameter
|
/// /Literal is more specific than /Parameter
|
||||||
/// /Route/With/{parameter} is more specific than /{multiple}/With/{parameters}
|
/// /Route/With/{parameter} is more specific than /{multiple}/With/{parameters}
|
||||||
|
/// /Product/{id:int} is more specific than /Product/{id}
|
||||||
///
|
///
|
||||||
/// Routes can be ambigous if:
|
/// Routes can be ambigous if:
|
||||||
/// They are composed of literals and those literals have the same values (case insensitive)
|
/// They are composed of literals and those literals have the same values (case insensitive)
|
||||||
|
|
@ -55,11 +57,13 @@ namespace Microsoft.AspNetCore.Blazor.Routing
|
||||||
/// For example:
|
/// For example:
|
||||||
/// * /literal and /Literal
|
/// * /literal and /Literal
|
||||||
/// /{parameter}/literal and /{something}/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:
|
/// To calculate the precedence we sort the list of routes as follows:
|
||||||
/// * Shorter routes go first.
|
/// * Shorter routes go first.
|
||||||
/// * A literal wins over a parameter in precedence.
|
/// * A literal wins over a parameter in precedence.
|
||||||
/// * For literals with different values (case insenitive) we choose the lexical order
|
/// * For literals with different values (case insenitive) 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 ambigous pair of routes.
|
/// If we get to the end of the comparison routing we've detected an ambigous pair of routes.
|
||||||
internal static int RouteComparison(RouteEntry x, RouteEntry y)
|
internal static int RouteComparison(RouteEntry x, RouteEntry y)
|
||||||
{
|
{
|
||||||
|
|
@ -83,22 +87,19 @@ namespace Microsoft.AspNetCore.Blazor.Routing
|
||||||
{
|
{
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = 0; i < xTemplate.Segments.Length; i++)
|
if (xSegment.IsParameter)
|
||||||
{
|
|
||||||
var xSegment = xTemplate.Segments[i];
|
|
||||||
var ySegment = yTemplate.Segments[i];
|
|
||||||
if (!xSegment.IsParameter && ySegment.IsParameter)
|
|
||||||
{
|
{
|
||||||
return -1;
|
if (xSegment.Constraints.Length > ySegment.Constraints.Length)
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
else if (xSegment.Constraints.Length < ySegment.Constraints.Length)
|
||||||
|
{
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (xSegment.IsParameter && !ySegment.IsParameter)
|
else
|
||||||
{
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!xSegment.IsParameter)
|
|
||||||
{
|
{
|
||||||
var comparison = string.Compare(xSegment.Value, ySegment.Value, StringComparison.OrdinalIgnoreCase);
|
var comparison = string.Compare(xSegment.Value, ySegment.Value, StringComparison.OrdinalIgnoreCase);
|
||||||
if (comparison != 0)
|
if (comparison != 0)
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,7 @@ namespace Microsoft.AspNetCore.Blazor.Routing
|
||||||
: str.Substring(0, firstIndex);
|
: str.Substring(0, firstIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected virtual void Render(RenderTreeBuilder builder, Type handler, IDictionary<string, string> parameters)
|
protected virtual void Render(RenderTreeBuilder builder, Type handler, IDictionary<string, object> parameters)
|
||||||
{
|
{
|
||||||
builder.OpenComponent(0, typeof(LayoutDisplay));
|
builder.OpenComponent(0, typeof(LayoutDisplay));
|
||||||
builder.AddAttribute(1, nameof(LayoutDisplay.Page), handler);
|
builder.AddAttribute(1, nameof(LayoutDisplay.Page), handler);
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ namespace Microsoft.AspNetCore.Blazor.Routing
|
||||||
// a more performant/properly designed routing set of abstractions.
|
// a more performant/properly designed routing set of abstractions.
|
||||||
// To be more precise these are some things we are scoping out:
|
// To be more precise these are some things we are scoping out:
|
||||||
// * We are not doing link generation.
|
// * We are not doing link generation.
|
||||||
// * We are not supporting route constraints.
|
// * 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
|
// The class in here just takes care of parsing a route and extracting
|
||||||
// simple parameters from it.
|
// simple parameters from it.
|
||||||
// Some differences with ASP.NET Core routes are:
|
// Some differences with ASP.NET Core routes are:
|
||||||
|
|
@ -21,10 +21,11 @@ namespace Microsoft.AspNetCore.Blazor.Routing
|
||||||
internal class TemplateParser
|
internal class TemplateParser
|
||||||
{
|
{
|
||||||
public static readonly char[] InvalidParameterNameCharacters =
|
public static readonly char[] InvalidParameterNameCharacters =
|
||||||
new char[] { '*', '?', '{', '}', '=', '.', ':' };
|
new char[] { '*', '?', '{', '}', '=', '.' };
|
||||||
|
|
||||||
internal static RouteTemplate ParseTemplate(string template)
|
internal static RouteTemplate ParseTemplate(string template)
|
||||||
{
|
{
|
||||||
|
var originalTemplate = template;
|
||||||
template = template.Trim('/');
|
template = template.Trim('/');
|
||||||
if (template == "")
|
if (template == "")
|
||||||
{
|
{
|
||||||
|
|
@ -50,7 +51,7 @@ namespace Microsoft.AspNetCore.Blazor.Routing
|
||||||
throw new InvalidOperationException(
|
throw new InvalidOperationException(
|
||||||
$"Invalid template '{template}'. Missing '{{' in parameter segment '{segment}'.");
|
$"Invalid template '{template}'. Missing '{{' in parameter segment '{segment}'.");
|
||||||
}
|
}
|
||||||
templateSegments[i] = new TemplateSegment(segment, isParameter: false);
|
templateSegments[i] = new TemplateSegment(originalTemplate, segment, isParameter: false);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
@ -73,7 +74,7 @@ namespace Microsoft.AspNetCore.Blazor.Routing
|
||||||
$"Invalid template '{template}'. The character '{segment[invalidCharacter]}' in parameter segment '{segment}' is not allowed.");
|
$"Invalid template '{template}'. The character '{segment[invalidCharacter]}' in parameter segment '{segment}' is not allowed.");
|
||||||
}
|
}
|
||||||
|
|
||||||
templateSegments[i] = new TemplateSegment(segment.Substring(1, segment.Length - 2), isParameter: true);
|
templateSegments[i] = new TemplateSegment(originalTemplate, segment.Substring(1, segment.Length - 2), isParameter: true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,15 +2,34 @@
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Blazor.Routing
|
namespace Microsoft.AspNetCore.Blazor.Routing
|
||||||
{
|
{
|
||||||
internal class TemplateSegment
|
internal class TemplateSegment
|
||||||
{
|
{
|
||||||
public TemplateSegment(string segment, bool isParameter)
|
public TemplateSegment(string template, string segment, bool isParameter)
|
||||||
{
|
{
|
||||||
Value = segment;
|
|
||||||
IsParameter = isParameter;
|
IsParameter = isParameter;
|
||||||
|
|
||||||
|
if (!isParameter || segment.IndexOf(':') < 0)
|
||||||
|
{
|
||||||
|
Value = segment;
|
||||||
|
Constraints = Array.Empty<RouteConstraint>();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var tokens = segment.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];
|
||||||
|
Constraints = tokens.Skip(1)
|
||||||
|
.Select(token => RouteConstraint.Parse(template, segment, token))
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The value of the segment. The exact text to match when is a literal.
|
// The value of the segment. The exact text to match when is a literal.
|
||||||
|
|
@ -19,14 +38,27 @@ namespace Microsoft.AspNetCore.Blazor.Routing
|
||||||
|
|
||||||
public bool IsParameter { get; }
|
public bool IsParameter { get; }
|
||||||
|
|
||||||
public bool Match(string pathSegment)
|
public RouteConstraint[] Constraints { get; }
|
||||||
|
|
||||||
|
public bool Match(string pathSegment, out object matchedParameterValue)
|
||||||
{
|
{
|
||||||
if (IsParameter)
|
if (IsParameter)
|
||||||
{
|
{
|
||||||
|
matchedParameterValue = pathSegment;
|
||||||
|
|
||||||
|
foreach (var constraint in Constraints)
|
||||||
|
{
|
||||||
|
if (!constraint.Match(pathSegment, out matchedParameterValue))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
matchedParameterValue = null;
|
||||||
return string.Equals(Value, pathSegment, StringComparison.OrdinalIgnoreCase);
|
return string.Equals(Value, pathSegment, StringComparison.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
// 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.Blazor.Routing
|
||||||
|
{
|
||||||
|
/// <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 TypeRouteConstraint<T> : RouteConstraint
|
||||||
|
{
|
||||||
|
public delegate bool TryParseDelegate(string str, out T result);
|
||||||
|
|
||||||
|
private readonly TryParseDelegate _parser;
|
||||||
|
|
||||||
|
public TypeRouteConstraint(TryParseDelegate 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -81,6 +81,28 @@ namespace Microsoft.AspNetCore.Blazor.Test.Routing
|
||||||
Assert.Null(context.Handler);
|
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]
|
[Theory]
|
||||||
[InlineData("/some")]
|
[InlineData("/some")]
|
||||||
[InlineData("/some/awesome/route/with/extra/segments")]
|
[InlineData("/some/awesome/route/with/extra/segments")]
|
||||||
|
|
@ -111,7 +133,7 @@ namespace Microsoft.AspNetCore.Blazor.Test.Routing
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.NotNull(context.Handler);
|
Assert.NotNull(context.Handler);
|
||||||
Assert.Single(context.Parameters, p => p.Key == "parameter" && p.Value == expectedValue);
|
Assert.Single(context.Parameters, p => p.Key == "parameter" && (string)p.Value == expectedValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -121,7 +143,7 @@ namespace Microsoft.AspNetCore.Blazor.Test.Routing
|
||||||
var routeTable = new TestRouteTableBuilder().AddRoute("/{some}/awesome/{route}/").Build();
|
var routeTable = new TestRouteTableBuilder().AddRoute("/{some}/awesome/{route}/").Build();
|
||||||
var context = new RouteContext("/an/awesome/path");
|
var context = new RouteContext("/an/awesome/path");
|
||||||
|
|
||||||
var expectedParameters = new Dictionary<string, string>
|
var expectedParameters = new Dictionary<string, object>
|
||||||
{
|
{
|
||||||
["some"] = "an",
|
["some"] = "an",
|
||||||
["route"] = "path"
|
["route"] = "path"
|
||||||
|
|
@ -135,6 +157,60 @@ namespace Microsoft.AspNetCore.Blazor.Test.Routing
|
||||||
Assert.Equal(expectedParameters, context.Parameters);
|
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:long}", "/9223372036854775807", long.MaxValue },
|
||||||
|
};
|
||||||
|
|
||||||
|
[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(context.Parameters, new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
{ "value", convertedValue }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CanMatchSegmentWithMultipleConstraints()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var routeTable = new TestRouteTableBuilder().AddRoute("/{value:double:int}/").Build();
|
||||||
|
var context = new RouteContext("/15");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
routeTable.Route(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(context.Handler);
|
||||||
|
Assert.Equal(context.Parameters, new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
{ "value", 15 } // Final constraint's convertedValue is used
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void PrefersLiteralTemplateOverTemplateWithParameters()
|
public void PrefersLiteralTemplateOverTemplateWithParameters()
|
||||||
{
|
{
|
||||||
|
|
@ -165,6 +241,26 @@ namespace Microsoft.AspNetCore.Blazor.Test.Routing
|
||||||
Assert.Equal("an/awesome", routeTable.Routes[0].Template.TemplateText);
|
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]
|
[Fact]
|
||||||
public void ProducesAStableOrderForNonAmbiguousRoutes()
|
public void ProducesAStableOrderForNonAmbiguousRoutes()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -104,7 +104,6 @@ namespace Microsoft.AspNetCore.Blazor.Routing
|
||||||
[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.")]
|
||||||
[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)
|
public void ParseRouteParameter_ThrowsIf_ParameterContainsSpecialCharacters(string template, string expectedMessage)
|
||||||
{
|
{
|
||||||
// Act & Assert
|
// Act & Assert
|
||||||
|
|
@ -139,13 +138,13 @@ namespace Microsoft.AspNetCore.Blazor.Routing
|
||||||
|
|
||||||
public ExpectedTemplateBuilder Literal(string value)
|
public ExpectedTemplateBuilder Literal(string value)
|
||||||
{
|
{
|
||||||
Segments.Add(new TemplateSegment(value, isParameter: false));
|
Segments.Add(new TemplateSegment("testtemplate", value, isParameter: false));
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ExpectedTemplateBuilder Parameter(string value)
|
public ExpectedTemplateBuilder Parameter(string value)
|
||||||
{
|
{
|
||||||
Segments.Add(new TemplateSegment(value, isParameter: true));
|
Segments.Add(new TemplateSegment("testtemplate", value, isParameter: true));
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue