Skip to content

Commit

Permalink
[Fixes aspnet#734] Attribute Routing: Implement Name
Browse files Browse the repository at this point in the history
1. Added support for Name in attribute routing. Name can be defined using [RouteAttribute]
and the different Http*Attributes, for example [HttpGet].

2. Names defined on actions always override names defined on the controller.

3. Actions with a non empty template don't inherit the name from the controller. The name
   is only inherited from the controller when the action template is null or empty.

4. Multiple attribute routes with different templates and the same name are not allowed.
  • Loading branch information
javiercn committed Aug 30, 2014
1 parent a931e21 commit ccc20a3
Show file tree
Hide file tree
Showing 21 changed files with 1,020 additions and 24 deletions.
3 changes: 3 additions & 0 deletions src/Microsoft.AspNet.Mvc.Core/HttpMethodAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,5 +70,8 @@ public int Order
return _order;
}
}

/// <inheritdoc />
public string Name { get; set; }
}
}
68 changes: 66 additions & 2 deletions src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,8 @@ public List<ReflectedActionDescriptor> Build(ReflectedApplicationModel model)
var attributeRouteInfo = combinedRoute == null ? null : new AttributeRouteInfo()
{
Template = combinedRoute.Template,
Order = combinedRoute.Order ?? DefaultAttributeRouteOrder
Order = combinedRoute.Order ?? DefaultAttributeRouteOrder,
Name = combinedRoute.Name,
};

var actionDescriptor = new ReflectedActionDescriptor()
Expand Down Expand Up @@ -291,6 +292,9 @@ public List<ReflectedActionDescriptor> Build(ReflectedApplicationModel model)
}
}

var actionsByRouteName = new Dictionary<string, IList<ActionDescriptor>>(
StringComparer.OrdinalIgnoreCase);

foreach (var actionDescriptor in actions)
{
if (actionDescriptor.AttributeRouteInfo == null ||
Expand All @@ -317,6 +321,25 @@ public List<ReflectedActionDescriptor> Build(ReflectedApplicationModel model)
}
else
{
var attributeRouteInfo = actionDescriptor.AttributeRouteInfo;
if (attributeRouteInfo.Name != null)
{
// Build a map of attribute route name to action descriptors to ensure that all
// attribute routes with a given name have the same template.
IList<ActionDescriptor> namedActionGroup;

if (actionsByRouteName.TryGetValue(attributeRouteInfo.Name, out namedActionGroup))
{
namedActionGroup.Add(actionDescriptor);
}
else
{
namedActionGroup = new List<ActionDescriptor>();
namedActionGroup.Add(actionDescriptor);
actionsByRouteName.Add(attributeRouteInfo.Name, namedActionGroup);
}
}

// We still want to add a 'null' for any constraint with DenyKey so that link generation
// works properly.
//
Expand All @@ -332,17 +355,86 @@ public List<ReflectedActionDescriptor> Build(ReflectedApplicationModel model)
}
}

var namedRoutedErrors = ValidateNamedAttributeRoutedActions(actionsByRouteName);
if (namedRoutedErrors.Any())
{
namedRoutedErrors = AddErrorNumbers(namedRoutedErrors);

var message = Resources.FormatAttributeRoute_AggregateErrorMessage(
Environment.NewLine,
string.Join(Environment.NewLine + Environment.NewLine, namedRoutedErrors));

throw new InvalidOperationException(message);
}

if (routeTemplateErrors.Any())
{
var message = Resources.FormatAttributeRoute_AggregateErrorMessage(
Environment.NewLine,
string.Join(Environment.NewLine + Environment.NewLine, routeTemplateErrors));

throw new InvalidOperationException(message);
}

return actions;
}

private static IList<string> AddErrorNumbers(IList<string> namedRoutedErrors)
{
return namedRoutedErrors
.Select((nre, i) =>
Resources.FormatAttributeRoute_AggregateErrorMessage_ErrorNumber(
i + 1,
Environment.NewLine,
nre))
.ToList();
}

