Basic URL Extension functionality working.

1. Template parser now allows a parameter to be an optional parameter in a complex segment if
   it is the last and only optional parameter and it is followed by a period.
2. Template matcher modified to take into consideration the optional parameter in the complex
   segment. Also the period shouldn't be present if the optional parameter is not present
This commit is contained in:
Mugdha Kulkarni 2015-01-06 16:48:21 -08:00
parent 5e55833168
commit 3626900bc9
10 changed files with 908 additions and 148 deletions

View File

@ -205,17 +205,17 @@ namespace Microsoft.AspNet.Routing
/// <summary>
/// A path segment that contains more than one section, such as a literal section or a parameter, cannot contain an optional parameter.
/// </summary>
internal static string TemplateRoute_CannotHaveOptionalParameterInMultiSegment
internal static string TemplateRoute_CanHaveOnlyLastParameterOptional_IfFollowingOptionalSeperator
{
get { return GetString("TemplateRoute_CannotHaveOptionalParameterInMultiSegment"); }
get { return GetString("TemplateRoute_CanHaveOnlyLastParameterOptional_IfFollowingOptionalSeperator"); }
}
/// <summary>
/// A path segment that contains more than one section, such as a literal section or a parameter, cannot contain an optional parameter.
/// </summary>
internal static string FormatTemplateRoute_CannotHaveOptionalParameterInMultiSegment()
internal static string FormatTemplateRoute_CanHaveOnlyLastParameterOptional_IfFollowingOptionalSeperator()
{
return GetString("TemplateRoute_CannotHaveOptionalParameterInMultiSegment");
return GetString("TemplateRoute_CanHaveOnlyLastParameterOptional_IfFollowingOptionalSeperator");
}
/// <summary>

View File

@ -1,17 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
@ -26,36 +26,36 @@
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
@ -153,8 +153,8 @@
<data name="TemplateRoute_CannotHaveConsecutiveSeparators" xml:space="preserve">
<value>The route template separator character '/' cannot appear consecutively. It must be separated by either a parameter or a literal value.</value>
</data>
<data name="TemplateRoute_CannotHaveOptionalParameterInMultiSegment" xml:space="preserve">
<value>A path segment that contains more than one section, such as a literal section or a parameter, cannot contain an optional parameter.</value>
<data name="TemplateRoute_CanHaveOnlyLastParameterOptional_IfFollowingOptionalSeperator" xml:space="preserve">
<value>In a path segment that contains more than one section, such as a literal section or a parameter, there can only be one optional parameter. The optional parameter must be the last parameter in the segment and must be preceded by one single period (.).</value>
</data>
<data name="TemplateRoute_CatchAllCannotBeOptional" xml:space="preserve">
<value>A catch-all parameter cannot be marked optional.</value>

View File

@ -220,14 +220,29 @@ namespace Microsoft.AspNet.Routing.Template
// we won't necessarily add it to the URI we generate.
if (!context.Buffer(converted))
{
return null;
return null;
}
}
else
{
// If the value is not accepted, it is null or empty value in the
// middle of the segment. We accept this if the parameter is an
// optional parameter and it is preceded by an optional seperator.
// I this case, we need to remove the optional seperator that we
// have added to the URI
// Example: template = {id}.{format?}. parameters: id=5
// In this case after we have generated "5.", we wont find any value
// for format, so we remove '.' and generate 5.
if (!context.Accept(converted))
{
return null;
if (j != 0 && part.IsOptional && segment.Parts[j - 1].IsOptionalSeperator)
{
context.Remove(segment.Parts[j - 1].Text);
}
else
{
return null;
}
}
}
}
@ -472,6 +487,11 @@ namespace Microsoft.AspNet.Routing.Template
return true;
}
public void Remove(string literal)
{
_uri.Length -= literal.Length;
}
public bool Buffer(string value)
{
if (string.IsNullOrEmpty(value))

View File

@ -168,17 +168,63 @@ namespace Microsoft.AspNet.Routing.Template
string requestSegment,
IReadOnlyDictionary<string, object> defaults,
RouteValueDictionary values)
{
var indexOfLastSegment = routeSegment.Parts.Count - 1;
// We match the request to the template starting at the rightmost parameter
// If the last segment of template is optional, then request can match the
// template with or without the last parameter. So we start with regular matching,
// but if it doesn't match, we start with next to last parameter. Example:
// Template: {p1}/{p2}.{p3?}. If the request is foo/bar.moo it will match right away
// giving p3 value of moo. But if the request is foo/bar, we start matching from the
// rightmost giving p3 the value of bar, then we end up not matching the segment.
// In this case we start again from p2 to match the request and we succeed giving
// the value bar to p2
if (routeSegment.Parts[indexOfLastSegment].IsOptional &&
routeSegment.Parts[indexOfLastSegment - 1].IsOptionalSeperator)
{
if (MatchComplexSegmentCore(routeSegment, requestSegment, Defaults, values, indexOfLastSegment))
{
return true;
}
else
{
if (requestSegment.EndsWith(routeSegment.Parts[indexOfLastSegment - 1].Text))
{
return false;
}
return MatchComplexSegmentCore(
routeSegment,
requestSegment,
Defaults,
values,
indexOfLastSegment - 2);
}
}
else
{
return MatchComplexSegmentCore(routeSegment, requestSegment, Defaults, values, indexOfLastSegment);
}
}
private bool MatchComplexSegmentCore(TemplateSegment routeSegment,
string requestSegment,
IReadOnlyDictionary<string, object> defaults,
RouteValueDictionary values,
int indexOfLastSegmentUsed)
{
Debug.Assert(routeSegment != null);
Debug.Assert(routeSegment.Parts.Count > 1);
// Find last literal segment and get its last index in the string
var lastIndex = requestSegment.Length;
var indexOfLastSegmentUsed = routeSegment.Parts.Count - 1;
TemplatePart parameterNeedsValue = null; // Keeps track of a parameter segment that is pending a value
TemplatePart lastLiteral = null; // Keeps track of the left-most literal we've encountered
var outValues = new RouteValueDictionary();
while (indexOfLastSegmentUsed >= 0)
{
var newLastIndex = lastIndex;
@ -187,7 +233,7 @@ namespace Microsoft.AspNet.Routing.Template
if (part.IsParameter)
{
// Hold on to the parameter so that we can fill it in when we locate the next literal
parameterNeedsValue = part;
parameterNeedsValue = part;
}
else
{
@ -209,10 +255,10 @@ namespace Microsoft.AspNet.Routing.Template
var indexOfLiteral = requestSegment.LastIndexOf(part.Text,
startIndex,
StringComparison.OrdinalIgnoreCase);
if (indexOfLiteral == -1)
if (indexOfLiteral == -1)
{
// If we couldn't find this literal index, this segment cannot match
return false;
return false;
}
// If the first subsegment is a literal, it must match at the right-most extent of the request URI.
@ -271,13 +317,14 @@ namespace Microsoft.AspNet.Routing.Template
{
// If we're here that means we have a segment that contains multiple sub-segments.
// For these segments all parameters must have non-empty values. If the parameter
// has an empty value it's not a match.
// has an empty value it's not a match.
return false;
}
else
{
// If there's a value in the segment for this parameter, use the subsegment value
values.Add(parameterNeedsValue.Name, parameterValueString);
outValues.Add(parameterNeedsValue.Name, parameterValueString);
}
parameterNeedsValue = null;
@ -294,7 +341,17 @@ namespace Microsoft.AspNet.Routing.Template
// the route "Foo" to the request URI "somethingFoo". Thus we have to check that we parsed the *entire*
// request URI in order for it to be a match.
// This check is related to the check we do earlier in this function for LiteralSubsegments.
return (lastIndex == 0) || routeSegment.Parts[0].IsParameter;
if (lastIndex == 0 || routeSegment.Parts[0].IsParameter)
{
foreach (var item in outValues)
{
values.Add(item.Key, item.Value);
}
return true;
}
return false;
}
}
}

