From d8995a77671eab20de0499a483edbe1d661045da Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Fri, 26 Sep 2014 16:18:42 -0700 Subject: [PATCH] Fix for #1192 - Support customizing reflected model through attributes This adds support for attributes which interact with reflected model. These conventions are applied after all of our built-in constructs so that you can see and modify the results. --- Mvc.sln | 13 +++ .../AutoGenerateRouteNamesAttribute.cs | 31 ++++++ samples/MvcSample.Web/SimpleRest.cs | 9 ++ .../ControllerDescriptor.cs | 18 +--- .../ReflectedActionDescriptorProvider.cs | 99 +++++++++++++++---- .../IReflectedActionModelConvention.cs | 25 +++++ .../IReflectedApplicationModelConvention.cs | 16 ++- .../IReflectedControllerModelConvention.cs | 25 +++++ .../IReflectedParameterModelConvention.cs | 24 +++++ .../ReflectedActionModel.cs | 2 + .../ReflectedControllerModel.cs | 2 + .../ReflectedParameterModel.cs | 2 + .../ActionAttributeTests.cs | 3 +- ...iscoveryConventionsActionSelectionTests.cs | 3 +- .../ReflectedActionDescriptorProviderTests.cs | 86 ++++++++++++++-- .../ReflectedModelTest.cs | 70 +++++++++++++ .../project.json | 1 + ...ApiExplorerVisibilityDisabledConvention.cs | 2 +- .../ApiExplorerVisibilityEnabledConvention.cs | 2 +- test/WebSites/AutofacWebSite/Startup.cs | 4 +- test/WebSites/AutofacWebSite/project.json | 5 +- .../ReflectedActionModelController.cs | 37 +++++++ .../ReflectedControllerModelController.cs | 37 +++++++ .../ReflectedParameterModelController.cs | 28 ++++++ .../ReflectedModelWebSite.kproj | 24 +++++ .../WebSites/ReflectedModelWebSite/Startup.cs | 23 +++++ .../ReflectedModelWebSite/project.json | 10 ++ 27 files changed, 549 insertions(+), 52 deletions(-) create mode 100644 samples/MvcSample.Web/AutoGenerateRouteNamesAttribute.cs create mode 100644 src/Microsoft.AspNet.Mvc.Core/ReflectedModelBuilder/IReflectedActionModelConvention.cs create mode 100644 src/Microsoft.AspNet.Mvc.Core/ReflectedModelBuilder/IReflectedControllerModelConvention.cs create mode 100644 src/Microsoft.AspNet.Mvc.Core/ReflectedModelBuilder/IReflectedParameterModelConvention.cs create mode 100644 test/Microsoft.AspNet.Mvc.FunctionalTests/ReflectedModelTest.cs create mode 100644 test/WebSites/ReflectedModelWebSite/Controllers/ReflectedActionModelController.cs create mode 100644 test/WebSites/ReflectedModelWebSite/Controllers/ReflectedControllerModelController.cs create mode 100644 test/WebSites/ReflectedModelWebSite/Controllers/ReflectedParameterModelController.cs create mode 100644 test/WebSites/ReflectedModelWebSite/ReflectedModelWebSite.kproj create mode 100644 test/WebSites/ReflectedModelWebSite/Startup.cs create mode 100644 test/WebSites/ReflectedModelWebSite/project.json diff --git a/Mvc.sln b/Mvc.sln index a00d847532..e13f657391 100644 --- a/Mvc.sln +++ b/Mvc.sln @@ -82,6 +82,8 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "UrlHelperWebSite", "test\We EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "ApiExplorerWebSite", "test\WebSites\ApiExplorerWebSite\ApiExplorerWebSite.kproj", "{61061528-071E-424E-965A-07BCC2F02672}" EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "ReflectedModelWebSite", "test\WebSites\ReflectedModelWebSite\ReflectedModelWebSite.kproj", "{C2EF54F8-8886-4260-A322-44F76245F95D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -422,6 +424,16 @@ Global {61061528-071E-424E-965A-07BCC2F02672}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {61061528-071E-424E-965A-07BCC2F02672}.Release|Mixed Platforms.Build.0 = Release|Any CPU {61061528-071E-424E-965A-07BCC2F02672}.Release|x86.ActiveCfg = Release|Any CPU + {C2EF54F8-8886-4260-A322-44F76245F95D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C2EF54F8-8886-4260-A322-44F76245F95D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C2EF54F8-8886-4260-A322-44F76245F95D}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {C2EF54F8-8886-4260-A322-44F76245F95D}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {C2EF54F8-8886-4260-A322-44F76245F95D}.Debug|x86.ActiveCfg = Debug|Any CPU + {C2EF54F8-8886-4260-A322-44F76245F95D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C2EF54F8-8886-4260-A322-44F76245F95D}.Release|Any CPU.Build.0 = Release|Any CPU + {C2EF54F8-8886-4260-A322-44F76245F95D}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {C2EF54F8-8886-4260-A322-44F76245F95D}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {C2EF54F8-8886-4260-A322-44F76245F95D}.Release|x86.ActiveCfg = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -461,5 +473,6 @@ Global {96107AC0-18E2-474D-BAB4-2FFF2185FBCD} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C} {A192E504-2881-41DC-90D1-B7F1DD1134E8} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C} {61061528-071E-424E-965A-07BCC2F02672} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C} + {C2EF54F8-8886-4260-A322-44F76245F95D} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C} EndGlobalSection EndGlobal diff --git a/samples/MvcSample.Web/AutoGenerateRouteNamesAttribute.cs b/samples/MvcSample.Web/AutoGenerateRouteNamesAttribute.cs new file mode 100644 index 0000000000..a9c114892c --- /dev/null +++ b/samples/MvcSample.Web/AutoGenerateRouteNamesAttribute.cs @@ -0,0 +1,31 @@ +// 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 System; +using Microsoft.AspNet.Mvc.ReflectedModelBuilder; + +namespace MvcSample.Web +{ + // Adds an auto-generated route-name to each action in the controller + public class AutoGenerateRouteNamesAttribute : Attribute, IReflectedControllerModelConvention + { + public void Apply(ReflectedControllerModel model) + { + foreach (var action in model.Actions) + { + if (action.AttributeRouteModel == null) + { + action.AttributeRouteModel = new ReflectedAttributeRouteModel(); + } + + if (action.AttributeRouteModel.Name == null) + { + action.AttributeRouteModel.Name = string.Format( + "{0}_{1}", + model.ControllerName, + action.ActionName); + } + } + } + } +} \ No newline at end of file diff --git a/samples/MvcSample.Web/SimpleRest.cs b/samples/MvcSample.Web/SimpleRest.cs index e98e1ef4cd..318f323609 100644 --- a/samples/MvcSample.Web/SimpleRest.cs +++ b/samples/MvcSample.Web/SimpleRest.cs @@ -2,6 +2,7 @@ using Microsoft.AspNet.Mvc; namespace MvcSample.Web { + [AutoGenerateRouteNames] [Route("api/REST")] public class SimpleRest : Controller { @@ -23,5 +24,13 @@ namespace MvcSample.Web { return Url.Action(action, controller); } + + [HttpGet("Link/{name}")] + public string GenerateLinkByName(string name = null) + { + // This action leverages [AutoGenerateRouteNames]. Try a URL like api/Rest/Link/SimpleRest_ThisIsAGetMethod + // which matches the auto-generated name for the ThisIsAGetMethod action. + return Url.RouteUrl(name); + } } } diff --git a/src/Microsoft.AspNet.Mvc.Core/ControllerDescriptor.cs b/src/Microsoft.AspNet.Mvc.Core/ControllerDescriptor.cs index 57c15eb241..0bd7b7024b 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ControllerDescriptor.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ControllerDescriptor.cs @@ -8,22 +8,8 @@ namespace Microsoft.AspNet.Mvc { public class ControllerDescriptor { - public ControllerDescriptor(TypeInfo controllerTypeInfo) - { - if (controllerTypeInfo == null) - { - throw new ArgumentNullException("controllerTypeInfo"); - } + public string Name { get; set; } - ControllerTypeInfo = controllerTypeInfo; - - Name = controllerTypeInfo.Name.EndsWith("Controller", StringComparison.Ordinal) - ? controllerTypeInfo.Name.Substring(0, controllerTypeInfo.Name.Length - "Controller".Length) - : controllerTypeInfo.Name; - } - - public string Name { get; private set; } - - public TypeInfo ControllerTypeInfo { get; private set; } + public TypeInfo ControllerTypeInfo { get; set; } } } diff --git a/src/Microsoft.AspNet.Mvc.Core/ReflectedActionDescriptorProvider.cs b/src/Microsoft.AspNet.Mvc.Core/ReflectedActionDescriptorProvider.cs index eda11359d9..9c60840740 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ReflectedActionDescriptorProvider.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ReflectedActionDescriptorProvider.cs @@ -31,19 +31,16 @@ namespace Microsoft.AspNet.Mvc private readonly IActionDiscoveryConventions _conventions; private readonly IReadOnlyList _globalFilters; private readonly IEnumerable _modelConventions; - private readonly IInlineConstraintResolver _constraintResolver; public ReflectedActionDescriptorProvider(IControllerAssemblyProvider controllerAssemblyProvider, IActionDiscoveryConventions conventions, IGlobalFilterProvider globalFilters, - IOptionsAccessor optionsAccessor, - IInlineConstraintResolver constraintResolver) + IOptionsAccessor optionsAccessor) { _controllerAssemblyProvider = controllerAssemblyProvider; _conventions = conventions; _globalFilters = globalFilters.Filters; _modelConventions = optionsAccessor.Options.ApplicationModelConventions; - _constraintResolver = constraintResolver; } public int Order @@ -60,12 +57,7 @@ namespace Microsoft.AspNet.Mvc public IEnumerable GetDescriptors() { var model = BuildModel(); - - foreach (var convention in _modelConventions) - { - convention.OnModelCreated(model); - } - + ApplyConventions(model); return Build(model); } @@ -80,7 +72,11 @@ namespace Microsoft.AspNet.Mvc foreach (var controllerType in controllerTypes) { - var controllerModel = new ReflectedControllerModel(controllerType); + var controllerModel = new ReflectedControllerModel(controllerType) + { + Application = applicationModel, + }; + applicationModel.Controllers.Add(controllerModel); foreach (var methodInfo in controllerType.AsType().GetMethods()) @@ -93,10 +89,13 @@ namespace Microsoft.AspNet.Mvc foreach (var actionInfo in actionInfos) { - var actionModel = new ReflectedActionModel(methodInfo); + var actionModel = new ReflectedActionModel(methodInfo) + { + ActionName = actionInfo.ActionName, + Controller = controllerModel, + IsActionNameMatchRequired = actionInfo.RequireActionNameMatch, + }; - actionModel.ActionName = actionInfo.ActionName; - actionModel.IsActionNameMatchRequired = actionInfo.RequireActionNameMatch; actionModel.HttpMethods.AddRange(actionInfo.HttpMethods ?? Enumerable.Empty()); if (actionInfo.AttributeRoute != null) @@ -107,7 +106,10 @@ namespace Microsoft.AspNet.Mvc foreach (var parameter in methodInfo.GetParameters()) { - actionModel.Parameters.Add(new ReflectedParameterModel(parameter)); + actionModel.Parameters.Add(new ReflectedParameterModel(parameter) + { + Action = actionModel, + }); } controllerModel.Actions.Add(actionModel); @@ -118,6 +120,62 @@ namespace Microsoft.AspNet.Mvc return applicationModel; } + public void ApplyConventions(ReflectedApplicationModel model) + { + // Conventions are applied from the outside-in to allow for scenarios where an action overrides + // a controller, etc. + foreach (var convention in _modelConventions) + { + convention.Apply(model); + } + + // First apply the conventions from attributes in decreasing order of scope. + foreach (var controller in model.Controllers) + { + // ToArray is needed here to prevent issues with modifying the attributes collection + // while iterating it. + var controllerConventions = + controller.Attributes + .OfType() + .ToArray(); + + foreach (var controllerConvention in controllerConventions) + { + controllerConvention.Apply(controller); + } + + foreach (var action in controller.Actions) + { + // ToArray is needed here to prevent issues with modifying the attributes collection + // while iterating it. + var actionConventions = + action.Attributes + .OfType() + .ToArray(); + + foreach (var actionConvention in actionConventions) + { + actionConvention.Apply(action); + } + + foreach (var parameter in action.Parameters) + { + // ToArray is needed here to prevent issues with modifying the attributes collection + // while iterating it. + var parameterConventions = + parameter.Attributes + .OfType() + .ToArray(); + + foreach (var parameterConvention in parameterConventions) + { + parameterConvention.Apply(parameter); + } + } + } + } + } + public List Build(ReflectedApplicationModel model) { var actions = new List(); @@ -132,7 +190,12 @@ namespace Microsoft.AspNet.Mvc foreach (var controller in model.Controllers) { - var controllerDescriptor = new ControllerDescriptor(controller.ControllerType); + var controllerDescriptor = new ControllerDescriptor() + { + ControllerTypeInfo = controller.ControllerType, + Name = controller.ControllerName, + }; + foreach (var action in controller.Actions) { // Controllers with multiple [Route] attributes (or user defined implementation of @@ -351,7 +414,7 @@ namespace Microsoft.AspNet.Mvc private static void AddApiExplorerInfo( ReflectedActionDescriptor actionDescriptor, - ReflectedActionModel action, + ReflectedActionModel action, ReflectedControllerModel controller) { var apiExplorerIsVisible = action.ApiExplorerIsVisible ?? controller.ApiExplorerIsVisible ?? false; @@ -360,7 +423,7 @@ namespace Microsoft.AspNet.Mvc var apiExplorerActionData = new ApiDescriptionActionData() { GroupName = action.ApiExplorerGroupName ?? controller.ApiExplorerGroupName, - }; + }; actionDescriptor.SetProperty(apiExplorerActionData); } diff --git a/src/Microsoft.AspNet.Mvc.Core/ReflectedModelBuilder/IReflectedActionModelConvention.cs b/src/Microsoft.AspNet.Mvc.Core/ReflectedModelBuilder/IReflectedActionModelConvention.cs new file mode 100644 index 0000000000..301c2de24e --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/ReflectedModelBuilder/IReflectedActionModelConvention.cs @@ -0,0 +1,25 @@ +// 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.ReflectedModelBuilder +{ + /// + /// Allows customization of the of the . + /// + /// + /// To use this interface, create an class which implements the interface and + /// place it on an action method. + /// + /// customizations run after + /// customications and before + /// customizations. + /// + public interface IReflectedActionModelConvention + { + /// + /// Called to apply the convention to the . + /// + /// The . + void Apply([NotNull] ReflectedActionModel model); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/ReflectedModelBuilder/IReflectedApplicationModelConvention.cs b/src/Microsoft.AspNet.Mvc.Core/ReflectedModelBuilder/IReflectedApplicationModelConvention.cs index 9e3345f963..e218c03254 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ReflectedModelBuilder/IReflectedApplicationModelConvention.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ReflectedModelBuilder/IReflectedApplicationModelConvention.cs @@ -3,8 +3,22 @@ namespace Microsoft.AspNet.Mvc.ReflectedModelBuilder { + /// + /// Allows customization of the of the . + /// + /// + /// Implementaions of this interface can be registered in + /// to customize metadata about the application. + /// + /// run before other types of customizations to the + /// reflected model. + /// public interface IReflectedApplicationModelConvention { - void OnModelCreated([NotNull] ReflectedApplicationModel model); + /// + /// Called to apply the convention to the . + /// + /// The . + void Apply([NotNull] ReflectedApplicationModel model); } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/ReflectedModelBuilder/IReflectedControllerModelConvention.cs b/src/Microsoft.AspNet.Mvc.Core/ReflectedModelBuilder/IReflectedControllerModelConvention.cs new file mode 100644 index 0000000000..c85e4b7b31 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/ReflectedModelBuilder/IReflectedControllerModelConvention.cs @@ -0,0 +1,25 @@ +// 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.ReflectedModelBuilder +{ + /// + /// Allows customization of the of the . + /// + /// + /// To use this interface, create an class which implements the interface and + /// place it on a controller class. + /// + /// customizations run after + /// customizations and before + /// customizations. + /// + public interface IReflectedControllerModelConvention + { + /// + /// Called to apply the convention to the . + /// + /// The . + void Apply([NotNull] ReflectedControllerModel model); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/ReflectedModelBuilder/IReflectedParameterModelConvention.cs b/src/Microsoft.AspNet.Mvc.Core/ReflectedModelBuilder/IReflectedParameterModelConvention.cs new file mode 100644 index 0000000000..3ee0e3ef19 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/ReflectedModelBuilder/IReflectedParameterModelConvention.cs @@ -0,0 +1,24 @@ +// 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.ReflectedModelBuilder +{ + /// + /// Allows customization of the of the . + /// + /// + /// To use this interface, create an class which implements the interface and + /// place it on an action method parameter. + /// + /// customizations run after + /// customizations. + /// + public interface IReflectedParameterModelConvention + { + /// + /// Called to apply the convention to the . + /// + /// The . + void Apply([NotNull] ReflectedParameterModel model); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/ReflectedModelBuilder/ReflectedActionModel.cs b/src/Microsoft.AspNet.Mvc.Core/ReflectedModelBuilder/ReflectedActionModel.cs index 6f62554746..4e37c2d100 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ReflectedModelBuilder/ReflectedActionModel.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ReflectedModelBuilder/ReflectedActionModel.cs @@ -51,6 +51,8 @@ namespace Microsoft.AspNet.Mvc.ReflectedModelBuilder public List Attributes { get; private set; } + public ReflectedControllerModel Controller { get; set; } + public List Filters { get; private set; } public List HttpMethods { get; private set; } diff --git a/src/Microsoft.AspNet.Mvc.Core/ReflectedModelBuilder/ReflectedControllerModel.cs b/src/Microsoft.AspNet.Mvc.Core/ReflectedModelBuilder/ReflectedControllerModel.cs index 88be13d70e..24a6b4a8bf 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ReflectedModelBuilder/ReflectedControllerModel.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ReflectedModelBuilder/ReflectedControllerModel.cs @@ -51,6 +51,8 @@ namespace Microsoft.AspNet.Mvc.ReflectedModelBuilder public List Actions { get; private set; } + public ReflectedApplicationModel Application { get; set; } + public List Attributes { get; private set; } public string ControllerName { get; set; } diff --git a/src/Microsoft.AspNet.Mvc.Core/ReflectedModelBuilder/ReflectedParameterModel.cs b/src/Microsoft.AspNet.Mvc.Core/ReflectedModelBuilder/ReflectedParameterModel.cs index 548029dbdf..e0c1d8acae 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ReflectedModelBuilder/ReflectedParameterModel.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ReflectedModelBuilder/ReflectedParameterModel.cs @@ -21,6 +21,8 @@ namespace Microsoft.AspNet.Mvc.ReflectedModelBuilder IsOptional = ParameterInfo.HasDefaultValue; } + public ReflectedActionModel Action { get; set; } + public List Attributes { get; private set; } public bool IsOptional { get; set; } diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ActionAttributeTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ActionAttributeTests.cs index 2e810cbf8d..607846e02a 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/ActionAttributeTests.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ActionAttributeTests.cs @@ -227,8 +227,7 @@ namespace Microsoft.AspNet.Mvc controllerAssemblyProvider, actionDiscoveryConventions, new TestGlobalFilterProvider(), - new MockMvcOptionsAccessor(), - Mock.Of()); + new MockMvcOptionsAccessor()); } private static HttpContext GetHttpContext(string httpMethod) diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/DefaultActionDiscoveryConventionsActionSelectionTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/DefaultActionDiscoveryConventionsActionSelectionTests.cs index 859943f24f..37088e7488 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/DefaultActionDiscoveryConventionsActionSelectionTests.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/DefaultActionDiscoveryConventionsActionSelectionTests.cs @@ -197,8 +197,7 @@ namespace Microsoft.AspNet.Mvc controllerAssemblyProvider.Object, actionDiscoveryConventions, new TestGlobalFilterProvider(), - new MockMvcOptionsAccessor(), - Mock.Of()); + new MockMvcOptionsAccessor()); } private static HttpContext GetHttpContext(string httpMethod) diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ReflectedActionDescriptorProviderTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ReflectedActionDescriptorProviderTests.cs index 2107a0a5a9..a83dbd96ce 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/ReflectedActionDescriptorProviderTests.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ReflectedActionDescriptorProviderTests.cs @@ -5,12 +5,12 @@ using System; using System.Collections.Generic; using System.Linq; using System.Reflection; -using Microsoft.AspNet.Routing; using Microsoft.AspNet.Mvc.Description; using Microsoft.AspNet.Mvc.Routing; +using Microsoft.AspNet.Mvc.ReflectedModelBuilder; +using Microsoft.Framework.OptionsModel; using Moq; using Xunit; -using Microsoft.AspNet.Mvc.Filters; namespace Microsoft.AspNet.Mvc.Test { @@ -1130,6 +1130,56 @@ namespace Microsoft.AspNet.Mvc.Test Assert.Equal("Store", action.GetProperty().GroupName); } + // Verifies the sequence of conventions running + [Fact] + public void ApplyConventions_RunsInOrderOfDecreasingScope() + { + // Arrange + var sequence = 0; + + var applicationConvention = new Mock(); + applicationConvention + .Setup(c => c.Apply(It.IsAny())) + .Callback(() => { Assert.Equal(0, sequence++); }); + + var controllerConvention = new Mock(); + controllerConvention + .Setup(c => c.Apply(It.IsAny())) + .Callback(() => { Assert.Equal(1, sequence++); }); + + var actionConvention = new Mock(); + actionConvention + .Setup(c => c.Apply(It.IsAny())) + .Callback(() => { Assert.Equal(2, sequence++); }); + + var parameterConvention = new Mock(); + parameterConvention + .Setup(c => c.Apply(It.IsAny())) + .Callback(() => { Assert.Equal(3, sequence++); }); + + var options = new MockMvcOptionsAccessor(); + options.Options.ApplicationModelConventions.Add(applicationConvention.Object); + + var provider = GetProvider(typeof(ConventionsController).GetTypeInfo(), options); + + var model = provider.BuildModel(); + + var controller = model.Controllers.Single(); + controller.Attributes.Add(controllerConvention.Object); + + var action = controller.Actions.Single(); + action.Attributes.Add(actionConvention.Object); + + var parameter = action.Parameters.Single(); + parameter.Attributes.Add(parameterConvention.Object); + + // Act + provider.ApplyConventions(model); + + // Assert + Assert.Equal(4, sequence); + } + private ReflectedActionDescriptorProvider GetProvider( TypeInfo controllerTypeInfo, IEnumerable filters = null) @@ -1145,8 +1195,7 @@ namespace Microsoft.AspNet.Mvc.Test assemblyProvider.Object, conventions, new TestGlobalFilterProvider(filters), - new MockMvcOptionsAccessor(), - Mock.Of()); + new MockMvcOptionsAccessor()); return provider; } @@ -1165,12 +1214,29 @@ namespace Microsoft.AspNet.Mvc.Test assemblyProvider.Object, conventions, new TestGlobalFilterProvider(), - new MockMvcOptionsAccessor(), - Mock.Of()); + new MockMvcOptionsAccessor()); return provider; } + private ReflectedActionDescriptorProvider GetProvider( + TypeInfo type, + IOptionsAccessor options) + { + var conventions = new StaticActionDiscoveryConventions(type); + + var assemblyProvider = new Mock(); + assemblyProvider + .SetupGet(ap => ap.CandidateAssemblies) + .Returns(new Assembly[] { type.Assembly }); + + return new ReflectedActionDescriptorProvider( + assemblyProvider.Object, + conventions, + new TestGlobalFilterProvider(), + options); + } + private IEnumerable GetDescriptors(params TypeInfo[] controllerTypeInfos) { var conventions = new StaticActionDiscoveryConventions(controllerTypeInfos); @@ -1184,8 +1250,7 @@ namespace Microsoft.AspNet.Mvc.Test assemblyProvider.Object, conventions, new TestGlobalFilterProvider(), - new MockMvcOptionsAccessor(), - null); + new MockMvcOptionsAccessor()); return provider.GetDescriptors(); } @@ -1588,5 +1653,10 @@ namespace Microsoft.AspNet.Mvc.Test public void Create() { } } + + private class ConventionsController + { + public void Create(int productId) { } + } } } diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/ReflectedModelTest.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/ReflectedModelTest.cs new file mode 100644 index 0000000000..2059646766 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/ReflectedModelTest.cs @@ -0,0 +1,70 @@ +// 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 System; +using System.Threading.Tasks; +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.TestHost; +using Xunit; +using System.Net; + +namespace Microsoft.AspNet.Mvc.FunctionalTests +{ + public class ReflectedModelTest + { + private readonly IServiceProvider _services = TestHelper.CreateServices(nameof(ReflectedModelWebSite)); + private readonly Action _app = new ReflectedModelWebSite.Startup().Configure; + + [Fact] + public async Task ControllerModel_CustomizedWithAttribute() + { + // Arrange + var server = TestServer.Create(_services, _app); + var client = server.CreateClient(); + + // Act + var response = await client.GetAsync("http://localhost/CoolController/GetControllerName"); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var body = await response.Content.ReadAsStringAsync(); + Assert.Equal("CoolController", body); + } + + [Fact] + public async Task ActionModel_CustomizedWithAttribute() + { + // Arrange + var server = TestServer.Create(_services, _app); + var client = server.CreateClient(); + + // Act + var response = await client.GetAsync("http://localhost/ReflectedActionModel/ActionName"); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var body = await response.Content.ReadAsStringAsync(); + Assert.Equal("ActionName", body); + } + + [Fact] + public async Task ParameterModel_CustomizedWithAttribute() + { + // Arrange + var server = TestServer.Create(_services, _app); + var client = server.CreateClient(); + + // Act + var response = await client.GetAsync("http://localhost/ReflectedParameterModel/GetParameterIsOptional"); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var body = await response.Content.ReadAsStringAsync(); + Assert.Equal("True", body); + + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/project.json b/test/Microsoft.AspNet.Mvc.FunctionalTests/project.json index f17e0ea94a..4ec9112516 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/project.json +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/project.json @@ -24,6 +24,7 @@ "Microsoft.AspNet.PipelineCore": "1.0.0-*", "ModelBindingWebSite": "", "MvcSample.Web": "", + "ReflectedModelWebSite": "", "RoutingWebSite": "", "RazorWebSite": "", "ValueProvidersSite": "", diff --git a/test/WebSites/ApiExplorerWebSite/ApiExplorerVisibilityDisabledConvention.cs b/test/WebSites/ApiExplorerWebSite/ApiExplorerVisibilityDisabledConvention.cs index 97ca9edb91..20cad66147 100644 --- a/test/WebSites/ApiExplorerWebSite/ApiExplorerVisibilityDisabledConvention.cs +++ b/test/WebSites/ApiExplorerWebSite/ApiExplorerVisibilityDisabledConvention.cs @@ -19,7 +19,7 @@ namespace ApiExplorer _type = type.GetTypeInfo(); } - public void OnModelCreated(ReflectedApplicationModel model) + public void Apply(ReflectedApplicationModel model) { foreach (var controller in model.Controllers) { diff --git a/test/WebSites/ApiExplorerWebSite/ApiExplorerVisibilityEnabledConvention.cs b/test/WebSites/ApiExplorerWebSite/ApiExplorerVisibilityEnabledConvention.cs index cd01caf9ee..940434e20c 100644 --- a/test/WebSites/ApiExplorerWebSite/ApiExplorerVisibilityEnabledConvention.cs +++ b/test/WebSites/ApiExplorerWebSite/ApiExplorerVisibilityEnabledConvention.cs @@ -10,7 +10,7 @@ namespace ApiExplorer // convention public class ApiExplorerVisibilityEnabledConvention : IReflectedApplicationModelConvention { - public void OnModelCreated(ReflectedApplicationModel model) + public void Apply(ReflectedApplicationModel model) { foreach (var controller in model.Controllers) { diff --git a/test/WebSites/AutofacWebSite/Startup.cs b/test/WebSites/AutofacWebSite/Startup.cs index 6cadb40b80..77fb4b31e4 100644 --- a/test/WebSites/AutofacWebSite/Startup.cs +++ b/test/WebSites/AutofacWebSite/Startup.cs @@ -11,8 +11,10 @@ namespace AutofacWebSite { public void Configure(IApplicationBuilder app) { + var configuration = app.GetTestConfiguration(); + app.UseServices(services => { - services.AddMvc(); + services.AddMvc(configuration); services.AddTransient(); var builder = new ContainerBuilder(); diff --git a/test/WebSites/AutofacWebSite/project.json b/test/WebSites/AutofacWebSite/project.json index 0e66863a89..f92d5d140c 100644 --- a/test/WebSites/AutofacWebSite/project.json +++ b/test/WebSites/AutofacWebSite/project.json @@ -2,10 +2,11 @@ "dependencies": { "Microsoft.AspNet.Server.IIS": "1.0.0-*", "Microsoft.AspNet.Mvc": "", + "Microsoft.AspNet.Mvc.TestConfiguration": "", "Microsoft.Framework.DependencyInjection.Autofac": "1.0.0-*", "Autofac": "3.3.0" }, - "frameworks" : { - "aspnet50" : { } + "frameworks": { + "aspnet50": { } } } diff --git a/test/WebSites/ReflectedModelWebSite/Controllers/ReflectedActionModelController.cs b/test/WebSites/ReflectedModelWebSite/Controllers/ReflectedActionModelController.cs new file mode 100644 index 0000000000..7d72dd97f3 --- /dev/null +++ b/test/WebSites/ReflectedModelWebSite/Controllers/ReflectedActionModelController.cs @@ -0,0 +1,37 @@ +// 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 System; +using Microsoft.AspNet.Mvc; +using Microsoft.AspNet.Mvc.ReflectedModelBuilder; + +namespace ReflectedModelWebSite +{ + // This controller uses an reflected model attribute to change an action name, and thus + // the URL. + public class ReflectedActionModelController : Controller + { + [ActionName2("ActionName")] + public string GetActionName() + { + var actionDescriptor = (ReflectedActionDescriptor)ActionContext.ActionDescriptor; + + return actionDescriptor.Name; + } + + private class ActionName2Attribute : Attribute, IReflectedActionModelConvention + { + private readonly string _actionName; + + public ActionName2Attribute(string actionName) + { + _actionName = actionName; + } + + public void Apply(ReflectedActionModel model) + { + model.ActionName = _actionName; + } + } + } +} \ No newline at end of file diff --git a/test/WebSites/ReflectedModelWebSite/Controllers/ReflectedControllerModelController.cs b/test/WebSites/ReflectedModelWebSite/Controllers/ReflectedControllerModelController.cs new file mode 100644 index 0000000000..df9a7f1f4e --- /dev/null +++ b/test/WebSites/ReflectedModelWebSite/Controllers/ReflectedControllerModelController.cs @@ -0,0 +1,37 @@ +// 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 System; +using Microsoft.AspNet.Mvc; +using Microsoft.AspNet.Mvc.ReflectedModelBuilder; + +namespace ReflectedModelWebSite +{ + // This controller uses an reflected model attribute to change the controller name, and thus + // the URL. + [ControllerName("CoolController")] + public class ReflectedControllerModelController : Controller + { + public string GetControllerName() + { + var actionDescriptor = (ReflectedActionDescriptor)ActionContext.ActionDescriptor; + + return actionDescriptor.ControllerName; + } + + private class ControllerNameAttribute : Attribute, IReflectedControllerModelConvention + { + private readonly string _controllerName; + + public ControllerNameAttribute(string controllerName) + { + _controllerName = controllerName; + } + + public void Apply(ReflectedControllerModel model) + { + model.ControllerName = _controllerName; + } + } + } +} \ No newline at end of file diff --git a/test/WebSites/ReflectedModelWebSite/Controllers/ReflectedParameterModelController.cs b/test/WebSites/ReflectedModelWebSite/Controllers/ReflectedParameterModelController.cs new file mode 100644 index 0000000000..f9ef95dcfa --- /dev/null +++ b/test/WebSites/ReflectedModelWebSite/Controllers/ReflectedParameterModelController.cs @@ -0,0 +1,28 @@ +// 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 System; +using Microsoft.AspNet.Mvc; +using Microsoft.AspNet.Mvc.ReflectedModelBuilder; + +namespace ReflectedModelWebSite +{ + // This controller uses an reflected model attribute to change a parameter to optional. + public class ReflectedParameterModelController : Controller + { + public string GetParameterIsOptional([Optional] int? id) + { + var actionDescriptor = (ReflectedActionDescriptor)ActionContext.ActionDescriptor; + + return actionDescriptor.Parameters[0].IsOptional.ToString(); + } + + private class OptionalAttribute : Attribute, IReflectedParameterModelConvention + { + public void Apply(ReflectedParameterModel model) + { + model.IsOptional = true; + } + } + } +} \ No newline at end of file diff --git a/test/WebSites/ReflectedModelWebSite/ReflectedModelWebSite.kproj b/test/WebSites/ReflectedModelWebSite/ReflectedModelWebSite.kproj new file mode 100644 index 0000000000..916eaf2b7c --- /dev/null +++ b/test/WebSites/ReflectedModelWebSite/ReflectedModelWebSite.kproj @@ -0,0 +1,24 @@ + + + + 12.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + c2ef54f8-8886-4260-a322-44f76245f95d + Web + ReflectedModelWebSite + + + ConsoleDebugger + + + WebDebugger + + + 2.0 + 9411 + + + \ No newline at end of file diff --git a/test/WebSites/ReflectedModelWebSite/Startup.cs b/test/WebSites/ReflectedModelWebSite/Startup.cs new file mode 100644 index 0000000000..2d5bb2bb84 --- /dev/null +++ b/test/WebSites/ReflectedModelWebSite/Startup.cs @@ -0,0 +1,23 @@ +// 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.Builder; +using Microsoft.Framework.DependencyInjection; + +namespace ReflectedModelWebSite +{ + public class Startup + { + public void Configure(IApplicationBuilder app) + { + var configuration = app.GetTestConfiguration(); + + app.UseServices(services => + { + services.AddMvc(configuration); + }); + + app.UseMvc(); + } + } +} diff --git a/test/WebSites/ReflectedModelWebSite/project.json b/test/WebSites/ReflectedModelWebSite/project.json new file mode 100644 index 0000000000..93e6606e45 --- /dev/null +++ b/test/WebSites/ReflectedModelWebSite/project.json @@ -0,0 +1,10 @@ +{ + "dependencies": { + "Microsoft.AspNet.Mvc": "", + "Microsoft.AspNet.Mvc.TestConfiguration": "" + }, + "frameworks": { + "aspnet50": { }, + "aspnetcore50": { } + } +}