private static IList<string> ValidateNamedAttributeRoutedActions(
IDictionary<string,
IList<ActionDescriptor>> actionsGroupedByRouteName)
{
var namedRouteErrors = new List<string>();

foreach (var kvp in actionsGroupedByRouteName)
{
// We are looking for attribute routed actions that have the same name but
// different route templates. We pick the first template of the group and
// we compare it against the rest of the templates that have that same name
// associated.
// The moment we find one that is different we report the whole group to the
// user in the error message so that he can see the different actions and the
// different templates for a given named attribute route.
var firstActionDescriptor = kvp.Value[0];
var firstTemplate = firstActionDescriptor.AttributeRouteInfo.Template;

for (var i = 1; i < kvp.Value.Count; i++)
{
var otherActionDescriptor = kvp.Value[i];
var otherActionTemplate = otherActionDescriptor.AttributeRouteInfo.Template;

if (!firstTemplate.Equals(otherActionTemplate, StringComparison.OrdinalIgnoreCase))
{
var descriptions = kvp.Value.Select(ad =>
Resources.FormatAttributeRoute_DuplicateNames_Item(
ad.DisplayName,
ad.AttributeRouteInfo.Template));

var errorDescription = string.Join(Environment.NewLine, descriptions);
var message = Resources.FormatAttributeRoute_DuplicateNames(
kvp.Key,
Environment.NewLine,
errorDescription);

namedRouteErrors.Add(message);
break;
}
}
}

return namedRouteErrors;
}

private static string GetRouteGroupValue(int order, string template)
{
var group = string.Format("{0}-{1}", order, template);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,15 @@ public ReflectedAttributeRouteModel([NotNull] IRouteTemplateProvider templatePro
{
Template = templateProvider.Template;
Order = templateProvider.Order;
Name = templateProvider.Name;
}

public string Template { get; set; }

public int? Order { get; set; }

public string Name { get; set; }

/// <summary>
/// Combines two <see cref="ReflectedAttributeRouteModel"/> instances and returns
/// a new <see cref="ReflectedAttributeRouteModel"/> instance with the result.
Expand Down Expand Up @@ -60,10 +63,25 @@ public static ReflectedAttributeRouteModel CombineReflectedAttributeRouteModel(
return new ReflectedAttributeRouteModel()
{
Template = combinedTemplate,
Order = right.Order ?? left.Order
Order = right.Order ?? left.Order,
Name = ChooseName(left, right),
};
}

private static string ChooseName(
ReflectedAttributeRouteModel left,
ReflectedAttributeRouteModel right)
{
if (right.Name == null && string.IsNullOrEmpty(right.Template))
{
return left.Name;
}
else
{
return right.Name;
}
}

internal static string CombineTemplates(string left, string right)
{
var result = CombineCore(left, right);
Expand Down Expand Up @@ -252,7 +270,7 @@ public static string ReplaceTokens(string template, IDictionary<string, object>
{
// This is an unclosed replacement token
var message = Resources.FormatAttributeRoute_TokenReplacement_InvalidSyntax(
template,
template,
Resources.AttributeRoute_TokenReplacement_UnclosedToken);
throw new InvalidOperationException(message);
}
Expand All @@ -272,7 +290,7 @@ public static string ReplaceTokens(string template, IDictionary<string, object>
{
// Unescaped left-bracket is not allowed inside a token.
var message = Resources.FormatAttributeRoute_TokenReplacement_InvalidSyntax(
template,
template,
Resources.AttributeRoute_TokenReplacement_UnescapedBraceInToken);
throw new InvalidOperationException(message);
}
Expand Down Expand Up @@ -303,7 +321,7 @@ public static string ReplaceTokens(string template, IDictionary<string, object>
}

builder.Append(value);

if (c == '[')
{
state = TemplateParserState.SeenLeft;
Expand Down
15 changes: 15 additions & 0 deletions src/Microsoft.AspNet.Mvc.Core/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -360,4 +360,19 @@
<data name="UnableToFindServices" xml:space="preserve">
<value>Unable to find the required services. Please add all the required services by calling '{0}' inside the call to '{1}' or '{2}' in the application startup code.</value>
</data>
<data name="AttributeRoute_DifferentLinkGenerationEntries_SameName" xml:space="preserve">
<value>Two or more routes named '{0}' have different templates.</value>
</data>
<data name="AttributeRoute_DuplicateNames_Item" xml:space="preserve">
<value>Action: '{0}' - Template: '{1}'</value>
<comment>Formats an action descriptor display name and it's associated template.</comment>
</data>
<data name="AttributeRoute_DuplicateNames" xml:space="preserve">
<value>Attribute routes with the same name '{0}' must have the same template:{1}{2}</value>
<comment>{0} is the name of the attribute route, {1} is the newline, {2} is the list of errors formatted using ActionDescriptor_WithNamedAttributeRouteAndDifferentTemplate</comment>
</data>
<data name="AttributeRoute_AggregateErrorMessage_ErrorNumber" xml:space="preserve">
<value>Error {0}:{1}{2}</value>
<comment>{0} is the error number, {1} is Environment.NewLine {2} is the error message</comment>
</data>
</root>
3 changes: 3 additions & 0 deletions src/Microsoft.AspNet.Mvc.Core/RouteAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,8 @@ public int Order
return _order;
}
}

/// <inheritdoc />
public string Name { get; set; }
}
}
Loading

0 comments on commit ccc20a3

Please sign in to comment.