View File

@ -16,6 +16,7 @@ namespace Microsoft.AspNet.Routing.Template
private const char EqualsSign = '=';
private const char QuestionMark = '?';
private const char Asterisk = '*';
private const string PeriodString = ".";
public static RouteTemplate Parse(string routeTemplate)
{
@ -318,18 +319,40 @@ namespace Microsoft.AspNet.Routing.Template
}
}
// if a segment has multiple parts, then the parameters can't be optional
// if a segment has multiple parts, then only the last one parameter can be optional
// if it is following a optional seperator.
for (var i = 0; i < segment.Parts.Count; i++)
{
var part = segment.Parts[i];
if (part.IsParameter && part.IsOptional && segment.Parts.Count > 1)
{
context.Error = Resources.TemplateRoute_CannotHaveOptionalParameterInMultiSegment;
return false;
// This is the last part
if (i == segment.Parts.Count - 1)
{
Debug.Assert(segment.Parts[i - 1].IsLiteral);
if (segment.Parts[i - 1].Text == PeriodString)
{
segment.Parts[i - 1].IsOptionalSeperator = true;
}
else
{
context.Error =
Resources.TemplateRoute_CanHaveOnlyLastParameterOptional_IfFollowingOptionalSeperator;
return false;
}
}
else
{
context.Error =
Resources.TemplateRoute_CanHaveOnlyLastParameterOptional_IfFollowingOptionalSeperator;
return false;
}
}
}
// A segment cannot containt two consecutive parameters
// A segment cannot contain two consecutive parameters
var isLastSegmentParameter = false;
for (var i = 0; i < segment.Parts.Count; i++)
{

View File

@ -40,6 +40,7 @@ namespace Microsoft.AspNet.Routing.Template
public bool IsLiteral { get; private set; }
public bool IsParameter { get; private set; }
public bool IsOptional { get; private set; }
public bool IsOptionalSeperator { get; set; }
public string Name { get; private set; }
public string Text { get; private set; }
public object DefaultValue { get; private set; }

View File

@ -197,6 +197,131 @@ namespace Microsoft.AspNet.Routing.Template.Tests
"language/axx-yy");
}
public static IEnumerable<object[]> OptionalParamValues
{
get
{
return new object[][]
{
// defaults
// ambient values
// values
new object[]
{
"Test/{val1}/{val2}.{val3?}",
new RouteValueDictionary(new {val1 = "someval1", val2 = "someval2"}),
new RouteValueDictionary(new {val3 = "someval3"}),
new RouteValueDictionary(new {val3 = "someval3"}),
"Test/someval1/someval2.someval3",
},
new object[]
{
"Test/{val1}/{val2}.{val3?}",
new RouteValueDictionary(new {val1 = "someval1", val2 = "someval2"}),
new RouteValueDictionary(new {val3 = "someval3a"}),
new RouteValueDictionary(new {val3 = "someval3v"}),
"Test/someval1/someval2.someval3v",
},
new object[]
{
"Test/{val1}/{val2}.{val3?}",
new RouteValueDictionary(new {val1 = "someval1", val2 = "someval2"}),
new RouteValueDictionary(new {val3 = "someval3a"}),
new RouteValueDictionary(),
"Test/someval1/someval2.someval3a",
},
new object[]
{
"Test/{val1}/{val2}.{val3?}",
new RouteValueDictionary(new {val1 = "someval1", val2 = "someval2"}),
new RouteValueDictionary(),
new RouteValueDictionary(new {val3 = "someval3v"}),
"Test/someval1/someval2.someval3v",
},
new object[]
{
"Test/{val1}/{val2}.{val3?}",
new RouteValueDictionary(new {val1 = "someval1", val2 = "someval2"}),
new RouteValueDictionary(),
new RouteValueDictionary(),
"Test/someval1/someval2",
},
new object[]
{
"Test/{val1}.{val2}.{val3}.{val4?}",
new RouteValueDictionary(new {val1 = "someval1", val2 = "someval2" }),
new RouteValueDictionary(),
new RouteValueDictionary(new {val4 = "someval4", val3 = "someval3" }),
"Test/someval1.someval2.someval3.someval4",
},
new object[]
{
"Test/{val1}.{val2}.{val3}.{val4?}",
new RouteValueDictionary(new {val1 = "someval1", val2 = "someval2" }),
new RouteValueDictionary(),
new RouteValueDictionary(new {val3 = "someval3" }),
"Test/someval1.someval2.someval3",
},
new object[]
{
"Test/.{val2?}",
new RouteValueDictionary(new { }),
new RouteValueDictionary(),
new RouteValueDictionary(new {val2 = "someval2" }),
"Test/.someval2",
},
new object[]
{
"Test/{val1}.{val2}",
new RouteValueDictionary(new {val1 = "someval1", val2 = "someval2" }),
new RouteValueDictionary(),
new RouteValueDictionary(new {val3 = "someval3" }),
"Test/someval1.someval2?val3=someval3",
},
};
}
}
[Theory]
[MemberData("OptionalParamValues")]
public void GetVirtualPathWithMultiSegmentWithOptionalParam(
string template,
IReadOnlyDictionary<string, object> defaults,
IDictionary<string, object> ambientValues,
IDictionary<string, object> values,
string expected)
{
// Arrange
var binder = new TemplateBinder(
TemplateParser.Parse(template),
defaults);
// Act & Assert
var result = binder.GetValues(ambientValues: ambientValues, values: values);
if (result == null)
{
if (expected == null)
{
return;
}
else
{
Assert.NotNull(result);
}
}
var boundTemplate = binder.BindValues(result.AcceptedValues);
if (expected == null)
{
Assert.Null(boundTemplate);
}
else
{
Assert.NotNull(boundTemplate);
Assert.Equal(expected, boundTemplate);
}
}
[Fact]
public void GetVirtualPathWithMultiSegmentParamsOnNeitherEndMatches()
{

View File

@ -101,6 +101,103 @@ namespace Microsoft.AspNet.Routing.Template.Tests
Assert.Equal("default p2", rd["p2"]);
}
[Theory]
[InlineData("moo/{p1}.{p2?}", "moo/foo.bar", "foo", "bar")]
[InlineData("moo/{p1?}", "moo/foo", "foo", null)]
[InlineData("moo/{p1?}", "moo", null, null)]
[InlineData("moo/{p1}.{p2?}", "moo/foo", "foo", null)]
[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.bar", "foo", "bar")]
[InlineData("moo/foo.{p1}.{p2?}", "moo/foo.moo.bar", "moo", "bar")]
[InlineData("moo/foo.{p1}.{p2?}", "moo/foo.moo", "moo", null)]
[InlineData("moo/.{p2?}", "moo/.foo", null, "foo")]
[InlineData("moo/.{p2?}", "moo", null, null)]
[InlineData("moo/{p1}.{p2?}", "moo/....", "..", ".")]
[InlineData("moo/{p1}.{p2?}", "moo/.bar", ".bar", null)]
public void MatchRoute_OptionalParameter_FollowedByPeriod_Valid(
string template,
string path,
string p1,
string p2)
{
// Arrange
var matcher = CreateMatcher(template);
// Act
var rd = matcher.Match(path);
// Assert
if (p1 != null)
{
Assert.Equal(p1, rd["p1"]);
}
if (p2 != null)
{
Assert.Equal(p2, rd["p2"]);
}
}
[Theory]
[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}.{p4?}", "moo/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")]
public void MatchRoute_OptionalParameter_FollowedByPeriod_3Parameters_Valid(
string template,
string path,
string p1,
string p2,
string p3)
{
// Arrange
var matcher = CreateMatcher(template);
// Act
var rd = matcher.Match(path);
// Assert
Assert.Equal(p1, rd["p1"]);
if (p2 != null)
{
Assert.Equal(p2, rd["p2"]);
}
if (p3 != null)
{
Assert.Equal(p3, rd["p3"]);
}
}
[Theory]
[InlineData("moo/{p1}.{p2?}", "moo/foo.")]
[InlineData("moo/{p1}.{p2?}", "moo/.")]
[InlineData("moo/{p1}.{p2}", "foo.")]
[InlineData("moo/{p1}.{p2}", "foo")]
[InlineData("moo/{p1}.{p2}.{p3?}", "moo/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")]
[InlineData("moo/{p1}.{p2}.{p3?}", "moo/foo")]
[InlineData("{p1}.{p2?}/{p3}", "foo./bar")]
[InlineData("moo/.{p2?}", "moo/.")]
[InlineData("{p1}.{p2}/{p3}", ".foo/bar")]
public void MatchRoute_OptionalParameter_FollowedByPeriod_Invalid(string template, string path)
{
// Arrange
var matcher = CreateMatcher(template);
// Act
var rd = matcher.Match(path);
// Assert
Assert.Null(rd);
}
[Fact]
public void MatchRouteWithOnlyLiterals()
{
@ -183,7 +280,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
// Assert
Assert.Null(rd);
}
[Fact]
public void NoMatchLongerUrl()
{

View File

@ -40,7 +40,8 @@ namespace Microsoft.AspNet.Routing.Template.Tests
var expected = new RouteTemplate(new List<TemplateSegment>());
expected.Segments.Add(new TemplateSegment());
expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p", false, false, defaultValue: null, inlineConstraints: null));
expected.Segments[0].Parts.Add(
TemplatePart.CreateParameter("p", false, false, defaultValue: null, inlineConstraints: null));
expected.Parameters.Add(expected.Segments[0].Parts[0]);
// Act
@ -58,7 +59,8 @@ namespace Microsoft.AspNet.Routing.Template.Tests
var expected = new RouteTemplate(new List<TemplateSegment>());
expected.Segments.Add(new TemplateSegment());
expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p", false, true, defaultValue: null, inlineConstraints: null));
expected.Segments[0].Parts.Add(
TemplatePart.CreateParameter("p", false, true, defaultValue: null, inlineConstraints: null));
expected.Parameters.Add(expected.Segments[0].Parts[0]);
// Act
@ -227,12 +229,275 @@ namespace Microsoft.AspNet.Routing.Template.Tests
Assert.Equal<RouteTemplate>(expected, actual, new TemplateEqualityComparer());
}
[Fact]
public void Parse_ComplexSegment_OptionalParameterFollowingPeriod()
{
// Arrange
var template = "{p1}.{p2?}";
var expected = new RouteTemplate(new List<TemplateSegment>());
expected.Segments.Add(new TemplateSegment());
expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1",
false,
false,
defaultValue: null,
inlineConstraints: null));
expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("."));
expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p2",
false,
true,
defaultValue: null,
inlineConstraints: null));
expected.Parameters.Add(expected.Segments[0].Parts[0]);
expected.Parameters.Add(expected.Segments[0].Parts[2]);
// Act
var actual = TemplateParser.Parse(template);
// Assert
Assert.Equal<RouteTemplate>(expected, actual, new TemplateEqualityComparer());
}
[Fact]
public void Parse_ComplexSegment_ParametersFollowingPeriod()
{
// Arrange
var template = "{p1}.{p2}";
var expected = new RouteTemplate(new List<TemplateSegment>());
expected.Segments.Add(new TemplateSegment());
expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1",
false,
false,
defaultValue: null,
inlineConstraints: null));
expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("."));
expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p2",
false,
false,
defaultValue: null,
inlineConstraints: null));
expected.Parameters.Add(expected.Segments[0].Parts[0]);
expected.Parameters.Add(expected.Segments[0].Parts[2]);
// Act
var actual = TemplateParser.Parse(template);
// Assert
Assert.Equal<RouteTemplate>(expected, actual, new TemplateEqualityComparer());
}
[Fact]
public void Parse_ComplexSegment_OptionalParameterFollowingPeriod_ThreeParameters()
{
// Arrange
var template = "{p1}.{p2}.{p3?}";
var expected = new RouteTemplate(new List<TemplateSegment>());
expected.Segments.Add(new TemplateSegment());
expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1",
false,
false,
defaultValue: null,
inlineConstraints: null));
expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("."));
expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p2",
false,
false,
defaultValue: null,
inlineConstraints: null));
expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("."));
expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p3",
false,
true,
defaultValue: null,
inlineConstraints: null));
expected.Parameters.Add(expected.Segments[0].Parts[0]);
expected.Parameters.Add(expected.Segments[0].Parts[2]);
expected.Parameters.Add(expected.Segments[0].Parts[4]);
// Act
var actual = TemplateParser.Parse(template);
// Assert
Assert.Equal<RouteTemplate>(expected, actual, new TemplateEqualityComparer());
}
[Fact]
public void Parse_ComplexSegment_ThreeParametersSeperatedByPeriod()
{
// Arrange
var template = "{p1}.{p2}.{p3}";
var expected = new RouteTemplate(new List<TemplateSegment>());
expected.Segments.Add(new TemplateSegment());
expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1",
false,
false,
defaultValue: null,
inlineConstraints: null));
expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("."));
expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p2",
false,
false,
defaultValue: null,
inlineConstraints: null));
expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("."));
expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p3",
false,
false,
defaultValue: null,
inlineConstraints: null));
expected.Parameters.Add(expected.Segments[0].Parts[0]);
expected.Parameters.Add(expected.Segments[0].Parts[2]);
expected.Parameters.Add(expected.Segments[0].Parts[4]);
// Act
var actual = TemplateParser.Parse(template);
// Assert
Assert.Equal<RouteTemplate>(expected, actual, new TemplateEqualityComparer());
}
[Fact]
public void Parse_ComplexSegment_OptionalParameterFollowingPeriod_MiddleSegment()
{
// Arrange
var template = "{p1}.{p2?}/{p3}";
var expected = new RouteTemplate(new List<TemplateSegment>());
expected.Segments.Add(new TemplateSegment());
expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1",
false,
false,
defaultValue: null,
inlineConstraints: null));
expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("."));
expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p2",
false,
true,
defaultValue: null,
inlineConstraints: null));
expected.Parameters.Add(expected.Segments[0].Parts[0]);
expected.Parameters.Add(expected.Segments[0].Parts[2]);
expected.Segments.Add(new TemplateSegment());
expected.Segments[1].Parts.Add(TemplatePart.CreateParameter("p3",
false,
false,
null,
null));
expected.Parameters.Add(expected.Segments[1].Parts[0]);
// Act
var actual = TemplateParser.Parse(template);
// Assert
Assert.Equal<RouteTemplate>(expected, actual, new TemplateEqualityComparer());
}
public void Parse_ComplexSegment_OptionalParameterFollowingPeriod_LastSegment()
{
// Arrange
var template = "{p1}/{p2}.{p3?}";
var expected = new RouteTemplate(new List<TemplateSegment>());
expected.Segments.Add(new TemplateSegment());
expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1",
false,
false,
defaultValue: null,
inlineConstraints: null));
expected.Segments.Add(new TemplateSegment());
expected.Segments[1].Parts.Add(TemplatePart.CreateParameter("p2",
false,
true,
defaultValue: null,
inlineConstraints: null));
expected.Segments[1].Parts.Add(TemplatePart.CreateLiteral("."));
expected.Segments[1].Parts.Add(TemplatePart.CreateParameter("p3",
false,
true,
null,
null));
expected.Parameters.Add(expected.Segments[0].Parts[0]);
expected.Parameters.Add(expected.Segments[1].Parts[0]);
expected.Parameters.Add(expected.Segments[1].Parts[2]);
// Act
var actual = TemplateParser.Parse(template);
// Assert
Assert.Equal<RouteTemplate>(expected, actual, new TemplateEqualityComparer());
}
[Fact]
public void Parse_ComplexSegment_OptionalParameterFollowingPeriod_PeriodAfterSlash()
{
// Arrange
var template = "{p2}/.{p3?}";
var expected = new RouteTemplate(new List<TemplateSegment>());
expected.Segments.Add(new TemplateSegment());
expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p2",
false,
false,
defaultValue: null,
inlineConstraints: null));
expected.Segments.Add(new TemplateSegment());
expected.Segments[1].Parts.Add(TemplatePart.CreateLiteral("."));
expected.Segments[1].Parts.Add(TemplatePart.CreateParameter("p3",
false,
true,
null,
null));
expected.Parameters.Add(expected.Segments[0].Parts[0]);
expected.Parameters.Add(expected.Segments[1].Parts[1]);
// Act
var actual = TemplateParser.Parse(template);
// Assert
Assert.Equal<RouteTemplate>(expected, actual, new TemplateEqualityComparer());
}
[Theory]
[InlineData("{p1?}.{p2?}")]
[InlineData("{p1}.{p2?}.{p3}")]
[InlineData("{p1?}.{p2}/{p3}")]
[InlineData("{p3}.{p1?}.{p2?}")]
[InlineData("{p1}-{p2?}")]
[InlineData("{p1}..{p2?}")]
[InlineData("..{p2?}")]
[InlineData("{p1}.abc.{p2?}")]
public void Parse_ComplexSegment_OptionalParametersSeperatedByPeriod_Invalid(string template)
{
// Act and Assert
ExceptionAssert.Throws<ArgumentException>(
() => TemplateParser.Parse(template),
"In a path segment that contains more than one section, such as a literal section or a parameter, " +
"there can only be one optional parameter. The optional parameter must be the last parameter in the " +
"segment and must be preceded by one single period (.)." + Environment.NewLine +
"Parameter name: routeTemplate");
}
[Fact]
public void InvalidTemplate_WithRepeatedParameter()
{
var ex = ExceptionAssert.Throws<ArgumentException>(
() => TemplateParser.Parse("{Controller}.mvc/{id}/{controller}"),
"The route parameter name 'controller' appears more than one time in the route template." + Environment.NewLine + "Parameter name: routeTemplate");
"The route parameter name 'controller' appears more than one time in the route template." +
Environment.NewLine + "Parameter name: routeTemplate");
}
[Theory]
@ -246,7 +511,8 @@ namespace Microsoft.AspNet.Routing.Template.Tests
{
ExceptionAssert.Throws<ArgumentException>(
() => TemplateParser.Parse(template),
@"There is an incomplete parameter in the route template. Check that each '{' character has a matching '}' character." + Environment.NewLine +
@"There is an incomplete parameter in the route template. Check that each '{' character has a " +
"matching '}' character." + Environment.NewLine +
"Parameter name: routeTemplate");
}
@ -255,7 +521,8 @@ namespace Microsoft.AspNet.Routing.Template.Tests
{
ExceptionAssert.Throws<ArgumentException>(
() => TemplateParser.Parse("123{a}abc{*moo}"),
"A path segment that contains more than one section, such as a literal section or a parameter, cannot contain a catch-all parameter." + Environment.NewLine +
"A path segment that contains more than one section, such as a literal section or a parameter, " +
"cannot contain a catch-all parameter." + Environment.NewLine +
"Parameter name: routeTemplate");
}
@ -264,7 +531,8 @@ namespace Microsoft.AspNet.Routing.Template.Tests
{
ExceptionAssert.Throws<ArgumentException>(
() => TemplateParser.Parse("{*p1}/{*p2}"),
"A catch-all parameter can only appear as the last segment of the route template." + Environment.NewLine +
"A catch-all parameter can only appear as the last segment of the route template." +
Environment.NewLine +
"Parameter name: routeTemplate");
}
@ -273,7 +541,8 @@ namespace Microsoft.AspNet.Routing.Template.Tests
{
ExceptionAssert.Throws<ArgumentException>(
() => TemplateParser.Parse("{*p1}abc{*p2}"),
"A path segment that contains more than one section, such as a literal section or a parameter, cannot contain a catch-all parameter." + Environment.NewLine +
"A path segment that contains more than one section, such as a literal section or a parameter, " +
"cannot contain a catch-all parameter." + Environment.NewLine +
"Parameter name: routeTemplate");
}
@ -315,7 +584,8 @@ namespace Microsoft.AspNet.Routing.Template.Tests
{
ExceptionAssert.Throws<ArgumentException>(
() => TemplateParser.Parse("foo/{{p1}"),
"There is an incomplete parameter in the route template. Check that each '{' character has a matching '}' character." + Environment.NewLine +
"There is an incomplete parameter in the route template. Check that each '{' character has a " +
"matching '}' character." + Environment.NewLine +
"Parameter name: routeTemplate");
}
@ -324,7 +594,8 @@ namespace Microsoft.AspNet.Routing.Template.Tests
{
ExceptionAssert.Throws<ArgumentException>(
() => TemplateParser.Parse("foo/{p1}}"),
"There is an incomplete parameter in the route template. Check that each '{' character has a matching '}' character." + Environment.NewLine +
"There is an incomplete parameter in the route template. Check that each '{' character has a " +
"matching '}' character." + Environment.NewLine +
"Parameter name: routeTemplate");
}
@ -333,7 +604,8 @@ namespace Microsoft.AspNet.Routing.Template.Tests
{
ExceptionAssert.Throws<ArgumentException>(
() => TemplateParser.Parse("{aaa}/{AAA}"),
"The route parameter name 'AAA' appears more than one time in the route template." + Environment.NewLine +
"The route parameter name 'AAA' appears more than one time in the route template." +
Environment.NewLine +
"Parameter name: routeTemplate");
}
@ -342,7 +614,8 @@ namespace Microsoft.AspNet.Routing.Template.Tests
{
ExceptionAssert.Throws<ArgumentException>(
() => TemplateParser.Parse("{aaa}/{*AAA}"),
"The route parameter name 'AAA' appears more than one time in the route template." + Environment.NewLine +
"The route parameter name 'AAA' appears more than one time in the route template." +
Environment.NewLine +
"Parameter name: routeTemplate");
}
@ -351,7 +624,8 @@ namespace Microsoft.AspNet.Routing.Template.Tests
{
ExceptionAssert.Throws<ArgumentException>(
() => TemplateParser.Parse("{a}/{aa}a}/{z}"),
"There is an incomplete parameter in the route template. Check that each '{' character has a matching '}' character." + Environment.NewLine +
"There is an incomplete parameter in the route template. Check that each '{' character has a " +
"matching '}' character." + Environment.NewLine +
"Parameter name: routeTemplate");
}
@ -396,7 +670,8 @@ namespace Microsoft.AspNet.Routing.Template.Tests
{
ExceptionAssert.Throws<ArgumentException>(
() => TemplateParser.Parse("{a}//{z}"),
"The route template separator character '/' cannot appear consecutively. It must be separated by either a parameter or a literal value." + Environment.NewLine +
"The route template separator character '/' cannot appear consecutively. It must be separated by " +
"either a parameter or a literal value." + Environment.NewLine +
"Parameter name: routeTemplate");
}
@ -405,7 +680,8 @@ namespace Microsoft.AspNet.Routing.Template.Tests
{
ExceptionAssert.Throws<ArgumentException>(
() => TemplateParser.Parse("foo/{p1}/{*p2}/{p3}"),
"A catch-all parameter can only appear as the last segment of the route template." + Environment.NewLine +
"A catch-all parameter can only appear as the last segment of the route template." +
Environment.NewLine +
"Parameter name: routeTemplate");
}
@ -414,7 +690,8 @@ namespace Microsoft.AspNet.Routing.Template.Tests
{
ExceptionAssert.Throws<ArgumentException>(
() => TemplateParser.Parse("foo/aa{p1}{p2}"),
"A path segment cannot contain two consecutive parameters. They must be separated by a '/' or by a literal string." + Environment.NewLine +
"A path segment cannot contain two consecutive parameters. They must be separated by a '/' or by " +
"a literal string." + Environment.NewLine +
"Parameter name: routeTemplate");
}
@ -441,7 +718,8 @@ namespace Microsoft.AspNet.Routing.Template.Tests
{
ExceptionAssert.Throws<ArgumentException>(
() => TemplateParser.Parse("foor?bar"),
"The literal section 'foor?bar' is invalid. Literal sections cannot contain the '?' character." + Environment.NewLine +
"The literal section 'foor?bar' is invalid. Literal sections cannot contain the '?' character." +
Environment.NewLine +
"Parameter name: routeTemplate");
}
@ -462,7 +740,9 @@ namespace Microsoft.AspNet.Routing.Template.Tests
{
ExceptionAssert.Throws<ArgumentException>(
() => TemplateParser.Parse("{foorb?}-bar-{z}"),
"A path segment that contains more than one section, such as a literal section or a parameter, cannot contain an optional parameter." + Environment.NewLine +
"In a path segment that contains more than one section, such as a literal section or a parameter, " +
"there can only be one optional parameter. The optional parameter must be the last parameter in " +
"the segment and must be preceded by one single period (.)." + Environment.NewLine +
"Parameter name: routeTemplate");
}

