diff --git a/src/Microsoft.AspNet.Mvc.Core/ApplicationModels/ActionModel.cs b/src/Microsoft.AspNet.Mvc.Core/ApplicationModels/ActionModel.cs index fbb06b8dfd..311959c2de 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ApplicationModels/ActionModel.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ApplicationModels/ActionModel.cs @@ -21,6 +21,7 @@ namespace Microsoft.AspNet.Mvc.ApplicationModels HttpMethods = new List(); Parameters = new List(); RouteConstraints = new List(); + Properties = new Dictionary(); } public ActionModel([NotNull] ActionModel other) @@ -36,6 +37,7 @@ namespace Microsoft.AspNet.Mvc.ApplicationModels Attributes = new List(other.Attributes); Filters = new List(other.Filters); HttpMethods = new List(other.HttpMethods); + Properties = new Dictionary(other.Properties); // Make a deep copy of other 'model' types. ApiExplorer = new ApiExplorerModel(other.ApiExplorer); @@ -79,5 +81,15 @@ namespace Microsoft.AspNet.Mvc.ApplicationModels public IList Parameters { get; private set; } public IList RouteConstraints { get; private set; } + + /// + /// Gets a set of properties associated with the action. + /// These properties will be copied to . + /// + /// + /// Entries will take precedence over entries with the same key in + /// and . + /// + public IDictionary Properties { get; } } } diff --git a/src/Microsoft.AspNet.Mvc.Core/ApplicationModels/ApplicationModel.cs b/src/Microsoft.AspNet.Mvc.Core/ApplicationModels/ApplicationModel.cs index aa93de8246..b42266d258 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ApplicationModels/ApplicationModel.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ApplicationModels/ApplicationModel.cs @@ -12,6 +12,7 @@ namespace Microsoft.AspNet.Mvc.ApplicationModels ApiExplorer = new ApiExplorerModel(); Controllers = new List(); Filters = new List(); + Properties = new Dictionary(); } /// @@ -30,5 +31,11 @@ namespace Microsoft.AspNet.Mvc.ApplicationModels public IList Controllers { get; private set; } public IList Filters { get; private set; } + + /// + /// Gets a set of properties associated with all actions. + /// These properties will be copied to . + /// + public IDictionary Properties { get; } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/ApplicationModels/ControllerModel.cs b/src/Microsoft.AspNet.Mvc.Core/ApplicationModels/ControllerModel.cs index 5377fadfed..ef2946204a 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ApplicationModels/ControllerModel.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ApplicationModels/ControllerModel.cs @@ -21,6 +21,7 @@ namespace Microsoft.AspNet.Mvc.ApplicationModels ActionConstraints = new List(); Filters = new List(); RouteConstraints = new List(); + Properties = new Dictionary(); } public ControllerModel([NotNull] ControllerModel other) @@ -36,6 +37,7 @@ namespace Microsoft.AspNet.Mvc.ApplicationModels Attributes = new List(other.Attributes); Filters = new List(other.Filters); RouteConstraints = new List(other.RouteConstraints); + Properties = new Dictionary(other.Properties); // Make a deep copy of other 'model' types. Actions = new List(other.Actions.Select(a => new ActionModel(a))); @@ -73,5 +75,15 @@ namespace Microsoft.AspNet.Mvc.ApplicationModels public IList Filters { get; private set; } public IList RouteConstraints { get; private set; } + + /// + /// Gets a set of properties associated with the controller. + /// These properties will be copied to . + /// + /// + /// Entries will take precedence over entries with the same key + /// in . + /// + public IDictionary Properties { get; } } } diff --git a/src/Microsoft.AspNet.Mvc.Core/ControllerActionDescriptorBuilder.cs b/src/Microsoft.AspNet.Mvc.Core/ControllerActionDescriptorBuilder.cs index e4998dbd65..b8a358c146 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ControllerActionDescriptorBuilder.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ControllerActionDescriptorBuilder.cs @@ -58,6 +58,7 @@ namespace Microsoft.AspNet.Mvc AddApiExplorerInfo(actionDescriptor, application, controller, action); AddRouteConstraints(removalConstraints, actionDescriptor, controller, action); + AddProperties(actionDescriptor, action, controller, application); if (IsAttributeRoutedAction(actionDescriptor)) { @@ -329,6 +330,28 @@ namespace Microsoft.AspNet.Mvc } } + private static void AddProperties( + ControllerActionDescriptor actionDescriptor, + ActionModel action, + ControllerModel controller, + ApplicationModel application) + { + foreach (var item in application.Properties) + { + actionDescriptor.Properties[item.Key] = item.Value; + } + + foreach (var item in controller.Properties) + { + actionDescriptor.Properties[item.Key] = item.Value; + } + + foreach (var item in action.Properties) + { + actionDescriptor.Properties[item.Key] = item.Value; + } + } + private static void AddActionFilters( ControllerActionDescriptor actionDescriptor, IEnumerable actionFilters, diff --git a/src/Microsoft.AspNet.Mvc.Core/Logging/ActionModelValues.cs b/src/Microsoft.AspNet.Mvc.Core/Logging/ActionModelValues.cs index f5f8d599aa..48624e37f0 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Logging/ActionModelValues.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Logging/ActionModelValues.cs @@ -33,6 +33,7 @@ namespace Microsoft.AspNet.Mvc.Logging } HttpMethods = inner.HttpMethods; ActionConstraints = inner.ActionConstraints?.Select(a => new ActionConstraintValues(a))?.ToList(); + Properties = new Dictionary(inner.Properties); } } @@ -86,6 +87,11 @@ namespace Microsoft.AspNet.Mvc.Logging /// public IList ActionConstraints { get; } + /// + /// Gets the set of properties associated with the action . + /// + public IDictionary Properties { get; } + public override string Format() { return LogFormatter.FormatStructure(this); diff --git a/src/Microsoft.AspNet.Mvc.Core/Logging/ControllerModelValues.cs b/src/Microsoft.AspNet.Mvc.Core/Logging/ControllerModelValues.cs index 3d8c909b0e..ae86c189d9 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Logging/ControllerModelValues.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Logging/ControllerModelValues.cs @@ -29,6 +29,7 @@ namespace Microsoft.AspNet.Mvc.Logging r => new RouteConstraintProviderValues(r)).ToList(); AttributeRoutes = inner.AttributeRoutes.Select( a => new AttributeRouteModelValues(a)).ToList(); + Properties = new Dictionary(inner.Properties); } } @@ -83,6 +84,11 @@ namespace Microsoft.AspNet.Mvc.Logging /// public List AttributeRoutes { get; set; } + /// + /// Gets the set of properties associated with the controller . + /// + public IDictionary Properties { get; } + public override string Format() { return LogFormatter.FormatStructure(this); diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ApplicationModel/ActionModelTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ApplicationModel/ActionModelTest.cs index 766c69f008..128c8d5ffc 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.RouteConstraints.Add(new AreaAttribute("Admin")); + action.Properties.Add(new KeyValuePair("test key", "test value")); // Act var action2 = new ActionModel(action); @@ -60,6 +61,7 @@ namespace Microsoft.AspNet.Mvc.ApplicationModels // Assert foreach (var property in typeof(ActionModel).GetProperties()) { + // Reflection is used to make sure the test fails when a new property is added. if (property.Name.Equals("ApiExplorer") || property.Name.Equals("AttributeRouteModel") || property.Name.Equals("Parameters")) @@ -78,6 +80,13 @@ namespace Microsoft.AspNet.Mvc.ApplicationModels // Ensure non-default value Assert.NotEmpty((IEnumerable)value1); } + else if (typeof(IDictionary).IsAssignableFrom(property.PropertyType)) + { + Assert.Equal(value1, value2); + + // Ensure non-default value + Assert.NotEmpty((IDictionary)value1); + } else if (property.PropertyType.IsValueType || Nullable.GetUnderlyingType(property.PropertyType) != null) { diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ApplicationModel/ControllerModelTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ApplicationModel/ControllerModelTest.cs index 2d9b0ba664..87699e9d0a 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/ApplicationModel/ControllerModelTest.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ApplicationModel/ControllerModelTest.cs @@ -56,6 +56,7 @@ namespace Microsoft.AspNet.Mvc.ApplicationModels controller.ControllerName = "cool"; controller.Filters.Add(new AuthorizeAttribute()); controller.RouteConstraints.Add(new AreaAttribute("Admin")); + controller.Properties.Add(new KeyValuePair("test key", "test value")); // Act var controller2 = new ControllerModel(controller); @@ -81,6 +82,13 @@ namespace Microsoft.AspNet.Mvc.ApplicationModels // Ensure non-default value Assert.NotEmpty((IEnumerable)value1); } + else if (typeof(IDictionary).IsAssignableFrom(property.PropertyType)) + { + Assert.Equal(value1, value2); + + // Ensure non-default value + Assert.NotEmpty((IDictionary)value1); + } else if (property.PropertyType.IsValueType || Nullable.GetUnderlyingType(property.PropertyType) != null) { diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ControllerActionDescriptorBuilderTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ControllerActionDescriptorBuilderTest.cs new file mode 100644 index 0000000000..04809fe3ea --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ControllerActionDescriptorBuilderTest.cs @@ -0,0 +1,94 @@ +// 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.Collections.Generic; +using System.Linq; +using System.Reflection; +using Microsoft.AspNet.Mvc.ApplicationModels; +using Xunit; + +namespace Microsoft.AspNet.Mvc +{ + public class ControllerActionDescriptorBuilderTest + { + [Fact] + public void Build_WithPropertiesSet_FromApplicationModel() + { + // Arrange + var applicationModel = new ApplicationModel(); + applicationModel.Properties["test"] = "application"; + + var controller = new ControllerModel(typeof(TestController).GetTypeInfo(), + new List() { }); + controller.Application = applicationModel; + applicationModel.Controllers.Add(controller); + + var methodInfo = typeof(TestController).GetMethod("SomeAction"); + var actionModel = new ActionModel(methodInfo, new List() { }); + actionModel.Controller = controller; + controller.Actions.Add(actionModel); + + // Act + var descriptors = ControllerActionDescriptorBuilder.Build(applicationModel); + + // Assert + Assert.Equal("application", descriptors.Single().Properties["test"]); + } + + [Fact] + public void Build_WithPropertiesSet_ControllerOverwritesApplicationModel() + { + // Arrange + var applicationModel = new ApplicationModel(); + applicationModel.Properties["test"] = "application"; + + var controller = new ControllerModel(typeof(TestController).GetTypeInfo(), + new List() { }); + controller.Application = applicationModel; + controller.Properties["test"] = "controller"; + applicationModel.Controllers.Add(controller); + + var methodInfo = typeof(TestController).GetMethod("SomeAction"); + var actionModel = new ActionModel(methodInfo, new List() { }); + actionModel.Controller = controller; + controller.Actions.Add(actionModel); + + // Act + var descriptors = ControllerActionDescriptorBuilder.Build(applicationModel); + + // Assert + Assert.Equal("controller", descriptors.Single().Properties["test"]); + } + + [Fact] + public void Build_WithPropertiesSet_ActionOverwritesApplicationAndControllerModel() + { + // Arrange + var applicationModel = new ApplicationModel(); + applicationModel.Properties["test"] = "application"; + + var controller = new ControllerModel(typeof(TestController).GetTypeInfo(), + new List() { }); + controller.Application = applicationModel; + controller.Properties["test"] = "controller"; + applicationModel.Controllers.Add(controller); + + var methodInfo = typeof(TestController).GetMethod("SomeAction"); + var actionModel = new ActionModel(methodInfo, new List() { }); + actionModel.Controller = controller; + actionModel.Properties["test"] = "action"; + controller.Actions.Add(actionModel); + + // Act + var descriptors = ControllerActionDescriptorBuilder.Build(applicationModel); + + // Assert + Assert.Equal("action", descriptors.Single().Properties["test"]); + } + + private class TestController + { + public void SomeAction() { } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/ApplicationModelTest.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/ApplicationModelTest.cs index 377020cf7a..2a01b52a6d 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/ApplicationModelTest.cs +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/ApplicationModelTest.cs @@ -64,7 +64,57 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests var body = await response.Content.ReadAsStringAsync(); Assert.Equal("CoolMetadata", body); - } + + [Fact] + public async Task ApplicationModel_AddPropertyToActionDescriptor_FromApplicationModel() + { + // Arrange + var server = TestServer.Create(_services, _app); + var client = server.CreateClient(); + + // Act + var response = await client.GetAsync("http://localhost/Home/GetCommonDescription"); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var body = await response.Content.ReadAsStringAsync(); + Assert.Equal("Common Application Description", body); + } + + [Fact] + public async Task ApplicationModel_AddPropertyToActionDescriptor_ControllerModelOverwritesCommonApplicationProperty() + { + // Arrange + var server = TestServer.Create(_services, _app); + var client = server.CreateClient(); + + // Act + var response = await client.GetAsync("http://localhost/ApplicationModel/GetControllerDescription"); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var body = await response.Content.ReadAsStringAsync(); + Assert.Equal("Common Controller Description", body); + } + + [Fact] + public async Task ApplicationModel_ProvidesMetadataToActionDescriptor_ActionModelOverwritesControllerModelProperty() + { + // Arrange + var server = TestServer.Create(_services, _app); + var client = server.CreateClient(); + + // Act + var response = await client.GetAsync("http://localhost/ApplicationModel/GetActionSpecificDescription"); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var body = await response.Content.ReadAsStringAsync(); + Assert.Equal("Specific Action Description", body); + } } } \ No newline at end of file diff --git a/test/WebSites/ApplicationModelWebSite/Controllers/ApplicationModelController.cs b/test/WebSites/ApplicationModelWebSite/Controllers/ApplicationModelController.cs new file mode 100644 index 0000000000..fafd66ff4d --- /dev/null +++ b/test/WebSites/ApplicationModelWebSite/Controllers/ApplicationModelController.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. + +using Microsoft.AspNet.Mvc; + +namespace ApplicationModelWebSite +{ + // This controller uses an reflected model attribute to add arbitrary data to controller and action model. + [ControllerDescription("Common Controller Description")] + public class ApplicationModelController : Controller + { + public string GetControllerDescription() + { + var actionDescriptor = (ControllerActionDescriptor)ActionContext.ActionDescriptor; + return actionDescriptor.Properties["description"].ToString(); + } + + [ActionDescription("Specific Action Description")] + public string GetActionSpecificDescription() + { + var actionDescriptor = (ControllerActionDescriptor)ActionContext.ActionDescriptor; + return actionDescriptor.Properties["description"].ToString(); + } + } +} \ No newline at end of file diff --git a/test/WebSites/ApplicationModelWebSite/Controllers/HomeController.cs b/test/WebSites/ApplicationModelWebSite/Controllers/HomeController.cs new file mode 100644 index 0000000000..5f56ab7b0c --- /dev/null +++ b/test/WebSites/ApplicationModelWebSite/Controllers/HomeController.cs @@ -0,0 +1,16 @@ +// 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.Mvc; + +namespace ApplicationModelWebSite +{ + public class HomeController : Controller + { + public string GetCommonDescription() + { + var actionDescriptor = (ControllerActionDescriptor)ActionContext.ActionDescriptor; + return actionDescriptor.Properties["description"].ToString(); + } + } +} \ No newline at end of file diff --git a/test/WebSites/ApplicationModelWebSite/Conventions/ActionDescriptionAttribute.cs b/test/WebSites/ApplicationModelWebSite/Conventions/ActionDescriptionAttribute.cs new file mode 100644 index 0000000000..afdbdda821 --- /dev/null +++ b/test/WebSites/ApplicationModelWebSite/Conventions/ActionDescriptionAttribute.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. + +using System; +using Microsoft.AspNet.Mvc.ApplicationModels; + + +namespace ApplicationModelWebSite +{ + public class ActionDescriptionAttribute : Attribute, IActionModelConvention + { + private object _value; + + public ActionDescriptionAttribute(object value) + { + _value = value; + } + + public void Apply(ActionModel model) + { + model.Properties["description"] = _value; + } + } +} \ No newline at end of file diff --git a/test/WebSites/ApplicationModelWebSite/Conventions/ApplicationDescription.cs b/test/WebSites/ApplicationModelWebSite/Conventions/ApplicationDescription.cs new file mode 100644 index 0000000000..3af96b916e --- /dev/null +++ b/test/WebSites/ApplicationModelWebSite/Conventions/ApplicationDescription.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.Mvc.ApplicationModels; + + +namespace ApplicationModelWebSite +{ + public class ApplicationDescription : IApplicationModelConvention + { + private string _description; + + public ApplicationDescription(string description) + { + _description = description; + } + + public void Apply(ApplicationModel application) + { + application.Properties["description"] = _description; + } + } +} \ No newline at end of file diff --git a/test/WebSites/ApplicationModelWebSite/Conventions/ControllerDescriptionAttribute.cs b/test/WebSites/ApplicationModelWebSite/Conventions/ControllerDescriptionAttribute.cs new file mode 100644 index 0000000000..28bfc990ec --- /dev/null +++ b/test/WebSites/ApplicationModelWebSite/Conventions/ControllerDescriptionAttribute.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. + +using System; +using Microsoft.AspNet.Mvc.ApplicationModels; + + +namespace ApplicationModelWebSite +{ + public class ControllerDescriptionAttribute : Attribute, IControllerModelConvention + { + private object _value; + + public ControllerDescriptionAttribute(object value) + { + _value = value; + } + + public void Apply(ControllerModel model) + { + model.Properties["description"] = _value; + } + } +} \ No newline at end of file diff --git a/test/WebSites/ApplicationModelWebSite/Startup.cs b/test/WebSites/ApplicationModelWebSite/Startup.cs index 0773fe961a..d390aa5459 100644 --- a/test/WebSites/ApplicationModelWebSite/Startup.cs +++ b/test/WebSites/ApplicationModelWebSite/Startup.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using Microsoft.AspNet.Builder; +using Microsoft.AspNet.Mvc; using Microsoft.Framework.DependencyInjection; namespace ApplicationModelWebSite @@ -15,6 +16,11 @@ namespace ApplicationModelWebSite app.UseServices(services => { services.AddMvc(configuration); + + services.Configure(options => + { + options.ApplicationModelConventions.Add(new ApplicationDescription("Common Application Description")); + }); }); app.UseMvc(routes =>