diff --git a/src/Microsoft.AspNet.Mvc.Core/ApplicationModels/ActionModel.cs b/src/Microsoft.AspNet.Mvc.Core/ApplicationModels/ActionModel.cs index 077b868482..04322a9c27 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ApplicationModels/ActionModel.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ApplicationModels/ActionModel.cs @@ -20,6 +20,7 @@ namespace Microsoft.AspNet.Mvc.ApplicationModels Filters = new List(); HttpMethods = new List(); Parameters = new List(); + RouteConstraints = new List(); } public ActionModel([NotNull] ActionModel other) @@ -40,6 +41,7 @@ namespace Microsoft.AspNet.Mvc.ApplicationModels // Make a deep copy of other 'model' types. ApiExplorer = new ApiExplorerModel(other.ApiExplorer); Parameters = new List(other.Parameters.Select(p => new ParameterModel(p))); + RouteConstraints = new List(other.RouteConstraints); if (other.AttributeRouteModel != null) { @@ -62,6 +64,8 @@ namespace Microsoft.AspNet.Mvc.ApplicationModels /// public ApiExplorerModel ApiExplorer { get; set; } + public AttributeRouteModel AttributeRouteModel { get; set; } + public IReadOnlyList Attributes { get; } public ControllerModel Controller { get; set; } @@ -74,6 +78,6 @@ namespace Microsoft.AspNet.Mvc.ApplicationModels public List Parameters { get; private set; } - public AttributeRouteModel AttributeRouteModel { get; set; } + public List RouteConstraints { get; private set; } } } diff --git a/src/Microsoft.AspNet.Mvc.Core/ApplicationModels/AttributeRouteModel.cs b/src/Microsoft.AspNet.Mvc.Core/ApplicationModels/AttributeRouteModel.cs index 1130ecac2c..199ad7f8a2 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ApplicationModels/AttributeRouteModel.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ApplicationModels/AttributeRouteModel.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Text; using Microsoft.AspNet.Mvc.Core; using Microsoft.AspNet.Mvc.Routing; @@ -336,7 +337,7 @@ namespace Microsoft.AspNet.Mvc.ApplicationModels var message = Resources.FormatAttributeRoute_TokenReplacement_ReplacementValueNotFound( template, token, - string.Join(", ", values.Keys)); + string.Join(", ", values.Keys.OrderBy(k => k, StringComparer.OrdinalIgnoreCase))); throw new InvalidOperationException(message); } diff --git a/src/Microsoft.AspNet.Mvc.Core/ApplicationModels/ControllerModel.cs b/src/Microsoft.AspNet.Mvc.Core/ApplicationModels/ControllerModel.cs index 247a8b1108..9b5ee6c2dd 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ApplicationModels/ControllerModel.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ApplicationModels/ControllerModel.cs @@ -20,7 +20,7 @@ namespace Microsoft.AspNet.Mvc.ApplicationModels AttributeRoutes = new List(); ActionConstraints = new List(); Filters = new List(); - RouteConstraints = new List(); + RouteConstraints = new List(); } public ControllerModel([NotNull] ControllerModel other) @@ -35,7 +35,7 @@ namespace Microsoft.AspNet.Mvc.ApplicationModels ActionConstraints = new List(other.ActionConstraints); Attributes = new List(other.Attributes); Filters = new List(other.Filters); - RouteConstraints = new List(other.RouteConstraints); + RouteConstraints = new List(other.RouteConstraints); // Make a deep copy of other 'model' types. Actions = new List(other.Actions.Select(a => new ActionModel(a))); @@ -65,6 +65,6 @@ namespace Microsoft.AspNet.Mvc.ApplicationModels public List Filters { get; private set; } - public List RouteConstraints { get; private set; } + public List RouteConstraints { get; private set; } } } diff --git a/src/Microsoft.AspNet.Mvc.Core/ApplicationModels/DefaultActionModelBuilder.cs b/src/Microsoft.AspNet.Mvc.Core/ApplicationModels/DefaultActionModelBuilder.cs index eb6c11ab56..5115940742 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ApplicationModels/DefaultActionModelBuilder.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ApplicationModels/DefaultActionModelBuilder.cs @@ -289,6 +289,8 @@ namespace Microsoft.AspNet.Mvc.ApplicationModels .SelectMany(a => a.HttpMethods) .Distinct()); + actionModel.RouteConstraints.AddRange(attributes.OfType()); + var routeTemplateProvider = attributes .OfType() diff --git a/src/Microsoft.AspNet.Mvc.Core/ApplicationModels/DefaultControllerModelBuilder.cs b/src/Microsoft.AspNet.Mvc.Core/ApplicationModels/DefaultControllerModelBuilder.cs index 0e1dc06014..b8fa51f4d5 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ApplicationModels/DefaultControllerModelBuilder.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ApplicationModels/DefaultControllerModelBuilder.cs @@ -122,7 +122,7 @@ namespace Microsoft.AspNet.Mvc.ApplicationModels controllerModel.ActionConstraints.AddRange(attributes.OfType()); controllerModel.Filters.AddRange(attributes.OfType()); - controllerModel.RouteConstraints.AddRange(attributes.OfType()); + controllerModel.RouteConstraints.AddRange(attributes.OfType()); controllerModel.AttributeRoutes.AddRange( attributes.OfType().Select(rtp => new AttributeRouteModel(rtp))); @@ -142,4 +142,4 @@ namespace Microsoft.AspNet.Mvc.ApplicationModels return controllerModel; } } -} \ No newline at end of file +} diff --git a/src/Microsoft.AspNet.Mvc.Core/ControllerActionDescriptorBuilder.cs b/src/Microsoft.AspNet.Mvc.Core/ControllerActionDescriptorBuilder.cs index 0cff9910c2..aff348f9fa 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ControllerActionDescriptorBuilder.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ControllerActionDescriptorBuilder.cs @@ -56,11 +56,7 @@ namespace Microsoft.AspNet.Mvc actionDescriptor.ControllerTypeInfo = controller.ControllerType; AddApiExplorerInfo(actionDescriptor, action, controller); - AddRouteConstraints(actionDescriptor, controller, action); - AddControllerRouteConstraints( - actionDescriptor, - controller.RouteConstraints, - removalConstraints); + AddRouteConstraints(removalConstraints, actionDescriptor, controller, action); if (IsAttributeRoutedAction(actionDescriptor)) { @@ -371,39 +367,17 @@ namespace Microsoft.AspNet.Mvc } public static void AddRouteConstraints( + ISet removalConstraints, ControllerActionDescriptor actionDescriptor, ControllerModel controller, ActionModel action) { - actionDescriptor.RouteConstraints.Add(new RouteDataActionConstraint( - "controller", - controller.ControllerName)); - - if (action.IsActionNameMatchRequired) - { - actionDescriptor.RouteConstraints.Add(new RouteDataActionConstraint( - "action", - action.ActionName)); - } - else - { - actionDescriptor.RouteConstraints.Add(new RouteDataActionConstraint( - "action", - string.Empty)); - } - } - - private static void AddControllerRouteConstraints( - ControllerActionDescriptor actionDescriptor, - IList routeconstraints, - ISet removalConstraints) - { - // Apply all the constraints defined on the controller (for example, [Area]) to the actions - // in that controller. Also keep track of all the constraints that require preventing actions + // Apply all the constraints defined on the action, then controller (for example, [Area]) + // to the actions. Also keep track of all the constraints that require preventing actions // without the constraint to match. For example, actions without an [Area] attribute on their // controller should not match when a value has been given for area when matching a url or // generating a link. - foreach (var constraintAttribute in routeconstraints) + foreach (var constraintAttribute in action.RouteConstraints) { if (constraintAttribute.BlockNonAttributedActions) { @@ -427,6 +401,55 @@ namespace Microsoft.AspNet.Mvc } } } + + foreach (var constraintAttribute in controller.RouteConstraints) + { + if (constraintAttribute.BlockNonAttributedActions) + { + removalConstraints.Add(constraintAttribute.RouteKey); + } + + // Skip duplicates - this also means that a value on the action will take precedence + if (!HasConstraint(actionDescriptor.RouteConstraints, constraintAttribute.RouteKey)) + { + if (constraintAttribute.RouteKeyHandling == RouteKeyHandling.CatchAll) + { + actionDescriptor.RouteConstraints.Add( + RouteDataActionConstraint.CreateCatchAll( + constraintAttribute.RouteKey)); + } + else + { + actionDescriptor.RouteConstraints.Add(new RouteDataActionConstraint( + constraintAttribute.RouteKey, + constraintAttribute.RouteValue)); + } + } + } + + // Lastly add the 'default' values + if (!HasConstraint(actionDescriptor.RouteConstraints, "action")) + { + if (action.IsActionNameMatchRequired) + { + actionDescriptor.RouteConstraints.Add(new RouteDataActionConstraint( + "action", + action.ActionName)); + } + else + { + actionDescriptor.RouteConstraints.Add(new RouteDataActionConstraint( + "action", + string.Empty)); + } + } + + if (!HasConstraint(actionDescriptor.RouteConstraints, "controller")) + { + actionDescriptor.RouteConstraints.Add(new RouteDataActionConstraint( + "controller", + controller.ControllerName)); + } } private static bool HasConstraint(List constraints, string routeKey) diff --git a/src/Microsoft.AspNet.Mvc.Core/IRouteConstraintProvider.cs b/src/Microsoft.AspNet.Mvc.Core/IRouteConstraintProvider.cs new file mode 100644 index 0000000000..4a72ce23f4 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/IRouteConstraintProvider.cs @@ -0,0 +1,33 @@ +// 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. + +namespace Microsoft.AspNet.Mvc +{ + /// + /// An interface for metadata which provides values + /// for a controller or action. + /// + public interface IRouteConstraintProvider + { + /// + /// The route value key. + /// + string RouteKey { get; } + + /// + /// The . + /// + RouteKeyHandling RouteKeyHandling { get; } + + /// + /// The expected route value. Will be null unless is + /// set to . + /// + string RouteValue { get; } + + /// + /// Set to true to negate this constraint on all actions that do not define a behavior for this route key. + /// + bool BlockNonAttributedActions { get; } + } +} diff --git a/src/Microsoft.AspNet.Mvc.Core/Logging/ActionModelValues.cs b/src/Microsoft.AspNet.Mvc.Core/Logging/ActionModelValues.cs index a4c5344f01..661a2cab65 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Logging/ActionModelValues.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Logging/ActionModelValues.cs @@ -24,6 +24,8 @@ namespace Microsoft.AspNet.Mvc.Logging ActionMethod = inner.ActionMethod; ApiExplorer = new ApiExplorerModelValues(inner.ApiExplorer); Parameters = inner.Parameters.Select(p => new ParameterModelValues(p)).ToList(); + RouteConstraints = inner.RouteConstraints.Select( + r => new RouteConstraintProviderValues(r)).ToList(); Filters = inner.Filters.Select(f => new FilterValues(f)).ToList(); if (inner.AttributeRouteModel != null) { @@ -60,6 +62,12 @@ namespace Microsoft.AspNet.Mvc.Logging /// See . /// public List Filters { get; } + + /// + /// The route constraints on the controller as . + /// See . + /// + public List RouteConstraints { get; set; } /// /// The attribute route model of the action as . diff --git a/src/Microsoft.AspNet.Mvc.Core/Logging/ControllerModelValues.cs b/src/Microsoft.AspNet.Mvc.Core/Logging/ControllerModelValues.cs index 7682934bb3..3ca2d23014 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Logging/ControllerModelValues.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Logging/ControllerModelValues.cs @@ -26,7 +26,7 @@ namespace Microsoft.AspNet.Mvc.Logging Filters = inner.Filters.Select(f => new FilterValues(f)).ToList(); ActionConstraints = inner.ActionConstraints?.Select(a => new ActionConstraintValues(a))?.ToList(); RouteConstraints = inner.RouteConstraints.Select( - r => new RouteConstraintAttributeValues(r)).ToList(); + r => new RouteConstraintProviderValues(r)).ToList(); AttributeRoutes = inner.AttributeRoutes.Select( a => new AttributeRouteModelValues(a)).ToList(); } @@ -72,10 +72,10 @@ namespace Microsoft.AspNet.Mvc.Logging public List ActionConstraints { get; } /// - /// The route constraints on the controller as . + /// The route constraints on the controller as . /// See . /// - public List RouteConstraints { get; set; } + public List RouteConstraints { get; set; } /// /// The attribute routes on the controller as . diff --git a/src/Microsoft.AspNet.Mvc.Core/Logging/RouteConstraintAttributeValues.cs b/src/Microsoft.AspNet.Mvc.Core/Logging/RouteConstraintProviderValues.cs similarity index 64% rename from src/Microsoft.AspNet.Mvc.Core/Logging/RouteConstraintAttributeValues.cs rename to src/Microsoft.AspNet.Mvc.Core/Logging/RouteConstraintProviderValues.cs index 75638d97b8..35c6a2c218 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Logging/RouteConstraintAttributeValues.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Logging/RouteConstraintProviderValues.cs @@ -6,12 +6,12 @@ using Microsoft.Framework.Logging; namespace Microsoft.AspNet.Mvc.Logging { /// - /// Logging representation of a . Logged as a substructure of + /// Logging representation of a . Logged as a substructure of /// /// - public class RouteConstraintAttributeValues : LoggerStructureBase + public class RouteConstraintProviderValues : LoggerStructureBase { - public RouteConstraintAttributeValues([NotNull] RouteConstraintAttribute inner) + public RouteConstraintProviderValues([NotNull] IRouteConstraintProvider inner) { RouteKey = inner.RouteKey; RouteValue = inner.RouteValue; @@ -20,22 +20,22 @@ namespace Microsoft.AspNet.Mvc.Logging } /// - /// The route value key. See . + /// The route value key. See . /// public string RouteKey { get; } /// - /// The expected route value. See . + /// The expected route value. See . /// public string RouteValue { get; } /// - /// The . See . + /// The . See . /// public RouteKeyHandling RouteKeyHandling { get; } /// - /// See . + /// See . /// public bool BlockNonAttributedActions { get; } @@ -44,4 +44,4 @@ namespace Microsoft.AspNet.Mvc.Logging return LogFormatter.FormatStructure(this); } } -} \ No newline at end of file +} diff --git a/src/Microsoft.AspNet.Mvc.Core/RouteConstraintAttribute.cs b/src/Microsoft.AspNet.Mvc.Core/RouteConstraintAttribute.cs index 99bd6dd016..3fad1d8553 100644 --- a/src/Microsoft.AspNet.Mvc.Core/RouteConstraintAttribute.cs +++ b/src/Microsoft.AspNet.Mvc.Core/RouteConstraintAttribute.cs @@ -15,11 +15,9 @@ namespace Microsoft.AspNet.Mvc /// /// When placed on a controller, unless overridden by the action, the constraint applies to all /// actions defined by the controller. - /// - /// /// [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)] - public abstract class RouteConstraintAttribute : Attribute + public abstract class RouteConstraintAttribute : Attribute, IRouteConstraintProvider { /// /// Creates a new . @@ -65,25 +63,16 @@ namespace Microsoft.AspNet.Mvc BlockNonAttributedActions = blockNonAttributedActions; } - /// - /// The route value key. - /// + /// public string RouteKey { get; private set; } - /// - /// The . - /// + /// public RouteKeyHandling RouteKeyHandling { get; private set; } - /// - /// The expected route value. Will be null unless is - /// set to . - /// + /// public string RouteValue { get; private set; } - /// - /// Set to true to negate this constraint on all actions that do not define a behavior for this route key. - /// + /// public bool BlockNonAttributedActions { get; private set; } } } diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ApplicationModel/ActionModelTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ApplicationModel/ActionModelTest.cs index bfdfc80290..3bafa0d5b9 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/ApplicationModel/ActionModelTest.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ApplicationModel/ActionModelTest.cs @@ -53,6 +53,7 @@ namespace Microsoft.AspNet.Mvc.ApplicationModels action.Filters.Add(new AuthorizeAttribute()); action.HttpMethods.Add("GET"); action.IsActionNameMatchRequired = true; + action.RouteConstraints.Add(new AreaAttribute("Admin")); // Act var action2 = new ActionModel(action); diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ApplicationModel/AttributeRouteModelTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ApplicationModel/AttributeRouteModelTests.cs index 233155c919..85f179b53a 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/ApplicationModel/AttributeRouteModelTests.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ApplicationModel/AttributeRouteModelTests.cs @@ -200,7 +200,7 @@ namespace Microsoft.AspNet.Mvc.ApplicationModels var expected = "While processing template '[area]/[controller]/[action2]', " + "a replacement value for the token 'action2' could not be found. " + - "Available tokens: 'area, controller, action'."; + "Available tokens: 'action, area, controller'."; // Act var ex = Assert.Throws( diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ControllerActionDescriptorProviderTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ControllerActionDescriptorProviderTests.cs index 6e9034a301..8e151af5ec 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/ControllerActionDescriptorProviderTests.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ControllerActionDescriptorProviderTests.cs @@ -547,7 +547,7 @@ namespace Microsoft.AspNet.Mvc.Test "For action: 'Microsoft.AspNet.Mvc.Test.ControllerActionDescriptorProviderTests+" + "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 + + "could not be found. Available tokens: 'action, controller'." + Environment.NewLine + Environment.NewLine + "Error 2:" + Environment.NewLine + "For action: 'Microsoft.AspNet.Mvc.Test.ControllerActionDescriptorProviderTests+" + diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/RouteConstraintAttributeValuesTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/RouteConstraintProviderValuesTest.cs similarity index 73% rename from test/Microsoft.AspNet.Mvc.Core.Test/RouteConstraintAttributeValuesTest.cs rename to test/Microsoft.AspNet.Mvc.Core.Test/RouteConstraintProviderValuesTest.cs index a184379dcf..a0c28a100c 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/RouteConstraintAttributeValuesTest.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/RouteConstraintProviderValuesTest.cs @@ -5,10 +5,10 @@ using Xunit; namespace Microsoft.AspNet.Mvc.Logging { - public class RouteConstraintAttributeValuesTest + public class RouteConstraintProviderValuesTest { [Fact] - public void RouteConstraintAttributeValues_IncludesAllProperties() + public void RouteConstraintProviderValues_IncludesAllProperties() { // Arrange var exclude = new[] { "TypeId" }; @@ -16,7 +16,7 @@ namespace Microsoft.AspNet.Mvc.Logging // Assert PropertiesAssert.PropertiesAreTheSame( typeof(RouteConstraintAttribute), - typeof(RouteConstraintAttributeValues), + typeof(RouteConstraintProviderValues), exclude); } }