Adding parameter replacement
This commit is contained in:
parent
96c759e25c
commit
2987f98283
|
|
@ -11,10 +11,11 @@ namespace MvcSample.Web
|
|||
return "Get method";
|
||||
}
|
||||
|
||||
[HttpGet("OtherThing")]
|
||||
[HttpGet("[action]")]
|
||||
public string GetOtherThing()
|
||||
{
|
||||
return "Get other thing";
|
||||
// Will be GetOtherThing
|
||||
return (string)ActionContext.RouteData.Values["action"];
|
||||
}
|
||||
|
||||
[HttpGet("Link")]
|
||||
|
|
|
|||
|
|
@ -1066,6 +1066,149 @@ namespace Microsoft.AspNet.Mvc.Core
|
|||
return string.Format(CultureInfo.CurrentCulture, GetString("OutputFormatterNoEncoding"), p0);
|
||||
}
|
||||
|
||||
/// The following errors occurred with attribute routing information:{0}{0}{1}
|
||||
/// </summary>
|
||||
internal static string AttributeRoute_AggregateErrorMessage
|
||||
{
|
||||
get { return GetString("AttributeRoute_AggregateErrorMessage"); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The following errors occurred with attribute routing information:{0}{0}{1}
|
||||
/// </summary>
|
||||
internal static string FormatAttributeRoute_AggregateErrorMessage(object p0, object p1)
|
||||
{
|
||||
return string.Format(CultureInfo.CurrentCulture, GetString("AttributeRoute_AggregateErrorMessage"), p0, p1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The attribute route '{0}' cannot contain a parameter named '{{{1}}}'. Use '[{1}]' in the route template to insert the value '{2}'.
|
||||
/// </summary>
|
||||
internal static string AttributeRoute_CannotContainParameter
|
||||
{
|
||||
get { return GetString("AttributeRoute_CannotContainParameter"); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The attribute route '{0}' cannot contain a parameter named '{{{1}}}'. Use '[{1}]' in the route template to insert the value '{2}'.
|
||||
/// </summary>
|
||||
internal static string FormatAttributeRoute_CannotContainParameter(object p0, object p1, object p2)
|
||||
{
|
||||
return string.Format(CultureInfo.CurrentCulture, GetString("AttributeRoute_CannotContainParameter"), p0, p1, p2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// For action: '{0}'{1}Error: {2}
|
||||
/// </summary>
|
||||
internal static string AttributeRoute_IndividualErrorMessage
|
||||
{
|
||||
get { return GetString("AttributeRoute_IndividualErrorMessage"); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// For action: '{0}'{1}Error: {2}
|
||||
/// </summary>
|
||||
internal static string FormatAttributeRoute_IndividualErrorMessage(object p0, object p1, object p2)
|
||||
{
|
||||
return string.Format(CultureInfo.CurrentCulture, GetString("AttributeRoute_IndividualErrorMessage"), p0, p1, p2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An empty replacement token ('[]') is not allowed.
|
||||
/// </summary>
|
||||
internal static string AttributeRoute_TokenReplacement_EmptyTokenNotAllowed
|
||||
{
|
||||
get { return GetString("AttributeRoute_TokenReplacement_EmptyTokenNotAllowed"); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An empty replacement token ('[]') is not allowed.
|
||||
/// </summary>
|
||||
internal static string FormatAttributeRoute_TokenReplacement_EmptyTokenNotAllowed()
|
||||
{
|
||||
return GetString("AttributeRoute_TokenReplacement_EmptyTokenNotAllowed");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Token delimiters ('[', ']') are imbalanced.
|
||||
/// </summary>
|
||||
internal static string AttributeRoute_TokenReplacement_ImbalancedSquareBrackets
|
||||
{
|
||||
get { return GetString("AttributeRoute_TokenReplacement_ImbalancedSquareBrackets"); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Token delimiters ('[', ']') are imbalanced.
|
||||
/// </summary>
|
||||
internal static string FormatAttributeRoute_TokenReplacement_ImbalancedSquareBrackets()
|
||||
{
|
||||
return GetString("AttributeRoute_TokenReplacement_ImbalancedSquareBrackets");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The route template '{0}' has invalid syntax. {1}
|
||||
/// </summary>
|
||||
internal static string AttributeRoute_TokenReplacement_InvalidSyntax
|
||||
{
|
||||
get { return GetString("AttributeRoute_TokenReplacement_InvalidSyntax"); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The route template '{0}' has invalid syntax. {1}
|
||||
/// </summary>
|
||||
internal static string FormatAttributeRoute_TokenReplacement_InvalidSyntax(object p0, object p1)
|
||||
{
|
||||
return string.Format(CultureInfo.CurrentCulture, GetString("AttributeRoute_TokenReplacement_InvalidSyntax"), p0, p1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// While processing template '{0}', a replacement value for the token '{1}' could not be found. Available tokens: '{2}'.
|
||||
/// </summary>
|
||||
internal static string AttributeRoute_TokenReplacement_ReplacementValueNotFound
|
||||
{
|
||||
get { return GetString("AttributeRoute_TokenReplacement_ReplacementValueNotFound"); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// While processing template '{0}', a replacement value for the token '{1}' could not be found. Available tokens: '{2}'.
|
||||
/// </summary>
|
||||
internal static string FormatAttributeRoute_TokenReplacement_ReplacementValueNotFound(object p0, object p1, object p2)
|
||||
{
|
||||
return string.Format(CultureInfo.CurrentCulture, GetString("AttributeRoute_TokenReplacement_ReplacementValueNotFound"), p0, p1, p2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A replacement token is not closed.
|
||||
/// </summary>
|
||||
internal static string AttributeRoute_TokenReplacement_UnclosedToken
|
||||
{
|
||||
get { return GetString("AttributeRoute_TokenReplacement_UnclosedToken"); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A replacement token is not closed.
|
||||
/// </summary>
|
||||
internal static string FormatAttributeRoute_TokenReplacement_UnclosedToken()
|
||||
{
|
||||
return GetString("AttributeRoute_TokenReplacement_UnclosedToken");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An unescaped '[' token is not allowed inside of a replacement token. Use '[[' to escape.
|
||||
/// </summary>
|
||||
internal static string AttributeRoute_TokenReplacement_UnescapedBraceInToken
|
||||
{
|
||||
get { return GetString("AttributeRoute_TokenReplacement_UnescapedBraceInToken"); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An unescaped '[' token is not allowed inside of a replacement token. Use '[[' to escape.
|
||||
/// </summary>
|
||||
internal static string FormatAttributeRoute_TokenReplacement_UnescapedBraceInToken()
|
||||
{
|
||||
return GetString("AttributeRoute_TokenReplacement_UnescapedBraceInToken");
|
||||
}
|
||||
|
||||
private static string GetString(string name, params string[] formatterNames)
|
||||
{
|
||||
var value = _resourceManager.GetString(name);
|
||||
|
|
|
|||
|
|
@ -7,10 +7,10 @@ using System.Linq;
|
|||
#if K10
|
||||
using System.Reflection;
|
||||
#endif
|
||||
using Microsoft.AspNet.Mvc.Core;
|
||||
using Microsoft.AspNet.Mvc.ReflectedModelBuilder;
|
||||
using Microsoft.AspNet.Mvc.Routing;
|
||||
using Microsoft.AspNet.Routing;
|
||||
using Microsoft.AspNet.Routing.Template;
|
||||
using Microsoft.Framework.OptionsModel;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc
|
||||
|
|
@ -112,12 +112,13 @@ namespace Microsoft.AspNet.Mvc
|
|||
|
||||
public List<ReflectedActionDescriptor> Build(ReflectedApplicationModel model)
|
||||
{
|
||||
var routeGroupsByTemplate = GetRouteGroupsByTemplate(model);
|
||||
|
||||
var actions = new List<ReflectedActionDescriptor>();
|
||||
|
||||
var routeGroupsByTemplate = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
var removalConstraints = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var routeTemplateErrors = new List<string>();
|
||||
|
||||
foreach (var controller in model.Controllers)
|
||||
{
|
||||
var controllerDescriptor = new ControllerDescriptor(controller.ControllerType);
|
||||
|
|
@ -136,7 +137,7 @@ namespace Microsoft.AspNet.Mvc
|
|||
ParameterBindingInfo = isFromBody
|
||||
? null
|
||||
: new ParameterBindingInfo(
|
||||
parameter.ParameterName,
|
||||
parameter.ParameterName,
|
||||
parameter.ParameterInfo.ParameterType),
|
||||
|
||||
BodyParameterInfo = isFromBody
|
||||
|
|
@ -201,56 +202,54 @@ namespace Microsoft.AspNet.Mvc
|
|||
}
|
||||
}
|
||||
|
||||
if (routeGroupsByTemplate.Any())
|
||||
var templateText = AttributeRouteTemplate.Combine(
|
||||
controller.RouteTemplate,
|
||||
action.RouteTemplate);
|
||||
|
||||
if (templateText != null)
|
||||
{
|
||||
var templateText = AttributeRouteTemplate.Combine(
|
||||
controller.RouteTemplate,
|
||||
action.RouteTemplate);
|
||||
|
||||
if (templateText == null)
|
||||
// An attribute routed action will ignore conventional routed constraints. We still
|
||||
// want to provide these values as ambient values.
|
||||
foreach (var constraint in actionDescriptor.RouteConstraints)
|
||||
{
|
||||
// A conventional routed action can't match any route group.
|
||||
actionDescriptor.RouteConstraints.Add(new RouteDataActionConstraint(
|
||||
AttributeRouting.RouteGroupKey,
|
||||
RouteKeyHandling.DenyKey));
|
||||
actionDescriptor.RouteValueDefaults.Add(constraint.RouteKey, constraint.RouteValue);
|
||||
}
|
||||
else
|
||||
|
||||
// Replaces tokens like [controller]/[action] in the route template with the actual values
|
||||
// for this action.
|
||||
try
|
||||
{
|
||||
// An attribute routed action will ignore conventional routed constraints. We still
|
||||
// want to provide these values as ambient values.
|
||||
foreach (var constraint in actionDescriptor.RouteConstraints)
|
||||
{
|
||||
actionDescriptor.RouteValueDefaults.Add(constraint.RouteKey, constraint.RouteValue);
|
||||
}
|
||||
|
||||
// TODO #738 - this currently has parity with what we did in MVC5 when a template uses
|
||||
// parameters like 'area', 'controller', and 'action. This needs to be changed as
|
||||
// part of #738.
|
||||
//
|
||||
// For instance, consider actions mapped with api/Blog/{action}. The value of {action}
|
||||
// needs to passed to action selection to choose the right action.
|
||||
var template = TemplateParser.Parse(templateText, _constraintResolver);
|
||||
|
||||
var routeConstraints = new List<RouteDataActionConstraint>();
|
||||
foreach (var constraint in actionDescriptor.RouteConstraints)
|
||||
{
|
||||
if (template.Parameters.Any(
|
||||
p => p.IsParameter &&
|
||||
string.Equals(p.Name, constraint.RouteKey, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
routeConstraints.Add(constraint);
|
||||
}
|
||||
}
|
||||
|
||||
var routeGroup = routeGroupsByTemplate[templateText];
|
||||
routeConstraints.Add(new RouteDataActionConstraint(
|
||||
AttributeRouting.RouteGroupKey,
|
||||
routeGroup));
|
||||
|
||||
actionDescriptor.RouteConstraints = routeConstraints;
|
||||
|
||||
actionDescriptor.AttributeRouteTemplate = templateText;
|
||||
templateText = AttributeRouteTemplate.ReplaceTokens(
|
||||
templateText,
|
||||
actionDescriptor.RouteValueDefaults);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
var message = Resources.FormatAttributeRoute_IndividualErrorMessage(
|
||||
actionDescriptor.DisplayName,
|
||||
Environment.NewLine,
|
||||
ex.Message);
|
||||
|
||||
routeTemplateErrors.Add(message);
|
||||
}
|
||||
|
||||
actionDescriptor.AttributeRouteTemplate = templateText;
|
||||
|
||||
// An attribute routed action is matched by its 'route group' which identifies all equivalent
|
||||
// actions.
|
||||
string routeGroup;
|
||||
if (!routeGroupsByTemplate.TryGetValue(templateText, out routeGroup))
|
||||
{
|
||||
routeGroup = GetRouteGroup(templateText);
|
||||
routeGroupsByTemplate.Add(templateText, routeGroup);
|
||||
}
|
||||
|
||||
var routeConstraints = new List<RouteDataActionConstraint>();
|
||||
routeConstraints.Add(new RouteDataActionConstraint(
|
||||
AttributeRouting.RouteGroupKey,
|
||||
routeGroup));
|
||||
|
||||
actionDescriptor.RouteConstraints = routeConstraints;
|
||||
}
|
||||
|
||||
actionDescriptor.FilterDescriptors =
|
||||
|
|
@ -270,6 +269,15 @@ namespace Microsoft.AspNet.Mvc
|
|||
{
|
||||
if (actionDescriptor.AttributeRouteTemplate == null)
|
||||
{
|
||||
// Any any attribute routes are in use, then non-attribute-routed ADs can't be selected
|
||||
// when a route group returned by the route.
|
||||
if (routeGroupsByTemplate.Any())
|
||||
{
|
||||
actionDescriptor.RouteConstraints.Add(new RouteDataActionConstraint(
|
||||
AttributeRouting.RouteGroupKey,
|
||||
RouteKeyHandling.DenyKey));
|
||||
}
|
||||
|
||||
if (!HasConstraint(actionDescriptor.RouteConstraints, key))
|
||||
{
|
||||
actionDescriptor.RouteConstraints.Add(new RouteDataActionConstraint(
|
||||
|
|
@ -292,27 +300,21 @@ namespace Microsoft.AspNet.Mvc
|
|||
}
|
||||
}
|
||||
|
||||
if (routeTemplateErrors.Any())
|
||||
{
|
||||
var message = Resources.FormatAttributeRoute_AggregateErrorMessage(
|
||||
Environment.NewLine,
|
||||
string.Join(Environment.NewLine + Environment.NewLine, routeTemplateErrors));
|
||||
throw new InvalidOperationException(message);
|
||||
}
|
||||
|
||||
return actions;
|
||||
}
|
||||
|
||||
// Groups the set of all attribute routing templates and returns mapping of [template -> group].
|
||||
private static Dictionary<string, string> GetRouteGroupsByTemplate(ReflectedApplicationModel model)
|
||||
// Returns a unique, stable key per-route-template (OrdinalIgnoreCase)
|
||||
private static string GetRouteGroup(string template)
|
||||
{
|
||||
var groupsByTemplate = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var controller in model.Controllers)
|
||||
{
|
||||
foreach (var action in controller.Actions)
|
||||
{
|
||||
var template = AttributeRouteTemplate.Combine(controller.RouteTemplate, action.RouteTemplate);
|
||||
if (template != null && !groupsByTemplate.ContainsKey(template))
|
||||
{
|
||||
groupsByTemplate.Add(template, "__route__" + template);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return groupsByTemplate;
|
||||
return ("__route__" + template).ToUpperInvariant();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -315,4 +315,34 @@
|
|||
<data name="OutputFormatterNoEncoding" xml:space="preserve">
|
||||
<value>No encoding found for output formatter '{0}'. There must be at least one supported encoding registered in order for the output formatter to write content.</value>
|
||||
</data>
|
||||
<data name="AttributeRoute_AggregateErrorMessage" xml:space="preserve">
|
||||
<value>The following errors occurred with attribute routing information:{0}{0}{1}</value>
|
||||
<comment>{0} is the newline. {1} is the formatted list of errors using AttributeRoute_IndividualErrorMessage</comment>
|
||||
</data>
|
||||
<data name="AttributeRoute_CannotContainParameter" xml:space="preserve">
|
||||
<value>The attribute route '{0}' cannot contain a parameter named '{{{1}}}'. Use '[{1}]' in the route template to insert the value '{2}'.</value>
|
||||
</data>
|
||||
<data name="AttributeRoute_IndividualErrorMessage" xml:space="preserve">
|
||||
<value>For action: '{0}'{1}Error: {2}</value>
|
||||
<comment>{1} is the newline.</comment>
|
||||
</data>
|
||||
<data name="AttributeRoute_TokenReplacement_EmptyTokenNotAllowed" xml:space="preserve">
|
||||
<value>An empty replacement token ('[]') is not allowed.</value>
|
||||
</data>
|
||||
<data name="AttributeRoute_TokenReplacement_ImbalancedSquareBrackets" xml:space="preserve">
|
||||
<value>Token delimiters ('[', ']') are imbalanced.</value>
|
||||
</data>
|
||||
<data name="AttributeRoute_TokenReplacement_InvalidSyntax" xml:space="preserve">
|
||||
<value>The route template '{0}' has invalid syntax. {1}</value>
|
||||
<comment>{1} is the specific error message.</comment>
|
||||
</data>
|
||||
<data name="AttributeRoute_TokenReplacement_ReplacementValueNotFound" xml:space="preserve">
|
||||
<value>While processing template '{0}', a replacement value for the token '{1}' could not be found. Available tokens: '{2}'.</value>
|
||||
</data>
|
||||
<data name="AttributeRoute_TokenReplacement_UnclosedToken" xml:space="preserve">
|
||||
<value>A replacement token is not closed.</value>
|
||||
</data>
|
||||
<data name="AttributeRoute_TokenReplacement_UnescapedBraceInToken" xml:space="preserve">
|
||||
<value>An unescaped '[' token is not allowed inside of a replacement token. Use '[[' to escape.</value>
|
||||
</data>
|
||||
</root>
|
||||
|
|
@ -2,6 +2,9 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using Microsoft.AspNet.Mvc.Core;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.Routing
|
||||
{
|
||||
|
|
@ -93,5 +96,207 @@ namespace Microsoft.AspNet.Mvc.Routing
|
|||
|
||||
return result.Substring(startIndex, subStringLength);
|
||||
}
|
||||
|
||||
public static string ReplaceTokens(string template, IDictionary<string, object> values)
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
var state = TemplateParserState.Plaintext;
|
||||
|
||||
int? tokenStart = null;
|
||||
|
||||
// We'll run the loop one extra time with 'null' to detect the end of the string.
|
||||
for (var i = 0; i <= template.Length; i++)
|
||||
{
|
||||
var c = i < template.Length ? (char?)template[i] : null;
|
||||
switch (state)
|
||||
{
|
||||
case TemplateParserState.Plaintext:
|
||||
if (c == '[')
|
||||
{
|
||||
state = TemplateParserState.SeenLeft;
|
||||
break;
|
||||
}
|
||||
else if (c == ']')
|
||||
{
|
||||
state = TemplateParserState.SeenRight;
|
||||
break;
|
||||
}
|
||||
else if (c == null)
|
||||
{
|
||||
// We're at the end of the string, nothing left to do.
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.Append(c);
|
||||
break;
|
||||
}
|
||||
case TemplateParserState.SeenLeft:
|
||||
if (c == '[')
|
||||
{
|
||||
// This is an escaped left-bracket
|
||||
builder.Append(c);
|
||||
state = TemplateParserState.Plaintext;
|
||||
break;
|
||||
}
|
||||
else if (c == ']')
|
||||
{
|
||||
// This is zero-width parameter - not allowed.
|
||||
var message = Resources.FormatAttributeRoute_TokenReplacement_InvalidSyntax(
|
||||
template,
|
||||
Resources.AttributeRoute_TokenReplacement_EmptyTokenNotAllowed);
|
||||
throw new InvalidOperationException(message);
|
||||
}
|
||||
else if (c == null)
|
||||
{
|
||||
// This is a left-bracket at the end of the string.
|
||||
var message = Resources.FormatAttributeRoute_TokenReplacement_InvalidSyntax(
|
||||
template,
|
||||
Resources.AttributeRoute_TokenReplacement_UnclosedToken);
|
||||
throw new InvalidOperationException(message);
|
||||
}
|
||||
else
|
||||
{
|
||||
tokenStart = i;
|
||||
state = TemplateParserState.InsideToken;
|
||||
break;
|
||||
}
|
||||
case TemplateParserState.SeenRight:
|
||||
if (c == ']')
|
||||
{
|
||||
// This is an escaped right-bracket
|
||||
builder.Append(c);
|
||||
state = TemplateParserState.Plaintext;
|
||||
break;
|
||||
}
|
||||
else if (c == null)
|
||||
{
|
||||
// This is an imbalanced right-bracket at the end of the string.
|
||||
var message = Resources.FormatAttributeRoute_TokenReplacement_InvalidSyntax(
|
||||
template,
|
||||
Resources.AttributeRoute_TokenReplacement_ImbalancedSquareBrackets);
|
||||
throw new InvalidOperationException(message);
|
||||
}
|
||||
else
|
||||
{
|
||||
// This is an imbalanced right-bracket.
|
||||
var message = Resources.FormatAttributeRoute_TokenReplacement_InvalidSyntax(
|
||||
template,
|
||||
Resources.AttributeRoute_TokenReplacement_ImbalancedSquareBrackets);
|
||||
throw new InvalidOperationException(message);
|
||||
}
|
||||
case TemplateParserState.InsideToken:
|
||||
if (c == '[')
|
||||
{
|
||||
state = TemplateParserState.InsideToken | TemplateParserState.SeenLeft;
|
||||
break;
|
||||
}
|
||||
else if (c == ']')
|
||||
{
|
||||
state = TemplateParserState.InsideToken | TemplateParserState.SeenRight;
|
||||
break;
|
||||
}
|
||||
else if (c == null)
|
||||
{
|
||||
// This is an unclosed replacement token
|
||||
var message = Resources.FormatAttributeRoute_TokenReplacement_InvalidSyntax(
|
||||
template,
|
||||
Resources.AttributeRoute_TokenReplacement_UnclosedToken);
|
||||
throw new InvalidOperationException(message);
|
||||
}
|
||||
else
|
||||
{
|
||||
// This is a just part of the parameter
|
||||
break;
|
||||
}
|
||||
case TemplateParserState.InsideToken | TemplateParserState.SeenLeft:
|
||||
if (c == '[')
|
||||
{
|
||||
// This is an escaped left-bracket
|
||||
state = TemplateParserState.InsideToken;
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Unescaped left-bracket is not allowed inside a token.
|
||||
var message = Resources.FormatAttributeRoute_TokenReplacement_InvalidSyntax(
|
||||
template,
|
||||
Resources.AttributeRoute_TokenReplacement_UnescapedBraceInToken);
|
||||
throw new InvalidOperationException(message);
|
||||
}
|
||||
case TemplateParserState.InsideToken | TemplateParserState.SeenRight:
|
||||
if (c == ']')
|
||||
{
|
||||
// This is an escaped right-bracket
|
||||
state = TemplateParserState.InsideToken;
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
// This is the end of a replacement token.
|
||||
var token = template
|
||||
.Substring(tokenStart.Value, i - tokenStart.Value - 1)
|
||||
.Replace("[[", "[")
|
||||
.Replace("]]", "]");
|
||||
|
||||
object value;
|
||||
if (!values.TryGetValue(token, out value))
|
||||
{
|
||||
// Value not found
|
||||
var message = Resources.FormatAttributeRoute_TokenReplacement_ReplacementValueNotFound(
|
||||
template,
|
||||
token,
|
||||
string.Join(", ", values.Keys));
|
||||
throw new InvalidOperationException(message);
|
||||
}
|
||||
|
||||
builder.Append(value);
|
||||
|
||||
if (c == '[')
|
||||
{
|
||||
state = TemplateParserState.SeenLeft;
|
||||
}
|
||||
else if (c == ']')
|
||||
{
|
||||
state = TemplateParserState.SeenRight;
|
||||
}
|
||||
else if (c == null)
|
||||
{
|
||||
state = TemplateParserState.Plaintext;
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.Append(c);
|
||||
state = TemplateParserState.Plaintext;
|
||||
}
|
||||
|
||||
tokenStart = null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
[Flags]
|
||||
private enum TemplateParserState : uint
|
||||
{
|
||||
// default state - allow non-special characters to pass through to the
|
||||
// buffer.
|
||||
Plaintext = 0,
|
||||
|
||||
// We're inside a replacement token, may be combined with other states to detect
|
||||
// a possible escaped bracket inside the token.
|
||||
InsideToken = 1,
|
||||
|
||||
// We've seen a left brace, need to see the next character to find out if it's escaped
|
||||
// or not.
|
||||
SeenLeft = 2,
|
||||
|
||||
// We've seen a right brace, need to see the next character to find out if it's escaped
|
||||
// or not.
|
||||
SeenRight = 4,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNet.Mvc.Core;
|
||||
using Microsoft.AspNet.Routing;
|
||||
using Microsoft.AspNet.Routing.Template;
|
||||
using Microsoft.Framework.DependencyInjection;
|
||||
|
|
@ -27,26 +28,18 @@ namespace Microsoft.AspNet.Mvc.Routing
|
|||
var actions = GetActionDescriptors(services);
|
||||
|
||||
var inlineConstraintResolver = services.GetService<IInlineConstraintResolver>();
|
||||
var routeInfos = GetRouteInfos(actions, inlineConstraintResolver);
|
||||
var routeInfos = GetRouteInfos(inlineConstraintResolver, actions);
|
||||
|
||||
// We're creating one AttributeRouteGenerationEntry per action. This allows us to match the intended
|
||||
// action by expected route values, and then use the TemplateBinder to generate the link.
|
||||
var generationEntries = new List<AttributeRouteLinkGenerationEntry>();
|
||||
foreach (var routeInfo in routeInfos)
|
||||
{
|
||||
var defaults = routeInfo.ParsedTemplate.Parameters
|
||||
.Where(p => p.DefaultValue != null)
|
||||
.ToDictionary(p => p.Name, p => p.DefaultValue, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var constraints = routeInfo.ParsedTemplate.Parameters
|
||||
.Where(p => p.InlineConstraint != null)
|
||||
.ToDictionary(p => p.Name, p => p.InlineConstraint, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
generationEntries.Add(new AttributeRouteLinkGenerationEntry()
|
||||
{
|
||||
Binder = new TemplateBinder(routeInfo.ParsedTemplate, defaults),
|
||||
Defaults = defaults,
|
||||
Constraints = constraints,
|
||||
Binder = new TemplateBinder(routeInfo.ParsedTemplate, routeInfo.Defaults),
|
||||
Defaults = routeInfo.Defaults,
|
||||
Constraints = routeInfo.Constraints,
|
||||
Precedence = routeInfo.Precedence,
|
||||
RequiredLinkValues = routeInfo.ActionDescriptor.RouteValueDefaults,
|
||||
RouteGroup = routeInfo.RouteGroup,
|
||||
|
|
@ -103,42 +96,133 @@ namespace Microsoft.AspNet.Mvc.Routing
|
|||
}
|
||||
|
||||
private static List<RouteInfo> GetRouteInfos(
|
||||
IReadOnlyList<ActionDescriptor> actions,
|
||||
IInlineConstraintResolver constraintResolver)
|
||||
IInlineConstraintResolver constraintResolver,
|
||||
IReadOnlyList<ActionDescriptor> actions)
|
||||
{
|
||||
var routeInfos = new List<RouteInfo>();
|
||||
var errors = new List<RouteInfo>();
|
||||
|
||||
// This keeps a cache of 'Template' objects. It's a fairly common case that multiple actions
|
||||
// will use the same route template string; thus, the `Template` object can be shared.
|
||||
//
|
||||
// For a relatively simple route template, the `Template` object will hold about 500 bytes
|
||||
// of memory, so sharing is worthwhile.
|
||||
var templateCache = new Dictionary<string, Template>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var action in actions.Where(a => a.AttributeRouteTemplate != null))
|
||||
{
|
||||
var constraint = action.RouteConstraints
|
||||
.Where(c => c.RouteKey == AttributeRouting.RouteGroupKey)
|
||||
.FirstOrDefault();
|
||||
if (constraint == null ||
|
||||
constraint.KeyHandling != RouteKeyHandling.RequireKey ||
|
||||
constraint.RouteValue == null)
|
||||
var routeInfo = GetRouteInfo(constraintResolver, templateCache, action);
|
||||
if (routeInfo.ErrorMessage == null)
|
||||
{
|
||||
// This is unlikely to happen by default, but could happen through extensibility. Just ignore it.
|
||||
continue;
|
||||
routeInfos.Add(routeInfo);
|
||||
}
|
||||
|
||||
var parsedTemplate = TemplateParser.Parse(action.AttributeRouteTemplate, constraintResolver);
|
||||
routeInfos.Add(new RouteInfo()
|
||||
else
|
||||
{
|
||||
ActionDescriptor = action,
|
||||
ParsedTemplate = parsedTemplate,
|
||||
Precedence = AttributeRoutePrecedence.Compute(parsedTemplate),
|
||||
RouteGroup = constraint.RouteValue,
|
||||
RouteTemplate = action.AttributeRouteTemplate,
|
||||
});
|
||||
errors.Add(routeInfo);
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.Count > 0)
|
||||
{
|
||||
var allErrors = string.Join(
|
||||
Environment.NewLine + Environment.NewLine,
|
||||
errors.Select(
|
||||
e => Resources.FormatAttributeRoute_IndividualErrorMessage(
|
||||
e.ActionDescriptor.DisplayName,
|
||||
Environment.NewLine,
|
||||
e.ErrorMessage)));
|
||||
|
||||
var message = Resources.FormatAttributeRoute_AggregateErrorMessage(Environment.NewLine, allErrors);
|
||||
throw new InvalidOperationException(message);
|
||||
}
|
||||
|
||||
return routeInfos;
|
||||
}
|
||||
|
||||
private static RouteInfo GetRouteInfo(
|
||||
IInlineConstraintResolver constraintResolver,
|
||||
Dictionary<string, Template> templateCache,
|
||||
ActionDescriptor action)
|
||||
{
|
||||
var constraint = action.RouteConstraints
|
||||
.Where(c => c.RouteKey == AttributeRouting.RouteGroupKey)
|
||||
.FirstOrDefault();
|
||||
if (constraint == null ||
|
||||
constraint.KeyHandling != RouteKeyHandling.RequireKey ||
|
||||
constraint.RouteValue == null)
|
||||
{
|
||||
// This can happen if an ActionDescriptor has a route template, but doesn't have one of our
|
||||
// special route group constraints. This is a good indication that the user is using a 3rd party
|
||||
// routing system, or has customized their ADs in a way that we can no longer understand them.
|
||||
//
|
||||
// We just treat this case as an 'opt-out' of our attribute routing system.
|
||||
return null;
|
||||
}
|
||||
|
||||
var routeInfo = new RouteInfo()
|
||||
{
|
||||
ActionDescriptor = action,
|
||||
RouteGroup = constraint.RouteValue,
|
||||
RouteTemplate = action.AttributeRouteTemplate,
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
Template parsedTemplate;
|
||||
if (!templateCache.TryGetValue(action.AttributeRouteTemplate, out parsedTemplate))
|
||||
{
|
||||
// Parsing with throw if the template is invalid.
|
||||
parsedTemplate = TemplateParser.Parse(action.AttributeRouteTemplate, constraintResolver);
|
||||
templateCache.Add(action.AttributeRouteTemplate, parsedTemplate);
|
||||
}
|
||||
|
||||
routeInfo.ParsedTemplate = parsedTemplate;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
routeInfo.ErrorMessage = ex.Message;
|
||||
return routeInfo;
|
||||
}
|
||||
|
||||
foreach (var kvp in action.RouteValueDefaults)
|
||||
{
|
||||
foreach (var parameter in routeInfo.ParsedTemplate.Parameters)
|
||||
{
|
||||
if (string.Equals(kvp.Key, parameter.Name, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
routeInfo.ErrorMessage = Resources.FormatAttributeRoute_CannotContainParameter(
|
||||
routeInfo.RouteTemplate,
|
||||
kvp.Key,
|
||||
kvp.Value);
|
||||
|
||||
return routeInfo;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
routeInfo.Precedence = AttributeRoutePrecedence.Compute(routeInfo.ParsedTemplate);
|
||||
|
||||
routeInfo.Constraints = routeInfo.ParsedTemplate.Parameters
|
||||
.Where(p => p.InlineConstraint != null)
|
||||
.ToDictionary(p => p.Name, p => p.InlineConstraint, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
routeInfo.Defaults = routeInfo.ParsedTemplate.Parameters
|
||||
.Where(p => p.DefaultValue != null)
|
||||
.ToDictionary(p => p.Name, p => p.DefaultValue, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
return routeInfo;
|
||||
}
|
||||
|
||||
private class RouteInfo
|
||||
{
|
||||
public ActionDescriptor ActionDescriptor { get; set; }
|
||||
|
||||
public IDictionary<string, IRouteConstraint> Constraints { get; set; }
|
||||
|
||||
public IDictionary<string, object> Defaults { get; set; }
|
||||
|
||||
public string ErrorMessage { get; set; }
|
||||
|
||||
public Template ParsedTemplate { get; set; }
|
||||
|
||||
public decimal Precedence { get; set; }
|
||||
|
|
|
|||
|
|
@ -89,6 +89,7 @@
|
|||
<Compile Include="KnownRouteValueConstraintTests.cs" />
|
||||
<Compile Include="Routing\AttributeRoutePrecedenceTests.cs" />
|
||||
<Compile Include="Routing\AttributeRouteTemplateTests.cs" />
|
||||
<Compile Include="Routing\AttributeRoutingTest.cs" />
|
||||
<Compile Include="StaticControllerAssemblyProvider.cs" />
|
||||
<Compile Include="Routing\AttributeRouteTests.cs" />
|
||||
<Compile Include="TestController.cs" />
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ using System.Collections.Generic;
|
|||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Microsoft.AspNet.Routing;
|
||||
using Microsoft.AspNet.Mvc.Routing;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
|
|
@ -209,6 +210,113 @@ namespace Microsoft.AspNet.Mvc.Test
|
|||
displayNames);
|
||||
}
|
||||
|
||||
public void AttributeRouting_TokenReplacement_IsAfterReflectedModel()
|
||||
{
|
||||
// Arrange
|
||||
var provider = GetProvider(typeof(TokenReplacementController).GetTypeInfo());
|
||||
|
||||
// Act
|
||||
var model = provider.BuildModel();
|
||||
|
||||
// Assert
|
||||
var controller = Assert.Single(model.Controllers);
|
||||
Assert.Equal("api/Token/[key]/[controller]", controller.RouteTemplate);
|
||||
|
||||
var action = Assert.Single(controller.Actions);
|
||||
Assert.Equal("stub/[action]", action.RouteTemplate);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AttributeRouting_TokenReplacement_InActionDescriptor()
|
||||
{
|
||||
// Arrange
|
||||
var provider = GetProvider(typeof(TokenReplacementController).GetTypeInfo());
|
||||
|
||||
// Act
|
||||
var actions = provider.GetDescriptors();
|
||||
|
||||
// Assert
|
||||
var action = Assert.Single(actions);
|
||||
Assert.Equal("api/Token/value/TokenReplacement/stub/ThisIsAnAction", action.AttributeRouteTemplate);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AttributeRouting_TokenReplacement_ThrowsWithMultipleMessages()
|
||||
{
|
||||
// Arrange
|
||||
var provider = GetProvider(typeof(MultipleErrorsController).GetTypeInfo());
|
||||
|
||||
var expectedMessage =
|
||||
"The following errors occurred with attribute routing information:" + Environment.NewLine +
|
||||
Environment.NewLine +
|
||||
"For action: 'Microsoft.AspNet.Mvc.Test.ReflectedActionDescriptorProviderTests+" +
|
||||
"MultipleErrorsController.Unknown'" + Environment.NewLine +
|
||||
"Error: While processing template 'stub/[action]/[unknown]', a replacement value for the token 'unknown' " +
|
||||
"could not be found. Available tokens: 'controller, action'." + Environment.NewLine +
|
||||
Environment.NewLine +
|
||||
"For action: 'Microsoft.AspNet.Mvc.Test.ReflectedActionDescriptorProviderTests+" +
|
||||
"MultipleErrorsController.Invalid'" + Environment.NewLine +
|
||||
"Error: The route template '[invalid/syntax' has invalid syntax. A replacement token is not closed.";
|
||||
|
||||
// Act
|
||||
var ex = Assert.Throws<InvalidOperationException>(() => { provider.GetDescriptors(); });
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedMessage, ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AttributeRouting_TokenReplacement_CaseInsensitive()
|
||||
{
|
||||
// Arrange
|
||||
var provider = GetProvider(typeof(CaseInsensitiveController).GetTypeInfo());
|
||||
|
||||
// Act
|
||||
var actions = provider.GetDescriptors();
|
||||
|
||||
// Assert
|
||||
var action = Assert.Single(actions);
|
||||
Assert.Equal("stub/ThisIsAnAction", action.AttributeRouteTemplate);
|
||||
}
|
||||
|
||||
// Token replacement happens before we 'group' routes. So two route templates
|
||||
// that are equivalent after token replacement go to the same 'group'.
|
||||
[Fact]
|
||||
public void AttributeRouting_TokenReplacement_BeforeGroupId()
|
||||
{
|
||||
// Arrange
|
||||
var provider = GetProvider(typeof(SameGroupIdController).GetTypeInfo());
|
||||
|
||||
// Act
|
||||
var actions = provider.GetDescriptors().ToArray();
|
||||
|
||||
var groupIds = actions.Select(
|
||||
a => a.RouteConstraints
|
||||
.Where(rc => rc.RouteKey == AttributeRouting.RouteGroupKey)
|
||||
.Select(rc => rc.RouteValue)
|
||||
.Single())
|
||||
.ToArray();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, groupIds.Length);
|
||||
Assert.Equal(groupIds[0], groupIds[1]);
|
||||
}
|
||||
|
||||
// Parameters are validated later. This action uses the forbidden {action} and {controller}
|
||||
[Fact]
|
||||
public void AttributeRouting_DoesNotValidateParameters()
|
||||
{
|
||||
// Arrange
|
||||
var provider = GetProvider(typeof(InvalidParametersController).GetTypeInfo());
|
||||
|
||||
// Act
|
||||
var actions = provider.GetDescriptors();
|
||||
|
||||
// Assert
|
||||
var action = Assert.Single(actions);
|
||||
Assert.Equal("stub/{controller}/{action}", action.AttributeRouteTemplate);
|
||||
}
|
||||
|
||||
private ReflectedActionDescriptorProvider GetProvider(
|
||||
TypeInfo controllerTypeInfo,
|
||||
IEnumerable<IFilter> filters = null)
|
||||
|
|
@ -312,5 +420,43 @@ namespace Microsoft.AspNet.Mvc.Test
|
|||
{
|
||||
}
|
||||
}
|
||||
|
||||
[Route("api/Token/[key]/[controller]")]
|
||||
[MyRouteConstraint(false)]
|
||||
private class TokenReplacementController
|
||||
{
|
||||
[HttpGet("stub/[action]")]
|
||||
public void ThisIsAnAction() { }
|
||||
}
|
||||
|
||||
private class CaseInsensitiveController
|
||||
{
|
||||
[HttpGet("stub/[ActIon]")]
|
||||
public void ThisIsAnAction() { }
|
||||
}
|
||||
|
||||
private class MultipleErrorsController
|
||||
{
|
||||
[HttpGet("stub/[action]/[unknown]")]
|
||||
public void Unknown() { }
|
||||
|
||||
[HttpGet("[invalid/syntax")]
|
||||
public void Invalid() { }
|
||||
}
|
||||
|
||||
private class InvalidParametersController
|
||||
{
|
||||
[HttpGet("stub/{controller}/{action}")]
|
||||
public void Action1() { }
|
||||
}
|
||||
|
||||
private class SameGroupIdController
|
||||
{
|
||||
[HttpGet("stub/[action]")]
|
||||
public void Action1() { }
|
||||
|
||||
[HttpGet("stub/Action1")]
|
||||
public void Action2() { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using Microsoft.AspNet.Routing;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.Routing
|
||||
|
|
@ -98,5 +100,218 @@ namespace Microsoft.AspNet.Mvc.Routing
|
|||
// Assert
|
||||
Assert.Equal(expected, combined);
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> ReplaceTokens_ValueValuesData
|
||||
{
|
||||
get
|
||||
{
|
||||
yield return new object[]
|
||||
{
|
||||
"[controller]/[action]",
|
||||
new { controller = "Home", action = "Index" },
|
||||
"Home/Index"
|
||||
};
|
||||
|
||||
yield return new object[]
|
||||
{
|
||||
"[controller]",
|
||||
new { controller = "Home", action = "Index" },
|
||||
"Home"
|
||||
};
|
||||
|
||||
yield return new object[]
|
||||
{
|
||||
"[controller][[",
|
||||
new { controller = "Home", action = "Index" },
|
||||
"Home["
|
||||
};
|
||||
|
||||
yield return new object[]
|
||||
{
|
||||
"[coNTroller]",
|
||||
new { contrOLler = "Home", action = "Index" },
|
||||
"Home"
|
||||
};
|
||||
|
||||
yield return new object[]
|
||||
{
|
||||
"thisisSomeText[action]",
|
||||
new { controller = "Home", action = "Index" },
|
||||
"thisisSomeTextIndex"
|
||||
};
|
||||
|
||||
yield return new object[]
|
||||
{
|
||||
"[[-]][[/[[controller]]",
|
||||
new { controller = "Home", action = "Index" },
|
||||
"[-][/[controller]"
|
||||
};
|
||||
|
||||
yield return new object[]
|
||||
{
|
||||
"[contr[[oller]/[act]]ion]",
|
||||
new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
{ "contr[oller", "Home" },
|
||||
{ "act]ion", "Index" }
|
||||
},
|
||||
"Home/Index"
|
||||
};
|
||||
|
||||
yield return new object[]
|
||||
{
|
||||
"[controller][action]",
|
||||
new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
{ "controller", "Home" },
|
||||
{ "action", "Index" }
|
||||
},
|
||||
"HomeIndex"
|
||||
};
|
||||
|
||||
yield return new object[]
|
||||
{
|
||||
"[contr}oller]/[act{ion]/{id}",
|
||||
new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
{ "contr}oller", "Home" },
|
||||
{ "act{ion", "Index" }
|
||||
},
|
||||
"Home/Index/{id}"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData("ReplaceTokens_ValueValuesData")]
|
||||
public void ReplaceTokens_ValidValues(string template, object values, string expected)
|
||||
{
|
||||
// Arrange
|
||||
var valuesDictionary = values as IDictionary<string, object>;
|
||||
if (valuesDictionary == null)
|
||||
{
|
||||
valuesDictionary = new RouteValueDictionary(values);
|
||||
}
|
||||
|
||||
// Act
|
||||
var result = AttributeRouteTemplate.ReplaceTokens(template, valuesDictionary);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> ReplaceTokens_InvalidFormatValuesData
|
||||
{
|
||||
get
|
||||
{
|
||||
yield return new object[]
|
||||
{
|
||||
"[",
|
||||
new { },
|
||||
"A replacement token is not closed."
|
||||
};
|
||||
|
||||
yield return new object[]
|
||||
{
|
||||
"text]",
|
||||
new { },
|
||||
"Token delimiters ('[', ']') are imbalanced.",
|
||||
};
|
||||
|
||||
yield return new object[]
|
||||
{
|
||||
"text]morecooltext",
|
||||
new { },
|
||||
"Token delimiters ('[', ']') are imbalanced.",
|
||||
};
|
||||
|
||||
yield return new object[]
|
||||
{
|
||||
"[action",
|
||||
new { },
|
||||
"A replacement token is not closed.",
|
||||
};
|
||||
|
||||
yield return new object[]
|
||||
{
|
||||
"[action]]][",
|
||||
new RouteValueDictionary()
|
||||
{
|
||||
{ "action]", "Index" }
|
||||
},
|
||||
"A replacement token is not closed.",
|
||||
};
|
||||
|
||||
yield return new object[]
|
||||
{
|
||||
"[action]]",
|
||||
new { },
|
||||
"A replacement token is not closed."
|
||||
};
|
||||
|
||||
yield return new object[]
|
||||
{
|
||||
"[ac[tion]",
|
||||
new { },
|
||||
"An unescaped '[' token is not allowed inside of a replacement token. Use '[[' to escape."
|
||||
};
|
||||
|
||||
yield return new object[]
|
||||
{
|
||||
"[]",
|
||||
new { },
|
||||
"An empty replacement token ('[]') is not allowed.",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData("ReplaceTokens_InvalidFormatValuesData")]
|
||||
public void ReplaceTokens_InvalidFormat(string template, object values, string reason)
|
||||
{
|
||||
// Arrange
|
||||
var valuesDictionary = values as IDictionary<string, object>;
|
||||
if (valuesDictionary == null)
|
||||
{
|
||||
valuesDictionary = new RouteValueDictionary(values);
|
||||
}
|
||||
|
||||
var expected = string.Format(
|
||||
"The route template '{0}' has invalid syntax. {1}",
|
||||
template,
|
||||
reason);
|
||||
|
||||
// Act
|
||||
var ex = Assert.Throws<InvalidOperationException>(
|
||||
() => { AttributeRouteTemplate.ReplaceTokens(template, valuesDictionary); });
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReplaceTokens_UnknownValue()
|
||||
{
|
||||
// Arrange
|
||||
var template = "[area]/[controller]/[action2]";
|
||||
var values = new RouteValueDictionary()
|
||||
{
|
||||
{ "area", "Help" },
|
||||
{ "controller", "Admin" },
|
||||
{ "action", "SeeUsers" },
|
||||
};
|
||||
|
||||
var expected =
|
||||
"While processing template '[area]/[controller]/[action2]', " +
|
||||
"a replacement value for the token 'action2' could not be found. " +
|
||||
"Available tokens: 'area, controller, action'.";
|
||||
|
||||
// Act
|
||||
var ex = Assert.Throws<InvalidOperationException>(
|
||||
() => { AttributeRouteTemplate.ReplaceTokens(template, values); });
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,184 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
#if NET45
|
||||
using Microsoft.AspNet.Routing;
|
||||
using Microsoft.Framework.OptionsModel;
|
||||
using Moq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.Routing
|
||||
{
|
||||
public class AttributeRoutingTest
|
||||
{
|
||||
[Fact]
|
||||
public void AttributeRouting_SyntaxErrorInTemplate()
|
||||
{
|
||||
// Arrange
|
||||
var action = CreateAction("InvalidTemplate", "{a/dkfk}");
|
||||
|
||||
var expectedMessage =
|
||||
"The following errors occurred with attribute routing information:" + Environment.NewLine +
|
||||
Environment.NewLine +
|
||||
"For action: 'InvalidTemplate'" + Environment.NewLine +
|
||||
"Error: There is an incomplete parameter in the route template. " +
|
||||
"Check that each '{' character has a matching '}' character." + Environment.NewLine +
|
||||
"Parameter name: routeTemplate";
|
||||
|
||||
var router = CreateRouter();
|
||||
var services = CreateServices(action);
|
||||
|
||||
// Act & Assert
|
||||
var ex = Assert.Throws<InvalidOperationException>(
|
||||
() => { AttributeRouting.CreateAttributeMegaRoute(router, services); });
|
||||
|
||||
Assert.Equal(expectedMessage, ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AttributeRouting_DisallowedParameter()
|
||||
{
|
||||
// Arrange
|
||||
var action = CreateAction("DisallowedParameter", "{foo}/{action}");
|
||||
action.RouteValueDefaults.Add("foo", "bleh");
|
||||
|
||||
var expectedMessage =
|
||||
"The following errors occurred with attribute routing information:" + Environment.NewLine +
|
||||
Environment.NewLine +
|
||||
"For action: 'DisallowedParameter'" + Environment.NewLine +
|
||||
"Error: The attribute route '{foo}/{action}' cannot contain a parameter named '{foo}'. " +
|
||||
"Use '[foo]' in the route template to insert the value 'bleh'.";
|
||||
|
||||
var router = CreateRouter();
|
||||
var services = CreateServices(action);
|
||||
|
||||
// Act & Assert
|
||||
var ex = Assert.Throws<InvalidOperationException>(
|
||||
() => { AttributeRouting.CreateAttributeMegaRoute(router, services); });
|
||||
|
||||
Assert.Equal(expectedMessage, ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AttributeRouting_MultipleErrors()
|
||||
{
|
||||
// Arrange
|
||||
var action1 = CreateAction("DisallowedParameter1", "{foo}/{action}");
|
||||
action1.RouteValueDefaults.Add("foo", "bleh");
|
||||
|
||||
var action2 = CreateAction("DisallowedParameter2", "cool/{action}");
|
||||
action2.RouteValueDefaults.Add("action", "hey");
|
||||
|
||||
var expectedMessage =
|
||||
"The following errors occurred with attribute routing information:" + Environment.NewLine +
|
||||
Environment.NewLine +
|
||||
"For action: 'DisallowedParameter1'" + Environment.NewLine +
|
||||
"Error: The attribute route '{foo}/{action}' cannot contain a parameter named '{foo}'. " +
|
||||
"Use '[foo]' in the route template to insert the value 'bleh'." + Environment.NewLine +
|
||||
Environment.NewLine +
|
||||
"For action: 'DisallowedParameter2'" + Environment.NewLine +
|
||||
"Error: The attribute route 'cool/{action}' cannot contain a parameter named '{action}'. " +
|
||||
"Use '[action]' in the route template to insert the value 'hey'.";
|
||||
|
||||
var router = CreateRouter();
|
||||
var services = CreateServices(action1, action2);
|
||||
|
||||
// Act & Assert
|
||||
var ex = Assert.Throws<InvalidOperationException>(
|
||||
() => { AttributeRouting.CreateAttributeMegaRoute(router, services); });
|
||||
|
||||
Assert.Equal(expectedMessage, ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AttributeRouting_WithReflectedActionDescriptor()
|
||||
{
|
||||
// Arrange
|
||||
var controllerType = typeof(HomeController);
|
||||
var actionMethod = controllerType.GetMethod("Index");
|
||||
|
||||
var action = new ReflectedActionDescriptor();
|
||||
action.DisplayName = "Microsoft.AspNet.Mvc.Routing.AttributeRoutingTest+HomeController.Index";
|
||||
action.MethodInfo = actionMethod;
|
||||
action.RouteConstraints = new List<RouteDataActionConstraint>()
|
||||
{
|
||||
new RouteDataActionConstraint(AttributeRouting.RouteGroupKey, "group"),
|
||||
};
|
||||
action.AttributeRouteTemplate = "{controller}/{action}";
|
||||
action.RouteValueDefaults.Add("controller", "Home");
|
||||
action.RouteValueDefaults.Add("action", "Index");
|
||||
|
||||
var expectedMessage =
|
||||
"The following errors occurred with attribute routing information:" + Environment.NewLine +
|
||||
Environment.NewLine +
|
||||
"For action: 'Microsoft.AspNet.Mvc.Routing.AttributeRoutingTest+HomeController.Index'" + Environment.NewLine +
|
||||
"Error: The attribute route '{controller}/{action}' cannot contain a parameter named '{controller}'. " +
|
||||
"Use '[controller]' in the route template to insert the value 'Home'.";
|
||||
|
||||
var router = CreateRouter();
|
||||
var services = CreateServices(action);
|
||||
|
||||
// Act & Assert
|
||||
var ex = Assert.Throws<InvalidOperationException>(
|
||||
() => { AttributeRouting.CreateAttributeMegaRoute(router, services); });
|
||||
|
||||
Assert.Equal(expectedMessage, ex.Message);
|
||||
}
|
||||
|
||||
private static ActionDescriptor CreateAction(string displayName, string template)
|
||||
{
|
||||
return new DisplayNameActionDescriptor()
|
||||
{
|
||||
DisplayName = displayName,
|
||||
RouteConstraints = new List<RouteDataActionConstraint>()
|
||||
{
|
||||
new RouteDataActionConstraint(AttributeRouting.RouteGroupKey, "whatever"),
|
||||
},
|
||||
AttributeRouteTemplate = template,
|
||||
};
|
||||
}
|
||||
|
||||
private static IRouter CreateRouter()
|
||||
{
|
||||
return Mock.Of<IRouter>();
|
||||
}
|
||||
|
||||
private static IServiceProvider CreateServices(params ActionDescriptor[] actions)
|
||||
{
|
||||
var collection = new ActionDescriptorsCollection(actions, version: 0);
|
||||
|
||||
var actionDescriptorProvider = new Mock<IActionDescriptorsCollectionProvider>();
|
||||
actionDescriptorProvider
|
||||
.Setup(a => a.ActionDescriptors)
|
||||
.Returns(collection);
|
||||
|
||||
var services = new Mock<IServiceProvider>();
|
||||
services
|
||||
.Setup(s => s.GetService(typeof(IActionDescriptorsCollectionProvider)))
|
||||
.Returns(actionDescriptorProvider.Object);
|
||||
|
||||
var routeOptions = new Mock<IOptionsAccessor<RouteOptions>>();
|
||||
routeOptions
|
||||
.SetupGet(o => o.Options)
|
||||
.Returns(new RouteOptions());
|
||||
|
||||
services
|
||||
.Setup(s => s.GetService(typeof(IInlineConstraintResolver)))
|
||||
.Returns(new DefaultInlineConstraintResolver(services.Object, routeOptions.Object));
|
||||
|
||||
return services.Object;
|
||||
}
|
||||
|
||||
private class DisplayNameActionDescriptor : ActionDescriptor
|
||||
{
|
||||
}
|
||||
|
||||
private class HomeController
|
||||
{
|
||||
public void Index() { }
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
|
@ -358,7 +358,7 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AttributeRoutedAction_LinkToAttribueRoutedController()
|
||||
public async Task AttributeRoutedAction_LinkToAttributeRoutedController()
|
||||
{
|
||||
// Arrange
|
||||
var server = TestServer.Create(_services, _app);
|
||||
|
|
@ -378,7 +378,7 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
|
|||
Assert.Equal("Employee", result.Controller);
|
||||
Assert.Equal("List", result.Action);
|
||||
|
||||
Assert.Equal("/Blog", result.Link);
|
||||
Assert.Equal("/Blog/ShowPosts", result.Link);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ using Microsoft.AspNet.Mvc;
|
|||
namespace RoutingWebSite.Admin
|
||||
{
|
||||
[Area("Admin")]
|
||||
[Route("{area}/Users")]
|
||||
[Route("[area]/Users")]
|
||||
public class UserManagementController : Controller
|
||||
{
|
||||
private readonly TestResponseGenerator _generator;
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ using Microsoft.AspNet.Mvc;
|
|||
namespace RoutingWebSite
|
||||
{
|
||||
// This controller contains actions mapped with a single controller-level route.
|
||||
[Route("Blog/{action=ShowPosts}/{postId?}")]
|
||||
[Route("Blog/[action]/{postId?}")]
|
||||
public class BlogController
|
||||
{
|
||||
private readonly TestResponseGenerator _generator;
|
||||
|
|
@ -18,7 +18,7 @@ namespace RoutingWebSite
|
|||
|
||||
public IActionResult ShowPosts()
|
||||
{
|
||||
return _generator.Generate("/Blog", "/Blog/ShowPosts");
|
||||
return _generator.Generate("/Blog/ShowPosts");
|
||||
}
|
||||
|
||||
public IActionResult Edit(int postId)
|
||||
|
|
|
|||
Loading…
Reference in New Issue