diff --git a/Mvc.sln b/Mvc.sln index 5506ce9d33..f5b4c9e0f9 100644 --- a/Mvc.sln +++ b/Mvc.sln @@ -37,6 +37,8 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Mvc.Functi EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "BasicWebSite", "test\WebSites\BasicWebSite\BasicWebSite.kproj", "{34DF1487-12C6-476C-BE0A-F31DF1939AE5}" EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "ActivatorWebSite", "test\WebSites\ActivatorWebSite\ActivatorWebSite.kproj", "{DB79BCBA-9538-4A53-87D9-77728E2BAA39}" +EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "InlineConstraintsWebSite", "test\WebSites\InlineConstraintsWebSite\InlineConstraintsWebSite.kproj", "{EA34877F-1AC1-42B7-B4E6-15A093F40CAE}" EndProject Global @@ -179,6 +181,16 @@ Global {34DF1487-12C6-476C-BE0A-F31DF1939AE5}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {34DF1487-12C6-476C-BE0A-F31DF1939AE5}.Release|Mixed Platforms.Build.0 = Release|Any CPU {34DF1487-12C6-476C-BE0A-F31DF1939AE5}.Release|x86.ActiveCfg = Release|Any CPU + {DB79BCBA-9538-4A53-87D9-77728E2BAA39}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DB79BCBA-9538-4A53-87D9-77728E2BAA39}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DB79BCBA-9538-4A53-87D9-77728E2BAA39}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {DB79BCBA-9538-4A53-87D9-77728E2BAA39}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {DB79BCBA-9538-4A53-87D9-77728E2BAA39}.Debug|x86.ActiveCfg = Debug|Any CPU + {DB79BCBA-9538-4A53-87D9-77728E2BAA39}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DB79BCBA-9538-4A53-87D9-77728E2BAA39}.Release|Any CPU.Build.0 = Release|Any CPU + {DB79BCBA-9538-4A53-87D9-77728E2BAA39}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {DB79BCBA-9538-4A53-87D9-77728E2BAA39}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {DB79BCBA-9538-4A53-87D9-77728E2BAA39}.Release|x86.ActiveCfg = Release|Any CPU {EA34877F-1AC1-42B7-B4E6-15A093F40CAE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {EA34877F-1AC1-42B7-B4E6-15A093F40CAE}.Debug|Any CPU.Build.0 = Debug|Any CPU {EA34877F-1AC1-42B7-B4E6-15A093F40CAE}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU @@ -208,6 +220,7 @@ Global {16703B76-C9F7-4C75-AE6C-53D92E308E3C} = {3BA657BF-28B1-42DA-B5B0-1C4601FCF7B1} {323D0C04-B518-4A8F-8A8E-3546AD153D34} = {3BA657BF-28B1-42DA-B5B0-1C4601FCF7B1} {34DF1487-12C6-476C-BE0A-F31DF1939AE5} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C} + {DB79BCBA-9538-4A53-87D9-77728E2BAA39} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C} {EA34877F-1AC1-42B7-B4E6-15A093F40CAE} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C} EndGlobalSection EndGlobal diff --git a/samples/MvcSample.Web/Home2Controller.cs b/samples/MvcSample.Web/Home2Controller.cs index 214e5fe1a2..1ec20284c8 100644 --- a/samples/MvcSample.Web/Home2Controller.cs +++ b/samples/MvcSample.Web/Home2Controller.cs @@ -8,15 +8,12 @@ namespace MvcSample.Web.RandomNameSpace { private User _user = new User() { Name = "User Name", Address = "Home Address" }; - public HttpContext Context + [Activate] + public HttpResponse Response { - get - { - return ActionContext.HttpContext; - } + get; set; } - // The property ActionContext gets injected by InitializeController from DefaultControllerFactory. public ActionContext ActionContext { get; set; } public string Index() @@ -42,7 +39,7 @@ namespace MvcSample.Web.RandomNameSpace public void Raw() { - Context.Response.WriteAsync("Hello World raw"); + Response.WriteAsync("Hello World raw"); } public ActionResult UserJson() diff --git a/src/Microsoft.AspNet.Mvc.Core/ActivateAttribute.cs b/src/Microsoft.AspNet.Mvc.Core/ActivateAttribute.cs new file mode 100644 index 0000000000..463e55d774 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/ActivateAttribute.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 System; + +namespace Microsoft.AspNet.Mvc +{ + /// + /// Specifies that a property or parameter value should be initialized via the dependency injection + /// container for activated types. + /// + [AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter, AllowMultiple = false)] + public sealed class ActivateAttribute : Attribute + { + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/Controller.cs b/src/Microsoft.AspNet.Mvc.Core/Controller.cs index 6ae4224016..9e7b5e6d2b 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Controller.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Controller.cs @@ -15,15 +15,6 @@ namespace Microsoft.AspNet.Mvc public class Controller : IActionFilter, IAsyncActionFilter { private DynamicViewData _viewBag; - private IServiceProvider _serviceProvider; - private IViewEngine _viewEngine; - - [NonAction] - public virtual void Initialize(IServiceProvider serviceProvider, IViewEngine viewEngine) - { - _serviceProvider = serviceProvider; - _viewEngine = viewEngine; - } public HttpContext Context { @@ -41,8 +32,16 @@ namespace Microsoft.AspNet.Mvc } } + [Activate] public ActionContext ActionContext { get; set; } + [Activate] + public IServiceProvider ServiceProvider { get; set; } + + [Activate] + public IViewEngine ViewEngine { get; set; } + + [Activate] public IUrlHelper Url { get; set; } public IPrincipal User @@ -58,6 +57,7 @@ namespace Microsoft.AspNet.Mvc } } + [Activate] public ViewDataDictionary ViewData { get; set; } public dynamic ViewBag @@ -122,7 +122,7 @@ namespace Microsoft.AspNet.Mvc ViewData.Model = model; } - return new ViewResult(_serviceProvider, _viewEngine) + return new ViewResult(ServiceProvider, ViewEngine) { ViewName = viewName, ViewData = ViewData, diff --git a/src/Microsoft.AspNet.Mvc.Core/DefaultControllerActivator.cs b/src/Microsoft.AspNet.Mvc.Core/DefaultControllerActivator.cs new file mode 100644 index 0000000000..87918e6172 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/DefaultControllerActivator.cs @@ -0,0 +1,131 @@ +// 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.Collections.Concurrent; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Reflection; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Mvc.Core; +using Microsoft.AspNet.Mvc.ModelBinding; +using Microsoft.Framework.DependencyInjection; + +namespace Microsoft.AspNet.Mvc +{ + /// + /// Represents the that is registered by default. + /// + public class DefaultControllerActivator : IControllerActivator + { + private readonly Func _getPropertiesToActivate; + private readonly Func _createActivateInfo; + private readonly ReadOnlyDictionary> _valueAccessorLookup; + private readonly ConcurrentDictionary _injectActions; + + /// + /// Initializes a new instance of the DefaultControllerActivator class. + /// + public DefaultControllerActivator() + { + _valueAccessorLookup = CreateValueAccessorLookup(); + _getPropertiesToActivate = GetPropertiesToActivate; + _createActivateInfo = CreateActivateInfo; + _injectActions = new ConcurrentDictionary(); + } + + /// + /// Activates the specified controller by using the specified action context. + /// + /// The controller to activate. + /// The context of the executing action. + public void Activate([NotNull] object controller, [NotNull] ActionContext context) + { + var controllerType = controller.GetType(); + var controllerTypeInfo = controllerType.GetTypeInfo(); + if (controllerTypeInfo.IsValueType) + { + var message = Resources.FormatValueTypesCannotBeActivated(GetType().FullName); + throw new InvalidOperationException(message); + } + var propertiesToActivate = _injectActions.GetOrAdd(controllerType, + _getPropertiesToActivate); + + for (var i = 0; i < propertiesToActivate.Length; i++) + { + var activateInfo = propertiesToActivate[i]; + activateInfo.Activate(controller, context); + } + } + + protected virtual ReadOnlyDictionary> CreateValueAccessorLookup() + { + var dictionary = new Dictionary> + { + { typeof(ActionContext), (context) => context }, + { typeof(HttpContext), (context) => context.HttpContext }, + { typeof(HttpRequest), (context) => context.HttpContext.Request }, + { typeof(HttpResponse), (context) => context.HttpContext.Response }, + { + typeof(ViewDataDictionary), + (context) => + { + var serviceProvider = context.HttpContext.RequestServices; + return new ViewDataDictionary( + serviceProvider.GetService(), + context.ModelState); + } + } + }; + return new ReadOnlyDictionary>(dictionary); + } + + private PropertyActivator[] GetPropertiesToActivate(Type controllerType) + { + var bindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; + return controllerType.GetProperties(bindingFlags) + .Where(property => property.IsDefined(typeof(ActivateAttribute)) && + property.GetSetMethod(nonPublic: true) != null) + .Select(_createActivateInfo) + .ToArray(); + } + + private PropertyActivator CreateActivateInfo(PropertyInfo property) + { + Func valueAccessor; + if (!_valueAccessorLookup.TryGetValue(property.PropertyType, out valueAccessor)) + { + valueAccessor = (actionContext) => + { + var serviceProvider = actionContext.HttpContext.RequestServices; + return serviceProvider.GetService(property.PropertyType); + }; + } + + return new PropertyActivator(property, + valueAccessor); + } + + private sealed class PropertyActivator + { + private readonly PropertyInfo _propertyInfo; + private readonly Func _valueAccessor; + private readonly Action _fastPropertySetter; + + public PropertyActivator(PropertyInfo propertyInfo, + Func valueAccessor) + { + _propertyInfo = propertyInfo; + _valueAccessor = valueAccessor; + _fastPropertySetter = PropertyHelper.MakeFastPropertySetter(propertyInfo); + } + + public void Activate(object instance, ActionContext context) + { + var value = _valueAccessor(context); + _fastPropertySetter(instance, value); + } + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.Core/DefaultControllerFactory.cs b/src/Microsoft.AspNet.Mvc.Core/DefaultControllerFactory.cs index 5de08df966..d46eca03f5 100644 --- a/src/Microsoft.AspNet.Mvc.Core/DefaultControllerFactory.cs +++ b/src/Microsoft.AspNet.Mvc.Core/DefaultControllerFactory.cs @@ -3,20 +3,23 @@ using System; using Microsoft.AspNet.Mvc.Core; -using Microsoft.AspNet.Mvc.ModelBinding; using Microsoft.Framework.DependencyInjection; namespace Microsoft.AspNet.Mvc { public class DefaultControllerFactory : IControllerFactory { - private readonly ITypeActivator _activator; private readonly IServiceProvider _serviceProvider; + private readonly ITypeActivator _typeActivator; + private readonly IControllerActivator _controllerActivator; - public DefaultControllerFactory(IServiceProvider serviceProvider, ITypeActivator activator) + public DefaultControllerFactory(IServiceProvider serviceProvider, + ITypeActivator typeActivator, + IControllerActivator controllerActivator) { _serviceProvider = serviceProvider; - _activator = activator; + _typeActivator = typeActivator; + _controllerActivator = controllerActivator; } public object CreateController(ActionContext actionContext) @@ -30,11 +33,12 @@ namespace Microsoft.AspNet.Mvc "actionContext"); } - var controller = _activator.CreateInstance( + var controller = _typeActivator.CreateInstance( _serviceProvider, actionDescriptor.ControllerDescriptor.ControllerTypeInfo.AsType()); - InitializeController(controller, actionContext); + actionContext.Controller = controller; + _controllerActivator.Activate(controller, actionContext); return controller; } @@ -48,20 +52,5 @@ namespace Microsoft.AspNet.Mvc disposableController.Dispose(); } } - - private void InitializeController(object controller, ActionContext actionContext) - { - Injector.InjectProperty(controller, "ActionContext", actionContext); - - var viewData = new ViewDataDictionary( - _serviceProvider.GetService(), - actionContext.ModelState); - Injector.InjectProperty(controller, "ViewData", viewData); - - var urlHelper = _serviceProvider.GetService(); - Injector.InjectProperty(controller, "Url", urlHelper); - - Injector.CallInitializer(controller, _serviceProvider); - } } } diff --git a/src/Microsoft.AspNet.Mvc.Core/IControllerActivator.cs b/src/Microsoft.AspNet.Mvc.Core/IControllerActivator.cs new file mode 100644 index 0000000000..e1f837f636 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/IControllerActivator.cs @@ -0,0 +1,18 @@ +// 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 +{ + /// + /// Provides methods to activate an instantiated controller. + /// + public interface IControllerActivator + { + /// + /// When implemented in a type, activates an instantiated controller. + /// + /// The controller to activate. + /// The for the executing action. + void Activate(object controller, ActionContext context); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/Internal/PropertyHelper.cs b/src/Microsoft.AspNet.Mvc.Core/Internal/PropertyHelper.cs index d2e5fb6df0..893f54cbef 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Internal/PropertyHelper.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Internal/PropertyHelper.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Concurrent; -using System.Collections.Generic; using System.Diagnostics.Contracts; using System.Linq; using System.Reflection; @@ -21,6 +20,9 @@ namespace Microsoft.AspNet.Mvc private static readonly MethodInfo CallPropertyGetterByReferenceOpenGenericMethod = typeof(PropertyHelper).GetTypeInfo().GetDeclaredMethod("CallPropertyGetterByReference"); + private static readonly MethodInfo CallPropertySetterOpenGenericMethod = + typeof(PropertyHelper).GetTypeInfo().GetDeclaredMethod("CallPropertySetter"); + private static readonly ConcurrentDictionary ReflectionCache = new ConcurrentDictionary(); @@ -109,6 +111,45 @@ namespace Microsoft.AspNet.Mvc return (Func)callPropertyGetterDelegate; } + /// + /// Creates a single fast property setter for reference types. The result is not cached. + /// + /// propertyInfo to extract the setter for. + /// a fast getter. + /// + /// This method is more memory efficient than a dynamically compiled lambda, and about the + /// same speed. This only works for reference types. + /// + public static Action MakeFastPropertySetter(PropertyInfo propertyInfo) + { + Contract.Assert(propertyInfo != null); + Contract.Assert(!propertyInfo.DeclaringType.GetTypeInfo().IsValueType); + + var setMethod = propertyInfo.SetMethod; + Contract.Assert(setMethod != null); + Contract.Assert(!setMethod.IsStatic); + Contract.Assert(setMethod.ReturnType == typeof(void)); + var parameters = setMethod.GetParameters(); + Contract.Assert(parameters.Length == 1); + + // Instance methods in the CLR can be turned into static methods where the first parameter + // is open over "target". This parameter is always passed by reference, so we have a code + // path for value types and a code path for reference types. + var typeInput = setMethod.DeclaringType; + var parameterType = parameters[0].ParameterType; + + // Create a delegate TDeclaringType -> { TDeclaringType.Property = TValue; } + var propertySetterAsAction = + setMethod.CreateDelegate(typeof(Action<,>).MakeGenericType(typeInput, parameterType)); + var callPropertySetterClosedGenericMethod = + CallPropertySetterOpenGenericMethod.MakeGenericMethod(typeInput, parameterType); + var callPropertySetterDelegate = + callPropertySetterClosedGenericMethod.CreateDelegate( + typeof(Action), propertySetterAsAction); + + return (Action)callPropertySetterDelegate; + } + private static PropertyHelper CreateInstance(PropertyInfo property) { return new PropertyHelper(property); @@ -131,6 +172,14 @@ namespace Microsoft.AspNet.Mvc return getter(ref unboxed); } + private static void CallPropertySetter( + Action setter, + object target, + object value) + { + setter((TDeclaringType)target, (TValue)value); + } + protected static PropertyHelper[] GetProperties( object instance, Func createPropertyHelper, diff --git a/src/Microsoft.AspNet.Mvc.Core/Microsoft.AspNet.Mvc.Core.kproj b/src/Microsoft.AspNet.Mvc.Core/Microsoft.AspNet.Mvc.Core.kproj index 8b33e8ec56..16c25e8b50 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Microsoft.AspNet.Mvc.Core.kproj +++ b/src/Microsoft.AspNet.Mvc.Core/Microsoft.AspNet.Mvc.Core.kproj @@ -47,6 +47,7 @@ + @@ -130,7 +131,9 @@ + + diff --git a/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs index 8f7246a32f..bbaaabeafb 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs @@ -1018,6 +1018,22 @@ namespace Microsoft.AspNet.Mvc.Core return string.Format(CultureInfo.CurrentCulture, GetString("ActionResult_ActionReturnValueCannotBeNull"), p0); } + /// + /// Value types cannot be activated by '{0}'. + /// + internal static string ValueTypesCannotBeActivated + { + get { return GetString("ValueTypesCannotBeActivated"); } + } + + /// + /// Value types cannot be activated by '{0}'. + /// + internal static string FormatValueTypesCannotBeActivated(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("ValueTypesCannotBeActivated"), p0); + } + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/src/Microsoft.AspNet.Mvc.Core/ReflectedActionInvokerProvider.cs b/src/Microsoft.AspNet.Mvc.Core/ReflectedActionInvokerProvider.cs index 76854d6477..c42754a6a8 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ReflectedActionInvokerProvider.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ReflectedActionInvokerProvider.cs @@ -8,20 +8,17 @@ namespace Microsoft.AspNet.Mvc { public class ReflectedActionInvokerProvider : IActionInvokerProvider { - private readonly IServiceProvider _serviceProvider; private readonly IControllerFactory _controllerFactory; private readonly IActionBindingContextProvider _bindingProvider; private readonly INestedProviderManager _filterProvider; public ReflectedActionInvokerProvider(IControllerFactory controllerFactory, IActionBindingContextProvider bindingProvider, - INestedProviderManager filterProvider, - IServiceProvider serviceProvider) + INestedProviderManager filterProvider) { _controllerFactory = controllerFactory; _bindingProvider = bindingProvider; _filterProvider = filterProvider; - _serviceProvider = serviceProvider; } public int Order diff --git a/src/Microsoft.AspNet.Mvc.Core/Resources.resx b/src/Microsoft.AspNet.Mvc.Core/Resources.resx index 34ce835a76..6b2eee5841 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Resources.resx +++ b/src/Microsoft.AspNet.Mvc.Core/Resources.resx @@ -306,4 +306,7 @@ Cannot return null from an action method with a return type of '{0}'. + + Value types cannot be activated by '{0}'. + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc/MvcServices.cs b/src/Microsoft.AspNet.Mvc/MvcServices.cs index ca27c04c9e..4c54d12bd5 100644 --- a/src/Microsoft.AspNet.Mvc/MvcServices.cs +++ b/src/Microsoft.AspNet.Mvc/MvcServices.cs @@ -26,6 +26,7 @@ namespace Microsoft.AspNet.Mvc var describe = new ServiceDescriber(configuration); yield return describe.Transient(); + yield return describe.Singleton(); yield return describe.Scoped(); yield return describe.Transient(); yield return describe.Transient(); diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/DefaultControllerActivatorTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/DefaultControllerActivatorTest.cs new file mode 100644 index 0000000000..3f1d310dca --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Core.Test/DefaultControllerActivatorTest.cs @@ -0,0 +1,151 @@ +// 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. + +#if NET45 +using System; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Mvc.ModelBinding; +using Microsoft.AspNet.Routing; +using Moq; +using Xunit; + +namespace Microsoft.AspNet.Mvc.Core.Test +{ + public class DefaultControllerActivatorTest + { + [Fact] + public void Activate_SetsPropertiesFromActionContextHierarchy() + { + // Arrange + var httpRequest = Mock.Of(); + var httpContext = new Mock(); + httpContext.SetupGet(c => c.Request) + .Returns(httpRequest); + httpContext.SetupGet(c => c.RequestServices) + .Returns(Mock.Of()); + var routeContext = new RouteContext(httpContext.Object); + var controller = new TestController(); + var context = new ActionContext(routeContext, new ActionDescriptor()) + { + Controller = controller + }; + var activator = new DefaultControllerActivator(); + + // Act + activator.Activate(controller, context); + + // Assert + Assert.Same(context, controller.ActionContext); + Assert.Same(httpContext.Object, controller.HttpContext); + Assert.Same(httpRequest, controller.GetHttpRequest()); + } + + [Fact] + public void Activate_SetsViewDatDictionary() + { + // Arrange + var service = new Mock(); + service.Setup(s => s.GetService(typeof(IModelMetadataProvider))) + .Returns(Mock.Of()); + + var httpContext = new Mock(); + httpContext.SetupGet(c => c.RequestServices) + .Returns(service.Object); + var routeContext = new RouteContext(httpContext.Object); + var controller = new TestController(); + var context = new ActionContext(routeContext, new ActionDescriptor()) + { + Controller = controller + }; + var activator = new DefaultControllerActivator(); + + // Act + activator.Activate(controller, context); + + // Assert + Assert.NotNull(controller.GetViewData()); + } + + [Fact] + public void Activate_PopulatesServicesFromServiceContainer() + { + // Arrange + var urlHelper = Mock.Of(); + var service = new Mock(); + service.Setup(s => s.GetService(typeof(IUrlHelper))) + .Returns(urlHelper); + + var httpContext = new Mock(); + httpContext.SetupGet(c => c.RequestServices) + .Returns(service.Object); + var routeContext = new RouteContext(httpContext.Object); + var controller = new TestController(); + var context = new ActionContext(routeContext, new ActionDescriptor()) + { + Controller = controller + }; + var activator = new DefaultControllerActivator(); + + // Act + activator.Activate(controller, context); + + // Assert + Assert.Same(urlHelper, controller.Helper); + } + + [Fact] + public void Activate_IgnoresPropertiesThatAreNotDecoratedWithActivateAttribute() + { + // Arrange + var httpContext = new Mock(); + httpContext.SetupGet(c => c.Response) + .Returns(Mock.Of()); + httpContext.SetupGet(c => c.RequestServices) + .Returns(Mock.Of()); + var routeContext = new RouteContext(httpContext.Object); + var controller = new TestController(); + var context = new ActionContext(routeContext, new ActionDescriptor()) + { + Controller = controller + }; + var activator = new DefaultControllerActivator(); + + // Act + activator.Activate(controller, context); + + // Assert + Assert.Null(controller.Response); + } + + public class TestController + { + [Activate] + public ActionContext ActionContext { get; set; } + + [Activate] + public HttpContext HttpContext { get; set; } + + [Activate] + protected HttpRequest Request { get; set; } + + [Activate] + private ViewDataDictionary ViewData { get; set; } + + [Activate] + public IUrlHelper Helper { get; set; } + + public HttpResponse Response { get; set; } + + public ViewDataDictionary GetViewData() + { + return ViewData; + } + + public HttpRequest GetHttpRequest() + { + return Request; + } + } + } +} +#endif \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/DefaultControllerFactoryTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/DefaultControllerFactoryTest.cs index 7d5945f2ce..71864cb196 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/DefaultControllerFactoryTest.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/DefaultControllerFactoryTest.cs @@ -15,8 +15,9 @@ namespace Microsoft.AspNet.Mvc.Core.Test { // Arrange var factory = new DefaultControllerFactory( - new Mock().Object, - new Mock().Object); + Mock.Of(), + Mock.Of(), + Mock.Of()); var controller = new MyController(); @@ -33,8 +34,9 @@ namespace Microsoft.AspNet.Mvc.Core.Test { // Arrange var factory = new DefaultControllerFactory( - new Mock().Object, - new Mock().Object); + Mock.Of(), + Mock.Of(), + Mock.Of()); var controller = new Object(); diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Microsoft.AspNet.Mvc.Core.Test.kproj b/test/Microsoft.AspNet.Mvc.Core.Test/Microsoft.AspNet.Mvc.Core.Test.kproj index 9506d5302d..76b1c2d20e 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/Microsoft.AspNet.Mvc.Core.Test.kproj +++ b/test/Microsoft.AspNet.Mvc.Core.Test/Microsoft.AspNet.Mvc.Core.Test.kproj @@ -38,6 +38,7 @@ + diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/PropertyHelperTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/PropertyHelperTest.cs index 119f467f0c..691296e5fe 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/PropertyHelperTest.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/PropertyHelperTest.cs @@ -186,10 +186,62 @@ namespace Microsoft.AspNet.Mvc var propAHelper = Assert.Single(helpers.Where(h => h.Name == "PropA")); var propBHelper = Assert.Single(helpers.Where(h => h.Name == "PropB")); - Assert.Equal("Overriden", propAHelper.GetValue(derived)); + Assert.Equal("OverridenpropAValue", propAHelper.GetValue(derived)); Assert.Equal("propBValue", propBHelper.GetValue(derived)); } + [Fact] + public void MakeFastPropertySetter_SetsPropertyValues_ForPublicAndNobPublicProperties() + { + // Arrange + var instance = new BaseClass(); + var typeInfo = instance.GetType().GetTypeInfo(); + var publicProperty = typeInfo.GetDeclaredProperty("PropA"); + var protectedProperty = typeInfo.GetDeclaredProperty("PropProtected"); + var publicPropertySetter = PropertyHelper.MakeFastPropertySetter(publicProperty); + var protectedPropertySetter = PropertyHelper.MakeFastPropertySetter(protectedProperty); + + // Act + publicPropertySetter(instance, "TestPublic"); + protectedPropertySetter(instance, "TestProtected"); + + // Assert + Assert.Equal("TestPublic", instance.PropA); + Assert.Equal("TestProtected", instance.GetPropProtected()); + } + + [Fact] + public void MakeFastPropertySetter_SetsPropertyValues_ForOverridenProperties() + { + // Arrange + var instance = new DerivedClassWithOverride(); + var typeInfo = instance.GetType().GetTypeInfo(); + var property = typeInfo.GetDeclaredProperty("PropA"); + var propertySetter = PropertyHelper.MakeFastPropertySetter(property); + + // Act + propertySetter(instance, "Test value"); + + // Assert + Assert.Equal("OverridenTest value", instance.PropA); + } + + [Fact] + public void MakeFastPropertySetter_SetsPropertyValues_ForNewedProperties() + { + // Arrange + var instance = new DerivedClassWithNew(); + var typeInfo = instance.GetType().GetTypeInfo(); + var property = typeInfo.GetDeclaredProperty("PropB"); + var propertySetter = PropertyHelper.MakeFastPropertySetter(property); + + // Act + propertySetter(instance, "Test value"); + + // Assert + Assert.Equal("NewedTest value", instance.PropB); + } + private class Static { public static int Prop2 { get; set; } @@ -220,6 +272,11 @@ namespace Microsoft.AspNet.Mvc public string PropA { get; set; } protected string PropProtected { get; set; } + + public string GetPropProtected() + { + return PropProtected; + } } public class DerivedClass : BaseClass @@ -235,12 +292,24 @@ namespace Microsoft.AspNet.Mvc public class DerivedClassWithNew : BaseClassWithVirtual { - public new string PropB { get { return "Newed"; } } + private string _value = "Newed"; + + public new string PropB + { + get { return _value; } + set { _value = "Newed" + value; } + } } public class DerivedClassWithOverride : BaseClassWithVirtual { - public override string PropA { get { return "Overriden"; } } + private string _value = "Overriden"; + + public override string PropA + { + get { return _value; } + set { _value = "Overriden" + value; } + } } } } diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ReflectedActionInvokerTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ReflectedActionInvokerTest.cs index 9e3536855b..6d4f8c56dc 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/ReflectedActionInvokerTest.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ReflectedActionInvokerTest.cs @@ -1297,7 +1297,7 @@ namespace Microsoft.AspNet.Mvc routeData: new RouteData(), actionDescriptor: actionDescriptor); - var controllerFactory = new Mock(MockBehavior.Strict); + var controllerFactory = new Mock(); controllerFactory.Setup(c => c.CreateController(It.IsAny())).Returns(this); var actionBindingContextProvider = new Mock(MockBehavior.Strict); diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/ActivatorTests.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/ActivatorTests.cs new file mode 100644 index 0000000000..d8f548d633 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/ActivatorTests.cs @@ -0,0 +1,65 @@ +// 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 ActivatorWebSite; +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.TestHost; +using Xunit; + +namespace Microsoft.AspNet.Mvc.FunctionalTests +{ + public class ActivatorTests + { + private readonly IServiceProvider _provider = TestHelper.CreateServices("ActivatorWebSite"); + private readonly Action _app = new Startup().Configure; + + [Fact] + public async Task ControllerThatCannotBeActivated_ThrowsWhenAttemptedToBeInvoked() + { + // Arrange + var server = TestServer.Create(_provider, _app); + var client = server.Handler; + var expectedMessage = "TODO: No service for type 'ActivatorWebSite.CannotBeActivatedController+FakeType' " + + "has been registered."; + + // Act & Assert + var ex = await Assert.ThrowsAsync(() => client.GetAsync("http://localhost/CannotBeActivated/Index")); + Assert.Equal(expectedMessage, ex.Message); + } + + [Fact] + public async Task PropertiesForPocoControllersAreInitialized() + { + // Arrange + var server = TestServer.Create(_provider, _app); + var client = server.Handler; + var expected = "4|some-text"; + + // Act + var result = await client.GetAsync("http://localhost/Plain?foo=some-text"); + + // Assert + Assert.Equal("Fake-Value", result.HttpContext.Response.Headers["X-Fake-Header"]); + var body = await result.HttpContext.Response.ReadBodyAsStringAsync(); + Assert.Equal(expected, body); + } + + [Fact] + public async Task PropertiesForTypesDerivingFromControllerAreInitialized() + { + // Arrange + var server = TestServer.Create(_provider, _app); + var client = server.Handler; + var expected = "Hello world"; + + // Act + var result = await client.GetAsync("http://localhost/Regular"); + + // Assert + var body = await result.HttpContext.Response.ReadBodyAsStringAsync(); + Assert.Equal(expected, body); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/Microsoft.AspNet.Mvc.FunctionalTests.kproj b/test/Microsoft.AspNet.Mvc.FunctionalTests/Microsoft.AspNet.Mvc.FunctionalTests.kproj index 030e6bfb80..6285f25067 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/Microsoft.AspNet.Mvc.FunctionalTests.kproj +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/Microsoft.AspNet.Mvc.FunctionalTests.kproj @@ -30,6 +30,7 @@ + diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/project.json b/test/Microsoft.AspNet.Mvc.FunctionalTests/project.json index ebf960cdeb..1df30bf1d6 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/project.json +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/project.json @@ -4,6 +4,7 @@ }, "dependencies": { "BasicWebSite": "", + "ActivatorWebSite": "", "InlineConstraintsWebSite": "", "Microsoft.AspNet.TestHost": "1.0.0-*", "Microsoft.Framework.Runtime.Interfaces": "1.0.0-*", diff --git a/test/WebSites/ActivatorWebSite/ActivatorWebSite.kproj b/test/WebSites/ActivatorWebSite/ActivatorWebSite.kproj new file mode 100644 index 0000000000..fe131b4912 --- /dev/null +++ b/test/WebSites/ActivatorWebSite/ActivatorWebSite.kproj @@ -0,0 +1,36 @@ + + + + 12.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + db79bcba-9538-4a53-87d9-77728e2baa39 + Library + + + ConsoleDebugger + + + WebDebugger + + + + + + + 2.0 + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/WebSites/ActivatorWebSite/Controllers/CannotBeActivatedController.cs b/test/WebSites/ActivatorWebSite/Controllers/CannotBeActivatedController.cs new file mode 100644 index 0000000000..0bf3e972aa --- /dev/null +++ b/test/WebSites/ActivatorWebSite/Controllers/CannotBeActivatedController.cs @@ -0,0 +1,22 @@ +// 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 ActivatorWebSite +{ + public class CannotBeActivatedController + { + [Activate] + private FakeType Service { get; set; } + + public IActionResult Index() + { + return new NoContentResult(); + } + + private sealed class FakeType + { + } + } +} \ No newline at end of file diff --git a/test/WebSites/ActivatorWebSite/Controllers/PlainController.cs b/test/WebSites/ActivatorWebSite/Controllers/PlainController.cs new file mode 100644 index 0000000000..b634f33e33 --- /dev/null +++ b/test/WebSites/ActivatorWebSite/Controllers/PlainController.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 Microsoft.AspNet.Http; +using Microsoft.AspNet.Mvc; + +namespace ActivatorWebSite +{ + public class PlainController + { + [Activate] + public MyService Service { get; set; } + + [Activate] + public HttpRequest Request { get; set; } + + [Activate] + public HttpResponse Response { get; set; } + + public IActionResult Index() + { + Response.Headers["X-Fake-Header"] = "Fake-Value"; + + var value = Request.Query["foo"]; + return new ContentResult { Content = Service.Random + "|" + value }; + } + } +} \ No newline at end of file diff --git a/test/WebSites/ActivatorWebSite/Controllers/RegularController.cs b/test/WebSites/ActivatorWebSite/Controllers/RegularController.cs new file mode 100644 index 0000000000..cbf8d9d25b --- /dev/null +++ b/test/WebSites/ActivatorWebSite/Controllers/RegularController.cs @@ -0,0 +1,19 @@ +// 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 ActivatorWebSite +{ + public class RegularController : Controller + { + public void Index() + { + // This verifies that ModelState and Context are activated. + if (ModelState.IsValid) + { + Context.Response.WriteAsync("Hello world").Wait(); + } + } + } +} \ No newline at end of file diff --git a/test/WebSites/ActivatorWebSite/Services/MyService.cs b/test/WebSites/ActivatorWebSite/Services/MyService.cs new file mode 100644 index 0000000000..04ac32b57a --- /dev/null +++ b/test/WebSites/ActivatorWebSite/Services/MyService.cs @@ -0,0 +1,15 @@ +// 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; + +namespace ActivatorWebSite +{ + public class MyService + { + public int Random + { + get { return 4; } + } + } +} \ No newline at end of file diff --git a/test/WebSites/ActivatorWebSite/Startup.cs b/test/WebSites/ActivatorWebSite/Startup.cs new file mode 100644 index 0000000000..f0a1431a93 --- /dev/null +++ b/test/WebSites/ActivatorWebSite/Startup.cs @@ -0,0 +1,30 @@ +// 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.AspNet.Routing; +using Microsoft.Framework.DependencyInjection; + +namespace ActivatorWebSite +{ + public class Startup + { + public void Configure(IBuilder app) + { + // Set up application services + app.UseServices(services => + { + // Add MVC services to the services container + services.AddMvc(); + services.AddInstance(new MyService()); + }); + + // Add MVC to the request pipeline + app.UseMvc(routes => + { + routes.MapRoute("ActionAsMethod", "{controller}/{action}", + defaults: new { controller = "Home", action = "Index" }); + }); + } + } +} diff --git a/test/WebSites/ActivatorWebSite/project.json b/test/WebSites/ActivatorWebSite/project.json new file mode 100644 index 0000000000..753f360c85 --- /dev/null +++ b/test/WebSites/ActivatorWebSite/project.json @@ -0,0 +1,10 @@ +{ + "dependencies": { + "Helios": "0.1-alpha-*", + "Microsoft.AspNet.Mvc": "" + }, + "configurations": { + "net45": { }, + "k10": { } + } +}