View File

@ -223,83 +223,7 @@ namespace Microsoft.AspNet.Routing.Template
Assert.NotSame(originalDataTokens, context.RouteData.DataTokens);
Assert.NotSame(route.DataTokens, context.RouteData.DataTokens);
}
[Fact]
public async Task RouteAsync_InlineConstrait_OptionalParameter()
{
// Arrange
var template = "{controller}/{action}/{id:int?}";
var context = CreateRouteContext("/Home/Index/5");
IDictionary<string, object> routeValues = null;
var mockTarget = new Mock<IRouter>(MockBehavior.Strict);
mockTarget
.Setup(s => s.RouteAsync(It.IsAny<RouteContext>()))
.Callback<RouteContext>(ctx =>
{
routeValues = ctx.RouteData.Values;
ctx.IsHandled = true;
})
.Returns(Task.FromResult(true));
var route = new TemplateRoute(
mockTarget.Object,
template,
defaults: null,
constraints: null,
dataTokens: null,
inlineConstraintResolver: _inlineConstraintResolver);
// Act
await route.RouteAsync(context);
// Assert
Assert.NotNull(routeValues);
Assert.True(routeValues.ContainsKey("id"));
Assert.Equal("5", routeValues["id"]);
Assert.True(context.RouteData.Values.ContainsKey("id"));
Assert.Equal("5", context.RouteData.Values["id"]);
}
[Fact]
public async Task RouteAsync_InlineConstrait_OptionalParameter_NotPresent()
{
// Arrange
var template = "{controller}/{action}/{id:int?}";
var context = CreateRouteContext("/Home/Index");
IDictionary<string, object> routeValues = null;
var mockTarget = new Mock<IRouter>(MockBehavior.Strict);
mockTarget
.Setup(s => s.RouteAsync(It.IsAny<RouteContext>()))
.Callback<RouteContext>(ctx =>
{
routeValues = ctx.RouteData.Values;
ctx.IsHandled = true;
})
.Returns(Task.FromResult(true));
var route = new TemplateRoute(
mockTarget.Object,
template,
defaults: null,
constraints: null,
dataTokens: null,
inlineConstraintResolver: _inlineConstraintResolver);
// Act
await route.RouteAsync(context);
// Assert
Assert.NotNull(routeValues);
Assert.False(routeValues.ContainsKey("id"));
Assert.False(context.RouteData.Values.ContainsKey("id"));
}
[Fact]
public async Task RouteAsync_MergesExistingRouteData_PassedToConstraint()
{
@ -862,6 +786,73 @@ namespace Microsoft.AspNet.Routing.Template
Assert.Null(context.RouteData.Values["1controller"]);
}
[Fact]
public async Task Match_Success_OptionalParameter_ValueProvided()
{
// Arrange
var route = CreateRoute("{controller}/{action}.{format?}", new { action = "Index" });
var context = CreateRouteContext("/Home/Create.xml");
// Act
await route.RouteAsync(context);
// Assert
Assert.True(context.IsHandled);
Assert.Equal(3, context.RouteData.Values.Count);
Assert.Equal("Home", context.RouteData.Values["controller"]);
Assert.Equal("Create", context.RouteData.Values["action"]);
Assert.Equal("xml", context.RouteData.Values["format"]);
}
[Fact]
public async Task Match_Success_OptionalParameter_ValueNotProvided()
{
// Arrange
var route = CreateRoute("{controller}/{action}.{format?}", new { action = "Index" });
var context = CreateRouteContext("/Home/Create");
// Act
await route.RouteAsync(context);
// Assert
Assert.True(context.IsHandled);
Assert.Equal(2, context.RouteData.Values.Count);
Assert.Equal("Home", context.RouteData.Values["controller"]);
Assert.Equal("Create", context.RouteData.Values["action"]);
}
[Fact]
public async Task Match_Success_OptionalParameter_DefaultValue()
{
// Arrange
var route = CreateRoute("{controller}/{action}.{format?}", new { action = "Index", format = "xml" });
var context = CreateRouteContext("/Home/Create");
// Act
await route.RouteAsync(context);
// Assert
Assert.True(context.IsHandled);
Assert.Equal(3, context.RouteData.Values.Count);
Assert.Equal("Home", context.RouteData.Values["controller"]);
Assert.Equal("Create", context.RouteData.Values["action"]);
Assert.Equal("xml", context.RouteData.Values["format"]);
}
[Fact]
public async Task Match_Success_OptionalParameter_EndsWithDot()
{
// Arrange
var route = CreateRoute("{controller}/{action}.{format?}", new { action = "Index" });
var context = CreateRouteContext("/Home/Create.");
// Act
await route.RouteAsync(context);
// Assert
Assert.False(context.IsHandled);
}
private static RouteContext CreateRouteContext(string requestPath, ILoggerFactory factory = null)
{
if (factory == null)
@ -1070,7 +1061,9 @@ namespace Microsoft.AspNet.Routing.Template
.Returns<string>(null);
var route = CreateRoute(target.Object, "{controller}/{action}");
var context = CreateVirtualPathContext(new { action = "Store" }, new { Controller = "Home", action = "Blog" });
var context = CreateVirtualPathContext(
new { action = "Store" },
new { Controller = "Home", action = "Blog" });
var expectedValues = new RouteValueDictionary(new { controller = "Home", action = "Store" });
@ -1094,9 +1087,11 @@ namespace Microsoft.AspNet.Routing.Template
.Returns<string>(null);
var route = CreateRoute(target.Object, "Admin/{controller}/{action}", new { area = "Admin" });
var context = CreateVirtualPathContext(new { action = "Store" }, new { Controller = "Home", action = "Blog" });
var context = CreateVirtualPathContext(
new { action = "Store" }, new { Controller = "Home", action = "Blog" });
var expectedValues = new RouteValueDictionary(new { controller = "Home", action = "Store", area = "Admin" });
var expectedValues = new RouteValueDictionary(
new { controller = "Home", action = "Store", area = "Admin" });
// Act
var path = route.GetVirtualPath(context);
@ -1118,7 +1113,8 @@ namespace Microsoft.AspNet.Routing.Template
.Returns<string>(null);
var route = CreateRoute(target.Object, "{controller}/{action}");
var context = CreateVirtualPathContext(new { action = "Store", id = 5 }, new { Controller = "Home", action = "Blog" });
var context = CreateVirtualPathContext(
new { action = "Store", id = 5 }, new { Controller = "Home", action = "Blog" });
var expectedValues = new RouteValueDictionary(new { controller = "Home", action = "Store" });
@ -1352,7 +1348,166 @@ namespace Microsoft.AspNet.Routing.Template
Assert.Equal("Home/Index/products", path);
}
[Fact]
public void GetVirtualPath_OptionalParameter_ParameterPresentInValues()
{
// Arrange
var route = CreateRoute(
template: "{controller}/{action}/{name}.{format?}",
defaults: null,
accept: true,
constraints: null);
var context = CreateVirtualPathContext(
values: new { action = "Index", controller = "Home", name = "products", format = "xml"});
// Act
var path = route.GetVirtualPath(context);
// Assert
Assert.Equal("Home/Index/products.xml", path);
}
[Fact]
public void GetVirtualPath_OptionalParameter_ParameterNotPresentInValues()
{
// Arrange
var route = CreateRoute(
template: "{controller}/{action}/{name}.{format?}",
defaults: null,
accept: true,
constraints: null);
var context = CreateVirtualPathContext(
values: new { action = "Index", controller = "Home", name = "products"});
// Act
var path = route.GetVirtualPath(context);
// Assert
Assert.Equal("Home/Index/products", path);
}
[Fact]
public void GetVirtualPath_OptionalParameter_ParameterPresentInValuesAndDefaults()
{
// Arrange
var route = CreateRoute(
template: "{controller}/{action}/{name}.{format?}",
defaults: new { format = "json" },
accept: true,
constraints: null);
var context = CreateVirtualPathContext(
values: new { action = "Index", controller = "Home", name = "products" , format = "xml"});
// Act
var path = route.GetVirtualPath(context);
// Assert
Assert.Equal("Home/Index/products.xml", path);
}
[Fact]
public void GetVirtualPath_OptionalParameter_ParameterNotPresentInValues_PresentInDefaults()
{
// Arrange
var route = CreateRoute(
template: "{controller}/{action}/{name}.{format?}",
defaults: new { format = "json" },
accept: true,
constraints: null);
var context = CreateVirtualPathContext(
values: new { action = "Index", controller = "Home", name = "products"});
// Act
var path = route.GetVirtualPath(context);
// Assert
Assert.Equal("Home/Index/products", path);
}
[Fact]
public void GetVirtualPath_OptionalParameter_ParameterNotPresentInTemplate_PresentInValues()
{
// Arrange
var route = CreateRoute(
template: "{controller}/{action}/{name}",
defaults: null,
accept: true,
constraints: null);
var context = CreateVirtualPathContext(
values: new { action = "Index", controller = "Home", name = "products", format = "json" });
// Act
var path = route.GetVirtualPath(context);
// Assert
Assert.Equal("Home/Index/products?format=json", path);
}
[Fact]
public void GetVirtualPath_OptionalParameter_FollowedByDotAfterSlash_ParameterPresent()
{
// Arrange
var route = CreateRoute(
template: "{controller}/{action}/.{name?}",
defaults: null,
accept: true,
constraints: null);
var context = CreateVirtualPathContext(
values: new { action = "Index", controller = "Home", name = "products"});
// Act
var path = route.GetVirtualPath(context);
// Assert
Assert.Equal("Home/Index/.products", path);
}
[Fact]
public void GetVirtualPath_OptionalParameter_FollowedByDotAfterSlash_ParameterNotPresent()
{
// Arrange
var route = CreateRoute(
template: "{controller}/{action}/.{name?}",
defaults: null,
accept: true,
constraints: null);
var context = CreateVirtualPathContext(
values: new { action = "Index", controller = "Home"});
// Act
var path = route.GetVirtualPath(context);
// Assert
Assert.Equal("Home/Index/", path);
}
[Fact]
public void GetVirtualPath_OptionalParameter_InSimpleSegment()
{
// Arrange
var route = CreateRoute(
template: "{controller}/{action}/{name?}",
defaults: null,
accept: true,
constraints: null);
var context = CreateVirtualPathContext(
values: new { action = "Index", controller = "Home"});
// Act
var path = route.GetVirtualPath(context);
// Assert
Assert.Equal("Home/Index", path);
}
private static VirtualPathContext CreateVirtualPathContext(object values)
{
return CreateVirtualPathContext(new RouteValueDictionary(values), null);
@ -1363,7 +1518,9 @@ namespace Microsoft.AspNet.Routing.Template
return CreateVirtualPathContext(new RouteValueDictionary(values), new RouteValueDictionary(ambientValues));
}
private static VirtualPathContext CreateVirtualPathContext(IDictionary<string, object> values, IDictionary<string, object> ambientValues)
private static VirtualPathContext CreateVirtualPathContext(
IDictionary<string, object> values,
IDictionary<string, object> ambientValues)
{
var context = new Mock<HttpContext>(MockBehavior.Strict);
context.Setup(m => m.RequestServices.GetService(typeof(ILoggerFactory)))