diff --git a/build.cmd b/build.cmd
index 86ca5bbbf1..220a1ff561 100644
--- a/build.cmd
+++ b/build.cmd
@@ -19,10 +19,10 @@ IF EXIST packages\KoreBuild goto run
.nuget\NuGet.exe install KoreBuild -ExcludeVersion -o packages -nocache -pre
.nuget\NuGet.exe install Sake -version 0.2 -o packages -ExcludeVersion
-IF "%SKIP_KRE_INSTALL%"=="1" goto run
-CALL packages\KoreBuild\build\kvm upgrade -runtime CLR -x86
-CALL packages\KoreBuild\build\kvm install default -runtime CoreCLR -x86
+IF "%SKIP_DOTNET_INSTALL%"=="1" goto run
+CALL packages\KoreBuild\build\dotnetsdk upgrade -runtime CLR -x86
+CALL packages\KoreBuild\build\dotnetsdk install default -runtime CoreCLR -x86
:run
-CALL packages\KoreBuild\build\kvm use default -runtime CLR -x86
+CALL packages\KoreBuild\build\dotnetsdk use default -runtime CLR -x86
packages\Sake\tools\Sake.exe -I packages\KoreBuild\build -f makefile.shade %*
diff --git a/build.sh b/build.sh
index c7873ef58e..350d7e389a 100644
--- a/build.sh
+++ b/build.sh
@@ -28,11 +28,11 @@ if test ! -d packages/KoreBuild; then
fi
if ! type k > /dev/null 2>&1; then
- source packages/KoreBuild/build/kvm.sh
+ source packages/KoreBuild/build/dotnetsdk.sh
fi
if ! type k > /dev/null 2>&1; then
- kvm upgrade
+ dotnetsdk upgrade
fi
mono packages/Sake/tools/Sake.exe -I packages/KoreBuild/build -f makefile.shade "$@"
diff --git a/src/Microsoft.AspNet.Routing/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Routing/Properties/Resources.Designer.cs
index c6d51b59e9..e9cca0d601 100644
--- a/src/Microsoft.AspNet.Routing/Properties/Resources.Designer.cs
+++ b/src/Microsoft.AspNet.Routing/Properties/Resources.Designer.cs
@@ -205,17 +205,17 @@ namespace Microsoft.AspNet.Routing
///
/// A path segment that contains more than one section, such as a literal section or a parameter, cannot contain an optional parameter.
///
- internal static string TemplateRoute_CannotHaveOptionalParameterInMultiSegment
+ internal static string TemplateRoute_CanHaveOnlyLastParameterOptional_IfFollowingOptionalSeperator
{
- get { return GetString("TemplateRoute_CannotHaveOptionalParameterInMultiSegment"); }
+ get { return GetString("TemplateRoute_CanHaveOnlyLastParameterOptional_IfFollowingOptionalSeperator"); }
}
///
/// A path segment that contains more than one section, such as a literal section or a parameter, cannot contain an optional parameter.
///
- internal static string FormatTemplateRoute_CannotHaveOptionalParameterInMultiSegment()
+ internal static string FormatTemplateRoute_CanHaveOnlyLastParameterOptional_IfFollowingOptionalSeperator()
{
- return GetString("TemplateRoute_CannotHaveOptionalParameterInMultiSegment");
+ return GetString("TemplateRoute_CanHaveOnlyLastParameterOptional_IfFollowingOptionalSeperator");
}
///
diff --git a/src/Microsoft.AspNet.Routing/Resources.resx b/src/Microsoft.AspNet.Routing/Resources.resx
index b4ce8a15c3..4b34eea2d3 100644
--- a/src/Microsoft.AspNet.Routing/Resources.resx
+++ b/src/Microsoft.AspNet.Routing/Resources.resx
@@ -1,17 +1,17 @@
-
@@ -153,8 +153,8 @@
The route template separator character '/' cannot appear consecutively. It must be separated by either a parameter or a literal value.
-
- A path segment that contains more than one section, such as a literal section or a parameter, cannot contain an optional parameter.
+
+ 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 (.).A catch-all parameter cannot be marked optional.
diff --git a/src/Microsoft.AspNet.Routing/RouteBuilderExtensions.cs b/src/Microsoft.AspNet.Routing/RouteBuilderExtensions.cs
index a45497c6b8..400edd386d 100644
--- a/src/Microsoft.AspNet.Routing/RouteBuilderExtensions.cs
+++ b/src/Microsoft.AspNet.Routing/RouteBuilderExtensions.cs
@@ -3,10 +3,11 @@
using System;
using System.Collections.Generic;
+using Microsoft.AspNet.Routing;
using Microsoft.AspNet.Routing.Template;
using Microsoft.Framework.DependencyInjection;
-namespace Microsoft.AspNet.Routing
+namespace Microsoft.AspNet.Builder
{
public static class RouteBuilderExtensions
{
diff --git a/src/Microsoft.AspNet.Routing/Template/TemplateBinder.cs b/src/Microsoft.AspNet.Routing/Template/TemplateBinder.cs
index 423d9e4af1..1a0adb9409 100644
--- a/src/Microsoft.AspNet.Routing/Template/TemplateBinder.cs
+++ b/src/Microsoft.AspNet.Routing/Template/TemplateBinder.cs
@@ -7,7 +7,7 @@ using System.Diagnostics;
using System.Globalization;
using System.Text;
using System.Text.RegularExpressions;
-using Microsoft.AspNet.WebUtilities;
+using Microsoft.AspNet.Http.Extensions;
namespace Microsoft.AspNet.Routing.Template
{
@@ -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))
diff --git a/src/Microsoft.AspNet.Routing/Template/TemplateMatcher.cs b/src/Microsoft.AspNet.Routing/Template/TemplateMatcher.cs
index f5768fad5e..36ee764815 100644
--- a/src/Microsoft.AspNet.Routing/Template/TemplateMatcher.cs
+++ b/src/Microsoft.AspNet.Routing/Template/TemplateMatcher.cs
@@ -168,17 +168,63 @@ namespace Microsoft.AspNet.Routing.Template
string requestSegment,
IReadOnlyDictionary 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 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;
}
}
}
diff --git a/src/Microsoft.AspNet.Routing/Template/TemplateParser.cs b/src/Microsoft.AspNet.Routing/Template/TemplateParser.cs
index a7fe485d31..03664f25f5 100644
--- a/src/Microsoft.AspNet.Routing/Template/TemplateParser.cs
+++ b/src/Microsoft.AspNet.Routing/Template/TemplateParser.cs
@@ -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++)
{
diff --git a/src/Microsoft.AspNet.Routing/Template/TemplatePart.cs b/src/Microsoft.AspNet.Routing/Template/TemplatePart.cs
index 65099bce1a..dc1195a82d 100644
--- a/src/Microsoft.AspNet.Routing/Template/TemplatePart.cs
+++ b/src/Microsoft.AspNet.Routing/Template/TemplatePart.cs
@@ -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; }
diff --git a/src/Microsoft.AspNet.Routing/project.json b/src/Microsoft.AspNet.Routing/project.json
index 09081ae8e3..69fc89dea4 100644
--- a/src/Microsoft.AspNet.Routing/project.json
+++ b/src/Microsoft.AspNet.Routing/project.json
@@ -6,7 +6,7 @@
},
"dependencies": {
"Microsoft.AspNet.RequestContainer": "1.0.0-*",
- "Microsoft.AspNet.WebUtilities": "1.0.0-*",
+ "Microsoft.AspNet.Http.Extensions": "1.0.0-*",
"Microsoft.Framework.Logging": "1.0.0-*"
},
"frameworks": {
diff --git a/test/Microsoft.AspNet.Routing.Tests/RouterMiddlewareTest.cs b/test/Microsoft.AspNet.Routing.Tests/RouterMiddlewareTest.cs
index 80217ce62d..faa8cad5c7 100644
--- a/test/Microsoft.AspNet.Routing.Tests/RouterMiddlewareTest.cs
+++ b/test/Microsoft.AspNet.Routing.Tests/RouterMiddlewareTest.cs
@@ -4,7 +4,7 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNet.Builder;
-using Microsoft.AspNet.PipelineCore;
+using Microsoft.AspNet.Http.Core;
using Microsoft.AspNet.Routing.Logging;
using Xunit;
diff --git a/test/Microsoft.AspNet.Routing.Tests/Template/TemplateBinderTests.cs b/test/Microsoft.AspNet.Routing.Tests/Template/TemplateBinderTests.cs
index 3750eaecc7..34ccb0376d 100644
--- a/test/Microsoft.AspNet.Routing.Tests/Template/TemplateBinderTests.cs
+++ b/test/Microsoft.AspNet.Routing.Tests/Template/TemplateBinderTests.cs
@@ -197,6 +197,131 @@ namespace Microsoft.AspNet.Routing.Template.Tests
"language/axx-yy");
}
+ public static IEnumerable