Remove string.Split from routing

This change removes the call to string.Split and a few substrings, and
replaces it with a tokenizer API. The tokenizer isn't really optimized
right now for compute - it should probably be an iterator- but it's a
significant improvement on what we're doing.
This commit is contained in:
Ryan Nowak 2015-10-02 10:51:12 -07:00
parent 2f8dba6659
commit 371d4e62da
6 changed files with 587 additions and 137 deletions

View File

@ -0,0 +1,136 @@
// 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.AspNet.Routing.Internal
{
public struct PathSegment : IEquatable<PathSegment>, IEquatable<string>
{
private readonly string _path;
private readonly int _start;
private readonly int _length;
private string _segment;
public PathSegment(string path, int start, int length)
{
if (path == null)
{
throw new ArgumentNullException(nameof(path));
}
if (start < 1 || start >= path.Length)
{
throw new ArgumentOutOfRangeException(nameof(start));
}
if (length < 0 || start + length > path.Length)
{
throw new ArgumentOutOfRangeException(nameof(length));
}
_path = path;
_start = start;
_length = length;
_segment = null;
}
public int Length => _length;
public string GetRemainingPath()
{
if (_path == null)
{
return string.Empty;
}
return _path.Substring(_start);
}
public override bool Equals(object obj)
{
var other = obj as PathSegment?;
if (other == null)
{
return false;
}
else
{
return Equals(other.Value);
}
}
public override int GetHashCode()
{
if (_path == null)
{
return 0;
}
return StringComparer.OrdinalIgnoreCase.GetHashCode(ToString());
}
public override string ToString()
{
if (_path == null)
{
return string.Empty;
}
if (_segment == null)
{
_segment = _path.Substring(_start, _length);
}
return _segment;
}
public bool Equals(PathSegment other)
{
if (_path == null)
{
return other._path == null;
}
if (other._length != _length)
{
return false;
}
return string.Compare(
_path,
_start,
other._path,
other._start,
_length,
StringComparison.OrdinalIgnoreCase) == 0;
}
public bool Equals(string other)
{
if (_path == null)
{
return other == null;
}
if (other.Length != _length)
{
return false;
}
return string.Compare(_path, _start, other, 0, _length, StringComparison.OrdinalIgnoreCase) == 0;
}
public static bool operator ==(PathSegment x, PathSegment y)
{
return x.Equals(y);
}
public static bool operator !=(PathSegment x, PathSegment y)
{
return !x.Equals(y);
}
}
}

View File

