Adding legacy rounting code - mostly unmodified to get it into history
This commit is contained in:
parent
7cfcdcebdc
commit
d4904e8701
|
|
@ -0,0 +1,145 @@
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// <auto-generated>
|
||||||
|
// This code was generated by a tool.
|
||||||
|
// Runtime Version:4.0.30319.34003
|
||||||
|
//
|
||||||
|
// Changes to this file may cause incorrect behavior and will be lost if
|
||||||
|
// the code is regenerated.
|
||||||
|
// </auto-generated>
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Routing {
|
||||||
|
using System;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A strongly-typed resource class, for looking up localized strings, etc.
|
||||||
|
/// </summary>
|
||||||
|
// This class was auto-generated by the StronglyTypedResourceBuilder
|
||||||
|
// class via a tool like ResGen or Visual Studio.
|
||||||
|
// To add or remove a member, edit your .ResX file then rerun ResGen
|
||||||
|
// with the /str option, or rebuild your VS project.
|
||||||
|
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
|
||||||
|
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||||
|
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||||
|
internal class Resources {
|
||||||
|
|
||||||
|
private static global::System.Resources.ResourceManager resourceMan;
|
||||||
|
|
||||||
|
private static global::System.Globalization.CultureInfo resourceCulture;
|
||||||
|
|
||||||
|
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
|
||||||
|
internal Resources() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the cached ResourceManager instance used by this class.
|
||||||
|
/// </summary>
|
||||||
|
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||||
|
internal static global::System.Resources.ResourceManager ResourceManager {
|
||||||
|
get {
|
||||||
|
if (object.ReferenceEquals(resourceMan, null)) {
|
||||||
|
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.AspNet.Routing.Resources", IntrospectionExtensions.GetTypeInfo(typeof(Resources)).Assembly);
|
||||||
|
resourceMan = temp;
|
||||||
|
}
|
||||||
|
return resourceMan;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Overrides the current thread's CurrentUICulture property for all
|
||||||
|
/// resource lookups using this strongly typed resource class.
|
||||||
|
/// </summary>
|
||||||
|
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||||
|
internal static global::System.Globalization.CultureInfo Culture {
|
||||||
|
get {
|
||||||
|
return resourceCulture;
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
resourceCulture = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to A path segment that contains more than one section, such as a literal section or a parameter, cannot contain a catch-all parameter..
|
||||||
|
/// </summary>
|
||||||
|
internal static string TemplateRoute_CannotHaveCatchAllInMultiSegment {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("TemplateRoute_CannotHaveCatchAllInMultiSegment", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to A path segment cannot contain two consecutive parameters. They must be separated by a '/' or by a literal string..
|
||||||
|
/// </summary>
|
||||||
|
internal static string TemplateRoute_CannotHaveConsecutiveParameters {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("TemplateRoute_CannotHaveConsecutiveParameters", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to The route template separator character '/' cannot appear consecutively. It must be separated by either a parameter or a literal value..
|
||||||
|
/// </summary>
|
||||||
|
internal static string TemplateRoute_CannotHaveConsecutiveSeparators {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("TemplateRoute_CannotHaveConsecutiveSeparators", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to A catch-all parameter can only appear as the last segment of the route template..
|
||||||
|
/// </summary>
|
||||||
|
internal static string TemplateRoute_CatchAllMustBeLast {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("TemplateRoute_CatchAllMustBeLast", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to The route parameter name '{0}' is invalid. Route parameter names must be non-empty and cannot contain these characters: "{{", "}}", "/", "?".
|
||||||
|
/// </summary>
|
||||||
|
internal static string TemplateRoute_InvalidParameterName {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("TemplateRoute_InvalidParameterName", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to The route template cannot start with a '/' or '~' character and it cannot contain a '?' character..
|
||||||
|
/// </summary>
|
||||||
|
internal static string TemplateRoute_InvalidRouteTemplate {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("TemplateRoute_InvalidRouteTemplate", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to There is an incomplete parameter in this path segment: '{0}'. Check that each '{{' character has a matching '}}' character..
|
||||||
|
/// </summary>
|
||||||
|
internal static string TemplateRoute_MismatchedParameter {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("TemplateRoute_MismatchedParameter", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to The route parameter name '{0}' appears more than one time in the route template..
|
||||||
|
/// </summary>
|
||||||
|
internal static string TemplateRoute_RepeatedParameter {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("TemplateRoute_RepeatedParameter", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to The constraint entry '{0}' on the route with route template '{1}' must have a string value or be of a type which implements '{2}'..
|
||||||
|
/// </summary>
|
||||||
|
internal static string TemplateRoute_ValidationMustBeStringOrCustomConstraint {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("TemplateRoute_ValidationMustBeStringOrCustomConstraint", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,147 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<root>
|
||||||
|
<!--
|
||||||
|
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
|
||||||
|
associated with the data types.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
... ado.net/XML headers & schema ...
|
||||||
|
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||||
|
<resheader name="version">2.0</resheader>
|
||||||
|
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||||
|
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||||
|
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||||
|
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||||
|
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||||
|
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||||
|
</data>
|
||||||
|
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||||
|
<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
|
||||||
|
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
|
||||||
|
mimetype set.
|
||||||
|
|
||||||
|
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
|
||||||
|
read any of the formats listed below.
|
||||||
|
|
||||||
|
mimetype: application/x-microsoft.net.object.binary.base64
|
||||||
|
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
|
||||||
|
: 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
|
||||||
|
: using a System.ComponentModel.TypeConverter
|
||||||
|
: and then encoded with base64 encoding.
|
||||||
|
-->
|
||||||
|
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||||
|
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||||
|
<xsd:element name="root" msdata:IsDataSet="true">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:choice maxOccurs="unbounded">
|
||||||
|
<xsd:element name="metadata">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||||
|
<xsd:attribute name="type" type="xsd:string" />
|
||||||
|
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||||
|
<xsd:attribute ref="xml:space" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="assembly">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:attribute name="alias" type="xsd:string" />
|
||||||
|
<xsd:attribute name="name" type="xsd:string" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="data">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||||
|
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||||
|
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||||
|
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||||
|
<xsd:attribute ref="xml:space" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="resheader">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
</xsd:choice>
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
</xsd:schema>
|
||||||
|
<resheader name="resmimetype">
|
||||||
|
<value>text/microsoft-resx</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="version">
|
||||||
|
<value>2.0</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="reader">
|
||||||
|
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="writer">
|
||||||
|
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
|
</resheader>
|
||||||
|
<data name="TemplateRoute_CannotHaveCatchAllInMultiSegment" xml:space="preserve">
|
||||||
|
<value>A path segment that contains more than one section, such as a literal section or a parameter, cannot contain a catch-all parameter.</value>
|
||||||
|
</data>
|
||||||
|
<data name="TemplateRoute_CannotHaveConsecutiveParameters" xml:space="preserve">
|
||||||
|
<value>A path segment cannot contain two consecutive parameters. They must be separated by a '/' or by a literal string.</value>
|
||||||
|
</data>
|
||||||
|
<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_CatchAllMustBeLast" xml:space="preserve">
|
||||||
|
<value>A catch-all parameter can only appear as the last segment of the route template.</value>
|
||||||
|
</data>
|
||||||
|
<data name="TemplateRoute_InvalidParameterName" xml:space="preserve">
|
||||||
|
<value>The route parameter name '{0}' is invalid. Route parameter names must be non-empty and cannot contain these characters: "{{", "}}", "/", "?"</value>
|
||||||
|
</data>
|
||||||
|
<data name="TemplateRoute_InvalidRouteTemplate" xml:space="preserve">
|
||||||
|
<value>The route template cannot start with a '/' or '~' character and it cannot contain a '?' character.</value>
|
||||||
|
</data>
|
||||||
|
<data name="TemplateRoute_MismatchedParameter" xml:space="preserve">
|
||||||
|
<value>There is an incomplete parameter in this path segment: '{0}'. Check that each '{{' character has a matching '}}' character.</value>
|
||||||
|
</data>
|
||||||
|
<data name="TemplateRoute_RepeatedParameter" xml:space="preserve">
|
||||||
|
<value>The route parameter name '{0}' appears more than one time in the route template.</value>
|
||||||
|
</data>
|
||||||
|
<data name="TemplateRoute_ValidationMustBeStringOrCustomConstraint" xml:space="preserve">
|
||||||
|
<value>The constraint entry '{0}' on the route with route template '{1}' must have a string value or be of a type which implements '{2}'.</value>
|
||||||
|
</data>
|
||||||
|
</root>
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Routing.Template
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a URI generated from a <see cref="TemplateParsedRoute"/>.
|
||||||
|
/// </summary>
|
||||||
|
public class BoundRouteTemplate
|
||||||
|
{
|
||||||
|
public string BoundTemplate { get; set; }
|
||||||
|
|
||||||
|
public IDictionary<string, object> Values { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Microsoft.AspNet.Abstractions;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Routing.Template
|
||||||
|
{
|
||||||
|
public interface ITemplateRouteConstraint
|
||||||
|
{
|
||||||
|
bool Match(HttpContext context, IRoute route, string parameterName, IDictionary<string, object> values, RouteDirection routeDirection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Routing.Template
|
||||||
|
{
|
||||||
|
public interface IVirtualPathData
|
||||||
|
{
|
||||||
|
IRoute Route { get; }
|
||||||
|
|
||||||
|
string VirtualPath { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Routing.Template
|
||||||
|
{
|
||||||
|
// Represents a segment of a URI that is not a separator. It contains subsegments such as literals and parameters.
|
||||||
|
internal sealed class PathContentSegment : PathSegment
|
||||||
|
{
|
||||||
|
public PathContentSegment(IList<PathSubsegment> subsegments)
|
||||||
|
{
|
||||||
|
Subsegments = subsegments;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SuppressMessage("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily", Justification = "Not changing original algorithm.")]
|
||||||
|
public bool IsCatchAll
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
// TODO: Verify this is correct. Maybe add an assert.
|
||||||
|
// Performance sensitive
|
||||||
|
// Caching count is faster for IList<T>
|
||||||
|
int subsegmentCount = Subsegments.Count;
|
||||||
|
for (int i = 0; i < subsegmentCount; i++)
|
||||||
|
{
|
||||||
|
PathSubsegment seg = Subsegments[i];
|
||||||
|
PathParameterSubsegment paramterSubSegment = seg as PathParameterSubsegment;
|
||||||
|
if (paramterSubSegment != null && paramterSubSegment.IsCatchAll)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public IList<PathSubsegment> Subsegments { get; private set; }
|
||||||
|
|
||||||
|
#if ROUTE_DEBUGGING
|
||||||
|
public override string LiteralText
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
List<string> s = new List<string>();
|
||||||
|
foreach (PathSubsegment subsegment in Subsegments)
|
||||||
|
{
|
||||||
|
s.Add(subsegment.LiteralText);
|
||||||
|
}
|
||||||
|
return String.Join(String.Empty, s.ToArray());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
List<string> s = new List<string>();
|
||||||
|
foreach (PathSubsegment subsegment in Subsegments)
|
||||||
|
{
|
||||||
|
s.Add(subsegment.ToString());
|
||||||
|
}
|
||||||
|
return "[ " + String.Join(", ", s.ToArray()) + " ]";
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Routing.Template
|
||||||
|
{
|
||||||
|
// Represents a literal subsegment of a ContentPathSegment
|
||||||
|
internal sealed class PathLiteralSubsegment : PathSubsegment
|
||||||
|
{
|
||||||
|
public PathLiteralSubsegment(string literal)
|
||||||
|
{
|
||||||
|
Literal = literal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Literal { get; private set; }
|
||||||
|
|
||||||
|
#if ROUTE_DEBUGGING
|
||||||
|
public override string LiteralText
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return Literal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return "\"" + Literal + "\"";
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Routing.Template
|
||||||
|
{
|
||||||
|
// Represents a parameter subsegment of a ContentPathSegment
|
||||||
|
internal sealed class PathParameterSubsegment : PathSubsegment
|
||||||
|
{
|
||||||
|
public PathParameterSubsegment(string parameterName)
|
||||||
|
{
|
||||||
|
if (parameterName.StartsWith("*", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
ParameterName = parameterName.Substring(1);
|
||||||
|
IsCatchAll = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ParameterName = parameterName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsCatchAll { get; private set; }
|
||||||
|
|
||||||
|
public string ParameterName { get; private set; }
|
||||||
|
|
||||||
|
#if ROUTE_DEBUGGING
|
||||||
|
public override string LiteralText
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return "{" + (IsCatchAll ? "*" : String.Empty) + ParameterName + "}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return "{" + (IsCatchAll ? "*" : String.Empty) + ParameterName + "}";
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Routing.Template
|
||||||
|
{
|
||||||
|
// Represents a segment of a URI such as a separator or content
|
||||||
|
public abstract class PathSegment
|
||||||
|
{
|
||||||
|
#if ROUTE_DEBUGGING
|
||||||
|
public abstract string LiteralText
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Routing.Template
|
||||||
|
{
|
||||||
|
// Represents a "/" separator in a URI
|
||||||
|
internal sealed class PathSeparatorSegment : PathSegment
|
||||||
|
{
|
||||||
|
#if ROUTE_DEBUGGING
|
||||||
|
public override string LiteralText
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return "/";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return "\"/\"";
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Routing.Template
|
||||||
|
{
|
||||||
|
// Represents a subsegment of a ContentPathSegment such as a parameter or a literal.
|
||||||
|
internal abstract class PathSubsegment
|
||||||
|
{
|
||||||
|
#if ROUTE_DEBUGGING
|
||||||
|
public abstract string LiteralText
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Routing.Template
|
||||||
|
{
|
||||||
|
public enum RouteDirection
|
||||||
|
{
|
||||||
|
UriResolution = 0,
|
||||||
|
UriGeneration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,842 @@
|
||||||
|
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.Diagnostics.Contracts;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Routing.Template
|
||||||
|
{
|
||||||
|
public sealed class TemplateParsedRoute
|
||||||
|
{
|
||||||
|
public TemplateParsedRoute(IList<PathSegment> pathSegments)
|
||||||
|
{
|
||||||
|
Contract.Assert(pathSegments != null);
|
||||||
|
PathSegments = pathSegments;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal IList<PathSegment> PathSegments { get; private set; }
|
||||||
|
|
||||||
|
[SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", Justification = "Not changing original algorithm")]
|
||||||
|
[SuppressMessage("Microsoft.Maintainability", "CA1505:AvoidUnmaintainableCode", Justification = "Not changing original algorithm")]
|
||||||
|
public BoundRouteTemplate Bind(IDictionary<string, object> currentValues, IDictionary<string, object> values, IDictionary<string, object> defaultValues, IDictionary<string, object> constraints)
|
||||||
|
{
|
||||||
|
if (currentValues == null)
|
||||||
|
{
|
||||||
|
currentValues = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (values == null)
|
||||||
|
{
|
||||||
|
values = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (defaultValues == null)
|
||||||
|
{
|
||||||
|
defaultValues = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The set of values we should be using when generating the URI in this route
|
||||||
|
IDictionary<string, object> acceptedValues = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
// Keep track of which new values have been used
|
||||||
|
HashSet<string> unusedNewValues = new HashSet<string>(values.Keys, StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
// Step 1: Get the list of values we're going to try to use to match and generate this URI
|
||||||
|
|
||||||
|
// Find out which entries in the URI are valid for the URI we want to generate.
|
||||||
|
// If the URI had ordered parameters a="1", b="2", c="3" and the new values
|
||||||
|
// specified that b="9", then we need to invalidate everything after it. The new
|
||||||
|
// values should then be a="1", b="9", c=<no value>.
|
||||||
|
ForEachParameter(PathSegments, delegate(PathParameterSubsegment parameterSubsegment)
|
||||||
|
{
|
||||||
|
// If it's a parameter subsegment, examine the current value to see if it matches the new value
|
||||||
|
string parameterName = parameterSubsegment.ParameterName;
|
||||||
|
|
||||||
|
object newParameterValue;
|
||||||
|
bool hasNewParameterValue = values.TryGetValue(parameterName, out newParameterValue);
|
||||||
|
if (hasNewParameterValue)
|
||||||
|
{
|
||||||
|
unusedNewValues.Remove(parameterName);
|
||||||
|
}
|
||||||
|
|
||||||
|
object currentParameterValue;
|
||||||
|
bool hasCurrentParameterValue = currentValues.TryGetValue(parameterName, out currentParameterValue);
|
||||||
|
|
||||||
|
if (hasNewParameterValue && hasCurrentParameterValue)
|
||||||
|
{
|
||||||
|
if (!RoutePartsEqual(currentParameterValue, newParameterValue))
|
||||||
|
{
|
||||||
|
// Stop copying current values when we find one that doesn't match
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the parameter is a match, add it to the list of values we will use for URI generation
|
||||||
|
if (hasNewParameterValue)
|
||||||
|
{
|
||||||
|
if (IsRoutePartNonEmpty(newParameterValue))
|
||||||
|
{
|
||||||
|
acceptedValues.Add(parameterName, newParameterValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (hasCurrentParameterValue)
|
||||||
|
{
|
||||||
|
acceptedValues.Add(parameterName, currentParameterValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add all remaining new values to the list of values we will use for URI generation
|
||||||
|
foreach (var newValue in values)
|
||||||
|
{
|
||||||
|
if (IsRoutePartNonEmpty(newValue.Value))
|
||||||
|
{
|
||||||
|
if (!acceptedValues.ContainsKey(newValue.Key))
|
||||||
|
{
|
||||||
|
acceptedValues.Add(newValue.Key, newValue.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add all current values that aren't in the URI at all
|
||||||
|
foreach (var currentValue in currentValues)
|
||||||
|
{
|
||||||
|
string parameterName = currentValue.Key;
|
||||||
|
if (!acceptedValues.ContainsKey(parameterName))
|
||||||
|
{
|
||||||
|
PathParameterSubsegment parameterSubsegment = GetParameterSubsegment(PathSegments, parameterName);
|
||||||
|
if (parameterSubsegment == null)
|
||||||
|
{
|
||||||
|
acceptedValues.Add(parameterName, currentValue.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add all remaining default values from the route to the list of values we will use for URI generation
|
||||||
|
ForEachParameter(PathSegments, delegate(PathParameterSubsegment parameterSubsegment)
|
||||||
|
{
|
||||||
|
if (!acceptedValues.ContainsKey(parameterSubsegment.ParameterName))
|
||||||
|
{
|
||||||
|
object defaultValue;
|
||||||
|
if (!IsParameterRequired(parameterSubsegment, defaultValues, out defaultValue))
|
||||||
|
{
|
||||||
|
// Add the default value only if there isn't already a new value for it and
|
||||||
|
// only if it actually has a default value, which we determine based on whether
|
||||||
|
// the parameter value is required.
|
||||||
|
acceptedValues.Add(parameterSubsegment.ParameterName, defaultValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// All required parameters in this URI must have values from somewhere (i.e. the accepted values)
|
||||||
|
bool hasAllRequiredValues = ForEachParameter(PathSegments, delegate(PathParameterSubsegment parameterSubsegment)
|
||||||
|
{
|
||||||
|
object defaultValue;
|
||||||
|
if (IsParameterRequired(parameterSubsegment, defaultValues, out defaultValue))
|
||||||
|
{
|
||||||
|
if (!acceptedValues.ContainsKey(parameterSubsegment.ParameterName))
|
||||||
|
{
|
||||||
|
// If the route parameter value is required that means there's
|
||||||
|
// no default value, so if there wasn't a new value for it
|
||||||
|
// either, this route won't match.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
if (!hasAllRequiredValues)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// All other default values must match if they are explicitly defined in the new values
|
||||||
|
IDictionary<string, object> otherDefaultValues = new Dictionary<string, object>(defaultValues, StringComparer.OrdinalIgnoreCase);
|
||||||
|
ForEachParameter(PathSegments, delegate(PathParameterSubsegment parameterSubsegment)
|
||||||
|
{
|
||||||
|
otherDefaultValues.Remove(parameterSubsegment.ParameterName);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
foreach (var defaultValue in otherDefaultValues)
|
||||||
|
{
|
||||||
|
object value;
|
||||||
|
if (values.TryGetValue(defaultValue.Key, out value))
|
||||||
|
{
|
||||||
|
unusedNewValues.Remove(defaultValue.Key);
|
||||||
|
if (!RoutePartsEqual(value, defaultValue.Value))
|
||||||
|
{
|
||||||
|
// If there is a non-parameterized value in the route and there is a
|
||||||
|
// new value for it and it doesn't match, this route won't match.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2: If the route is a match generate the appropriate URI
|
||||||
|
|
||||||
|
StringBuilder uri = new StringBuilder();
|
||||||
|
StringBuilder pendingParts = new StringBuilder();
|
||||||
|
|
||||||
|
bool pendingPartsAreAllSafe = false;
|
||||||
|
bool blockAllUriAppends = false;
|
||||||
|
|
||||||
|
for (int i = 0; i < PathSegments.Count; i++)
|
||||||
|
{
|
||||||
|
PathSegment pathSegment = PathSegments[i]; // parsedRouteUriPart
|
||||||
|
|
||||||
|
if (pathSegment is PathSeparatorSegment)
|
||||||
|
{
|
||||||
|
if (pendingPartsAreAllSafe)
|
||||||
|
{
|
||||||
|
// Accept
|
||||||
|
if (pendingParts.Length > 0)
|
||||||
|
{
|
||||||
|
if (blockAllUriAppends)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append any pending literals to the URI
|
||||||
|
uri.Append(pendingParts.ToString());
|
||||||
|
pendingParts.Length = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pendingPartsAreAllSafe = false;
|
||||||
|
|
||||||
|
// Guard against appending multiple separators for empty segments
|
||||||
|
if (pendingParts.Length > 0 && pendingParts[pendingParts.Length - 1] == '/')
|
||||||
|
{
|
||||||
|
// Dev10 676725: Route should not be matched if that causes mismatched tokens
|
||||||
|
// Dev11 86819: We will allow empty matches if all subsequent segments are null
|
||||||
|
if (blockAllUriAppends)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append any pending literals to the URI (without the trailing slash) and prevent any future appends
|
||||||
|
uri.Append(pendingParts.ToString(0, pendingParts.Length - 1));
|
||||||
|
pendingParts.Length = 0;
|
||||||
|
blockAllUriAppends = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
pendingParts.Append("/");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
PathContentSegment contentPathSegment = pathSegment as PathContentSegment;
|
||||||
|
if (contentPathSegment != null)
|
||||||
|
{
|
||||||
|
// Segments are treated as all-or-none. We should never output a partial segment.
|
||||||
|
// If we add any subsegment of this segment to the generated URI, we have to add
|
||||||
|
// the complete match. For example, if the subsegment is "{p1}-{p2}.xml" and we
|
||||||
|
// used a value for {p1}, we have to output the entire segment up to the next "/".
|
||||||
|
// Otherwise we could end up with the partial segment "v1" instead of the entire
|
||||||
|
// segment "v1-v2.xml".
|
||||||
|
bool addedAnySubsegments = false;
|
||||||
|
|
||||||
|
foreach (PathSubsegment subsegment in contentPathSegment.Subsegments)
|
||||||
|
{
|
||||||
|
PathLiteralSubsegment literalSubsegment = subsegment as PathLiteralSubsegment;
|
||||||
|
if (literalSubsegment != null)
|
||||||
|
{
|
||||||
|
// If it's a literal we hold on to it until we are sure we need to add it
|
||||||
|
pendingPartsAreAllSafe = true;
|
||||||
|
pendingParts.Append(literalSubsegment.Literal);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
PathParameterSubsegment parameterSubsegment = subsegment as PathParameterSubsegment;
|
||||||
|
if (parameterSubsegment != null)
|
||||||
|
{
|
||||||
|
if (pendingPartsAreAllSafe)
|
||||||
|
{
|
||||||
|
// Accept
|
||||||
|
if (pendingParts.Length > 0)
|
||||||
|
{
|
||||||
|
if (blockAllUriAppends)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append any pending literals to the URI
|
||||||
|
uri.Append(pendingParts.ToString());
|
||||||
|
pendingParts.Length = 0;
|
||||||
|
|
||||||
|
addedAnySubsegments = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pendingPartsAreAllSafe = false;
|
||||||
|
|
||||||
|
// If it's a parameter, get its value
|
||||||
|
object acceptedParameterValue;
|
||||||
|
bool hasAcceptedParameterValue = acceptedValues.TryGetValue(parameterSubsegment.ParameterName, out acceptedParameterValue);
|
||||||
|
if (hasAcceptedParameterValue)
|
||||||
|
{
|
||||||
|
unusedNewValues.Remove(parameterSubsegment.ParameterName);
|
||||||
|
}
|
||||||
|
|
||||||
|
object defaultParameterValue;
|
||||||
|
defaultValues.TryGetValue(parameterSubsegment.ParameterName, out defaultParameterValue);
|
||||||
|
|
||||||
|
if (RoutePartsEqual(acceptedParameterValue, defaultParameterValue))
|
||||||
|
{
|
||||||
|
// If the accepted value is the same as the default value, mark it as pending since
|
||||||
|
// we won't necessarily add it to the URI we generate.
|
||||||
|
pendingParts.Append(Convert.ToString(acceptedParameterValue, CultureInfo.InvariantCulture));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (blockAllUriAppends)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the new part to the URI as well as any pending parts
|
||||||
|
if (pendingParts.Length > 0)
|
||||||
|
{
|
||||||
|
// Append any pending literals to the URI
|
||||||
|
uri.Append(pendingParts.ToString());
|
||||||
|
pendingParts.Length = 0;
|
||||||
|
}
|
||||||
|
uri.Append(Convert.ToString(acceptedParameterValue, CultureInfo.InvariantCulture));
|
||||||
|
|
||||||
|
addedAnySubsegments = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Contract.Assert(false, "Invalid path subsegment type");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (addedAnySubsegments)
|
||||||
|
{
|
||||||
|
// See comment above about why we add the pending parts
|
||||||
|
if (pendingParts.Length > 0)
|
||||||
|
{
|
||||||
|
if (blockAllUriAppends)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append any pending literals to the URI
|
||||||
|
uri.Append(pendingParts.ToString());
|
||||||
|
pendingParts.Length = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Contract.Assert(false, "Invalid path segment type");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pendingPartsAreAllSafe)
|
||||||
|
{
|
||||||
|
// Accept
|
||||||
|
if (pendingParts.Length > 0)
|
||||||
|
{
|
||||||
|
if (blockAllUriAppends)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append any pending literals to the URI
|
||||||
|
uri.Append(pendingParts.ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process constraints keys
|
||||||
|
if (constraints != null)
|
||||||
|
{
|
||||||
|
// If there are any constraints, mark all the keys as being used so that we don't
|
||||||
|
// generate query string items for custom constraints that don't appear as parameters
|
||||||
|
// in the URI format.
|
||||||
|
foreach (var constraintsItem in constraints)
|
||||||
|
{
|
||||||
|
unusedNewValues.Remove(constraintsItem.Key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode the URI before we append the query string, otherwise we would double encode the query string
|
||||||
|
StringBuilder encodedUri = new StringBuilder();
|
||||||
|
encodedUri.Append(UriEncode(uri.ToString()));
|
||||||
|
uri = encodedUri;
|
||||||
|
|
||||||
|
// Add remaining new values as query string parameters to the URI
|
||||||
|
if (unusedNewValues.Count > 0)
|
||||||
|
{
|
||||||
|
// Generate the query string
|
||||||
|
bool firstParam = true;
|
||||||
|
foreach (string unusedNewValue in unusedNewValues)
|
||||||
|
{
|
||||||
|
object value;
|
||||||
|
if (acceptedValues.TryGetValue(unusedNewValue, out value))
|
||||||
|
{
|
||||||
|
uri.Append(firstParam ? '?' : '&');
|
||||||
|
firstParam = false;
|
||||||
|
uri.Append(Uri.EscapeDataString(unusedNewValue));
|
||||||
|
uri.Append('=');
|
||||||
|
uri.Append(Uri.EscapeDataString(Convert.ToString(value, CultureInfo.InvariantCulture)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new BoundRouteTemplate
|
||||||
|
{
|
||||||
|
BoundTemplate = uri.ToString(),
|
||||||
|
Values = acceptedValues
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string EscapeReservedCharacters(Match m)
|
||||||
|
{
|
||||||
|
return "%" + Convert.ToUInt16(m.Value[0]).ToString("x2", CultureInfo.InvariantCulture);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool ForEachParameter(IList<PathSegment> pathSegments, Func<PathParameterSubsegment, bool> action)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < pathSegments.Count; i++)
|
||||||
|
{
|
||||||
|
PathSegment pathSegment = pathSegments[i];
|
||||||
|
|
||||||
|
if (pathSegment is PathSeparatorSegment)
|
||||||
|
{
|
||||||
|
// We only care about parameter subsegments, so skip this
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
PathContentSegment contentPathSegment = pathSegment as PathContentSegment;
|
||||||
|
if (contentPathSegment != null)
|
||||||
|
{
|
||||||
|
foreach (PathSubsegment subsegment in contentPathSegment.Subsegments)
|
||||||
|
{
|
||||||
|
PathLiteralSubsegment literalSubsegment = subsegment as PathLiteralSubsegment;
|
||||||
|
if (literalSubsegment != null)
|
||||||
|
{
|
||||||
|
// We only care about parameter subsegments, so skip this
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
PathParameterSubsegment parameterSubsegment = subsegment as PathParameterSubsegment;
|
||||||
|
if (parameterSubsegment != null)
|
||||||
|
{
|
||||||
|
if (!action(parameterSubsegment))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Contract.Assert(false, "Invalid path subsegment type");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Contract.Assert(false, "Invalid path segment type");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PathParameterSubsegment GetParameterSubsegment(IList<PathSegment> pathSegments, string parameterName)
|
||||||
|
{
|
||||||
|
PathParameterSubsegment foundParameterSubsegment = null;
|
||||||
|
|
||||||
|
ForEachParameter(pathSegments, delegate(PathParameterSubsegment parameterSubsegment)
|
||||||
|
{
|
||||||
|
if (String.Equals(parameterName, parameterSubsegment.ParameterName, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
foundParameterSubsegment = parameterSubsegment;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return foundParameterSubsegment;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsParameterRequired(PathParameterSubsegment parameterSubsegment, IDictionary<string, object> defaultValues, out object defaultValue)
|
||||||
|
{
|
||||||
|
if (parameterSubsegment.IsCatchAll)
|
||||||
|
{
|
||||||
|
defaultValue = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return !defaultValues.TryGetValue(parameterSubsegment.ParameterName, out defaultValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsRoutePartNonEmpty(object routePart)
|
||||||
|
{
|
||||||
|
string routePartString = routePart as string;
|
||||||
|
if (routePartString != null)
|
||||||
|
{
|
||||||
|
return routePartString.Length > 0;
|
||||||
|
}
|
||||||
|
return routePart != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IDictionary<string, object> Match(string virtualPath, IDictionary<string, object> defaultValues)
|
||||||
|
{
|
||||||
|
IList<string> requestPathSegments = TemplateRouteParser.SplitUriToPathSegmentStrings(virtualPath);
|
||||||
|
|
||||||
|
if (defaultValues == null)
|
||||||
|
{
|
||||||
|
defaultValues = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
IDictionary<string, object> matchedValues = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
// This flag gets set once all the data in the URI has been parsed through, but
|
||||||
|
// the route we're trying to match against still has more parts. At this point
|
||||||
|
// we'll only continue matching separator characters and parameters that have
|
||||||
|
// default values.
|
||||||
|
bool ranOutOfStuffToParse = false;
|
||||||
|
|
||||||
|
// This value gets set once we start processing a catchall parameter (if there is one
|
||||||
|
// at all). Once we set this value we consume all remaining parts of the URI into its
|
||||||
|
// parameter value.
|
||||||
|
bool usedCatchAllParameter = false;
|
||||||
|
|
||||||
|
for (int i = 0; i < PathSegments.Count; i++)
|
||||||
|
{
|
||||||
|
PathSegment pathSegment = PathSegments[i];
|
||||||
|
|
||||||
|
if (requestPathSegments.Count <= i)
|
||||||
|
{
|
||||||
|
ranOutOfStuffToParse = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
string requestPathSegment = ranOutOfStuffToParse ? null : requestPathSegments[i];
|
||||||
|
|
||||||
|
if (pathSegment is PathSeparatorSegment)
|
||||||
|
{
|
||||||
|
if (ranOutOfStuffToParse)
|
||||||
|
{
|
||||||
|
// If we're trying to match a separator in the route but there's no more content, that's OK
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (!String.Equals(requestPathSegment, "/", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
PathContentSegment contentPathSegment = pathSegment as PathContentSegment;
|
||||||
|
if (contentPathSegment != null)
|
||||||
|
{
|
||||||
|
if (contentPathSegment.IsCatchAll)
|
||||||
|
{
|
||||||
|
Contract.Assert(i == (PathSegments.Count - 1), "If we're processing a catch-all, we should be on the last route segment.");
|
||||||
|
MatchCatchAll(contentPathSegment, requestPathSegments.Skip(i), defaultValues, matchedValues);
|
||||||
|
usedCatchAllParameter = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (!MatchContentPathSegment(contentPathSegment, requestPathSegment, defaultValues, matchedValues))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Contract.Assert(false, "Invalid path segment type");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!usedCatchAllParameter)
|
||||||
|
{
|
||||||
|
if (PathSegments.Count < requestPathSegments.Count)
|
||||||
|
{
|
||||||
|
// If we've already gone through all the parts defined in the route but the URI
|
||||||
|
// still contains more content, check that the remaining content is all separators.
|
||||||
|
for (int i = PathSegments.Count; i < requestPathSegments.Count; i++)
|
||||||
|
{
|
||||||
|
if (!TemplateRouteParser.IsSeparator(requestPathSegments[i]))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy all remaining default values to the route data
|
||||||
|
if (defaultValues != null)
|
||||||
|
{
|
||||||
|
foreach (var defaultValue in defaultValues)
|
||||||
|
{
|
||||||
|
if (!matchedValues.ContainsKey(defaultValue.Key))
|
||||||
|
{
|
||||||
|
matchedValues.Add(defaultValue.Key, defaultValue.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return matchedValues;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void MatchCatchAll(PathContentSegment contentPathSegment, IEnumerable<string> remainingRequestSegments, IDictionary<string, object> defaultValues, IDictionary<string, object> matchedValues)
|
||||||
|
{
|
||||||
|
string remainingRequest = String.Join(String.Empty, remainingRequestSegments.ToArray());
|
||||||
|
|
||||||
|
PathParameterSubsegment catchAllSegment = contentPathSegment.Subsegments[0] as PathParameterSubsegment;
|
||||||
|
|
||||||
|
object catchAllValue;
|
||||||
|
|
||||||
|
if (remainingRequest.Length > 0)
|
||||||
|
{
|
||||||
|
catchAllValue = remainingRequest;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
defaultValues.TryGetValue(catchAllSegment.ParameterName, out catchAllValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
matchedValues.Add(catchAllSegment.ParameterName, catchAllValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool MatchContentPathSegment(PathContentSegment routeSegment, string requestPathSegment, IDictionary<string, object> defaultValues, IDictionary<string, object> matchedValues)
|
||||||
|
{
|
||||||
|
if (String.IsNullOrEmpty(requestPathSegment))
|
||||||
|
{
|
||||||
|
// If there's no data to parse, we must have exactly one parameter segment and no other segments - otherwise no match
|
||||||
|
|
||||||
|
if (routeSegment.Subsegments.Count > 1)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
PathParameterSubsegment parameterSubsegment = routeSegment.Subsegments[0] as PathParameterSubsegment;
|
||||||
|
if (parameterSubsegment == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We must have a default value since there's no value in the request URI
|
||||||
|
object parameterValue;
|
||||||
|
if (defaultValues.TryGetValue(parameterSubsegment.ParameterName, out parameterValue))
|
||||||
|
{
|
||||||
|
// If there's a default value for this parameter, use that default value
|
||||||
|
matchedValues.Add(parameterSubsegment.ParameterName, parameterValue);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// If there's no default value, this segment doesn't match
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optimize for the common case where there is only one subsegment in the segment - either a parameter or a literal
|
||||||
|
if (routeSegment.Subsegments.Count == 1)
|
||||||
|
{
|
||||||
|
return MatchSingleContentPathSegment(routeSegment.Subsegments[0], requestPathSegment, matchedValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find last literal segment and get its last index in the string
|
||||||
|
|
||||||
|
int lastIndex = requestPathSegment.Length;
|
||||||
|
int indexOfLastSegmentUsed = routeSegment.Subsegments.Count - 1;
|
||||||
|
|
||||||
|
PathParameterSubsegment parameterNeedsValue = null; // Keeps track of a parameter segment that is pending a value
|
||||||
|
PathLiteralSubsegment lastLiteral = null; // Keeps track of the left-most literal we've encountered
|
||||||
|
|
||||||
|
while (indexOfLastSegmentUsed >= 0)
|
||||||
|
{
|
||||||
|
int newLastIndex = lastIndex;
|
||||||
|
|
||||||
|
PathParameterSubsegment parameterSubsegment = routeSegment.Subsegments[indexOfLastSegmentUsed] as PathParameterSubsegment;
|
||||||
|
if (parameterSubsegment != null)
|
||||||
|
{
|
||||||
|
// Hold on to the parameter so that we can fill it in when we locate the next literal
|
||||||
|
parameterNeedsValue = parameterSubsegment;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
PathLiteralSubsegment literalSubsegment = routeSegment.Subsegments[indexOfLastSegmentUsed] as PathLiteralSubsegment;
|
||||||
|
if (literalSubsegment != null)
|
||||||
|
{
|
||||||
|
lastLiteral = literalSubsegment;
|
||||||
|
|
||||||
|
int startIndex = lastIndex - 1;
|
||||||
|
// If we have a pending parameter subsegment, we must leave at least one character for that
|
||||||
|
if (parameterNeedsValue != null)
|
||||||
|
{
|
||||||
|
startIndex--;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startIndex < 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int indexOfLiteral = requestPathSegment.LastIndexOf(literalSubsegment.Literal, startIndex, StringComparison.OrdinalIgnoreCase);
|
||||||
|
if (indexOfLiteral == -1)
|
||||||
|
{
|
||||||
|
// If we couldn't find this literal index, this segment cannot match
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the first subsegment is a literal, it must match at the right-most extent of the request URI.
|
||||||
|
// Without this check if your route had "/Foo/" we'd match the request URI "/somethingFoo/".
|
||||||
|
// This check is related to the check we do at the very end of this function.
|
||||||
|
if (indexOfLastSegmentUsed == (routeSegment.Subsegments.Count - 1))
|
||||||
|
{
|
||||||
|
if ((indexOfLiteral + literalSubsegment.Literal.Length) != requestPathSegment.Length)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
newLastIndex = indexOfLiteral;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Contract.Assert(false, "Invalid path segment type");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((parameterNeedsValue != null) && (((lastLiteral != null) && (parameterSubsegment == null)) || (indexOfLastSegmentUsed == 0)))
|
||||||
|
{
|
||||||
|
// If we have a pending parameter that needs a value, grab that value
|
||||||
|
|
||||||
|
int parameterStartIndex;
|
||||||
|
int parameterTextLength;
|
||||||
|
|
||||||
|
if (lastLiteral == null)
|
||||||
|
{
|
||||||
|
if (indexOfLastSegmentUsed == 0)
|
||||||
|
{
|
||||||
|
parameterStartIndex = 0;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
parameterStartIndex = newLastIndex;
|
||||||
|
Contract.Assert(false, "indexOfLastSegementUsed should always be 0 from the check above");
|
||||||
|
}
|
||||||
|
parameterTextLength = lastIndex;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// If we're getting a value for a parameter that is somewhere in the middle of the segment
|
||||||
|
if ((indexOfLastSegmentUsed == 0) && (parameterSubsegment != null))
|
||||||
|
{
|
||||||
|
parameterStartIndex = 0;
|
||||||
|
parameterTextLength = lastIndex;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
parameterStartIndex = newLastIndex + lastLiteral.Literal.Length;
|
||||||
|
parameterTextLength = lastIndex - parameterStartIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
string parameterValueString = requestPathSegment.Substring(parameterStartIndex, parameterTextLength);
|
||||||
|
|
||||||
|
if (String.IsNullOrEmpty(parameterValueString))
|
||||||
|
{
|
||||||
|
// 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.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// If there's a value in the segment for this parameter, use the subsegment value
|
||||||
|
matchedValues.Add(parameterNeedsValue.ParameterName, parameterValueString);
|
||||||
|
}
|
||||||
|
|
||||||
|
parameterNeedsValue = null;
|
||||||
|
lastLiteral = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
lastIndex = newLastIndex;
|
||||||
|
indexOfLastSegmentUsed--;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the last subsegment is a parameter, it's OK that we didn't parse all the way to the left extent of
|
||||||
|
// the string since the parameter will have consumed all the remaining text anyway. If the last subsegment
|
||||||
|
// is a literal then we *must* have consumed the entire text in that literal. Otherwise we end up matching
|
||||||
|
// 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.Subsegments[0] is PathParameterSubsegment);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool MatchSingleContentPathSegment(PathSubsegment pathSubsegment, string requestPathSegment, IDictionary<string, object> matchedValues)
|
||||||
|
{
|
||||||
|
PathParameterSubsegment parameterSubsegment = pathSubsegment as PathParameterSubsegment;
|
||||||
|
if (parameterSubsegment == null)
|
||||||
|
{
|
||||||
|
// Handle a single literal segment
|
||||||
|
PathLiteralSubsegment literalSubsegment = pathSubsegment as PathLiteralSubsegment;
|
||||||
|
Contract.Assert(literalSubsegment != null, "Invalid path segment type");
|
||||||
|
return literalSubsegment.Literal.Equals(requestPathSegment, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Handle a single parameter segment
|
||||||
|
matchedValues.Add(parameterSubsegment.ParameterName, requestPathSegment);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool RoutePartsEqual(object a, object b)
|
||||||
|
{
|
||||||
|
string sa = a as string;
|
||||||
|
string sb = b as string;
|
||||||
|
if (sa != null && sb != null)
|
||||||
|
{
|
||||||
|
// For strings do a case-insensitive comparison
|
||||||
|
return String.Equals(sa, sb, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (a != null && b != null)
|
||||||
|
{
|
||||||
|
// Explicitly call .Equals() in case it is overridden in the type
|
||||||
|
return a.Equals(b);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// At least one of them is null. Return true if they both are
|
||||||
|
return a == b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string UriEncode(string str)
|
||||||
|
{
|
||||||
|
string escape = Uri.EscapeUriString(str);
|
||||||
|
return Regex.Replace(escape, "([#?])", new MatchEvaluator(EscapeReservedCharacters));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,229 @@
|
||||||
|
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using Microsoft.AspNet.Abstractions;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Routing.Template
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Route class for self-host (i.e. hosted outside of ASP.NET). This class is mostly the
|
||||||
|
/// same as the System.Web.Routing.Route implementation.
|
||||||
|
/// This class has the same URL matching functionality as System.Web.Routing.Route. However,
|
||||||
|
/// in order for this route to match when generating URLs, a special "httproute" key must be
|
||||||
|
/// specified when generating the URL.
|
||||||
|
/// </summary>
|
||||||
|
public class TemplateRoute : IRoute
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Key used to signify that a route URL generation request should include HTTP routes (e.g. Web API).
|
||||||
|
/// If this key is not specified then no HTTP routes will match.
|
||||||
|
/// </summary>
|
||||||
|
public static readonly string HttpRouteKey = "httproute";
|
||||||
|
|
||||||
|
private string _routeTemplate;
|
||||||
|
private IDictionary<string, object> _defaults;
|
||||||
|
private IDictionary<string, object> _constraints;
|
||||||
|
private IDictionary<string, object> _dataTokens;
|
||||||
|
|
||||||
|
public TemplateRoute()
|
||||||
|
: this(routeTemplate: null, defaults: null, constraints: null, dataTokens: null, handler: null)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public TemplateRoute(string routeTemplate)
|
||||||
|
: this(routeTemplate, defaults: null, constraints: null, dataTokens: null, handler: null)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public TemplateRoute(string routeTemplate, IDictionary<string, object> defaults)
|
||||||
|
: this(routeTemplate, defaults, constraints: null, dataTokens: null, handler: null)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public TemplateRoute(string routeTemplate, IDictionary<string, object> defaults, IDictionary<string, object> constraints)
|
||||||
|
: this(routeTemplate, defaults, constraints, dataTokens: null, handler: null)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public TemplateRoute(string routeTemplate, IDictionary<string, object> defaults, IDictionary<string, object> constraints, IDictionary<string, object> dataTokens)
|
||||||
|
: this(routeTemplate, defaults, constraints, dataTokens, handler: null)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public TemplateRoute(string routeTemplate, IDictionary<string, object> defaults, IDictionary<string, object> constraints, IDictionary<string, object> dataTokens, IRouteEndpoint handler)
|
||||||
|
{
|
||||||
|
_routeTemplate = routeTemplate == null ? String.Empty : routeTemplate;
|
||||||
|
_defaults = defaults ?? new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
_constraints = constraints ?? new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
_dataTokens = dataTokens ?? new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
Handler = handler;
|
||||||
|
|
||||||
|
// The parser will throw for invalid routes.
|
||||||
|
ParsedRoute = TemplateRouteParser.Parse(RouteTemplate);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IDictionary<string, object> Defaults
|
||||||
|
{
|
||||||
|
get { return _defaults; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public IDictionary<string, object> Constraints
|
||||||
|
{
|
||||||
|
get { return _constraints; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public IDictionary<string, object> DataTokens
|
||||||
|
{
|
||||||
|
get { return _dataTokens; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public IRouteEndpoint Handler { get; private set; }
|
||||||
|
|
||||||
|
public string RouteTemplate
|
||||||
|
{
|
||||||
|
get { return _routeTemplate; }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal TemplateParsedRoute ParsedRoute { get; private set; }
|
||||||
|
|
||||||
|
public virtual RouteMatch GetRouteData(HttpContext request)
|
||||||
|
{
|
||||||
|
if (request == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException("request");
|
||||||
|
}
|
||||||
|
|
||||||
|
var requestPath = request.Request.Path.Value;
|
||||||
|
if (!String.IsNullOrEmpty(requestPath) && requestPath[0] == '/')
|
||||||
|
{
|
||||||
|
requestPath = requestPath.Substring(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
IDictionary<string, object> values = ParsedRoute.Match(requestPath, _defaults);
|
||||||
|
if (values == null)
|
||||||
|
{
|
||||||
|
// If we got back a null value set, that means the URI did not match
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate the values
|
||||||
|
if (!ProcessConstraints(request, values, RouteDirection.UriResolution))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new RouteMatch(null, values);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attempt to generate a URI that represents the values passed in based on current
|
||||||
|
/// values from the <see cref="HttpRouteData"/> and new values using the specified <see cref="TemplateRoute"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">The HTTP request message.</param>
|
||||||
|
/// <param name="values">The route values.</param>
|
||||||
|
/// <returns>A <see cref="VirtualPathData"/> instance or null if URI cannot be generated.</returns>
|
||||||
|
public virtual IVirtualPathData GetVirtualPath(HttpContext request, IDictionary<string, object> values)
|
||||||
|
{
|
||||||
|
if (request == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException("request");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only perform URL generation if the "httproute" key was specified. This allows these
|
||||||
|
// routes to be ignored when a regular MVC app tries to generate URLs. Without this special
|
||||||
|
// key an HTTP route used for Web API would normally take over almost all the routes in a
|
||||||
|
// typical app.
|
||||||
|
if (values != null && !values.Keys.Contains(HttpRouteKey, StringComparer.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// Remove the value from the collection so that it doesn't affect the generated URL
|
||||||
|
var newValues = GetRouteDictionaryWithoutHttpRouteKey(values);
|
||||||
|
|
||||||
|
IRouteValues routeData = request.GetFeature<IRouteValues>();
|
||||||
|
IDictionary<string, object> requestValues = routeData == null ? null : routeData.Values;
|
||||||
|
|
||||||
|
BoundRouteTemplate result = ParsedRoute.Bind(requestValues, newValues, _defaults, _constraints);
|
||||||
|
if (result == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assert that the route matches the validation rules
|
||||||
|
if (!ProcessConstraints(request, result.Values, RouteDirection.UriGeneration))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new VirtualPathData(this, result.BoundTemplate);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IDictionary<string, object> GetRouteDictionaryWithoutHttpRouteKey(IDictionary<string, object> routeValues)
|
||||||
|
{
|
||||||
|
var newRouteValues = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
if (routeValues != null)
|
||||||
|
{
|
||||||
|
foreach (var routeValue in routeValues)
|
||||||
|
{
|
||||||
|
if (!String.Equals(routeValue.Key, HttpRouteKey, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
newRouteValues.Add(routeValue.Key, routeValue.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return newRouteValues;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual bool ProcessConstraint(HttpContext request, object constraint, string parameterName, IDictionary<string, object> values, RouteDirection routeDirection)
|
||||||
|
{
|
||||||
|
ITemplateRouteConstraint customConstraint = constraint as ITemplateRouteConstraint;
|
||||||
|
if (customConstraint != null)
|
||||||
|
{
|
||||||
|
return customConstraint.Match(request, this, parameterName, values, routeDirection);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there was no custom constraint, then treat the constraint as a string which represents a Regex.
|
||||||
|
string constraintsRule = constraint as string;
|
||||||
|
if (constraintsRule == null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(String.Format(
|
||||||
|
CultureInfo.CurrentCulture,
|
||||||
|
Resources.TemplateRoute_ValidationMustBeStringOrCustomConstraint,
|
||||||
|
parameterName,
|
||||||
|
RouteTemplate,
|
||||||
|
typeof(ITemplateRouteConstraint).Name));
|
||||||
|
}
|
||||||
|
|
||||||
|
object parameterValue;
|
||||||
|
values.TryGetValue(parameterName, out parameterValue);
|
||||||
|
string parameterValueString = Convert.ToString(parameterValue, CultureInfo.InvariantCulture);
|
||||||
|
string constraintsRegEx = "^(" + constraintsRule + ")$";
|
||||||
|
return Regex.IsMatch(parameterValueString, constraintsRegEx, RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool ProcessConstraints(HttpContext request, IDictionary<string, object> values, RouteDirection routeDirection)
|
||||||
|
{
|
||||||
|
if (Constraints != null)
|
||||||
|
{
|
||||||
|
foreach (KeyValuePair<string, object> constraintsItem in Constraints)
|
||||||
|
{
|
||||||
|
if (!ProcessConstraint(request, constraintsItem.Value, constraintsItem.Key, values, routeDirection))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RouteMatch Match(RouteContext context)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,373 @@
|
||||||
|
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.Diagnostics.Contracts;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Routing.Template
|
||||||
|
{
|
||||||
|
public static class TemplateRouteParser
|
||||||
|
{
|
||||||
|
private static string GetLiteral(string segmentLiteral)
|
||||||
|
{
|
||||||
|
// Scan for errant single { and } and convert double {{ to { and double }} to }
|
||||||
|
|
||||||
|
// First we eliminate all escaped braces and then check if any other braces are remaining
|
||||||
|
string newLiteral = segmentLiteral.Replace("{{", String.Empty).Replace("}}", String.Empty);
|
||||||
|
if (newLiteral.Contains("{") || newLiteral.Contains("}"))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it's a valid format, we unescape the braces
|
||||||
|
return segmentLiteral.Replace("{{", "{").Replace("}}", "}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int IndexOfFirstOpenParameter(string segment, int startIndex)
|
||||||
|
{
|
||||||
|
// Find the first unescaped open brace
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
startIndex = segment.IndexOf('{', startIndex);
|
||||||
|
if (startIndex == -1)
|
||||||
|
{
|
||||||
|
// If there are no more open braces, stop
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if ((startIndex + 1 == segment.Length) ||
|
||||||
|
((startIndex + 1 < segment.Length) && (segment[startIndex + 1] != '{')))
|
||||||
|
{
|
||||||
|
// If we found an open brace that is followed by a non-open brace, it's
|
||||||
|
// a parameter delimiter.
|
||||||
|
// It's also a delimiter if the open brace is the last character - though
|
||||||
|
// it ends up being being called out as invalid later on.
|
||||||
|
return startIndex;
|
||||||
|
}
|
||||||
|
// Increment by two since we want to skip both the open brace that
|
||||||
|
// we're on as well as the subsequent character since we know for
|
||||||
|
// sure that it is part of an escape sequence.
|
||||||
|
startIndex += 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static bool IsSeparator(string s)
|
||||||
|
{
|
||||||
|
return String.Equals(s, "/", StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsValidParameterName(string parameterName)
|
||||||
|
{
|
||||||
|
if (parameterName.Length == 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < parameterName.Length; i++)
|
||||||
|
{
|
||||||
|
char c = parameterName[i];
|
||||||
|
if (c == '/' || c == '{' || c == '}')
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static bool IsInvalidRouteTemplate(string routeTemplate)
|
||||||
|
{
|
||||||
|
return routeTemplate.StartsWith("~", StringComparison.Ordinal) ||
|
||||||
|
routeTemplate.StartsWith("/", StringComparison.Ordinal) ||
|
||||||
|
(routeTemplate.IndexOf('?') != -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TemplateParsedRoute Parse(string routeTemplate)
|
||||||
|
{
|
||||||
|
if (routeTemplate == null)
|
||||||
|
{
|
||||||
|
routeTemplate = String.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsInvalidRouteTemplate(routeTemplate))
|
||||||
|
{
|
||||||
|
throw new ArgumentException(Resources.TemplateRoute_InvalidRouteTemplate, "routeTemplate");
|
||||||
|
}
|
||||||
|
|
||||||
|
IList<string> uriParts = SplitUriToPathSegmentStrings(routeTemplate);
|
||||||
|
Exception ex = ValidateUriParts(uriParts);
|
||||||
|
if (ex != null)
|
||||||
|
{
|
||||||
|
throw ex;
|
||||||
|
}
|
||||||
|
|
||||||
|
IList<PathSegment> pathSegments = SplitUriToPathSegments(uriParts);
|
||||||
|
|
||||||
|
Contract.Assert(uriParts.Count == pathSegments.Count, "The number of string segments should be the same as the number of path segments");
|
||||||
|
|
||||||
|
return new TemplateParsedRoute(pathSegments);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SuppressMessage("Microsoft.Usage", "CA2208:InstantiateArgumentExceptionsCorrectly",
|
||||||
|
Justification = "The exceptions are just constructed here, but they are thrown from a method that does have those parameter names.")]
|
||||||
|
private static IList<PathSubsegment> ParseUriSegment(string segment, out Exception exception)
|
||||||
|
{
|
||||||
|
int startIndex = 0;
|
||||||
|
|
||||||
|
List<PathSubsegment> pathSubsegments = new List<PathSubsegment>();
|
||||||
|
|
||||||
|
while (startIndex < segment.Length)
|
||||||
|
{
|
||||||
|
int nextParameterStart = IndexOfFirstOpenParameter(segment, startIndex);
|
||||||
|
if (nextParameterStart == -1)
|
||||||
|
{
|
||||||
|
// If there are no more parameters in the segment, capture the remainder as a literal and stop
|
||||||
|
string lastLiteralPart = GetLiteral(segment.Substring(startIndex));
|
||||||
|
if (lastLiteralPart == null)
|
||||||
|
{
|
||||||
|
exception = new ArgumentException(
|
||||||
|
String.Format(CultureInfo.CurrentCulture, Resources.TemplateRoute_MismatchedParameter, segment),
|
||||||
|
"routeTemplate");
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastLiteralPart.Length > 0)
|
||||||
|
{
|
||||||
|
pathSubsegments.Add(new PathLiteralSubsegment(lastLiteralPart));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
int nextParameterEnd = segment.IndexOf('}', nextParameterStart + 1);
|
||||||
|
if (nextParameterEnd == -1)
|
||||||
|
{
|
||||||
|
exception = new ArgumentException(
|
||||||
|
String.Format(CultureInfo.CurrentCulture, Resources.TemplateRoute_MismatchedParameter, segment),
|
||||||
|
"routeTemplate");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
string literalPart = GetLiteral(segment.Substring(startIndex, nextParameterStart - startIndex));
|
||||||
|
if (literalPart == null)
|
||||||
|
{
|
||||||
|
exception = new ArgumentException(
|
||||||
|
String.Format(CultureInfo.CurrentCulture, Resources.TemplateRoute_MismatchedParameter, segment),
|
||||||
|
"routeTemplate");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (literalPart.Length > 0)
|
||||||
|
{
|
||||||
|
pathSubsegments.Add(new PathLiteralSubsegment(literalPart));
|
||||||
|
}
|
||||||
|
|
||||||
|
string parameterName = segment.Substring(nextParameterStart + 1, nextParameterEnd - nextParameterStart - 1);
|
||||||
|
pathSubsegments.Add(new PathParameterSubsegment(parameterName));
|
||||||
|
|
||||||
|
startIndex = nextParameterEnd + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
exception = null;
|
||||||
|
return pathSubsegments;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IList<PathSegment> SplitUriToPathSegments(IList<string> uriParts)
|
||||||
|
{
|
||||||
|
List<PathSegment> pathSegments = new List<PathSegment>();
|
||||||
|
|
||||||
|
foreach (string pathSegment in uriParts)
|
||||||
|
{
|
||||||
|
bool isCurrentPartSeparator = IsSeparator(pathSegment);
|
||||||
|
if (isCurrentPartSeparator)
|
||||||
|
{
|
||||||
|
pathSegments.Add(new PathSeparatorSegment());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Exception exception;
|
||||||
|
IList<PathSubsegment> subsegments = ParseUriSegment(pathSegment, out exception);
|
||||||
|
Contract.Assert(exception == null, "This only gets called after the path has been validated, so there should never be an exception here");
|
||||||
|
pathSegments.Add(new PathContentSegment(subsegments));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pathSegments;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static IList<string> SplitUriToPathSegmentStrings(string uri)
|
||||||
|
{
|
||||||
|
List<string> parts = new List<string>();
|
||||||
|
|
||||||
|
if (String.IsNullOrEmpty(uri))
|
||||||
|
{
|
||||||
|
return parts;
|
||||||
|
}
|
||||||
|
|
||||||
|
int currentIndex = 0;
|
||||||
|
|
||||||
|
// Split the incoming URI into individual parts
|
||||||
|
while (currentIndex < uri.Length)
|
||||||
|
{
|
||||||
|
int indexOfNextSeparator = uri.IndexOf('/', currentIndex);
|
||||||
|
if (indexOfNextSeparator == -1)
|
||||||
|
{
|
||||||
|
// If there are no more separators, the rest of the string is the last part
|
||||||
|
string finalPart = uri.Substring(currentIndex);
|
||||||
|
if (finalPart.Length > 0)
|
||||||
|
{
|
||||||
|
parts.Add(finalPart);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
string nextPart = uri.Substring(currentIndex, indexOfNextSeparator - currentIndex);
|
||||||
|
if (nextPart.Length > 0)
|
||||||
|
{
|
||||||
|
parts.Add(nextPart);
|
||||||
|
}
|
||||||
|
|
||||||
|
Contract.Assert(uri[indexOfNextSeparator] == '/', "The separator char itself should always be a '/'.");
|
||||||
|
parts.Add("/");
|
||||||
|
currentIndex = indexOfNextSeparator + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SuppressMessage("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily", Justification = "Not changing original algorithm")]
|
||||||
|
[SuppressMessage("Microsoft.Usage", "CA2208:InstantiateArgumentExceptionsCorrectly",
|
||||||
|
Justification = "The exceptions are just constructed here, but they are thrown from a method that does have those parameter names.")]
|
||||||
|
private static Exception ValidateUriParts(IList<string> pathSegments)
|
||||||
|
{
|
||||||
|
Contract.Assert(pathSegments != null, "The value should always come from SplitUri(), and that function should never return null.");
|
||||||
|
|
||||||
|
HashSet<string> usedParameterNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
bool? isPreviousPartSeparator = null;
|
||||||
|
|
||||||
|
bool foundCatchAllParameter = false;
|
||||||
|
|
||||||
|
foreach (string pathSegment in pathSegments)
|
||||||
|
{
|
||||||
|
if (foundCatchAllParameter)
|
||||||
|
{
|
||||||
|
// If we ever start an iteration of the loop and we've already found a
|
||||||
|
// catchall parameter then we have an invalid URI format.
|
||||||
|
return new ArgumentException(Resources.TemplateRoute_CatchAllMustBeLast, "routeTemplate");
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isCurrentPartSeparator;
|
||||||
|
if (isPreviousPartSeparator == null)
|
||||||
|
{
|
||||||
|
// Prime the loop with the first value
|
||||||
|
isPreviousPartSeparator = IsSeparator(pathSegment);
|
||||||
|
isCurrentPartSeparator = isPreviousPartSeparator.Value;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
isCurrentPartSeparator = IsSeparator(pathSegment);
|
||||||
|
|
||||||
|
// If both the previous part and the current part are separators, it's invalid
|
||||||
|
if (isCurrentPartSeparator && isPreviousPartSeparator.Value)
|
||||||
|
{
|
||||||
|
return new ArgumentException(Resources.TemplateRoute_CannotHaveConsecutiveSeparators, "routeTemplate");
|
||||||
|
}
|
||||||
|
|
||||||
|
Contract.Assert(isCurrentPartSeparator != isPreviousPartSeparator.Value, "This assert should only happen if both the current and previous parts are non-separators. This should never happen because consecutive non-separators are always parsed as a single part.");
|
||||||
|
isPreviousPartSeparator = isCurrentPartSeparator;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it's not a separator, parse the segment for parameters and validate it
|
||||||
|
if (!isCurrentPartSeparator)
|
||||||
|
{
|
||||||
|
Exception exception;
|
||||||
|
IList<PathSubsegment> subsegments = ParseUriSegment(pathSegment, out exception);
|
||||||
|
if (exception != null)
|
||||||
|
{
|
||||||
|
return exception;
|
||||||
|
}
|
||||||
|
|
||||||
|
exception = ValidateUriSegment(subsegments, usedParameterNames);
|
||||||
|
if (exception != null)
|
||||||
|
{
|
||||||
|
return exception;
|
||||||
|
}
|
||||||
|
|
||||||
|
foundCatchAllParameter = subsegments.Any<PathSubsegment>(seg => (seg is PathParameterSubsegment) && ((PathParameterSubsegment)seg).IsCatchAll);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SuppressMessage("Microsoft.Usage", "CA2208:InstantiateArgumentExceptionsCorrectly",
|
||||||
|
Justification = "The exceptions are just constructed here, but they are thrown from a method that does have those parameter names.")]
|
||||||
|
private static Exception ValidateUriSegment(IList<PathSubsegment> pathSubsegments, HashSet<string> usedParameterNames)
|
||||||
|
{
|
||||||
|
bool segmentContainsCatchAll = false;
|
||||||
|
|
||||||
|
Type previousSegmentType = null;
|
||||||
|
|
||||||
|
foreach (PathSubsegment subsegment in pathSubsegments)
|
||||||
|
{
|
||||||
|
if (previousSegmentType != null)
|
||||||
|
{
|
||||||
|
if (previousSegmentType == subsegment.GetType())
|
||||||
|
{
|
||||||
|
return new ArgumentException(Resources.TemplateRoute_CannotHaveConsecutiveParameters, "routeTemplate");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
previousSegmentType = subsegment.GetType();
|
||||||
|
|
||||||
|
PathLiteralSubsegment literalSubsegment = subsegment as PathLiteralSubsegment;
|
||||||
|
if (literalSubsegment != null)
|
||||||
|
{
|
||||||
|
// Nothing to validate for literals - everything is valid
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
PathParameterSubsegment parameterSubsegment = subsegment as PathParameterSubsegment;
|
||||||
|
if (parameterSubsegment != null)
|
||||||
|
{
|
||||||
|
string parameterName = parameterSubsegment.ParameterName;
|
||||||
|
|
||||||
|
if (parameterSubsegment.IsCatchAll)
|
||||||
|
{
|
||||||
|
segmentContainsCatchAll = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for valid characters in the parameter name
|
||||||
|
if (!IsValidParameterName(parameterName))
|
||||||
|
{
|
||||||
|
return new ArgumentException(
|
||||||
|
String.Format(CultureInfo.CurrentCulture, Resources.TemplateRoute_InvalidParameterName, parameterName),
|
||||||
|
"routeTemplate");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (usedParameterNames.Contains(parameterName))
|
||||||
|
{
|
||||||
|
return new ArgumentException(
|
||||||
|
String.Format(CultureInfo.CurrentCulture, Resources.TemplateRoute_RepeatedParameter, parameterName),
|
||||||
|
"routeTemplate");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
usedParameterNames.Add(parameterName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Contract.Assert(false, "Invalid path subsegment type");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (segmentContainsCatchAll && (pathSubsegments.Count != 1))
|
||||||
|
{
|
||||||
|
return new ArgumentException(Resources.TemplateRoute_CannotHaveCatchAllInMultiSegment, "routeTemplate");
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
namespace Microsoft.AspNet.Routing.Template
|
||||||
|
{
|
||||||
|
public class VirtualPathData : IVirtualPathData
|
||||||
|
{
|
||||||
|
private string _virtualPath;
|
||||||
|
|
||||||
|
public VirtualPathData(IRoute route, string virtualPath)
|
||||||
|
{
|
||||||
|
if (route == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException("route");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (virtualPath == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException("virtualPath");
|
||||||
|
}
|
||||||
|
|
||||||
|
Route = route;
|
||||||
|
VirtualPath = virtualPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IRoute Route { get; private set; }
|
||||||
|
|
||||||
|
public string VirtualPath
|
||||||
|
{
|
||||||
|
get { return _virtualPath; }
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException("value");
|
||||||
|
}
|
||||||
|
|
||||||
|
_virtualPath = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Routing.Template.Tests
|
||||||
|
{
|
||||||
|
public class Assert : Xunit.Assert
|
||||||
|
{
|
||||||
|
public static T Throws<T>(Assert.ThrowsDelegate action, string message) where T : Exception
|
||||||
|
{
|
||||||
|
T exception = Assert.Throws<T>(action);
|
||||||
|
Assert.Equal<string>(message, exception.Message);
|
||||||
|
return exception;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static T Throws<T>(Assert.ThrowsDelegateWithReturn action, string message) where T : Exception
|
||||||
|
{
|
||||||
|
T exception = Assert.Throws<T>(action);
|
||||||
|
Assert.Equal<string>(message, exception.Message);
|
||||||
|
return exception;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,198 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Xunit;
|
||||||
|
using Xunit.Extensions;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Routing.Template.Tests
|
||||||
|
{
|
||||||
|
public class TemplateRouteParserTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void InvalidTemplate_WithRepeatedParameter()
|
||||||
|
{
|
||||||
|
var ex = Assert.Throws<ArgumentException>(
|
||||||
|
() => TemplateRouteParser.Parse("{Controller}.mvc/{id}/{controller}"),
|
||||||
|
"The route parameter name 'controller' appears more than one time in the route template." + Environment.NewLine + "Parameter name: routeTemplate");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("123{a}abc{")]
|
||||||
|
[InlineData("123{a}abc}")]
|
||||||
|
[InlineData("xyz}123{a}abc}")]
|
||||||
|
[InlineData("{{p1}")]
|
||||||
|
[InlineData("{p1}}")]
|
||||||
|
[InlineData("p1}}p2{")]
|
||||||
|
public void InvalidTemplate_WithMismatchedBraces(string template)
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentException>(
|
||||||
|
() => TemplateRouteParser.Parse(template),
|
||||||
|
@"There is an incomplete parameter in this path segment: '" + template + @"'. Check that each '{' character has a matching '}' character." + Environment.NewLine +
|
||||||
|
"Parameter name: routeTemplate");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InvalidTemplate_CannotHaveCatchAllInMultiSegment()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentException>(
|
||||||
|
() => TemplateRouteParser.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 +
|
||||||
|
"Parameter name: routeTemplate");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InvalidTemplate_CannotHaveMoreThanOneCatchAll()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentException>(
|
||||||
|
() => TemplateRouteParser.Parse("{*p1}/{*p2}"),
|
||||||
|
"A catch-all parameter can only appear as the last segment of the route template." + Environment.NewLine +
|
||||||
|
"Parameter name: routeTemplate");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InvalidTemplate_CannotHaveMoreThanOneCatchAllInMultiSegment()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentException>(
|
||||||
|
() => TemplateRouteParser.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 +
|
||||||
|
"Parameter name: routeTemplate");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InvalidTemplate_CannotHaveCatchAllWithNoName()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentException>(
|
||||||
|
() => TemplateRouteParser.Parse("foo/{*}"),
|
||||||
|
@"The route parameter name '' is invalid. Route parameter names must be non-empty and cannot contain these characters: ""{"", ""}"", ""/"", ""?""" + Environment.NewLine +
|
||||||
|
"Parameter name: routeTemplate");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InvalidTemplate_CannotHaveConsecutiveOpenBrace()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentException>(
|
||||||
|
() => TemplateRouteParser.Parse("foo/{{p1}"),
|
||||||
|
"There is an incomplete parameter in this path segment: '{{p1}'. Check that each '{' character has a matching '}' character." + Environment.NewLine +
|
||||||
|
"Parameter name: routeTemplate");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InvalidTemplate_CannotHaveConsecutiveCloseBrace()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentException>(
|
||||||
|
() => TemplateRouteParser.Parse("foo/{p1}}"),
|
||||||
|
"There is an incomplete parameter in this path segment: '{p1}}'. Check that each '{' character has a matching '}' character." + Environment.NewLine +
|
||||||
|
"Parameter name: routeTemplate");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InvalidTemplate_SameParameterTwiceThrows()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentException>(
|
||||||
|
() => TemplateRouteParser.Parse("{aaa}/{AAA}"),
|
||||||
|
"The route parameter name 'AAA' appears more than one time in the route template." + Environment.NewLine +
|
||||||
|
"Parameter name: routeTemplate");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InvalidTemplate_SameParameterTwiceAndOneCatchAllThrows()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentException>(
|
||||||
|
() => TemplateRouteParser.Parse("{aaa}/{*AAA}"),
|
||||||
|
"The route parameter name 'AAA' appears more than one time in the route template." + Environment.NewLine +
|
||||||
|
"Parameter name: routeTemplate");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InvalidTemplate_InvalidParameterNameWithCloseBracketThrows()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentException>(
|
||||||
|
() => TemplateRouteParser.Parse("{a}/{aa}a}/{z}"),
|
||||||
|
"There is an incomplete parameter in this path segment: '{aa}a}'. Check that each '{' character has a matching '}' character." + Environment.NewLine +
|
||||||
|
"Parameter name: routeTemplate");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InvalidTemplate_InvalidParameterNameWithOpenBracketThrows()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentException>(
|
||||||
|
() => TemplateRouteParser.Parse("{a}/{a{aa}/{z}"),
|
||||||
|
@"The route parameter name 'a{aa' is invalid. Route parameter names must be non-empty and cannot contain these characters: ""{"", ""}"", ""/"", ""?""" + Environment.NewLine +
|
||||||
|
"Parameter name: routeTemplate");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InvalidTemplate_InvalidParameterNameWithEmptyNameThrows()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentException>(
|
||||||
|
() => TemplateRouteParser.Parse("{a}/{}/{z}"),
|
||||||
|
@"The route parameter name '' is invalid. Route parameter names must be non-empty and cannot contain these characters: ""{"", ""}"", ""/"", ""?""" + Environment.NewLine +
|
||||||
|
"Parameter name: routeTemplate");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InvalidTemplate_InvalidParameterNameWithQuestionThrows()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentException>(
|
||||||
|
() => TemplateRouteParser.Parse("{Controller}.mvc/{?}"),
|
||||||
|
"The route template cannot start with a '/' or '~' character and it cannot contain a '?' character." + Environment.NewLine +
|
||||||
|
"Parameter name: routeTemplate");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InvalidTemplate_ConsecutiveSeparatorsSlashSlashThrows()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentException>(
|
||||||
|
() => TemplateRouteParser.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 +
|
||||||
|
"Parameter name: routeTemplate");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InvalidTemplate_WithCatchAllNotAtTheEndThrows()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentException>(
|
||||||
|
() => TemplateRouteParser.Parse("foo/{p1}/{*p2}/{p3}"),
|
||||||
|
"A catch-all parameter can only appear as the last segment of the route template." + Environment.NewLine +
|
||||||
|
"Parameter name: routeTemplate");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InvalidTemplate_RepeatedParametersThrows()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentException>(
|
||||||
|
() => TemplateRouteParser.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 +
|
||||||
|
"Parameter name: routeTemplate");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InvalidTemplate_CannotStartWithSlash()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentException>(
|
||||||
|
() => TemplateRouteParser.Parse("/foo"),
|
||||||
|
"The route template cannot start with a '/' or '~' character and it cannot contain a '?' character." + Environment.NewLine +
|
||||||
|
"Parameter name: routeTemplate");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InvalidTemplate_CannotStartWithTilde()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentException>(
|
||||||
|
() => TemplateRouteParser.Parse("~foo"),
|
||||||
|
"The route template cannot start with a '/' or '~' character and it cannot contain a '?' character." + Environment.NewLine +
|
||||||
|
"Parameter name: routeTemplate");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InvalidTemplate_CannotContainQuestionMark()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentException>(
|
||||||
|
() => TemplateRouteParser.Parse("foor?bar"),
|
||||||
|
"The route template cannot start with a '/' or '~' character and it cannot contain a '?' character." + Environment.NewLine +
|
||||||
|
"Parameter name: routeTemplate");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,12 +1,15 @@
|
||||||
{
|
{
|
||||||
"version": "0.1-alpha-*",
|
"version": "0.1-alpha-*",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"Microsoft.AspNet.Abstractions" : "0.1-alpha-*",
|
||||||
"Microsoft.AspNet.Routing" : ""
|
"Microsoft.AspNet.Routing" : ""
|
||||||
},
|
},
|
||||||
"configurations": {
|
"configurations": {
|
||||||
"net45": {
|
"net45": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Owin": "1.0"
|
"Owin": "1.0",
|
||||||
|
"xunit": "1.9.2",
|
||||||
|
"xunit.extensions": "1.9.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue