diff --git a/src/Microsoft.AspNetCore.Blazor/Layouts/LayoutDisplay.cs b/src/Microsoft.AspNetCore.Blazor/Layouts/LayoutDisplay.cs
index cb95a0aa7e..dab3c7fba1 100644
--- a/src/Microsoft.AspNetCore.Blazor/Layouts/LayoutDisplay.cs
+++ b/src/Microsoft.AspNetCore.Blazor/Layouts/LayoutDisplay.cs
@@ -26,7 +26,7 @@ namespace Microsoft.AspNetCore.Blazor.Layouts
///
/// Gets or sets the parameters to pass to the page.
///
- public IDictionary PageParameters { get; set; }
+ public IDictionary PageParameters { get; set; }
///
public void Init(RenderHandle renderHandle)
diff --git a/src/Microsoft.AspNetCore.Blazor/Routing/RouteConstraint.cs b/src/Microsoft.AspNetCore.Blazor/Routing/RouteConstraint.cs
new file mode 100644
index 0000000000..ebce1daecc
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Blazor/Routing/RouteConstraint.cs
@@ -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 _cachedConstraints
+ = new Dictionary();
+
+ 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.TryParse);
+ case "datetime":
+ return new TypeRouteConstraint((string str, out DateTime result)
+ => DateTime.TryParse(str, CultureInfo.InvariantCulture, DateTimeStyles.None, out result));
+ case "decimal":
+ return new TypeRouteConstraint((string str, out decimal result)
+ => decimal.TryParse(str, NumberStyles.Number, CultureInfo.InvariantCulture, out result));
+ case "double":
+ return new TypeRouteConstraint((string str, out double result)
+ => double.TryParse(str, NumberStyles.Number, CultureInfo.InvariantCulture, out result));
+ case "float":
+ return new TypeRouteConstraint((string str, out float result)
+ => float.TryParse(str, NumberStyles.Number, CultureInfo.InvariantCulture, out result));
+ case "guid":
+ return new TypeRouteConstraint(Guid.TryParse);
+ case "int":
+ return new TypeRouteConstraint((string str, out int result)
+ => int.TryParse(str, NumberStyles.None, CultureInfo.InvariantCulture, out result));
+ case "long":
+ return new TypeRouteConstraint((string str, out long result)
+ => long.TryParse(str, NumberStyles.None, CultureInfo.InvariantCulture, out result));
+ default:
+ return null;
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Blazor/Routing/RouteContext.cs b/src/Microsoft.AspNetCore.Blazor/Routing/RouteContext.cs
index cf7fcfa4e0..605e45581e 100644
--- a/src/Microsoft.AspNetCore.Blazor/Routing/RouteContext.cs
+++ b/src/Microsoft.AspNetCore.Blazor/Routing/RouteContext.cs
@@ -21,6 +21,6 @@ namespace Microsoft.AspNetCore.Blazor.Routing
public Type Handler { get; set; }
- public IDictionary Parameters { get; set; }
+ public IDictionary Parameters { get; set; }
}
}
\ No newline at end of file
diff --git a/src/Microsoft.AspNetCore.Blazor/Routing/RouteEntry.cs b/src/Microsoft.AspNetCore.Blazor/Routing/RouteEntry.cs
index 3fc73ab475..c84b189ba1 100644
--- a/src/Microsoft.AspNetCore.Blazor/Routing/RouteEntry.cs
+++ b/src/Microsoft.AspNetCore.Blazor/Routing/RouteEntry.cs
@@ -26,12 +26,12 @@ namespace Microsoft.AspNetCore.Blazor.Routing
}
// Parameters will be lazily initialized.
- IDictionary parameters = null;
+ IDictionary parameters = null;
for (int i = 0; i < Template.Segments.Length; i++)
{
var segment = Template.Segments[i];
var pathSegment = context.Segments[i];
- if (!segment.Match(pathSegment))
+ if (!segment.Match(pathSegment, out var matchedParameterValue))
{
return;
}
@@ -39,7 +39,7 @@ namespace Microsoft.AspNetCore.Blazor.Routing
{
if (segment.IsParameter)
{
- GetParameters()[segment.Value] = pathSegment;
+ GetParameters()[segment.Value] = matchedParameterValue;
}
}
}
@@ -47,11 +47,11 @@ namespace Microsoft.AspNetCore.Blazor.Routing
context.Parameters = parameters;
context.Handler = Handler;
- IDictionary GetParameters()
+ IDictionary GetParameters()
{
if (parameters == null)
{
- parameters = new Dictionary();
+ parameters = new Dictionary();
}
return parameters;
diff --git a/src/Microsoft.AspNetCore.Blazor/Routing/RouteTable.cs b/src/Microsoft.AspNetCore.Blazor/Routing/RouteTable.cs
index efaeff92db..2d982b3889 100644
--- a/src/Microsoft.AspNetCore.Blazor/Routing/RouteTable.cs
+++ b/src/Microsoft.AspNetCore.Blazor/Routing/RouteTable.cs
@@ -43,10 +43,12 @@ namespace Microsoft.AspNetCore.Blazor.Routing
/// 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 ambigous if:
/// 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:
/// * /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 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.
internal static int RouteComparison(RouteEntry x, RouteEntry y)
{
@@ -83,22 +87,19 @@ namespace Microsoft.AspNetCore.Blazor.Routing
{
return 1;
}
- }
- for (int i = 0; i < xTemplate.Segments.Length; i++)
- {
- var xSegment = xTemplate.Segments[i];
- var ySegment = yTemplate.Segments[i];
- if (!xSegment.IsParameter && ySegment.IsParameter)
+ if (xSegment.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)
- {
- return 1;
- }
-
- if (!xSegment.IsParameter)
+ else
{
var comparison = string.Compare(xSegment.Value, ySegment.Value, StringComparison.OrdinalIgnoreCase);
if (comparison != 0)
diff --git a/src/Microsoft.AspNetCore.Blazor/Routing/Router.cs b/src/Microsoft.AspNetCore.Blazor/Routing/Router.cs
index 1a9ffaa02b..cf7e7b344b 100644
--- a/src/Microsoft.AspNetCore.Blazor/Routing/Router.cs
+++ b/src/Microsoft.AspNetCore.Blazor/Routing/Router.cs
@@ -65,7 +65,7 @@ namespace Microsoft.AspNetCore.Blazor.Routing
: str.Substring(0, firstIndex);
}
- protected virtual void Render(RenderTreeBuilder builder, Type handler, IDictionary parameters)
+ protected virtual void Render(RenderTreeBuilder builder, Type handler, IDictionary parameters)
{
builder.OpenComponent(0, typeof(LayoutDisplay));
builder.AddAttribute(1, nameof(LayoutDisplay.Page), handler);
diff --git a/src/Microsoft.AspNetCore.Blazor/Routing/TemplateParser.cs b/src/Microsoft.AspNetCore.Blazor/Routing/TemplateParser.cs
index d999c0f32e..d41500d00d 100644
--- a/src/Microsoft.AspNetCore.Blazor/Routing/TemplateParser.cs
+++ b/src/Microsoft.AspNetCore.Blazor/Routing/TemplateParser.cs
@@ -8,7 +8,7 @@ namespace Microsoft.AspNetCore.Blazor.Routing
// 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 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
// simple parameters from it.
// Some differences with ASP.NET Core routes are:
@@ -21,10 +21,11 @@ namespace Microsoft.AspNetCore.Blazor.Routing
internal class TemplateParser
{
public static readonly char[] InvalidParameterNameCharacters =
- new char[] { '*', '?', '{', '}', '=', '.', ':' };
+ new char[] { '*', '?', '{', '}', '=', '.' };
internal static RouteTemplate ParseTemplate(string template)
{
+ var originalTemplate = template;
template = template.Trim('/');
if (template == "")
{
@@ -50,7 +51,7 @@ namespace Microsoft.AspNetCore.Blazor.Routing
throw new InvalidOperationException(
$"Invalid template '{template}'. Missing '{{' in parameter segment '{segment}'.");
}
- templateSegments[i] = new TemplateSegment(segment, isParameter: false);
+ templateSegments[i] = new TemplateSegment(originalTemplate, segment, isParameter: false);
}
else
{
@@ -73,7 +74,7 @@ namespace Microsoft.AspNetCore.Blazor.Routing
$"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);
}
}
diff --git a/src/Microsoft.AspNetCore.Blazor/Routing/TemplateSegment.cs b/src/Microsoft.AspNetCore.Blazor/Routing/TemplateSegment.cs
index b7e46086c6..6e698662ad 100644
--- a/src/Microsoft.AspNetCore.Blazor/Routing/TemplateSegment.cs
+++ b/src/Microsoft.AspNetCore.Blazor/Routing/TemplateSegment.cs
@@ -2,15 +2,34 @@
// 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.Blazor.Routing
{
internal class TemplateSegment
{
- public TemplateSegment(string segment, bool isParameter)
+ public TemplateSegment(string template, string segment, bool isParameter)
{
- Value = segment;
IsParameter = isParameter;
+
+ if (!isParameter || segment.IndexOf(':') < 0)
+ {
+ Value = segment;
+ Constraints = Array.Empty();
+ }
+ 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.
@@ -19,14 +38,27 @@ namespace Microsoft.AspNetCore.Blazor.Routing
public bool IsParameter { get; }
- public bool Match(string pathSegment)
+ public RouteConstraint[] 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);
}
}
diff --git a/src/Microsoft.AspNetCore.Blazor/Routing/TypeRouteConstraint.cs b/src/Microsoft.AspNetCore.Blazor/Routing/TypeRouteConstraint.cs
new file mode 100644
index 0000000000..fcb4458083
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Blazor/Routing/TypeRouteConstraint.cs
@@ -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
+{
+ ///
+ /// A route constraint that requires the value to be parseable as a specified type.
+ ///
+ /// The type to which the value must be parseable.
+ internal class TypeRouteConstraint : 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;
+ }
+ }
+ }
+}
diff --git a/test/Microsoft.AspNetCore.Blazor.Test/Routing/RouteTableTests.cs b/test/Microsoft.AspNetCore.Blazor.Test/Routing/RouteTableTests.cs
index 3a0087beaa..61790aa561 100644
--- a/test/Microsoft.AspNetCore.Blazor.Test/Routing/RouteTableTests.cs
+++ b/test/Microsoft.AspNetCore.Blazor.Test/Routing/RouteTableTests.cs
@@ -81,6 +81,28 @@ namespace Microsoft.AspNetCore.Blazor.Test.Routing
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")]
@@ -111,7 +133,7 @@ namespace Microsoft.AspNetCore.Blazor.Test.Routing
// Assert
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]
@@ -121,7 +143,7 @@ namespace Microsoft.AspNetCore.Blazor.Test.Routing
var routeTable = new TestRouteTableBuilder().AddRoute("/{some}/awesome/{route}/").Build();
var context = new RouteContext("/an/awesome/path");
- var expectedParameters = new Dictionary
+ var expectedParameters = new Dictionary
{
["some"] = "an",
["route"] = "path"
@@ -135,6 +157,60 @@ namespace Microsoft.AspNetCore.Blazor.Test.Routing
Assert.Equal(expectedParameters, context.Parameters);
}
+ public static IEnumerable