@ -0,0 +1,204 @@
// 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;
using System.Collections.Generic;
using System.Diagnostics;
using Microsoft.AspNet.Http;
namespace Microsoft.AspNet.Routing.Internal
{
public struct PathTokenizer : IReadOnlyList<PathSegment>
{
private readonly string _path;
private int _count;
public PathTokenizer(PathString path)
{
_path = path.Value;
_count = -1;
}
public int Count
{
get
{
if (_count == -1)
{
// We haven't computed the real count of segments yet.
if (_path.Length == 0)
{
// The empty string has length of 0.
_count = 0;
return _count;
}
// A string of length 1 must be "/" - all PathStrings start with '/'
if (_path.Length == 1)
{
// We treat this as empty - there's nothing to parse here for routing, because routing ignores
// a trailing slash.
Debug.Assert(_path[0] == '/');
_count = 0;
return _count;
}
// This is a non-trival PathString
_count = 1;
// Since a non-empty PathString must begin with a `/`, we can just count the number of occurrences
// of `/` to find the number of segments. However, we don't look at the last character, because
// routing ignores a trailing slash.
for (var i = 1; i < _path.Length - 1; i++)
{
if (_path[i] == '/')
{
_count++;
}
}
}
return _count;
}
}
public PathSegment this[int index]
{
get
{
if (index >= Count)
{
throw new IndexOutOfRangeException();
}
var currentSegmentIndex = 0;
var currentSegmentStart = 1;
// Skip the first `/`.
var delimiterIndex = 1;
while ((delimiterIndex = _path.IndexOf('/', delimiterIndex)) != -1)
{
if (currentSegmentIndex++ == index)
{
return new PathSegment(_path, currentSegmentStart, delimiterIndex - currentSegmentStart);
}
else
{
currentSegmentStart = delimiterIndex + 1;
delimiterIndex++;
}
}
// If we get here we're at the end of the string. The implementation of .Count should protect us
// from these cases.
Debug.Assert(_path[_path.Length - 1] != '/');
Debug.Assert(currentSegmentIndex == index);
return new PathSegment(_path, currentSegmentStart, _path.Length - currentSegmentStart);
}
}
public Enumerator GetEnumerator()
{
return new Enumerator(this);
}
IEnumerator<PathSegment> IEnumerable<PathSegment>.GetEnumerator()
{
return GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
public struct Enumerator : IEnumerator<PathSegment>
{
private readonly string _path;
private int _index;
private int _length;
public Enumerator(PathTokenizer tokenizer)
{
_path = tokenizer._path;
_index = -1;
_length = -1;
}
public PathSegment Current
{
get
{
return new PathSegment(_path, _index, _length);
}
}
object IEnumerator.Current
{
get
{
return Current;
}
}
public void Dispose()
{
}
public bool MoveNext()
{
if (_path == null || _path.Length <= 1)
{
return false;
}
if (_index == -1)
{
// Skip the first `/`.
_index = 1;
}
else
{
// Skip to the end of the previous segment + the separator.
_index += _length + 1;
}
if (_index >= _path.Length)
{
// We're at the end
return false;
}
var delimiterIndex = _path.IndexOf('/', _index);
if (delimiterIndex != -1)
{
_length = delimiterIndex - _index;
return true;
}
// We might have some trailing text after the last separator.
if (_path[_path.Length - 1] == '/')
{
// If the last char is a '/' then it's just a trailing slash, we don't have another segment.
return false;
}
else
{
_length = _path.Length - _index;
return true;
}
}
public void Reset()
{
_index = -1;
_length = -1;
}
}
}
}

View File

@ -4,6 +4,8 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.Routing.Internal;
namespace Microsoft.AspNet.Routing.Template namespace Microsoft.AspNet.Routing.Template
{ {
@ -36,18 +38,12 @@ namespace Microsoft.AspNet.Routing.Template
public RouteTemplate Template { get; private set; } public RouteTemplate Template { get; private set; }
public IDictionary<string, object> Match(string requestPath) public IDictionary<string, object> Match(PathString path)
{ {
if (requestPath == null)
{
throw new ArgumentNullException(nameof(requestPath));
}
var requestSegments = requestPath.Split(Delimiters);
var values = new RouteValueDictionary(); var values = new RouteValueDictionary();
for (var i = 0; i < requestSegments.Length; i++) var requestSegments = new PathTokenizer(path);
for (var i = 0; i < requestSegments.Count; i++)
{ {
var routeSegment = Template.Segments.Count > i ? Template.Segments[i] : null; var routeSegment = Template.Segments.Count > i ? Template.Segments[i] : null;
var requestSegment = requestSegments[i]; var requestSegment = requestSegments[i];
@ -67,7 +63,7 @@ namespace Microsoft.AspNet.Routing.Template
var part = routeSegment.Parts[0]; var part = routeSegment.Parts[0];
if (part.IsLiteral) if (part.IsLiteral)
{ {
if (!string.Equals(part.Text, requestSegment, StringComparison.OrdinalIgnoreCase)) if (!requestSegment.Equals(part.Text))
{ {
return null; return null;
} }
@ -78,7 +74,7 @@ namespace Microsoft.AspNet.Routing.Template
if (part.IsCatchAll) if (part.IsCatchAll)
{ {
var captured = string.Join(SeparatorString, requestSegments, i, requestSegments.Length - i); var captured = requestSegment.GetRemainingPath();
if (captured.Length > 0) if (captured.Length > 0)
{ {
values.Add(part.Name, captured); values.Add(part.Name, captured);
@ -99,7 +95,7 @@ namespace Microsoft.AspNet.Routing.Template
{ {
if (requestSegment.Length > 0) if (requestSegment.Length > 0)
{ {
values.Add(part.Name, requestSegment); values.Add(part.Name, requestSegment.ToString());
} }
else else
{ {
@ -124,14 +120,14 @@ namespace Microsoft.AspNet.Routing.Template
} }
else else
{ {
if (!MatchComplexSegment(routeSegment, requestSegment, Defaults, values)) if (!MatchComplexSegment(routeSegment, requestSegment.ToString(), Defaults, values))
{ {
return null; return null;
} }
} }
} }
for (var i = requestSegments.Length; i < Template.Segments.Count; i++) for (var i = requestSegments.Count; i < Template.Segments.Count; i++)
{ {
// We've matched the request path so far, but still have remaining route segments. These need // We've matched the request path so far, but still have remaining route segments. These need
// to be all single-part parameter segments with default values or else they won't match. // to be all single-part parameter segments with default values or else they won't match.
@ -179,10 +175,11 @@ namespace Microsoft.AspNet.Routing.Template
return values; return values;
} }
private bool MatchComplexSegment(TemplateSegment routeSegment, private bool MatchComplexSegment(
string requestSegment, TemplateSegment routeSegment,
IReadOnlyDictionary<string, object> defaults, string requestSegment,
RouteValueDictionary values) IReadOnlyDictionary<string, object> defaults,
RouteValueDictionary values)
{ {
var indexOfLastSegment = routeSegment.Parts.Count - 1; var indexOfLastSegment = routeSegment.Parts.Count - 1;
@ -225,11 +222,12 @@ namespace Microsoft.AspNet.Routing.Template
} }
} }
private bool MatchComplexSegmentCore(TemplateSegment routeSegment, private bool MatchComplexSegmentCore(
string requestSegment, TemplateSegment routeSegment,
IReadOnlyDictionary<string, object> defaults, string requestSegment,
RouteValueDictionary values, IReadOnlyDictionary<string, object> defaults,
int indexOfLastSegmentUsed) RouteValueDictionary values,
int indexOfLastSegmentUsed)
{ {
Debug.Assert(routeSegment != null); Debug.Assert(routeSegment != null);
Debug.Assert(routeSegment.Parts.Count > 1); Debug.Assert(routeSegment.Parts.Count > 1);
@ -269,9 +267,10 @@ namespace Microsoft.AspNet.Routing.Template
return false; return false;
} }
var indexOfLiteral = requestSegment.LastIndexOf(part.Text, var indexOfLiteral = requestSegment.LastIndexOf(
startIndex, part.Text,
StringComparison.OrdinalIgnoreCase); startIndex,
StringComparison.OrdinalIgnoreCase);
if (indexOfLiteral == -1) if (indexOfLiteral == -1)
{ {
// If we couldn't find this literal index, this segment cannot match // If we couldn't find this literal index, this segment cannot match

View File

@ -115,13 +115,7 @@ namespace Microsoft.AspNet.Routing.Template
EnsureLoggers(context.HttpContext); EnsureLoggers(context.HttpContext);
var requestPath = context.HttpContext.Request.Path.Value; var requestPath = context.HttpContext.Request.Path;
if (!string.IsNullOrEmpty(requestPath) && requestPath[0] == '/')
{
requestPath = requestPath.Substring(1);
}
var values = _matcher.Match(requestPath); var values = _matcher.Match(requestPath);
if (values == null) if (values == null)

View File

@ -0,0 +1,116 @@
// 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 Microsoft.AspNet.Http;
using Xunit;
namespace Microsoft.AspNet.Routing.Internal
{
public class PathTokenizerTest
{
public static TheoryData<string, PathSegment[]> TokenizationData
{
get
{
return new TheoryData<string, PathSegment[]>
{
{ string.Empty, new PathSegment[] { } },
{ "/", new PathSegment[] { } },
{ "//", new PathSegment[] { new PathSegment("//", 1, 0) } },
{
"///",
new PathSegment[]
{
new PathSegment("///", 1, 0),
new PathSegment("///", 2, 0),
}
},
{
"////",
new PathSegment[]
{
new PathSegment("////", 1, 0),
new PathSegment("////", 2, 0),
new PathSegment("////", 3, 0),
}
},
{ "/zero", new PathSegment[] { new PathSegment("/zero", 1, 4) } },
{ "/zero/", new PathSegment[] { new PathSegment("/zero/", 1, 4) } },
{
"/zero/one",
new PathSegment[]
{
new PathSegment("/zero/one", 1, 4),
new PathSegment("/zero/one", 6, 3),
}
},
{
"/zero/one/",
new PathSegment[]
{
new PathSegment("/zero/one/", 1, 4),
new PathSegment("/zero/one/", 6, 3),
}
},
{
"/zero/one/two",
new PathSegment[]
{
new PathSegment("/zero/one/two", 1, 4),
new PathSegment("/zero/one/two", 6, 3),
new PathSegment("/zero/one/two", 10, 3),
}
},
{
"/zero/one/two/",
new PathSegment[]
{
new PathSegment("/zero/one/two/", 1, 4),
new PathSegment("/zero/one/two/", 6, 3),
new PathSegment("/zero/one/two/", 10, 3),
}
},
};
}
}
[Theory]
[MemberData(nameof(TokenizationData))]
public void PathTokenizer_Count(string path, PathSegment[] expectedSegments)
{
// Arrange
var tokenizer = new PathTokenizer(new PathString(path));
// Act
var count = tokenizer.Count;
// Assert
Assert.Equal(expectedSegments.Length, count);
}
[Theory]
[MemberData(nameof(TokenizationData))]
public void PathTokenizer_Indexer(string path, PathSegment[] expectedSegments)
{
// Arrange
var tokenizer = new PathTokenizer(new PathString(path));
// Act & Assert
for (var i = 0; i < expectedSegments.Length; i++)
{
Assert.Equal(expectedSegments[i], tokenizer[i]);
}
}
[Theory]
[MemberData(nameof(TokenizationData))]
public void PathTokenizer_Enumerator(string path, PathSegment[] expectedSegments)
{
// Arrange
var tokenizer = new PathTokenizer(new PathString(path));
// Act & Assert
Assert.Equal<PathSegment>(expectedSegments, tokenizer);
}
}
}

View File

@ -2,6 +2,7 @@
// 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.Collections.Generic; using System.Collections.Generic;
using Microsoft.AspNet.Http;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.OptionsModel; using Microsoft.Extensions.OptionsModel;
using Xunit; using Xunit;
@ -19,7 +20,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
var matcher = CreateMatcher("{controller}/{action}/{id}"); var matcher = CreateMatcher("{controller}/{action}/{id}");
// Act // Act
var match = matcher.Match("Bank/DoAction/123"); var match = matcher.Match("/Bank/DoAction/123");
// Assert // Assert
Assert.NotNull(match); Assert.NotNull(match);
@ -35,7 +36,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
var matcher = CreateMatcher("{controller}/{action}/{id}"); var matcher = CreateMatcher("{controller}/{action}/{id}");
// Act // Act
var match = matcher.Match("Bank/DoAction"); var match = matcher.Match("/Bank/DoAction");
// Assert // Assert
Assert.Null(match); Assert.Null(match);
@ -48,7 +49,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
var matcher = CreateMatcher("{controller}/{action}/{id}", new { id = "default id" }); var matcher = CreateMatcher("{controller}/{action}/{id}", new { id = "default id" });
// Act // Act
var rd = matcher.Match("Bank/DoAction"); var rd = matcher.Match("/Bank/DoAction");
// Assert // Assert
Assert.Equal("Bank", rd["controller"]); Assert.Equal("Bank", rd["controller"]);
@ -63,7 +64,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
var matcher = CreateMatcher("{controller}/{action}/{id}", new { id = "default id" }); var matcher = CreateMatcher("{controller}/{action}/{id}", new { id = "default id" });
// Act // Act
var rd = matcher.Match("Bank"); var rd = matcher.Match("/Bank");
// Assert // Assert
Assert.Null(rd); Assert.Null(rd);
@ -76,7 +77,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
var matcher = CreateMatcher("moo/{p1}/bar/{p2}", new { p2 = "default p2" }); var matcher = CreateMatcher("moo/{p1}/bar/{p2}", new { p2 = "default p2" });
// Act // Act
var rd = matcher.Match("moo/111/bar/222"); var rd = matcher.Match("/moo/111/bar/222");
// Assert // Assert
Assert.Equal("111", rd["p1"]); Assert.Equal("111", rd["p1"]);
@ -90,7 +91,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
var matcher = CreateMatcher("moo/{p1}/bar/{p2}", new { p2 = "default p2" }); var matcher = CreateMatcher("moo/{p1}/bar/{p2}", new { p2 = "default p2" });
// Act // Act
var rd = matcher.Match("moo/111/bar/"); var rd = matcher.Match("/moo/111/bar/");
// Assert // Assert
Assert.Equal("111", rd["p1"]); Assert.Equal("111", rd["p1"]);
@ -98,10 +99,10 @@ namespace Microsoft.AspNet.Routing.Template.Tests
} }
[Theory] [Theory]
[InlineData(@"{p1:regex(^\d{{3}}-\d{{3}}-\d{{4}}$)}", "123-456-7890")] // ssn [InlineData(@"{p1:regex(^\d{{3}}-\d{{3}}-\d{{4}}$)}", "/123-456-7890")] // ssn
[InlineData(@"{p1:regex(^\w+\@\w+\.\w+)}", "asd@assds.com")] // email [InlineData(@"{p1:regex(^\w+\@\w+\.\w+)}", "/asd@assds.com")] // email
[InlineData(@"{p1:regex(([}}])\w+)}", "}sda")] // Not balanced } [InlineData(@"{p1:regex(([}}])\w+)}", "/}sda")] // Not balanced }
[InlineData(@"{p1:regex(([{{)])\w+)}", "})sda")] // Not balanced { [InlineData(@"{p1:regex(([{{)])\w+)}", "/})sda")] // Not balanced {
public void MatchRoute_RegularExpression_Valid( public void MatchRoute_RegularExpression_Valid(
string template, string template,
string path) string path)
@ -117,19 +118,19 @@ namespace Microsoft.AspNet.Routing.Template.Tests
} }
[Theory] [Theory]
[InlineData("moo/{p1}.{p2?}", "moo/foo.bar", "foo", "bar")] [InlineData("moo/{p1}.{p2?}", "/moo/foo.bar", "foo", "bar")]
[InlineData("moo/{p1?}", "moo/foo", "foo", null)] [InlineData("moo/{p1?}", "/moo/foo", "foo", null)]
[InlineData("moo/{p1?}", "moo", null, null)] [InlineData("moo/{p1?}", "/moo", null, null)]
[InlineData("moo/{p1}.{p2?}", "moo/foo", "foo", null)] [InlineData("moo/{p1}.{p2?}", "/moo/foo", "foo", null)]
[InlineData("moo/{p1}.{p2?}", "moo/foo..bar", "foo.", "bar")] [InlineData("moo/{p1}.{p2?}", "/moo/foo..bar", "foo.", "bar")]
[InlineData("moo/{p1}.{p2?}", "moo/foo.moo.bar", "foo.moo", "bar")] [InlineData("moo/{p1}.{p2?}", "/moo/foo.moo.bar", "foo.moo", "bar")]
[InlineData("moo/{p1}.{p2}", "moo/foo.bar", "foo", "bar")] [InlineData("moo/{p1}.{p2}", "/moo/foo.bar", "foo", "bar")]
[InlineData("moo/foo.{p1}.{p2?}", "moo/foo.moo.bar", "moo", "bar")] [InlineData("moo/foo.{p1}.{p2?}", "/moo/foo.moo.bar", "moo", "bar")]
[InlineData("moo/foo.{p1}.{p2?}", "moo/foo.moo", "moo", null)] [InlineData("moo/foo.{p1}.{p2?}", "/moo/foo.moo", "moo", null)]
[InlineData("moo/.{p2?}", "moo/.foo", null, "foo")] [InlineData("moo/.{p2?}", "/moo/.foo", null, "foo")]
[InlineData("moo/.{p2?}", "moo", null, null)] [InlineData("moo/.{p2?}", "/moo", null, null)]
[InlineData("moo/{p1}.{p2?}", "moo/....", "..", ".")] [InlineData("moo/{p1}.{p2?}", "/moo/....", "..", ".")]
[InlineData("moo/{p1}.{p2?}", "moo/.bar", ".bar", null)] [InlineData("moo/{p1}.{p2?}", "/moo/.bar", ".bar", null)]
public void MatchRoute_OptionalParameter_FollowedByPeriod_Valid( public void MatchRoute_OptionalParameter_FollowedByPeriod_Valid(
string template, string template,
string path, string path,
@ -154,13 +155,13 @@ namespace Microsoft.AspNet.Routing.Template.Tests
} }
[Theory] [Theory]
[InlineData("moo/{p1}.{p2}.{p3?}", "moo/foo.moo.bar", "foo", "moo", "bar")] [InlineData("moo/{p1}.{p2}.{p3?}", "/moo/foo.moo.bar", "foo", "moo", "bar")]
[InlineData("moo/{p1}.{p2}.{p3?}", "moo/foo.moo", "foo", "moo", null)] [InlineData("moo/{p1}.{p2}.{p3?}", "/moo/foo.moo", "foo", "moo", null)]
[InlineData("moo/{p1}.{p2}.{p3}.{p4?}", "moo/foo.moo.bar", "foo", "moo", "bar")] [InlineData("moo/{p1}.{p2}.{p3}.{p4?}", "/moo/foo.moo.bar", "foo", "moo", "bar")]
[InlineData("{p1}.{p2?}/{p3}", "foo.moo/bar", "foo", "moo", "bar")] [InlineData("{p1}.{p2?}/{p3}", "/foo.moo/bar", "foo", "moo", "bar")]
[InlineData("{p1}.{p2?}/{p3}", "foo/bar", "foo", null, "bar")] [InlineData("{p1}.{p2?}/{p3}", "/foo/bar", "foo", null, "bar")]
[InlineData("{p1}.{p2?}/{p3}", ".foo/bar", ".foo", null, "bar")] [InlineData("{p1}.{p2?}/{p3}", "/.foo/bar", ".foo", null, "bar")]
[InlineData("{p1}/{p2}/{p3?}", "foo/bar/baz", "foo", "bar", "baz")] [InlineData("{p1}/{p2}/{p3?}", "/foo/bar/baz", "foo", "bar", "baz")]
public void MatchRoute_OptionalParameter_FollowedByPeriod_3Parameters_Valid( public void MatchRoute_OptionalParameter_FollowedByPeriod_3Parameters_Valid(
string template, string template,
string path, string path,
@ -189,18 +190,18 @@ namespace Microsoft.AspNet.Routing.Template.Tests
} }
[Theory] [Theory]
[InlineData("moo/{p1}.{p2?}", "moo/foo.")] [InlineData("moo/{p1}.{p2?}", "/moo/foo.")]
[InlineData("moo/{p1}.{p2?}", "moo/.")] [InlineData("moo/{p1}.{p2?}", "/moo/.")]
[InlineData("moo/{p1}.{p2}", "foo.")] [InlineData("moo/{p1}.{p2}", "/foo.")]
[InlineData("moo/{p1}.{p2}", "foo")] [InlineData("moo/{p1}.{p2}", "/foo")]
[InlineData("moo/{p1}.{p2}.{p3?}", "moo/foo.moo.")] [InlineData("moo/{p1}.{p2}.{p3?}", "/moo/foo.moo.")]
[InlineData("moo/foo.{p2}.{p3?}", "moo/bar.foo.moo")] [InlineData("moo/foo.{p2}.{p3?}", "/moo/bar.foo.moo")]
[InlineData("moo/foo.{p2}.{p3?}", "moo/kungfoo.moo.bar")] [InlineData("moo/foo.{p2}.{p3?}", "/moo/kungfoo.moo.bar")]
[InlineData("moo/foo.{p2}.{p3?}", "moo/kungfoo.moo")] [InlineData("moo/foo.{p2}.{p3?}", "/moo/kungfoo.moo")]
[InlineData("moo/{p1}.{p2}.{p3?}", "moo/foo")] [InlineData("moo/{p1}.{p2}.{p3?}", "/moo/foo")]
[InlineData("{p1}.{p2?}/{p3}", "foo./bar")] [InlineData("{p1}.{p2?}/{p3}", "/foo./bar")]
[InlineData("moo/.{p2?}", "moo/.")] [InlineData("moo/.{p2?}", "/moo/.")]
[InlineData("{p1}.{p2}/{p3}", ".foo/bar")] [InlineData("{p1}.{p2}/{p3}", "/.foo/bar")]
public void MatchRoute_OptionalParameter_FollowedByPeriod_Invalid(string template, string path) public void MatchRoute_OptionalParameter_FollowedByPeriod_Invalid(string template, string path)
{ {
// Arrange // Arrange
@ -220,7 +221,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
var matcher = CreateMatcher("moo/bar"); var matcher = CreateMatcher("moo/bar");
// Act // Act
var rd = matcher.Match("moo/bar"); var rd = matcher.Match("/moo/bar");
// Assert // Assert
Assert.NotNull(rd); Assert.NotNull(rd);
@ -234,7 +235,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
var matcher = CreateMatcher("moo/bars"); var matcher = CreateMatcher("moo/bars");
// Act // Act
var rd = matcher.Match("moo/bar"); var rd = matcher.Match("/moo/bar");
// Assert // Assert
Assert.Null(rd); Assert.Null(rd);
@ -247,7 +248,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
var matcher = CreateMatcher("moo/bar"); var matcher = CreateMatcher("moo/bar");
// Act // Act
var rd = matcher.Match("moo/bar/"); var rd = matcher.Match("/moo/bar/");
// Assert // Assert
Assert.NotNull(rd); Assert.NotNull(rd);
@ -261,7 +262,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
var matcher = CreateMatcher("moo/bar/"); var matcher = CreateMatcher("moo/bar/");
// Act // Act
var rd = matcher.Match("moo/bar"); var rd = matcher.Match("/moo/bar");
// Assert // Assert
Assert.NotNull(rd); Assert.NotNull(rd);
@ -275,7 +276,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
var matcher = CreateMatcher("{p1}/{p2}/"); var matcher = CreateMatcher("{p1}/{p2}/");
// Act // Act
var rd = matcher.Match("moo/bar"); var rd = matcher.Match("/moo/bar");
// Assert // Assert
Assert.NotNull(rd); Assert.NotNull(rd);
@ -290,7 +291,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
var matcher = CreateMatcher("{p1}/{p2}/baz"); var matcher = CreateMatcher("{p1}/{p2}/baz");
// Act // Act
var rd = matcher.Match("moo/bar/boo"); var rd = matcher.Match("/moo/bar/boo");
// Assert // Assert
Assert.Null(rd); Assert.Null(rd);
@ -303,7 +304,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
var matcher = CreateMatcher("{p1}"); var matcher = CreateMatcher("{p1}");
// Act // Act
var rd = matcher.Match("moo/bar"); var rd = matcher.Match("/moo/bar");
// Assert // Assert
Assert.Null(rd); Assert.Null(rd);
@ -316,21 +317,21 @@ namespace Microsoft.AspNet.Routing.Template.Tests
var matcher = CreateMatcher("DEFAULT.ASPX"); var matcher = CreateMatcher("DEFAULT.ASPX");
// Act // Act
var rd = matcher.Match("default.aspx"); var rd = matcher.Match("/default.aspx");
// Assert // Assert
Assert.NotNull(rd); Assert.NotNull(rd);
} }
[Theory] [Theory]
[InlineData("{prefix}x{suffix}", "xxxxxxxxxx")] [InlineData("{prefix}x{suffix}", "/xxxxxxxxxx")]
[InlineData("{prefix}xyz{suffix}", "xxxxyzxyzxxxxxxyz")] [InlineData("{prefix}xyz{suffix}", "/xxxxyzxyzxxxxxxyz")]
[InlineData("{prefix}xyz{suffix}", "abcxxxxyzxyzxxxxxxyzxx")] [InlineData("{prefix}xyz{suffix}", "/abcxxxxyzxyzxxxxxxyzxx")]
[InlineData("{prefix}xyz{suffix}", "xyzxyzxyzxyzxyz")] [InlineData("{prefix}xyz{suffix}", "/xyzxyzxyzxyzxyz")]
[InlineData("{prefix}xyz{suffix}", "xyzxyzxyzxyzxyz1")] [InlineData("{prefix}xyz{suffix}", "/xyzxyzxyzxyzxyz1")]
[InlineData("{prefix}xyz{suffix}", "xyzxyzxyz")] [InlineData("{prefix}xyz{suffix}", "/xyzxyzxyz")]
[InlineData("{prefix}aa{suffix}", "aaaaa")] [InlineData("{prefix}aa{suffix}", "/aaaaa")]
[InlineData("{prefix}aaa{suffix}", "aaaaa")] [InlineData("{prefix}aaa{suffix}", "/aaaaa")]
public void VerifyRouteMatchesWithContext(string template, string path) public void VerifyRouteMatchesWithContext(string template, string path)
{ {
var matcher = CreateMatcher(template); var matcher = CreateMatcher(template);
@ -349,7 +350,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
var matcher = CreateMatcher("{p1}/{p2}", new { p2 = (string)null, foo = "bar" }); var matcher = CreateMatcher("{p1}/{p2}", new { p2 = (string)null, foo = "bar" });
// Act // Act
var rd = matcher.Match("v1"); var rd = matcher.Match("/v1");
// Assert // Assert
Assert.NotNull(rd); Assert.NotNull(rd);
@ -368,7 +369,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
new { controller = "blog", action = "showpost", m = (string)null, d = (string)null }); new { controller = "blog", action = "showpost", m = (string)null, d = (string)null });
// Act // Act
var rd = matcher.Match("date/2007/08"); var rd = matcher.Match("/date/2007/08");
// Assert // Assert
Assert.NotNull(rd); Assert.NotNull(rd);
@ -385,7 +386,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
{ {
RunTest( RunTest(
"language/{lang}-{region}", "language/{lang}-{region}",
"language/en-US", "/language/en-US",
null, null,
new RouteValueDictionary(new { lang = "en", region = "US" })); new RouteValueDictionary(new { lang = "en", region = "US" }));
} }
@ -395,7 +396,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
{ {
RunTest( RunTest(
"language/{lang}-{region}a", "language/{lang}-{region}a",
"language/en-USa", "/language/en-USa",
null, null,
new RouteValueDictionary(new { lang = "en", region = "US" })); new RouteValueDictionary(new { lang = "en", region = "US" }));
} }
@ -405,7 +406,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
{ {
RunTest( RunTest(
"language/a{lang}-{region}", "language/a{lang}-{region}",
"language/aen-US", "/language/aen-US",
null, null,
new RouteValueDictionary(new { lang = "en", region = "US" })); new RouteValueDictionary(new { lang = "en", region = "US" }));
} }
@ -415,7 +416,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
{ {
RunTest( RunTest(
"language/a{lang}-{region}a", "language/a{lang}-{region}a",
"language/aen-USa", "/language/aen-USa",
null, null,
new RouteValueDictionary(new { lang = "en", region = "US" })); new RouteValueDictionary(new { lang = "en", region = "US" }));
} }
@ -425,7 +426,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
{ {
RunTest( RunTest(
"language/a{lang}-{region}a", "language/a{lang}-{region}a",
"language/a-USa", "/language/a-USa",
null, null,
null); null);
} }
@ -435,7 +436,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
{ {
RunTest( RunTest(
"language/a{lang}-{region}a", "language/a{lang}-{region}a",
"language/aen-a", "/language/aen-a",
null, null,
null); null);
} }
@ -445,7 +446,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
{ {
RunTest( RunTest(
"language/{lang}", "language/{lang}",
"language/en", "/language/en",
null, null,
new RouteValueDictionary(new { lang = "en" })); new RouteValueDictionary(new { lang = "en" }));
} }
@ -455,7 +456,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
{ {
RunTest( RunTest(
"language/{lang}", "language/{lang}",
"language/", "/language/",
null, null,
null); null);
} }
@ -465,7 +466,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
{ {
RunTest( RunTest(
"language/{lang}", "language/{lang}",
"language", "/language",
null, null,
null); null);
} }
@ -475,7 +476,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
{ {
RunTest( RunTest(
"language/{lang}-", "language/{lang}-",
"language/en-", "/language/en-",
null, null,
new RouteValueDictionary(new { lang = "en" })); new RouteValueDictionary(new { lang = "en" }));
} }
@ -485,7 +486,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
{ {
RunTest( RunTest(
"language/a{lang}", "language/a{lang}",
"language/aen", "/language/aen",
null, null,
new RouteValueDictionary(new { lang = "en" })); new RouteValueDictionary(new { lang = "en" }));
} }
@ -495,7 +496,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
{ {
RunTest( RunTest(
"language/a{lang}a", "language/a{lang}a",
"language/aena", "/language/aena",
null, null,
new RouteValueDictionary(new { lang = "en" })); new RouteValueDictionary(new { lang = "en" }));
} }
@ -505,7 +506,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
{ {
RunTest( RunTest(
"{controller}.mvc/{action}/{id}", "{controller}.mvc/{action}/{id}",
"home.mvc/index", "/home.mvc/index",
new RouteValueDictionary(new { action = "Index", id = (string)null }), new RouteValueDictionary(new { action = "Index", id = (string)null }),
new RouteValueDictionary(new { controller = "home", action = "index", id = (string)null })); new RouteValueDictionary(new { controller = "home", action = "index", id = (string)null }));
} }
@ -515,7 +516,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
{ {
RunTest( RunTest(
"language/{lang}-{region}", "language/{lang}-{region}",
"language/-", "/language/-",
new RouteValueDictionary(new { lang = "xx", region = "yy" }), new RouteValueDictionary(new { lang = "xx", region = "yy" }),
null); null);
} }
@ -525,7 +526,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
{ {
RunTest( RunTest(
"{Controller}..mvc/{id}/{Param1}", "{Controller}..mvc/{id}/{Param1}",
"Home..mvc/123/p1", "/Home..mvc/123/p1",
null, null,
new RouteValueDictionary(new { Controller = "Home", id = "123", Param1 = "p1" })); new RouteValueDictionary(new { Controller = "Home", id = "123", Param1 = "p1" }));
} }
@ -535,7 +536,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
{ {
RunTest( RunTest(
"{Controller}.mvc/../{action}", "{Controller}.mvc/../{action}",
"Home.mvc/../index", "/Home.mvc/../index",
null, null,
new RouteValueDictionary(new { Controller = "Home", action = "index" })); new RouteValueDictionary(new { Controller = "Home", action = "index" }));
} }
@ -545,7 +546,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
{ {
RunTest( RunTest(
"{Controller}.mvc/.../{action}", "{Controller}.mvc/.../{action}",
"Home.mvc/.../index", "/Home.mvc/.../index",
null, null,
new RouteValueDictionary(new { Controller = "Home", action = "index" })); new RouteValueDictionary(new { Controller = "Home", action = "index" }));
} }
@ -555,7 +556,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
{ {
RunTest( RunTest(
"{Controller}.mvc/../../../{action}", "{Controller}.mvc/../../../{action}",
"Home.mvc/../../../index", "/Home.mvc/../../../index",
null, null,
new RouteValueDictionary(new { Controller = "Home", action = "index" })); new RouteValueDictionary(new { Controller = "Home", action = "index" }));
} }
@ -565,7 +566,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
{ {
RunTest( RunTest(
"{Controller}.mvc!/{action}", "{Controller}.mvc!/{action}",
"Home.mvc!/index", "/Home.mvc!/index",
null, null,
new RouteValueDictionary(new { Controller = "Home", action = "index" })); new RouteValueDictionary(new { Controller = "Home", action = "index" }));
} }
@ -575,7 +576,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
{ {
RunTest( RunTest(
"../{Controller}.mvc", "../{Controller}.mvc",
"../Home.mvc", "/../Home.mvc",
null, null,
new RouteValueDictionary(new { Controller = "Home" })); new RouteValueDictionary(new { Controller = "Home" }));
} }
@ -585,7 +586,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
{ {
RunTest( RunTest(
@"\{Controller}.mvc", @"\{Controller}.mvc",
@"\Home.mvc", @"/\Home.mvc",
null, null,
new RouteValueDictionary(new { Controller = "Home" })); new RouteValueDictionary(new { Controller = "Home" }));
} }
@ -595,7 +596,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
{ {
RunTest( RunTest(
@"{Controller}.mvc\{id}\{Param1}", @"{Controller}.mvc\{id}\{Param1}",
@"Home.mvc\123\p1", @"/Home.mvc\123\p1",
null, null,
new RouteValueDictionary(new { Controller = "Home", id = "123", Param1 = "p1" })); new RouteValueDictionary(new { Controller = "Home", id = "123", Param1 = "p1" }));
} }
@ -605,7 +606,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
{ {
RunTest( RunTest(
@"(Controller).mvc", @"(Controller).mvc",
@"(Controller).mvc", @"/(Controller).mvc",
null, null,
new RouteValueDictionary()); new RouteValueDictionary());
} }
@ -615,7 +616,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
{ {
RunTest( RunTest(
@"Controller.mvc/ ", @"Controller.mvc/ ",
@"Controller.mvc/ ", @"/Controller.mvc/ ",
null, null,
new RouteValueDictionary()); new RouteValueDictionary());
} }
@ -625,7 +626,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
{ {
RunTest( RunTest(
@"Controller.mvc ", @"Controller.mvc ",
@"Controller.mvc ", @"/Controller.mvc ",
null, null,
new RouteValueDictionary()); new RouteValueDictionary());
} }
@ -636,7 +637,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
// DevDiv Bugs 189892: UrlRouting: Catch all parameter cannot capture url segments that contain the "." // DevDiv Bugs 189892: UrlRouting: Catch all parameter cannot capture url segments that contain the "."
RunTest( RunTest(
"Home/ShowPilot/{missionId}/{*name}", "Home/ShowPilot/{missionId}/{*name}",
"Home/ShowPilot/777/12345./foobar", "/Home/ShowPilot/777/12345./foobar",
new RouteValueDictionary(new new RouteValueDictionary(new
{ {
controller = "Home", controller = "Home",
@ -654,7 +655,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
var matcher = CreateMatcher("{p1}/{*p2}"); var matcher = CreateMatcher("{p1}/{*p2}");
// Act // Act
var rd = matcher.Match("v1/v2/v3"); var rd = matcher.Match("/v1/v2/v3");
// Assert // Assert
Assert.NotNull(rd); Assert.NotNull(rd);
@ -670,7 +671,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
var matcher = CreateMatcher("{p1}/{*p2}"); var matcher = CreateMatcher("{p1}/{*p2}");
// Act // Act
var rd = matcher.Match("v1/"); var rd = matcher.Match("/v1/");
// Assert // Assert
Assert.NotNull(rd); Assert.NotNull(rd);
@ -686,7 +687,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
var matcher = CreateMatcher("{p1}/{*p2}"); var matcher = CreateMatcher("{p1}/{*p2}");
// Act // Act
var rd = matcher.Match("v1"); var rd = matcher.Match("/v1");
// Assert // Assert
Assert.NotNull(rd); Assert.NotNull(rd);
@ -702,7 +703,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
var matcher = CreateMatcher("{p1}/{*p2}", new { p2 = "catchall" }); var matcher = CreateMatcher("{p1}/{*p2}", new { p2 = "catchall" });
// Act // Act
var rd = matcher.Match("v1"); var rd = matcher.Match("/v1");
// Assert // Assert
Assert.NotNull(rd); Assert.NotNull(rd);
@ -718,7 +719,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
var matcher = CreateMatcher("{p1}/{*p2}", new { p2 = "catchall" }); var matcher = CreateMatcher("{p1}/{*p2}", new { p2 = "catchall" });
// Act // Act
var rd = matcher.Match("v1/hello/whatever"); var rd = matcher.Match("/v1/hello/whatever");
// Assert // Assert
Assert.NotNull(rd); Assert.NotNull(rd);
@ -733,7 +734,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
// DevDiv Bugs 191180: UrlRouting: Wrong template getting matched if a url segment is a substring of the requested url // DevDiv Bugs 191180: UrlRouting: Wrong template getting matched if a url segment is a substring of the requested url
RunTest( RunTest(
"foo", "foo",
"fooBAR", "/fooBAR",
null, null,
null); null);
} }
@ -744,7 +745,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
// DevDiv Bugs 191180: UrlRouting: Wrong template getting matched if a url segment is a substring of the requested url // DevDiv Bugs 191180: UrlRouting: Wrong template getting matched if a url segment is a substring of the requested url
RunTest( RunTest(
"foo", "foo",
"BARfoo", "/BARfoo",
null, null,
null); null);
} }
@ -755,7 +756,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
// DevDiv Bugs 191180: UrlRouting: Wrong template getting matched if a url segment is a substring of the requested url // DevDiv Bugs 191180: UrlRouting: Wrong template getting matched if a url segment is a substring of the requested url
RunTest( RunTest(
"foo", "foo",
"BARfooBAR", "/BARfooBAR",
null, null,
null); null);
} }
@ -766,7 +767,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
// DevDiv Bugs 191180: UrlRouting: Wrong template getting matched if a url segment is a substring of the requested url // DevDiv Bugs 191180: UrlRouting: Wrong template getting matched if a url segment is a substring of the requested url
RunTest( RunTest(
"foo", "foo",
"foo", "/foo",
null, null,
new RouteValueDictionary()); new RouteValueDictionary());
} }
@ -776,7 +777,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
{ {
RunTest( RunTest(
"foo/{ }/{.!$%}/{dynamic.data}/{op.tional}", "foo/{ }/{.!$%}/{dynamic.data}/{op.tional}",
"foo/space/weird/orderid", "/foo/space/weird/orderid",
new RouteValueDictionary() { { " ", "not a space" }, { "op.tional", "default value" }, { "ran!dom", "va@lue" } }, new RouteValueDictionary() { { " ", "not a space" }, { "op.tional", "default value" }, { "ran!dom", "va@lue" } },
new RouteValueDictionary() { { " ", "space" }, { ".!$%", "weird" }, { "dynamic.data", "orderid" }, { "op.tional", "default value" }, { "ran!dom", "va@lue" } }); new RouteValueDictionary() { { " ", "space" }, { ".!$%", "weird" }, { "dynamic.data", "orderid" }, { "op.tional", "default value" }, { "ran!dom", "va@lue" } });
} }
@ -786,7 +787,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
{ {
RunTest( RunTest(
"{controller}/{language}-{locale}", "{controller}/{language}-{locale}",
"foo", "/foo",
new RouteValueDictionary(new { language = "en", locale = "US" }), new RouteValueDictionary(new { language = "en", locale = "US" }),
null); null);
} }
@ -796,7 +797,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
{ {
RunTest( RunTest(
"{controller}/{language}-{locale}", "{controller}/{language}-{locale}",
"foo/xx-", "/foo/xx-",
new RouteValueDictionary(new { language = "en", locale = "US" }), new RouteValueDictionary(new { language = "en", locale = "US" }),
null); null);
} }
@ -806,7 +807,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
{ {
RunTest( RunTest(
"{controller}/{language}-{locale}", "{controller}/{language}-{locale}",
"foo/-yy", "/foo/-yy",
new RouteValueDictionary(new { language = "en", locale = "US" }), new RouteValueDictionary(new { language = "en", locale = "US" }),
null); null);
} }
@ -816,7 +817,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
{ {
RunTest( RunTest(
"{controller}/{language}-{locale}", "{controller}/{language}-{locale}",
"foo/xx-yy", "/foo/xx-yy",
new RouteValueDictionary(new { language = "en", locale = "US" }), new RouteValueDictionary(new { language = "en", locale = "US" }),
new RouteValueDictionary { { "language", "xx" }, { "locale", "yy" }, { "controller", "foo" } }); new RouteValueDictionary { { "language", "xx" }, { "locale", "yy" }, { "controller", "foo" } });
} }
@ -826,7 +827,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
{ {
// Arrange // Arrange
var route = CreateMatcher("{controller}/{action?}"); var route = CreateMatcher("{controller}/{action?}");
var url = "Home/Index"; var url = "/Home/Index";
// Act // Act
var match = route.Match(url); var match = route.Match(url);
@ -843,7 +844,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
{ {
// Arrange // Arrange
var route = CreateMatcher("{controller}/{action?}"); var route = CreateMatcher("{controller}/{action?}");
var url = "Home"; var url = "/Home";
// Act // Act
var match = route.Match(url); var match = route.Match(url);
@ -891,7 +892,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
{ {
// Arrange // Arrange
var route = CreateMatcher("{controller}/{action?}/{id?}"); var route = CreateMatcher("{controller}/{action?}/{id?}");
var url = "Home/Index"; var url = "/Home/Index";
// Act // Act
var match = route.Match(url); var match = route.Match(url);
@ -923,7 +924,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
defaults ?? new Dictionary<string, object>()); defaults ?? new Dictionary<string, object>());
// Act // Act
var match = matcher.Match(path); var match = matcher.Match(new PathString(path));
// Assert // Assert
if (expected == null) if (expected == null)