Part 2 of fix for #2248 - Support 'override' of attribute routes on action

methods

This change applies the 'override' semantic to attribute routes on virtual
methods.

A method override can either inherit attribute routes from a base
definition, or can replace them. It's not possible to inherit routes and
also add to them.
This commit is contained in:
Ryan Nowak 2015-05-07 12:11:22 -07:00
parent 46db71cfce
commit 8a476b56d8
2 changed files with 121 additions and 0 deletions

View File

@ -37,10 +37,60 @@ namespace Microsoft.AspNet.Mvc.ApplicationModels
return Enumerable.Empty<ActionModel>();
}
// For attribute routes on a action, we want want to support 'overriding' routes on a
// virtual method, but allow 'overriding'. So we need to walk up the hierarchy looking
// for the first definition to define routes.
//
// Then we want to 'filter' the set of attributes, so that only the effective routes apply.
var currentMethodInfo = methodInfo;
IRouteTemplateProvider[] routeAttributes = null;
while (true)
{
routeAttributes = currentMethodInfo
.GetCustomAttributes(inherit: false)
.OfType<IRouteTemplateProvider>()
.ToArray();
if (routeAttributes.Length > 0)
{
// Found 1 or more route attributes.
break;
}
// GetBaseDefinition returns 'this' when it gets to the bottom of the chain.
var nextMethodInfo = currentMethodInfo.GetBaseDefinition();
if (currentMethodInfo == nextMethodInfo)
{
break;
}
currentMethodInfo = nextMethodInfo;
}
// CoreCLR returns IEnumerable<Attribute> from GetCustomAttributes - the OfType<object>
// is needed to so that the result of ToArray() is object
var attributes = methodInfo.GetCustomAttributes(inherit: true).OfType<object>().ToArray();
// This is fairly complicated so that we maintain referential equality between items in
// ActionModel.Attributes and ActionModel.Attributes[*].Attribute.
var applicableAttributes = new List<object>();
foreach (var attribute in attributes)
{
if (attribute is IRouteTemplateProvider)
{
// This attribute is a route-attribute, leave it out.
}
else
{
applicableAttributes.Add(attribute);
}
}
applicableAttributes.AddRange(routeAttributes);
attributes = applicableAttributes.ToArray();
// Route attributes create multiple actions, we want to split the set of
// attributes based on these so each action only has the attributes that affect it.
//

View File

@ -706,6 +706,52 @@ namespace Microsoft.AspNet.Mvc.ApplicationModels
Assert.Equal<string>(new string[] { "GET" }, action.HttpMethods);
}
[Fact]
public void GetActions_InheritedAttributeRoutes()
{
// Arrange
var builder = CreateTestDefaultActionModelBuilder();
var typeInfo = typeof(DerivedClassInheritsAttributeRoutesController).GetTypeInfo();
var actionName = nameof(DerivedClassInheritsAttributeRoutesController.Edit);
// Act
var actions = builder.BuildActionModels(typeInfo, typeInfo.GetMethod(actionName));
// Assert
Assert.Equal(2, actions.Count());
var action = Assert.Single(actions, a => a.AttributeRouteModel?.Template == "A");
Assert.Equal(1, action.Attributes.Count);
Assert.Contains(action.AttributeRouteModel.Attribute, action.Attributes);
action = Assert.Single(actions, a => a.AttributeRouteModel?.Template == "B");
Assert.Equal(1, action.Attributes.Count);
Assert.Contains(action.AttributeRouteModel.Attribute, action.Attributes);
}
[Fact]
public void GetActions_InheritedAttributeRoutesOverridden()
{
// Arrange
var builder = CreateTestDefaultActionModelBuilder();
var typeInfo = typeof(DerivedClassOverridesAttributeRoutesController).GetTypeInfo();
var actionName = nameof(DerivedClassOverridesAttributeRoutesController.Edit);
// Act
var actions = builder.BuildActionModels(typeInfo, typeInfo.GetMethod(actionName));
// Assert
Assert.Equal(2, actions.Count());
var action = Assert.Single(actions, a => a.AttributeRouteModel?.Template == "C");
Assert.Equal(1, action.Attributes.Count);
Assert.Contains(action.AttributeRouteModel.Attribute, action.Attributes);
action = Assert.Single(actions, a => a.AttributeRouteModel?.Template == "D");
Assert.Equal(1, action.Attributes.Count);
Assert.Contains(action.AttributeRouteModel.Attribute, action.Attributes);
}
private static DefaultActionModelBuilder CreateTestDefaultActionModelBuilder(
AuthorizationOptions authOptions = null)
{
@ -731,6 +777,31 @@ namespace Microsoft.AspNet.Mvc.ApplicationModels
}
}
private class BaseClassWithAttributeRoutesController
{
[Route("A")]
[Route("B")]
public virtual void Edit()
{
}
}
private class DerivedClassInheritsAttributeRoutesController : BaseClassWithAttributeRoutesController
{
public override void Edit()
{
}
}
private class DerivedClassOverridesAttributeRoutesController : BaseClassWithAttributeRoutesController
{
[Route("C")]
[Route("D")]
public override void Edit()
{
}
}
private class BaseController : Controller
{
public void GetFromBase() // Valid action method.