diff --git a/src/Microsoft.AspNet.Mvc.Core/ApplicationModels/DefaultControllerModelBuilder.cs b/src/Microsoft.AspNet.Mvc.Core/ApplicationModels/DefaultControllerModelBuilder.cs index 230540f8b5..7a73d0d5c8 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ApplicationModels/DefaultControllerModelBuilder.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ApplicationModels/DefaultControllerModelBuilder.cs @@ -76,10 +76,57 @@ namespace Microsoft.AspNet.Mvc.ApplicationModels /// A for the given . protected virtual ControllerModel CreateControllerModel([NotNull] TypeInfo typeInfo) { + // For attribute routes on a controller, we want want to support 'overriding' routes on a derived + // class. So we need to walk up the hierarchy looking for the first class to define routes. + // + // Then we want to 'filter' the set of attributes, so that only the effective routes apply. + var currentTypeInfo = typeInfo; + var objectTypeInfo = typeof(object).GetTypeInfo(); + + IRouteTemplateProvider[] routeAttributes = null; + + do + { + routeAttributes = currentTypeInfo + .GetCustomAttributes(inherit: false) + .OfType() + .ToArray(); + + if (routeAttributes.Length > 0) + { + // Found 1 or more route attributes. + break; + } + + currentTypeInfo = currentTypeInfo.BaseType.GetTypeInfo(); + } + while (currentTypeInfo != objectTypeInfo); + // CoreCLR returns IEnumerable from GetCustomAttributes - the OfType // is needed to so that the result of ToArray() is object var attributes = typeInfo.GetCustomAttributes(inherit: true).OfType().ToArray(); + + // This is fairly complicated so that we maintain referential equality between items in + // ControllerModel.Attributes and ControllerModel.Attributes[*].Attribute. + var filteredAttributes = new List(); + foreach (var attribute in attributes) + { + if (attribute is IRouteTemplateProvider) + { + // This attribute is a route-attribute, leave it out. + } + else + { + filteredAttributes.Add(attribute); + } + } + filteredAttributes.AddRange(routeAttributes); + + attributes = filteredAttributes.ToArray(); + var controllerModel = new ControllerModel(typeInfo, attributes); + AddRange( + controllerModel.AttributeRoutes, routeAttributes.Select(a => new AttributeRouteModel(a))); controllerModel.ControllerName = typeInfo.Name.EndsWith("Controller", StringComparison.OrdinalIgnoreCase) ? @@ -108,10 +155,6 @@ namespace Microsoft.AspNet.Mvc.ApplicationModels controllerModel.Filters.Add(new AuthorizeFilter(policy)); } - AddRange( - controllerModel.AttributeRoutes, - attributes.OfType().Select(rtp => new AttributeRouteModel(rtp))); - var apiVisibility = attributes.OfType().FirstOrDefault(); if (apiVisibility != null) { diff --git a/src/Microsoft.AspNet.Mvc.Core/ControllerActionDescriptorBuilder.cs b/src/Microsoft.AspNet.Mvc.Core/ControllerActionDescriptorBuilder.cs index e630bbacd7..ab2e5b0d3f 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ControllerActionDescriptorBuilder.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ControllerActionDescriptorBuilder.cs @@ -11,7 +11,6 @@ using Microsoft.AspNet.Mvc.ApiExplorer; using Microsoft.AspNet.Mvc.ApplicationModels; using Microsoft.AspNet.Mvc.Core; using Microsoft.AspNet.Mvc.Routing; -using Microsoft.AspNet.Mvc.ModelBinding; namespace Microsoft.AspNet.Mvc { diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ApplicationModel/DefaultControllerModelBuilderTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ApplicationModel/DefaultControllerModelBuilderTest.cs index 29c59cef09..4166aba409 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/ApplicationModel/DefaultControllerModelBuilderTest.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ApplicationModel/DefaultControllerModelBuilderTest.cs @@ -152,6 +152,64 @@ namespace Microsoft.AspNet.Mvc.ApplicationModels Assert.Empty(model.Filters); } + [Fact] + public void BuildControllerModel_ClassWithInheritedRoutes() + { + // Arrange + var builder = new DefaultControllerModelBuilder(new DefaultActionModelBuilder(null), null); + var typeInfo = typeof(DerivedClassInheritingRoutesController).GetTypeInfo(); + + // Act + var model = builder.BuildControllerModel(typeInfo); + + // Assert + Assert.Equal(2, model.AttributeRoutes.Count); + Assert.Equal(2, model.Attributes.Count); + + var route = Assert.Single(model.AttributeRoutes, r => r.Template == "A"); + Assert.Contains(route.Attribute, model.Attributes); + + route = Assert.Single(model.AttributeRoutes, r => r.Template == "B"); + Assert.Contains(route.Attribute, model.Attributes); + } + + [Fact] + public void BuildControllerModel_ClassWithHiddenInheritedRoutes() + { + // Arrange + var builder = new DefaultControllerModelBuilder(new DefaultActionModelBuilder(null), null); + var typeInfo = typeof(DerivedClassHidingRoutesController).GetTypeInfo(); + + // Act + var model = builder.BuildControllerModel(typeInfo); + + // Assert + Assert.Equal(2, model.AttributeRoutes.Count); + Assert.Equal(2, model.Attributes.Count); + + var route = Assert.Single(model.AttributeRoutes, r => r.Template == "C"); + Assert.Contains(route.Attribute, model.Attributes); + + route = Assert.Single(model.AttributeRoutes, r => r.Template == "D"); + Assert.Contains(route.Attribute, model.Attributes); + } + + [Route("A")] + [Route("B")] + private class BaseClassWithRoutesController + { + } + + private class DerivedClassInheritingRoutesController : BaseClassWithRoutesController + { + } + + [Route("C")] + [Route("D")] + private class DerivedClassHidingRoutesController : BaseClassWithRoutesController + { + } + private class StoreController : Mvc.Controller { } diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ControllerActionDescriptorProviderTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ControllerActionDescriptorProviderTests.cs index c4df19f262..f9f6cdf502 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/ControllerActionDescriptorProviderTests.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ControllerActionDescriptorProviderTests.cs @@ -1391,6 +1391,7 @@ namespace Microsoft.AspNet.Mvc.Test { // Arrange var actionName = nameof(MultipleRouteProviderOnActionAndControllerController.Delete); + var provider = GetProvider(typeof(MultipleRouteProviderOnActionAndControllerController).GetTypeInfo()); // Act diff --git a/test/WebSites/RoutingWebSite/RoutingWebSite.xproj b/test/WebSites/RoutingWebSite/RoutingWebSite.xproj index ee01dc4ba4..a71d233141 100644 --- a/test/WebSites/RoutingWebSite/RoutingWebSite.xproj +++ b/test/WebSites/RoutingWebSite/RoutingWebSite.xproj @@ -10,6 +10,12 @@ ..\..\..\artifacts\obj\$(MSBuildProjectName) ..\..\..\artifacts\bin\$(MSBuildProjectName)\ + + RoutingWebSite + + + RoutingWebSite + 2.0 49632