diff --git a/Mvc.sln b/Mvc.sln index 87891aba29..45da5d60ac 100644 --- a/Mvc.sln +++ b/Mvc.sln @@ -132,6 +132,10 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Mvc.Xml.Te EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "FormatFilterWebSite", "test\WebSites\FormatFilterWebSite\FormatFilterWebSite.kproj", "{AC9BE567-540E-4C70-90C2-AAF021307A80}" EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "ControllersFromServicesWebSite", "test\WebSites\ControllersFromServicesWebSite\ControllersFromServicesWebSite.kproj", "{983741B2-4424-4ED1-9B03-7675A67230C8}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "ControllersFromServicesClassLibrary", "test\WebSites\ControllersFromServicesClassLibrary\ControllersFromServicesClassLibrary.kproj", "{551DC89E-2A13-4CF2-83D7-1ADD802443D5}" +EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "RazorCompilerCacheWebSite", "test\WebSites\RazorCompilerCacheWebSite\RazorCompilerCacheWebSite.kproj", "{42C5D417-4060-48F4-BB28-E9E179007779}" EndProject Global @@ -732,6 +736,18 @@ Global {87AB84B2-22C1-43C6-BB8A-1D327B446FB0}.Release|Mixed Platforms.Build.0 = Release|Any CPU {87AB84B2-22C1-43C6-BB8A-1D327B446FB0}.Release|x86.ActiveCfg = Release|Any CPU {87AB84B2-22C1-43C6-BB8A-1D327B446FB0}.Release|x86.Build.0 = Release|Any CPU + {87AB84B2-22C1-43C6-BB8A-1D327B446FB0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {87AB84B2-22C1-43C6-BB8A-1D327B446FB0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {87AB84B2-22C1-43C6-BB8A-1D327B446FB0}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {87AB84B2-22C1-43C6-BB8A-1D327B446FB0}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {87AB84B2-22C1-43C6-BB8A-1D327B446FB0}.Debug|x86.ActiveCfg = Debug|Any CPU + {87AB84B2-22C1-43C6-BB8A-1D327B446FB0}.Debug|x86.Build.0 = Debug|Any CPU + {87AB84B2-22C1-43C6-BB8A-1D327B446FB0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {87AB84B2-22C1-43C6-BB8A-1D327B446FB0}.Release|Any CPU.Build.0 = Release|Any CPU + {87AB84B2-22C1-43C6-BB8A-1D327B446FB0}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {87AB84B2-22C1-43C6-BB8A-1D327B446FB0}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {87AB84B2-22C1-43C6-BB8A-1D327B446FB0}.Release|x86.ActiveCfg = Release|Any CPU + {87AB84B2-22C1-43C6-BB8A-1D327B446FB0}.Release|x86.Build.0 = Release|Any CPU {22019146-BDFA-442E-8C8E-345FB9644578}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {22019146-BDFA-442E-8C8E-345FB9644578}.Debug|Any CPU.Build.0 = Debug|Any CPU {22019146-BDFA-442E-8C8E-345FB9644578}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU @@ -756,6 +772,30 @@ Global {AC9BE567-540E-4C70-90C2-AAF021307A80}.Release|Mixed Platforms.Build.0 = Release|Any CPU {AC9BE567-540E-4C70-90C2-AAF021307A80}.Release|x86.ActiveCfg = Release|Any CPU {AC9BE567-540E-4C70-90C2-AAF021307A80}.Release|x86.Build.0 = Release|Any CPU + {983741B2-4424-4ED1-9B03-7675A67230C8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {983741B2-4424-4ED1-9B03-7675A67230C8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {983741B2-4424-4ED1-9B03-7675A67230C8}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {983741B2-4424-4ED1-9B03-7675A67230C8}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {983741B2-4424-4ED1-9B03-7675A67230C8}.Debug|x86.ActiveCfg = Debug|Any CPU + {983741B2-4424-4ED1-9B03-7675A67230C8}.Debug|x86.Build.0 = Debug|Any CPU + {983741B2-4424-4ED1-9B03-7675A67230C8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {983741B2-4424-4ED1-9B03-7675A67230C8}.Release|Any CPU.Build.0 = Release|Any CPU + {983741B2-4424-4ED1-9B03-7675A67230C8}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {983741B2-4424-4ED1-9B03-7675A67230C8}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {983741B2-4424-4ED1-9B03-7675A67230C8}.Release|x86.ActiveCfg = Release|Any CPU + {983741B2-4424-4ED1-9B03-7675A67230C8}.Release|x86.Build.0 = Release|Any CPU + {551DC89E-2A13-4CF2-83D7-1ADD802443D5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {551DC89E-2A13-4CF2-83D7-1ADD802443D5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {551DC89E-2A13-4CF2-83D7-1ADD802443D5}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {551DC89E-2A13-4CF2-83D7-1ADD802443D5}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {551DC89E-2A13-4CF2-83D7-1ADD802443D5}.Debug|x86.ActiveCfg = Debug|Any CPU + {551DC89E-2A13-4CF2-83D7-1ADD802443D5}.Debug|x86.Build.0 = Debug|Any CPU + {551DC89E-2A13-4CF2-83D7-1ADD802443D5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {551DC89E-2A13-4CF2-83D7-1ADD802443D5}.Release|Any CPU.Build.0 = Release|Any CPU + {551DC89E-2A13-4CF2-83D7-1ADD802443D5}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {551DC89E-2A13-4CF2-83D7-1ADD802443D5}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {551DC89E-2A13-4CF2-83D7-1ADD802443D5}.Release|x86.ActiveCfg = Release|Any CPU + {551DC89E-2A13-4CF2-83D7-1ADD802443D5}.Release|x86.Build.0 = Release|Any CPU {42C5D417-4060-48F4-BB28-E9E179007779}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {42C5D417-4060-48F4-BB28-E9E179007779}.Debug|Any CPU.Build.0 = Debug|Any CPU {42C5D417-4060-48F4-BB28-E9E179007779}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU @@ -832,6 +872,8 @@ Global {87AB84B2-22C1-43C6-BB8A-1D327B446FB0} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C} {22019146-BDFA-442E-8C8E-345FB9644578} = {3BA657BF-28B1-42DA-B5B0-1C4601FCF7B1} {AC9BE567-540E-4C70-90C2-AAF021307A80} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C} + {983741B2-4424-4ED1-9B03-7675A67230C8} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C} + {551DC89E-2A13-4CF2-83D7-1ADD802443D5} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C} {42C5D417-4060-48F4-BB28-E9E179007779} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C} EndGlobalSection EndGlobal diff --git a/samples/MvcSample.Web/HomeController.cs b/samples/MvcSample.Web/HomeController.cs index 9389183c64..829b8f0b81 100644 --- a/samples/MvcSample.Web/HomeController.cs +++ b/samples/MvcSample.Web/HomeController.cs @@ -92,7 +92,7 @@ namespace MvcSample.Web return View("MyView", user); } - [Activate] + [FromServices] public IHostingEnvironment HostingEnvironment { get; set; } /// diff --git a/src/Microsoft.AspNet.Mvc.Core/ApplicationModels/DefaultControllerModelBuilder.cs b/src/Microsoft.AspNet.Mvc.Core/ApplicationModels/DefaultControllerModelBuilder.cs index e06c9e1e69..5c44f1240b 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ApplicationModels/DefaultControllerModelBuilder.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ApplicationModels/DefaultControllerModelBuilder.cs @@ -7,7 +7,6 @@ using System.Linq; using System.Reflection; using Microsoft.AspNet.Mvc.Description; using Microsoft.AspNet.Mvc.Filters; -using Microsoft.AspNet.Mvc.Logging; using Microsoft.AspNet.Mvc.Routing; using Microsoft.Framework.Logging; @@ -34,11 +33,6 @@ namespace Microsoft.AspNet.Mvc.ApplicationModels /// public ControllerModel BuildControllerModel([NotNull] TypeInfo typeInfo) { - if (!IsController(typeInfo)) - { - return null; - } - var controllerModel = CreateControllerModel(typeInfo); foreach (var methodInfo in typeInfo.AsType().GetMethods()) @@ -57,54 +51,6 @@ namespace Microsoft.AspNet.Mvc.ApplicationModels return controllerModel; } - /// - /// Returns true if the is a controller. Otherwise false. - /// - /// The . - /// true if the is a controller. Otherwise false. - /// - /// Override this method to provide custom logic to determine which types are considered controllers. - /// - protected virtual bool IsController([NotNull] TypeInfo typeInfo) - { - var status = ControllerStatus.IsController; - - if (!typeInfo.IsClass) - { - status |= ControllerStatus.IsNotAClass; - } - if (typeInfo.IsAbstract) - { - status |= ControllerStatus.IsAbstract; - } - // We only consider public top-level classes as controllers. IsPublic returns false for nested - // classes, regardless of visibility modifiers - if (!typeInfo.IsPublic) - { - status |= ControllerStatus.IsNotPublicOrTopLevel; - } - if (typeInfo.ContainsGenericParameters) - { - status |= ControllerStatus.ContainsGenericParameters; - } - if (typeInfo.Name.Equals("Controller", StringComparison.OrdinalIgnoreCase)) - { - status |= ControllerStatus.NameIsController; - } - if (!typeInfo.Name.EndsWith("Controller", StringComparison.OrdinalIgnoreCase) && - !typeof(Controller).GetTypeInfo().IsAssignableFrom(typeInfo)) - { - status |= ControllerStatus.DoesNotEndWithControllerAndIsNotAssignable; - } - if (_logger.IsEnabled(LogLevel.Verbose)) - { - _logger.WriteVerbose(new IsControllerValues( - typeInfo.AsType(), - status)); - } - return status == ControllerStatus.IsController; - } - /// /// Creates an for the given . /// diff --git a/src/Microsoft.AspNet.Mvc.Core/Controller.cs b/src/Microsoft.AspNet.Mvc.Core/Controller.cs index 4350b2fb62..feb1f22050 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Controller.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Controller.cs @@ -100,10 +100,10 @@ namespace Microsoft.AspNet.Mvc [Activate] public ActionBindingContext BindingContext { get; set; } - [Activate] + [FromServices] public IModelMetadataProvider MetadataProvider { get; set; } - [Activate] + [FromServices] public IUrlHelper Url { get; set; } public IPrincipal User diff --git a/src/Microsoft.AspNet.Mvc.Core/ControllerActionDescriptorProvider.cs b/src/Microsoft.AspNet.Mvc.Core/ControllerActionDescriptorProvider.cs index 1e6b6589cf..75aade5bf6 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ControllerActionDescriptorProvider.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ControllerActionDescriptorProvider.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Linq; using Microsoft.AspNet.Mvc.ApplicationModels; using Microsoft.AspNet.Mvc.Filters; using Microsoft.AspNet.Mvc.Logging; @@ -15,18 +14,18 @@ namespace Microsoft.AspNet.Mvc public class ControllerActionDescriptorProvider : IActionDescriptorProvider { private readonly IControllerModelBuilder _applicationModelBuilder; - private readonly IAssemblyProvider _assemblyProvider; + private readonly IControllerTypeProvider _controllerTypeProvider; private readonly IReadOnlyList _globalFilters; private readonly IEnumerable _conventions; private readonly ILogger _logger; - public ControllerActionDescriptorProvider([NotNull] IAssemblyProvider assemblyProvider, + public ControllerActionDescriptorProvider([NotNull] IControllerTypeProvider controllerTypeProvider, [NotNull] IControllerModelBuilder applicationModelBuilder, [NotNull] IGlobalFilterProvider globalFilters, [NotNull] IOptions optionsAccessor, [NotNull] ILoggerFactory loggerFactory) { - _assemblyProvider = assemblyProvider; + _controllerTypeProvider = controllerTypeProvider; _applicationModelBuilder = applicationModelBuilder; _globalFilters = globalFilters.Filters; _conventions = optionsAccessor.Options.Conventions; @@ -66,17 +65,7 @@ namespace Microsoft.AspNet.Mvc applicationModel.Filters.Add(filter); } - var assemblies = _assemblyProvider.CandidateAssemblies; - var types = assemblies.SelectMany(a => a.DefinedTypes); - if (_logger.IsEnabled(LogLevel.Verbose)) - { - foreach (var assembly in assemblies) - { - _logger.WriteVerbose(new AssemblyValues(assembly)); - } - } - - foreach (var type in types) + foreach (var type in _controllerTypeProvider.ControllerTypes) { var controllerModel = _applicationModelBuilder.BuildControllerModel(type); if (controllerModel != null) diff --git a/src/Microsoft.AspNet.Mvc.Core/DefaultAssemblyProvider.cs b/src/Microsoft.AspNet.Mvc.Core/DefaultAssemblyProvider.cs index aa7f43835e..f54ed43e4d 100644 --- a/src/Microsoft.AspNet.Mvc.Core/DefaultAssemblyProvider.cs +++ b/src/Microsoft.AspNet.Mvc.Core/DefaultAssemblyProvider.cs @@ -38,7 +38,8 @@ namespace Microsoft.AspNet.Mvc { get { - return GetCandidateLibraries().SelectMany(l => l.LoadableAssemblies).Select(Load); + return GetCandidateLibraries().SelectMany(l => l.LoadableAssemblies) + .Select(Load); } } diff --git a/src/Microsoft.AspNet.Mvc.Core/DefaultControllerActivator.cs b/src/Microsoft.AspNet.Mvc.Core/DefaultControllerActivator.cs index 69c95be847..615d614ef4 100644 --- a/src/Microsoft.AspNet.Mvc.Core/DefaultControllerActivator.cs +++ b/src/Microsoft.AspNet.Mvc.Core/DefaultControllerActivator.cs @@ -3,106 +3,26 @@ using System; using System.Collections.Concurrent; -using System.Collections.Generic; -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. + /// that uses type activation to create controllers. /// public class DefaultControllerActivator : IControllerActivator { - private readonly Func[]> _getPropertiesToActivate; - private readonly IDictionary> _valueAccessorLookup; - private readonly ConcurrentDictionary[]> _injectActions; + private static readonly Func _createControllerFactory = + type => ActivatorUtilities.CreateFactory(type, Type.EmptyTypes); - /// - /// Initializes a new instance of the DefaultControllerActivator class. - /// - public DefaultControllerActivator() + private readonly ConcurrentDictionary _controllerFactories = + new ConcurrentDictionary(); + + /// + public object Create([NotNull] ActionContext actionContext, [NotNull] Type controllerType) { - _valueAccessorLookup = CreateValueAccessorLookup(); - _injectActions = new ConcurrentDictionary[]>(); - _getPropertiesToActivate = type => - PropertyActivator.GetPropertiesToActivate(type, - typeof(ActivateAttribute), - CreateActivateInfo); - } - - /// - /// Activates the specified controller by using the specified action context. - /// - /// The controller to activate. - /// The context of the executing action. - public virtual 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 IDictionary> 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.GetRequiredService(), - context.ModelState); - } - }, - { - typeof(ActionBindingContext), - (context) => - { - var serviceProvider = context.HttpContext.RequestServices; - var accessor = serviceProvider.GetRequiredService>(); - return accessor.Value; - } - } - }; - return dictionary; - } - - private PropertyActivator CreateActivateInfo( - PropertyInfo property) - { - Func valueAccessor; - if (!_valueAccessorLookup.TryGetValue(property.PropertyType, out valueAccessor)) - { - valueAccessor = (actionContext) => - { - var serviceProvider = actionContext.HttpContext.RequestServices; - return serviceProvider.GetRequiredService(property.PropertyType); - }; - } - - return new PropertyActivator(property, valueAccessor); + var factory = _controllerFactories.GetOrAdd(controllerType, _createControllerFactory); + return factory(actionContext.HttpContext.RequestServices, arguments: null); } } } diff --git a/src/Microsoft.AspNet.Mvc.Core/DefaultControllerFactory.cs b/src/Microsoft.AspNet.Mvc.Core/DefaultControllerFactory.cs index 8f88a1594e..c39ee7b168 100644 --- a/src/Microsoft.AspNet.Mvc.Core/DefaultControllerFactory.cs +++ b/src/Microsoft.AspNet.Mvc.Core/DefaultControllerFactory.cs @@ -2,27 +2,43 @@ // 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.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 { + /// + /// Default implementation for . + /// public class DefaultControllerFactory : IControllerFactory { - private readonly IServiceProvider _serviceProvider; - private readonly ITypeActivator _typeActivator; private readonly IControllerActivator _controllerActivator; + private readonly IDictionary> _valueAccessorLookup; + private readonly ConcurrentDictionary[]> _activateActions; + private readonly Func[]> _getPropertiesToActivate; + private readonly Func> _getRequiredService = GetRequiredService; - public DefaultControllerFactory(IServiceProvider serviceProvider, - ITypeActivator typeActivator, - IControllerActivator controllerActivator) + /// + /// Initializes a new instance of . + /// + /// used to create controller + /// instances. + public DefaultControllerFactory(IControllerActivator controllerActivator) { - _serviceProvider = serviceProvider; - _typeActivator = typeActivator; _controllerActivator = controllerActivator; + _valueAccessorLookup = CreateValueAccessorLookup(); + _activateActions = new ConcurrentDictionary[]>(); + _getPropertiesToActivate = GetPropertiesToActivate; } - public object CreateController(ActionContext actionContext) + /// + public object CreateController([NotNull] ActionContext actionContext) { var actionDescriptor = actionContext.ActionDescriptor as ControllerActionDescriptor; if (actionDescriptor == null) @@ -33,15 +49,14 @@ namespace Microsoft.AspNet.Mvc nameof(actionContext)); } - var controller = _typeActivator.CreateInstance( - _serviceProvider, - actionDescriptor.ControllerTypeInfo.AsType()); - - _controllerActivator.Activate(controller, actionContext); + var controllerType = actionDescriptor.ControllerTypeInfo.AsType(); + var controller = _controllerActivator.Create(actionContext, controllerType); + ActivateProperties(controller, actionContext); return controller; } + /// public void ReleaseController(object controller) { var disposableController = controller as IDisposable; @@ -51,5 +66,111 @@ namespace Microsoft.AspNet.Mvc disposableController.Dispose(); } } + + /// + /// Activates the specified controller using the specified action context. + /// + /// The controller to activate. + /// The context of the executing action. + protected virtual void ActivateProperties([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 = _activateActions.GetOrAdd(controllerType, + _getPropertiesToActivate); + + for (var i = 0; i < propertiesToActivate.Length; i++) + { + var activateInfo = propertiesToActivate[i]; + activateInfo.Activate(controller, context); + } + } + + /// + /// Returns a of property types to delegates used to activate + /// controller properties annotated with . + /// + /// A dictionary containing the property type to activator delegate mapping. + /// Override this method to provide custom activation behavior for controller properties + /// annotated with . + protected virtual IDictionary> 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.GetRequiredService(), + context.ModelState); + } + }, + { + typeof(ActionBindingContext), + (context) => + { + var serviceProvider = context.HttpContext.RequestServices; + var accessor = serviceProvider.GetRequiredService>(); + return accessor.Value; + } + } + }; + + return dictionary; + } + + private PropertyActivator[] GetPropertiesToActivate(Type type) + { + var activatorsForActivateProperties = PropertyActivator.GetPropertiesToActivate( + type, + typeof(ActivateAttribute), + CreateActivateInfo); + var activatorsForFromServiceProperties = PropertyActivator.GetPropertiesToActivate( + type, + typeof(FromServicesAttribute), + CreateFromServicesInfo); + + return Enumerable.Concat(activatorsForActivateProperties, activatorsForFromServiceProperties) + .ToArray(); + } + + private PropertyActivator CreateActivateInfo( + PropertyInfo property) + { + Func valueAccessor; + if (!_valueAccessorLookup.TryGetValue(property.PropertyType, out valueAccessor)) + { + var message = Resources.FormatControllerFactory_PropertyCannotBeActivated( + property.Name, + property.DeclaringType.FullName); + throw new InvalidOperationException(message); + } + + return new PropertyActivator(property, valueAccessor); + } + + private PropertyActivator CreateFromServicesInfo( + PropertyInfo property) + { + var valueAccessor = _getRequiredService(property.PropertyType); + return new PropertyActivator(property, valueAccessor); + } + + private static Func GetRequiredService(Type propertyType) + { + return actionContext => actionContext.HttpContext.RequestServices.GetRequiredService(propertyType); + } } } diff --git a/src/Microsoft.AspNet.Mvc.Core/DefaultControllerTypeProvider.cs b/src/Microsoft.AspNet.Mvc.Core/DefaultControllerTypeProvider.cs new file mode 100644 index 0000000000..4e4ae48742 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/DefaultControllerTypeProvider.cs @@ -0,0 +1,92 @@ +// 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.Generic; +using System.Linq; +using System.Reflection; +using Microsoft.AspNet.Mvc.Logging; +using Microsoft.Framework.Logging; + +namespace Microsoft.AspNet.Mvc +{ + /// + /// A that identifies controller types from assemblies + /// specified by the registered . + /// + public class DefaultControllerTypeProvider : IControllerTypeProvider + { + private readonly IAssemblyProvider _assemblyProvider; + private readonly ILogger _logger; + + /// + /// Initializes a new instance of . + /// + /// that provides assemblies to look for + /// controllers in. + /// The . + public DefaultControllerTypeProvider(IAssemblyProvider assemblyProvider, + ILoggerFactory loggerFactory) + { + _assemblyProvider = assemblyProvider; + _logger = loggerFactory.Create(); + } + + /// + public virtual IEnumerable ControllerTypes + { + get + { + var assemblies = _assemblyProvider.CandidateAssemblies; + if (_logger.IsEnabled(LogLevel.Verbose)) + { + foreach (var assembly in assemblies) + { + _logger.WriteVerbose(new AssemblyValues(assembly)); + } + } + + var types = assemblies.SelectMany(a => a.DefinedTypes); + return types.Where(IsController); + } + } + + /// + /// Returns true if the is a controller. Otherwise false. + /// + /// The . + /// true if the is a controller. Otherwise false. + protected internal virtual bool IsController([NotNull] TypeInfo typeInfo) + { + if (!typeInfo.IsClass) + { + return false; + } + if (typeInfo.IsAbstract) + { + return false; + } + // We only consider public top-level classes as controllers. IsPublic returns false for nested + // classes, regardless of visibility modifiers + if (!typeInfo.IsPublic) + { + return false; + } + if (typeInfo.ContainsGenericParameters) + { + return false; + } + if (typeInfo.Name.Equals("Controller", StringComparison.OrdinalIgnoreCase)) + { + return false; + } + if (!typeInfo.Name.EndsWith("Controller", StringComparison.OrdinalIgnoreCase) && + !typeof(Controller).GetTypeInfo().IsAssignableFrom(typeInfo)) + { + return false; + } + + return true; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/FixedSetAssemblyProvider.cs b/src/Microsoft.AspNet.Mvc.Core/FixedSetAssemblyProvider.cs new file mode 100644 index 0000000000..f5c5364026 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/FixedSetAssemblyProvider.cs @@ -0,0 +1,21 @@ +// 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.Reflection; + +namespace Microsoft.AspNet.Mvc +{ + /// + /// A with a fixed set of candidate assemblies. + /// + public class FixedSetAssemblyProvider : IAssemblyProvider + { + /// + /// Gets the list of candidate assemblies. + /// + public IList CandidateAssemblies { get; } = new List(); + + IEnumerable IAssemblyProvider.CandidateAssemblies => CandidateAssemblies; + } +} diff --git a/src/Microsoft.AspNet.Mvc.Core/FixedSetControllerTypeProvider.cs b/src/Microsoft.AspNet.Mvc.Core/FixedSetControllerTypeProvider.cs new file mode 100644 index 0000000000..b95f43fdc0 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/FixedSetControllerTypeProvider.cs @@ -0,0 +1,39 @@ +// 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; + +namespace Microsoft.AspNet.Mvc +{ + /// + /// A with a fixed set of types that are used as controllers. + /// + public class FixedSetControllerTypeProvider : IControllerTypeProvider + { + /// + /// Initializes a new instance of . + /// + public FixedSetControllerTypeProvider() + : this(Enumerable.Empty()) + { + } + + /// + /// Initializes a new instance of . + /// + /// The sequence of controller . + public FixedSetControllerTypeProvider([NotNull] IEnumerable controllerTypes) + { + ControllerTypes = new List(controllerTypes); + } + + /// + /// Gets the list of controller s. + /// + public IList ControllerTypes { get; } + + IEnumerable IControllerTypeProvider.ControllerTypes => ControllerTypes; + } +} diff --git a/src/Microsoft.AspNet.Mvc.Core/IAssemblyProvider.cs b/src/Microsoft.AspNet.Mvc.Core/IAssemblyProvider.cs index a5a9cdfbb3..8a6fc23710 100644 --- a/src/Microsoft.AspNet.Mvc.Core/IAssemblyProvider.cs +++ b/src/Microsoft.AspNet.Mvc.Core/IAssemblyProvider.cs @@ -6,8 +6,16 @@ using System.Reflection; namespace Microsoft.AspNet.Mvc { + /// + /// Specifies the contract for discovering assemblies that may contain Mvc specific types such as controllers, + /// view components and precompiled views. + /// public interface IAssemblyProvider { + /// + /// Gets the sequence of candidate ies that the application + /// uses for discovery of Mvc specific types. + /// IEnumerable CandidateAssemblies { get; } } } diff --git a/src/Microsoft.AspNet.Mvc.Core/IControllerActivator.cs b/src/Microsoft.AspNet.Mvc.Core/IControllerActivator.cs index e1f837f636..4c7ec1d9c4 100644 --- a/src/Microsoft.AspNet.Mvc.Core/IControllerActivator.cs +++ b/src/Microsoft.AspNet.Mvc.Core/IControllerActivator.cs @@ -1,18 +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 System; + namespace Microsoft.AspNet.Mvc { /// - /// Provides methods to activate an instantiated controller. + /// Provides methods to create a controller. /// public interface IControllerActivator { /// - /// When implemented in a type, activates an instantiated controller. + /// Creates a controller. /// - /// The controller to activate. /// The for the executing action. - void Activate(object controller, ActionContext context); + object Create(ActionContext context, Type controllerType); } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/IControllerFactory.cs b/src/Microsoft.AspNet.Mvc.Core/IControllerFactory.cs index b58197ee92..de6f262d13 100644 --- a/src/Microsoft.AspNet.Mvc.Core/IControllerFactory.cs +++ b/src/Microsoft.AspNet.Mvc.Core/IControllerFactory.cs @@ -3,9 +3,22 @@ namespace Microsoft.AspNet.Mvc { + /// + /// Provides methods for creation and disposal of controllers. + /// public interface IControllerFactory { + /// + /// Creates a new controller for the specified . + /// + /// for the action to execute. + /// The controller. object CreateController(ActionContext actionContext); + + /// + /// Releases a controller instance. + /// + /// The controller. void ReleaseController(object controller); } } diff --git a/src/Microsoft.AspNet.Mvc.Core/IControllerTypeProvider.cs b/src/Microsoft.AspNet.Mvc.Core/IControllerTypeProvider.cs new file mode 100644 index 0000000000..b47259f24a --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/IControllerTypeProvider.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 System.Collections.Generic; +using System.Reflection; + +namespace Microsoft.AspNet.Mvc +{ + /// + /// Provides methods for discovery of controller types. + /// + public interface IControllerTypeProvider + { + /// + /// Gets a of controller s. + /// + IEnumerable ControllerTypes { get; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/Logging/ControllerStatus.cs b/src/Microsoft.AspNet.Mvc.Core/Logging/ControllerStatus.cs deleted file mode 100644 index d64b8f8483..0000000000 --- a/src/Microsoft.AspNet.Mvc.Core/Logging/ControllerStatus.cs +++ /dev/null @@ -1,24 +0,0 @@ -// 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.Logging -{ - /// - /// Indicates the status of a class during controller discovery. - /// All values except 0 represent a reason why a type is not a controller. - /// - [Flags] - public enum ControllerStatus - { - IsController = 0, - IsNotAClass = 1, - IsNotPublicOrTopLevel = 2, - IsAbstract = 4, - ContainsGenericParameters = 8, - // The name of the controller class is "Controller" - NameIsController = 16, - DoesNotEndWithControllerAndIsNotAssignable = 32 - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/Logging/IsControllerValues.cs b/src/Microsoft.AspNet.Mvc.Core/Logging/IsControllerValues.cs deleted file mode 100644 index 3d6ddb8b9e..0000000000 --- a/src/Microsoft.AspNet.Mvc.Core/Logging/IsControllerValues.cs +++ /dev/null @@ -1,36 +0,0 @@ -// 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.Framework.Logging; - -namespace Microsoft.AspNet.Mvc.Logging -{ - /// - /// Logged to indicate the state of a class during controller discovery. Logs the type - /// of the controller as well as the . - /// - public class IsControllerValues : LoggerStructureBase - { - public IsControllerValues(Type type, ControllerStatus status) - { - Type = type; - Status = status; - } - - /// - /// The of the potential class. - /// - public Type Type { get; } - - /// - /// The of the . - /// - public ControllerStatus Status { get; } - - public override string Format() - { - return LogFormatter.FormatStructure(this); - } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs index 2199f4decc..4954301d8f 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs @@ -1642,6 +1642,22 @@ namespace Microsoft.AspNet.Mvc.Core return string.Format(CultureInfo.CurrentCulture, GetString("Format_NotValid"), p0); } + /// + /// The property '{0}' on controller '{1}' cannot be activated. + /// + internal static string ControllerFactory_PropertyCannotBeActivated + { + get { return GetString("ControllerFactory_PropertyCannotBeActivated"); } + } + + /// + /// The property '{0}' on controller '{1}' cannot be activated. + /// + internal static string FormatControllerFactory_PropertyCannotBeActivated(object p0, object p1) + { + return string.Format(CultureInfo.CurrentCulture, GetString("ControllerFactory_PropertyCannotBeActivated"), p0, p1); + } + /// /// No URL for remote validation could be found. /// diff --git a/src/Microsoft.AspNet.Mvc.Core/Resources.resx b/src/Microsoft.AspNet.Mvc.Core/Resources.resx index 78a8b4faf5..c5434b5443 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Resources.resx +++ b/src/Microsoft.AspNet.Mvc.Core/Resources.resx @@ -433,6 +433,9 @@ The format provided is invalid '{0}'. A format must be a non-empty file-extension, optionally prefixed with a '.' character. + + The property '{0}' on controller '{1}' cannot be activated. + No URL for remote validation could be found. diff --git a/src/Microsoft.AspNet.Mvc.Core/ServiceBasedControllerActivator.cs b/src/Microsoft.AspNet.Mvc.Core/ServiceBasedControllerActivator.cs new file mode 100644 index 0000000000..87ac9879c6 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/ServiceBasedControllerActivator.cs @@ -0,0 +1,21 @@ +// 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.Framework.DependencyInjection; + +namespace Microsoft.AspNet.Mvc +{ + /// + /// A that retrieves controllers as services from the request's + /// . + /// + public class ServiceBasedControllerActivator : IControllerActivator + { + /// + public object Create([NotNull] ActionContext actionContext, [NotNull] Type controllerType) + { + return actionContext.HttpContext.RequestServices.GetRequiredService(controllerType); + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/ApiController.cs b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/ApiController.cs index ac9db2ea00..8397ac997e 100644 --- a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/ApiController.cs +++ b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/ApiController.cs @@ -51,7 +51,7 @@ namespace System.Web.Http /// Gets the . /// /// The setter is intended for unit testing purposes only. - [Activate] + [FromServices] public IModelMetadataProvider MetadataProvider { get; set; } /// @@ -91,7 +91,7 @@ namespace System.Web.Http /// Gets a factory used to generate URLs to other APIs. /// /// The setter is intended for unit testing purposes only. - [Activate] + [FromServices] public IUrlHelper Url { get; set; } /// diff --git a/src/Microsoft.AspNet.Mvc/MvcServiceCollectionExtensions.cs b/src/Microsoft.AspNet.Mvc/MvcServiceCollectionExtensions.cs index 3f50f69fe0..0b586212e7 100644 --- a/src/Microsoft.AspNet.Mvc/MvcServiceCollectionExtensions.cs +++ b/src/Microsoft.AspNet.Mvc/MvcServiceCollectionExtensions.cs @@ -2,9 +2,13 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; using Microsoft.AspNet.Mvc; using Microsoft.AspNet.Routing; using Microsoft.Framework.ConfigurationModel; +using Microsoft.Framework.Logging; namespace Microsoft.Framework.DependencyInjection { @@ -36,6 +40,92 @@ namespace Microsoft.Framework.DependencyInjection services.Configure(setupAction); } + /// + /// Register the specified as services and as a source for controller + /// discovery. + /// + /// The . + /// A sequence of controller s to register in the + /// and used for controller discovery. + /// The . + public static IServiceCollection WithControllersAsServices( + [NotNull] this IServiceCollection services, + [NotNull] IEnumerable controllerTypes) + { + return WithControllersAsServices(services, controllerTypes, configuration: null); + } + + /// + /// Register the specified as services and as a source for controller + /// discovery. + /// + /// The . + /// A sequence of controller s to register + /// in the and used for controller discovery. + /// The application's . + /// The . + public static IServiceCollection WithControllersAsServices( + [NotNull] this IServiceCollection services, + [NotNull] IEnumerable controllerTypes, + IConfiguration configuration) + { + var controllerTypeProvider = new FixedSetControllerTypeProvider(); + foreach (var type in controllerTypes) + { + services.AddTransient(type); + controllerTypeProvider.ControllerTypes.Add(type.GetTypeInfo()); + } + + var describer = new ServiceDescriber(configuration); + services.Replace(describer.Transient()); + services.Replace(describer.Instance(controllerTypeProvider)); + + return services; + } + + /// + /// Registers controller types from the specified as services and as a source + /// for controller discovery. + /// + /// The . + /// Assemblies to scan. + /// The . + public static IServiceCollection WithControllersAsServices( + [NotNull] this IServiceCollection services, + [NotNull] IEnumerable controllerAssemblies) + { + return WithControllersAsServices(services, + controllerAssemblies, + configuration: null); + } + + /// + /// Registers controller types from the specified as services and as a source + /// for controller discovery. + /// + /// The . + /// Assemblies to scan. + /// The application's . + /// The . + public static IServiceCollection WithControllersAsServices( + [NotNull] this IServiceCollection services, + [NotNull] IEnumerable controllerAssemblies, + IConfiguration configuration) + { + var assemblyProvider = new FixedSetAssemblyProvider(); + foreach (var assembly in controllerAssemblies) + { + assemblyProvider.CandidateAssemblies.Add(assembly); + } + var loggerFactory = NullLoggerFactory.Instance; + var controllerTypeProvider = new DefaultControllerTypeProvider(assemblyProvider, loggerFactory); + var controllerTypes = controllerTypeProvider.ControllerTypes; + + return WithControllersAsServices(services, + controllerTypes.Select(type => type.AsType()), + configuration); + } + private static void ConfigureDefaultServices(IServiceCollection services, IConfiguration configuration) { services.AddOptions(configuration); diff --git a/src/Microsoft.AspNet.Mvc/MvcServices.cs b/src/Microsoft.AspNet.Mvc/MvcServices.cs index 43921f5e99..62f1f5d090 100644 --- a/src/Microsoft.AspNet.Mvc/MvcServices.cs +++ b/src/Microsoft.AspNet.Mvc/MvcServices.cs @@ -42,11 +42,12 @@ namespace Microsoft.AspNet.Mvc // Core action discovery, filters and action execution. // These are consumed only when creating action descriptors, then they can be de-allocated + yield return describe.Transient(); yield return describe.Transient(); yield return describe.Transient(); - // This accesses per-request services to activate the controller - yield return describe.Transient(); + // This has a cache, so it needs to be a singleton + yield return describe.Singleton(); // This has a cache, so it needs to be a singleton yield return describe.Singleton(); diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ApplicationModel/DefaultControllerModelBuilderTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ApplicationModel/DefaultControllerModelBuilderTest.cs index cb37c4aea9..7505e2e021 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/ApplicationModel/DefaultControllerModelBuilderTest.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ApplicationModel/DefaultControllerModelBuilderTest.cs @@ -1,176 +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 System.Reflection; -using Microsoft.AspNet.Mvc.Filters; -using Microsoft.AspNet.Mvc.ApplicationModels.DefaultControllerModelBuilderTestControllers; -using Xunit; using System; +using System.Reflection; using System.Threading.Tasks; +using Microsoft.AspNet.Mvc.Filters; +using Xunit; namespace Microsoft.AspNet.Mvc.ApplicationModels { public class DefaultControllerModelBuilderTest { - [Fact] - public void IsController_UserDefinedClass() - { - // Arrange - var builder = new AccessibleControllerModelBuilder(); - var typeInfo = typeof(StoreController).GetTypeInfo(); - - // Act - var isController = builder.IsController(typeInfo); - - // Assert - Assert.True(isController); - } - - [Fact] - public void IsController_FrameworkControllerClass() - { - // Arrange - var builder = new AccessibleControllerModelBuilder(); - var typeInfo = typeof(Controller).GetTypeInfo(); - - // Act - var isController = builder.IsController(typeInfo); - - // Assert - Assert.False(isController); - } - - [Fact] - public void IsController_UserDefinedControllerClass() - { - // Arrange - var builder = new AccessibleControllerModelBuilder(); - var typeInfo = typeof(DefaultControllerModelBuilderTestControllers.Controller).GetTypeInfo(); - - // Act - var isController = builder.IsController(typeInfo); - - // Assert - Assert.False(isController); - } - - [Fact] - public void IsController_Interface() - { - // Arrange - var builder = new AccessibleControllerModelBuilder(); - var typeInfo = typeof(IController).GetTypeInfo(); - - // Act - var isController = builder.IsController(typeInfo); - - // Assert - Assert.False(isController); - } - - [Fact] - public void IsController_AbstractClass() - { - // Arrange - var builder = new AccessibleControllerModelBuilder(); - var typeInfo = typeof(AbstractController).GetTypeInfo(); - - // Act - var isController = builder.IsController(typeInfo); - - // Assert - Assert.False(isController); - } - - [Fact] - public void IsController_DerivedAbstractClass() - { - // Arrange - var builder = new AccessibleControllerModelBuilder(); - var typeInfo = typeof(DerivedAbstractController).GetTypeInfo(); - - // Act - var isController = builder.IsController(typeInfo); - - // Assert - Assert.True(isController); - } - - [Fact] - public void IsController_OpenGenericClass() - { - // Arrange - var builder = new AccessibleControllerModelBuilder(); - var typeInfo = typeof(OpenGenericController<>).GetTypeInfo(); - - // Act - var isController = builder.IsController(typeInfo); - - // Assert - Assert.False(isController); - } - - [Fact] - public void IsController_ClosedGenericClass() - { - // Arrange - var builder = new AccessibleControllerModelBuilder(); - var typeInfo = typeof(OpenGenericController).GetTypeInfo(); - - // Act - var isController = builder.IsController(typeInfo); - - // Assert - Assert.True(isController); - } - - [Fact] - public void IsController_DerivedGenericClass() - { - // Arrange - var builder = new AccessibleControllerModelBuilder(); - var typeInfo = typeof(DerivedGenericController).GetTypeInfo(); - - // Act - var isController = builder.IsController(typeInfo); - - // Assert - Assert.True(isController); - } - - [Fact] - public void IsController_Poco_WithNamingConvention() - { - // Arrange - var builder = new AccessibleControllerModelBuilder(); - var typeInfo = typeof(PocoController).GetTypeInfo(); - - // Act - var isController = builder.IsController(typeInfo); - - // Assert - Assert.True(isController); - } - - [Fact] - public void IsController_NoControllerSuffix() - { - // Arrange - var builder = new AccessibleControllerModelBuilder(); - var typeInfo = typeof(NoSuffix).GetTypeInfo(); - - // Act - var isController = builder.IsController(typeInfo); - - // Assert - Assert.True(isController); - } - [Fact] public void BuildControllerModel_DerivedFromControllerClass_HasFilter() { // Arrange - var builder = new AccessibleControllerModelBuilder(); + var builder = new DefaultControllerModelBuilder(new DefaultActionModelBuilder(), + NullLoggerFactory.Instance); var typeInfo = typeof(StoreController).GetTypeInfo(); // Act @@ -187,7 +33,8 @@ namespace Microsoft.AspNet.Mvc.ApplicationModels public void BuildControllerModel_ClassWithoutFilterInterfaces_HasNoControllerFilter() { // Arrange - var builder = new AccessibleControllerModelBuilder(); + var builder = new DefaultControllerModelBuilder(new DefaultActionModelBuilder(), + NullLoggerFactory.Instance); var typeInfo = typeof(NoFiltersController).GetTypeInfo(); // Act @@ -202,7 +49,8 @@ namespace Microsoft.AspNet.Mvc.ApplicationModels public void BuildControllerModel_ClassWithFilterInterfaces_HasFilter() { // Arrange - var builder = new AccessibleControllerModelBuilder(); + var builder = new DefaultControllerModelBuilder(new DefaultActionModelBuilder(), + NullLoggerFactory.Instance); var typeInfo = typeof(SomeFiltersController).GetTypeInfo(); // Act @@ -217,7 +65,8 @@ namespace Microsoft.AspNet.Mvc.ApplicationModels public void BuildControllerModel_ClassWithFilterInterfaces_UnsupportedType() { // Arrange - var builder = new AccessibleControllerModelBuilder(); + var builder = new DefaultControllerModelBuilder(new DefaultActionModelBuilder(), + NullLoggerFactory.Instance); var typeInfo = typeof(UnsupportedFiltersController).GetTypeInfo(); // Act @@ -227,102 +76,49 @@ namespace Microsoft.AspNet.Mvc.ApplicationModels Assert.Empty(model.Filters); } - private class AccessibleControllerModelBuilder : DefaultControllerModelBuilder + private class StoreController : Mvc.Controller { - public AccessibleControllerModelBuilder() - : base(new DefaultActionModelBuilder(), new NullLoggerFactory()) + } + + [Produces("application/json")] + private class NoFiltersController + { + } + + private class SomeFiltersController : IAsyncActionFilter, IResultFilter + { + public Task OnActionExecutionAsync( + [NotNull] ActionExecutingContext context, + [NotNull] ActionExecutionDelegate next) + { + return null; + } + + public void OnResultExecuted([NotNull] ResultExecutedContext context) { } - public new bool IsController([NotNull]TypeInfo typeInfo) + public void OnResultExecuting([NotNull]ResultExecutingContext context) { - return base.IsController(typeInfo); + } + } + + private class UnsupportedFiltersController : IExceptionFilter, IAuthorizationFilter, IAsyncResourceFilter + { + public void OnAuthorization([NotNull]AuthorizationContext context) + { + throw new NotImplementedException(); + } + + public void OnException([NotNull]ExceptionContext context) + { + throw new NotImplementedException(); + } + + public Task OnResourceExecutionAsync([NotNull]ResourceExecutingContext context, [NotNull]ResourceExecutionDelegate next) + { + throw new NotImplementedException(); } } } } - -// These controllers are used to test the DefaultActionDiscoveryConventions implementation -// which REQUIRES that they be public top-level classes. To avoid having to stub out the -// implementation of this class to test it, they are just top level classes. Don't reuse -// these outside this test - find a better way or use nested classes to keep the tests -// independent. -namespace Microsoft.AspNet.Mvc.ApplicationModels.DefaultControllerModelBuilderTestControllers -{ - public abstract class AbstractController : Mvc.Controller - { - } - - public class DerivedAbstractController : AbstractController - { - } - - public class StoreController : Mvc.Controller - { - } - - public class Controller - { - } - - public class OpenGenericController : Mvc.Controller - { - } - - public class DerivedGenericController : OpenGenericController - { - } - - public interface IController - { - } - - public class NoSuffix : Mvc.Controller - { - } - - public class PocoController - { - } - - [Produces("application/json")] - public class NoFiltersController - { - } - - public class SomeFiltersController : IAsyncActionFilter, IResultFilter - { - public Task OnActionExecutionAsync( - [NotNull] ActionExecutingContext context, - [NotNull] ActionExecutionDelegate next) - { - return null; - } - - public void OnResultExecuted([NotNull] ResultExecutedContext context) - { - } - - public void OnResultExecuting([NotNull ]ResultExecutingContext context) - { - } - } - - public class UnsupportedFiltersController : IExceptionFilter, IAuthorizationFilter, IAsyncResourceFilter - { - public void OnAuthorization([NotNull]AuthorizationContext context) - { - throw new NotImplementedException(); - } - - public void OnException([NotNull]ExceptionContext context) - { - throw new NotImplementedException(); - } - - public Task OnResourceExecutionAsync([NotNull]ResourceExecutingContext context, [NotNull]ResourceExecutionDelegate next) - { - throw new NotImplementedException(); - } - } -} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ControllerActionDescriptorProviderTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ControllerActionDescriptorProviderTests.cs index 9459fae59d..859eb8123e 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/ControllerActionDescriptorProviderTests.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ControllerActionDescriptorProviderTests.cs @@ -1362,16 +1362,13 @@ namespace Microsoft.AspNet.Mvc.Test TypeInfo controllerTypeInfo, IEnumerable filters = null) { - var modelBuilder = new StaticControllerModelBuilder(controllerTypeInfo); - - var assemblyProvider = new Mock(); - assemblyProvider - .SetupGet(ap => ap.CandidateAssemblies) - .Returns(new Assembly[] { controllerTypeInfo.Assembly }); + var controllerTypeProvider = new FixedSetControllerTypeProvider(new[] { controllerTypeInfo }); + var controllerModelBuilder = new DefaultControllerModelBuilder(new DefaultActionModelBuilder(), + NullLoggerFactory.Instance); var provider = new ControllerActionDescriptorProvider( - assemblyProvider.Object, - modelBuilder, + controllerTypeProvider, + controllerModelBuilder, new TestGlobalFilterProvider(filters), new MockMvcOptionsAccessor(), new NullLoggerFactory()); @@ -1382,16 +1379,13 @@ namespace Microsoft.AspNet.Mvc.Test private ControllerActionDescriptorProvider GetProvider( params TypeInfo[] controllerTypeInfo) { - var modelBuilder = new StaticControllerModelBuilder(controllerTypeInfo); - - var assemblyProvider = new Mock(); - assemblyProvider - .SetupGet(ap => ap.CandidateAssemblies) - .Returns(new Assembly[] { controllerTypeInfo.First().Assembly }); + var controllerTypeProvider = new FixedSetControllerTypeProvider(controllerTypeInfo); + var controllerModelBuilder = new DefaultControllerModelBuilder(new DefaultActionModelBuilder(), + NullLoggerFactory.Instance); var provider = new ControllerActionDescriptorProvider( - assemblyProvider.Object, - modelBuilder, + controllerTypeProvider, + controllerModelBuilder, new TestGlobalFilterProvider(), new MockMvcOptionsAccessor(), new NullLoggerFactory()); @@ -1403,18 +1397,15 @@ namespace Microsoft.AspNet.Mvc.Test TypeInfo type, IApplicationModelConvention convention) { - var modelBuilder = new StaticControllerModelBuilder(type); - - var assemblyProvider = new Mock(); - assemblyProvider - .SetupGet(ap => ap.CandidateAssemblies) - .Returns(new Assembly[] { type.Assembly }); + var controllerTypeProvider = new FixedSetControllerTypeProvider(new[] { type }); + var modelBuilder = new DefaultControllerModelBuilder(new DefaultActionModelBuilder(), + NullLoggerFactory.Instance); var options = new MockMvcOptionsAccessor(); options.Options.Conventions.Add(convention); return new ControllerActionDescriptorProvider( - assemblyProvider.Object, + controllerTypeProvider, modelBuilder, new TestGlobalFilterProvider(), options, @@ -1423,15 +1414,12 @@ namespace Microsoft.AspNet.Mvc.Test private IEnumerable GetDescriptors(params TypeInfo[] controllerTypeInfos) { - var modelBuilder = new StaticControllerModelBuilder(controllerTypeInfos); - - var assemblyProvider = new Mock(); - assemblyProvider - .SetupGet(ap => ap.CandidateAssemblies) - .Returns(controllerTypeInfos.Select(cti => cti.Assembly).Distinct()); + var controllerTypeProvider = new FixedSetControllerTypeProvider(controllerTypeInfos); + var modelBuilder = new DefaultControllerModelBuilder(new DefaultActionModelBuilder(), + NullLoggerFactory.Instance); var provider = new ControllerActionDescriptorProvider( - assemblyProvider.Object, + controllerTypeProvider, modelBuilder, new TestGlobalFilterProvider(), new MockMvcOptionsAccessor(), diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/DefaultActionDescriptorCollectionProviderLoggingTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/DefaultActionDescriptorCollectionProviderLoggingTest.cs index 5e8761f81c..5efdd49675 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/DefaultActionDescriptorCollectionProviderLoggingTest.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/DefaultActionDescriptorCollectionProviderLoggingTest.cs @@ -8,32 +8,12 @@ using Microsoft.AspNet.Mvc.ApplicationModels; using Microsoft.Framework.DependencyInjection; using Microsoft.Framework.DependencyInjection.NestedProviders; using Microsoft.Framework.Logging; -using Moq; using Xunit; namespace Microsoft.AspNet.Mvc.Logging { public class DefaultActionDescriptorCollectionProviderLoggingTest { - [Fact] - public void SimpleController_AssemblyDiscovery() - { - // Arrange - var sink = new TestSink(); - var loggerFactory = new TestLoggerFactory(sink); - - // Act - var provider = GetProvider(loggerFactory, typeof(SimpleController).GetTypeInfo()); - provider.BuildModel(); - - // Assert - Assert.Single(sink.Writes); - - var assemblyValues = sink.Writes[0].State as AssemblyValues; - Assert.NotNull(assemblyValues); - Assert.True(assemblyValues.AssemblyName.Contains("Microsoft.AspNet.Mvc.Core.Test")); - } - [Fact] public void ControllerDiscovery() { @@ -49,10 +29,10 @@ namespace Microsoft.AspNet.Mvc.Logging provider.GetDescriptors(); // Assert - // 1 assembly, 2 controllers - Assert.Equal(3, sink.Writes.Count); + // 2 controllers + Assert.Equal(2, sink.Writes.Count); - var controllerModelValues = sink.Writes[1].State as ControllerModelValues; + var controllerModelValues = Assert.IsType(sink.Writes[0].State); Assert.NotNull(controllerModelValues); Assert.Equal("Simple", controllerModelValues.ControllerName); Assert.Equal(typeof(SimpleController), controllerModelValues.ControllerType); @@ -62,7 +42,7 @@ namespace Microsoft.AspNet.Mvc.Logging Assert.Empty(controllerModelValues.Attributes); Assert.Empty(controllerModelValues.Filters); - controllerModelValues = sink.Writes[2].State as ControllerModelValues; + controllerModelValues = Assert.IsType(sink.Writes[1].State); Assert.NotNull(controllerModelValues); Assert.Equal("Basic", controllerModelValues.ControllerName); Assert.Equal(typeof(BasicController), controllerModelValues.ControllerType); @@ -88,10 +68,12 @@ namespace Microsoft.AspNet.Mvc.Logging typeof(BasicController).GetTypeInfo()); // Assert - // 1 assembly, 2 controllers, 3 actions - Assert.Equal(6, sink.Writes.Count); + // 2 controllers, 3 actions + Assert.Equal(5, sink.Writes.Count); + Assert.IsType(sink.Writes[0].State); + Assert.IsType(sink.Writes[1].State); - var actionDescriptorValues = sink.Writes[3].State as ActionDescriptorValues; + var actionDescriptorValues = Assert.IsType(sink.Writes[2].State); Assert.NotNull(actionDescriptorValues); Assert.Equal("EmptyAction", actionDescriptorValues.Name); Assert.Equal("Simple", actionDescriptorValues.ControllerName); @@ -101,7 +83,7 @@ namespace Microsoft.AspNet.Mvc.Logging Assert.Empty(actionDescriptorValues.FilterDescriptors); Assert.Empty(actionDescriptorValues.Parameters); - actionDescriptorValues = sink.Writes[4].State as ActionDescriptorValues; + actionDescriptorValues = Assert.IsType(sink.Writes[3].State); Assert.NotNull(actionDescriptorValues); Assert.Equal("Basic", actionDescriptorValues.Name); Assert.Equal("Basic", actionDescriptorValues.ControllerName); @@ -111,7 +93,7 @@ namespace Microsoft.AspNet.Mvc.Logging Assert.Equal(2, actionDescriptorValues.FilterDescriptors.Count); Assert.Empty(actionDescriptorValues.Parameters); - actionDescriptorValues = sink.Writes[5].State as ActionDescriptorValues; + actionDescriptorValues = Assert.IsType(sink.Writes[4].State); Assert.NotNull(actionDescriptorValues); Assert.Equal("Basic", actionDescriptorValues.Name); Assert.Equal("Basic", actionDescriptorValues.ControllerName); @@ -140,15 +122,12 @@ namespace Microsoft.AspNet.Mvc.Logging private ControllerActionDescriptorProvider GetProvider( ILoggerFactory loggerFactory, params TypeInfo[] controllerTypeInfo) { - var modelBuilder = new StaticControllerModelBuilder(controllerTypeInfo); - - var assemblyProvider = new Mock(); - assemblyProvider - .SetupGet(ap => ap.CandidateAssemblies) - .Returns(new Assembly[] { controllerTypeInfo.First().Assembly }); + var controllerTypeProvider = new FixedSetControllerTypeProvider(controllerTypeInfo); + var modelBuilder = new DefaultControllerModelBuilder(new DefaultActionModelBuilder(), + loggerFactory); var provider = new ControllerActionDescriptorProvider( - assemblyProvider.Object, + controllerTypeProvider, modelBuilder, new TestGlobalFilterProvider(), new MockMvcOptionsAccessor(), diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/DefaultActionSelectorTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/DefaultActionSelectorTests.cs index 745cf1ee39..2cd5e37b66 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/DefaultActionSelectorTests.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/DefaultActionSelectorTests.cs @@ -759,16 +759,17 @@ namespace Microsoft.AspNet.Mvc private ControllerActionDescriptorProvider GetActionDescriptorProvider() { - var assemblyProvider = new StaticAssemblyProvider(); - var controllerTypes = typeof(DefaultActionSelectorTests) .GetNestedTypes(BindingFlags.NonPublic) - .Select(t => t.GetTypeInfo()); + .Select(t => t.GetTypeInfo()) + .ToList(); - var modelBuilder = new StaticControllerModelBuilder(controllerTypes.ToArray()); + var controllerTypeProvider = new FixedSetControllerTypeProvider(controllerTypes); + var modelBuilder = new DefaultControllerModelBuilder(new DefaultActionModelBuilder(), + NullLoggerFactory.Instance); return new ControllerActionDescriptorProvider( - assemblyProvider, + controllerTypeProvider, modelBuilder, new TestGlobalFilterProvider(), new MockMvcOptionsAccessor(), diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/DefaultControllerActivatorTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/DefaultControllerActivatorTest.cs index 03d9400313..e349e416ee 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/DefaultControllerActivatorTest.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/DefaultControllerActivatorTest.cs @@ -1,182 +1,79 @@ // 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 ASPNET50 using System; -using Microsoft.AspNet.Http; -using Microsoft.AspNet.Mvc.ModelBinding; -using Microsoft.AspNet.Mvc.Rendering; +using Microsoft.AspNet.Http.Core; using Microsoft.AspNet.Routing; -using Microsoft.Framework.DependencyInjection; using Moq; using Xunit; -namespace Microsoft.AspNet.Mvc.Core.Test +namespace Microsoft.AspNet.Mvc { public class DefaultControllerActivatorTest { - [Fact] - public void Activate_SetsPropertiesFromActionContextHierarchy() + [Theory] + [InlineData(typeof(TypeDerivingFromController))] + [InlineData(typeof(PocoType))] + public void Create_CreatesInstancesOfTypes(Type type) { // Arrange - var services = GetServices(); - - var httpRequest = Mock.Of(); - var httpContext = new Mock(); - httpContext.SetupGet(c => c.Request) - .Returns(httpRequest); - httpContext.SetupGet(c => c.RequestServices) - .Returns(services); - - var controller = new TestController(); - var context = new ActionContext(httpContext.Object, new RouteData(), new ActionDescriptor()); var activator = new DefaultControllerActivator(); - + var actionContext = new ActionContext(new DefaultHttpContext(), + new RouteData(), + new ActionDescriptor()); // Act - activator.Activate(controller, context); + var instance = activator.Create(actionContext, type); // Assert - Assert.Same(context, controller.ActionContext); - Assert.Same(httpContext.Object, controller.HttpContext); - Assert.Same(httpRequest, controller.GetHttpRequest()); + Assert.IsType(type, instance); } [Fact] - public void Activate_SetsViewDatDictionary() + public void Create_TypeActivatesTypesWithServices() { // Arrange - var services = GetServices(); - - var httpContext = new Mock(); - httpContext.SetupGet(c => c.RequestServices) - .Returns(services); - - var controller = new TestController(); - var context = new ActionContext(httpContext.Object, new RouteData(), new ActionDescriptor()); var activator = new DefaultControllerActivator(); - - // Act - activator.Activate(controller, context); - - // Assert - Assert.NotNull(controller.GetViewData()); - } - - [Fact] - public void Activate_SetsBindingContext() - { - // Arrange - var bindingContext = new ActionBindingContext(); - - var services = GetServices(); - services.GetRequiredService>().Value = bindingContext; - - var httpContext = new Mock(); - httpContext.SetupGet(c => c.RequestServices) - .Returns(services); - - var controller = new TestController(); - var context = new ActionContext(httpContext.Object, new RouteData(), new ActionDescriptor()); - - var activator = new DefaultControllerActivator(); - - // Act - activator.Activate(controller, context); - - // Assert - Assert.Same(bindingContext, controller.BindingContext); - } - - [Fact] - public void Activate_PopulatesServicesFromServiceContainer() - { - // Arrange - var services = GetServices(); - var urlHelper = services.GetRequiredService(); - - var httpContext = new Mock(); - httpContext.SetupGet(c => c.RequestServices) - .Returns(services); - - var controller = new TestController(); - var context = new ActionContext(httpContext.Object, new RouteData(), new ActionDescriptor()); - var activator = new DefaultControllerActivator(); - - // Act - activator.Activate(controller, context); - - // Assert - Assert.Same(urlHelper, controller.Helper); - } - - [Fact] - public void Activate_IgnoresPropertiesThatAreNotDecoratedWithActivateAttribute() - { - // Arrange - var services = GetServices(); - - var httpContext = new Mock(); - httpContext.SetupGet(c => c.Response) - .Returns(Mock.Of()); - httpContext.SetupGet(c => c.RequestServices) - .Returns(services); - - var controller = new TestController(); - var context = new ActionContext(httpContext.Object, new RouteData(), new ActionDescriptor()); - var activator = new DefaultControllerActivator(); - - // Act - activator.Activate(controller, context); - - // Assert - Assert.Null(controller.Response); - } - - private IServiceProvider GetServices() - { - var services = new Mock(); - services.Setup(s => s.GetService(typeof(IUrlHelper))) - .Returns(Mock.Of()); - services.Setup(s => s.GetService(typeof(IModelMetadataProvider))) - .Returns(new EmptyModelMetadataProvider()); - services - .Setup(s => s.GetService(typeof(IScopedInstance))) - .Returns(new MockScopedInstance()); - return services.Object; - } - - public class TestController - { - [Activate] - public ActionContext ActionContext { get; set; } - - [Activate] - public ActionBindingContext BindingContext { 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() + var serviceProvider = new Mock(MockBehavior.Strict); + var testService = new TestService(); + serviceProvider.Setup(s => s.GetService(typeof(TestService))) + .Returns(testService) + .Verifiable(); + var httpContext = new DefaultHttpContext { - return ViewData; + RequestServices = serviceProvider.Object + }; + var actionContext = new ActionContext(httpContext, + new RouteData(), + new ActionDescriptor()); + // Act + var instance = activator.Create(actionContext, typeof(TypeDerivingFromControllerWithServices)); + + // Assert + var controller = Assert.IsType(instance); + Assert.Same(testService, controller.TestService); + serviceProvider.Verify(); + } + + private class TypeDerivingFromController : Controller + { + } + + private class TypeDerivingFromControllerWithServices : Controller + { + public TypeDerivingFromControllerWithServices(TestService service) + { + TestService = service; } - public HttpRequest GetHttpRequest() - { - return Request; - } + public TestService TestService { get; } + } + + private class PocoType + { + } + + private class TestService + { } } } -#endif diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/DefaultControllerFactoryTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/DefaultControllerFactoryTest.cs index 6a40b3db9b..a2051abd59 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/DefaultControllerFactoryTest.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/DefaultControllerFactoryTest.cs @@ -2,23 +2,249 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Reflection; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Http.Core; +using Microsoft.AspNet.Mvc.ModelBinding; +using Microsoft.AspNet.Routing; using Microsoft.Framework.DependencyInjection; using Moq; using Xunit; -namespace Microsoft.AspNet.Mvc.Core.Test +namespace Microsoft.AspNet.Mvc.Core { public class DefaultControllerFactoryTest { + [Fact] + public void CreateController_ThrowsIfActionDescriptorIsNotControllerActionDescriptor() + { + // Arrange + var expected = "The action descriptor must be of type 'Microsoft.AspNet.Mvc.ControllerActionDescriptor'." + + Environment.NewLine + "Parameter name: actionContext"; + var actionDescriptor = new ActionDescriptor(); + var controllerFactory = new DefaultControllerFactory(Mock.Of()); + var httpContext = new DefaultHttpContext(); + var actionContext = new ActionContext(httpContext, + new RouteData(), + actionDescriptor); + + // Act and Assert + var ex = Assert.Throws(() => + controllerFactory.CreateController(actionContext)); + Assert.Equal(expected, ex.Message); + Assert.Equal("actionContext", ex.ParamName); + } + + [Fact] + public void CreateController_UsesControllerActivatorToInstantiateController() + { + // Arrange + var expected = new MyController(); + var actionDescriptor = new ControllerActionDescriptor + { + ControllerTypeInfo = typeof(MyController).GetTypeInfo() + }; + var httpContext = new DefaultHttpContext(); + httpContext.RequestServices = GetServices(); + var actionContext = new ActionContext(httpContext, + new RouteData(), + actionDescriptor); + var activator = new Mock(); + activator.Setup(a => a.Create(actionContext, typeof(MyController))) + .Returns(expected) + .Verifiable(); + + var controllerFactory = new DefaultControllerFactory(activator.Object); + + // Act + var result = controllerFactory.CreateController(actionContext); + + // Assert + var controller = Assert.IsType(result); + Assert.Same(expected, controller); + activator.Verify(); + } + + [Fact] + public void CreateController_SetsPropertiesFromActionContextHierarchy() + { + // Arrange + var actionDescriptor = new ControllerActionDescriptor + { + ControllerTypeInfo = typeof(ControllerWithActivateAndFromServices).GetTypeInfo() + }; + var services = GetServices(); + var httpContext = new DefaultHttpContext + { + RequestServices = services + }; + var context = new ActionContext(httpContext, new RouteData(), actionDescriptor); + var factory = new DefaultControllerFactory(new DefaultControllerActivator()); + + // Act + var result = factory.CreateController(context); + + // Assert + var controller = Assert.IsType(result); + Assert.Same(context, controller.ActionContext); + Assert.Same(httpContext, controller.HttpContext); + } + + [Fact] + public void CreateController_SetsViewDataDictionary() + { + // Arrange + var actionDescriptor = new ControllerActionDescriptor + { + ControllerTypeInfo = typeof(ControllerWithActivateAndFromServices).GetTypeInfo() + }; + + var services = GetServices(); + var httpContext = new DefaultHttpContext + { + RequestServices = services + }; + var context = new ActionContext(httpContext, new RouteData(), actionDescriptor); + var factory = new DefaultControllerFactory(new DefaultControllerActivator()); + + // Act + var result = factory.CreateController(context); + + // Assert + var controller = Assert.IsType(result); + Assert.NotNull(controller.GetViewData()); + } + + [Fact] + public void CreateController_SetsBindingContext() + { + // Arrange + var actionDescriptor = new ControllerActionDescriptor + { + ControllerTypeInfo = typeof(ControllerWithActivateAndFromServices).GetTypeInfo() + }; + var bindingContext = new ActionBindingContext(); + + var services = GetServices(); + services.GetRequiredService>().Value = bindingContext; + var httpContext = new DefaultHttpContext + { + RequestServices = services + }; + var context = new ActionContext(httpContext, new RouteData(), actionDescriptor); + var factory = new DefaultControllerFactory(new DefaultControllerActivator()); + + // Act + var result = factory.CreateController(context); + + // Assert + var controller = Assert.IsType(result); + Assert.Same(bindingContext, controller.BindingContext); + } + + [Fact] + public void CreateController_PopulatesServicesFromServiceContainer() + { + // Arrange + var actionDescriptor = new ControllerActionDescriptor + { + ControllerTypeInfo = typeof(ControllerWithActivateAndFromServices).GetTypeInfo() + }; + var services = GetServices(); + var urlHelper = services.GetRequiredService(); + + var httpContext = new DefaultHttpContext + { + RequestServices = services + }; + var context = new ActionContext(httpContext, new RouteData(), actionDescriptor); + var factory = new DefaultControllerFactory(new DefaultControllerActivator()); + + // Act + var result = factory.CreateController(context); + + // Assert + var controller = Assert.IsType(result); + Assert.Same(urlHelper, controller.Helper); + } + + [Fact] + public void CreateController_PopulatesUserServicesFromServiceContainer() + { + // Arrange + var actionDescriptor = new ControllerActionDescriptor + { + ControllerTypeInfo = typeof(ControllerWithActivateAndFromServices).GetTypeInfo() + }; + var services = GetServices(); + var testService = services.GetService(); + + var httpContext = new DefaultHttpContext + { + RequestServices = services + }; + var context = new ActionContext(httpContext, new RouteData(), actionDescriptor); + var factory = new DefaultControllerFactory(new DefaultControllerActivator()); + + // Act + var result = factory.CreateController(context); + + // Assert + var controller = Assert.IsType(result); + Assert.Same(testService, controller.TestService); + } + + [Fact] + public void CreateController_IgnoresPropertiesThatAreNotDecoratedWithActivateAttribute() + { + // Arrange + var actionDescriptor = new ControllerActionDescriptor + { + ControllerTypeInfo = typeof(ControllerWithActivateAndFromServices).GetTypeInfo() + }; + var services = GetServices(); + var httpContext = new DefaultHttpContext + { + RequestServices = services + }; + var context = new ActionContext(httpContext, new RouteData(), actionDescriptor); + var factory = new DefaultControllerFactory(new DefaultControllerActivator()); + + // Act + var result = factory.CreateController(context); + + // Assert + var controller = Assert.IsType(result); + Assert.Null(controller.Response); + } + + [Fact] + public void CreateController_ThrowsIfPropertyCannotBeActivated() + { + // Arrange + var actionDescriptor = new ControllerActionDescriptor + { + ControllerTypeInfo = typeof(ControllerThatCannotBeActivated).GetTypeInfo() + }; + var services = GetServices(); + var httpContext = new DefaultHttpContext + { + RequestServices = services + }; + var context = new ActionContext(httpContext, new RouteData(), actionDescriptor); + var factory = new DefaultControllerFactory(new DefaultControllerActivator()); + + // Act and Assert + var exception = Assert.Throws(() => factory.CreateController(context)); + Assert.Equal("The property 'Service' on controller '" + typeof(ControllerThatCannotBeActivated) + + "' cannot be activated.", exception.Message); + } + [Fact] public void DefaultControllerFactory_DisposesIDisposableController() { // Arrange - var factory = new DefaultControllerFactory( - Mock.Of(), - Mock.Of(), - Mock.Of()); - + var factory = new DefaultControllerFactory(Mock.Of()); var controller = new MyController(); // Act + Assert @@ -33,17 +259,64 @@ namespace Microsoft.AspNet.Mvc.Core.Test public void DefaultControllerFactory_ReleasesNonIDisposableController() { // Arrange - var factory = new DefaultControllerFactory( - Mock.Of(), - Mock.Of(), - Mock.Of()); - - var controller = new Object(); + var factory = new DefaultControllerFactory(Mock.Of()); + var controller = new object(); // Act + Assert (does not throw) factory.ReleaseController(controller); } + private IServiceProvider GetServices() + { + var services = new Mock(); + services.Setup(s => s.GetService(typeof(IUrlHelper))) + .Returns(Mock.Of()); + services.Setup(s => s.GetService(typeof(IModelMetadataProvider))) + .Returns(new EmptyModelMetadataProvider()); + services.Setup(s => s.GetService(typeof(TestService))) + .Returns(new TestService()); + services + .Setup(s => s.GetService(typeof(IScopedInstance))) + .Returns(new MockScopedInstance()); + return services.Object; + } + + private class ControllerWithActivateAndFromServices + { + [Activate] + public ActionContext ActionContext { get; set; } + + [Activate] + public ActionBindingContext BindingContext { get; set; } + + [Activate] + public HttpContext HttpContext { get; set; } + + [Activate] + protected HttpRequest Request { get; set; } + + [Activate] + private ViewDataDictionary ViewData { get; set; } + + [FromServices] + public IUrlHelper Helper { get; set; } + + [FromServices] + public TestService TestService { get; set; } + + public HttpResponse Response { get; set; } + + public ViewDataDictionary GetViewData() + { + return ViewData; + } + + public HttpRequest GetHttpRequest() + { + return Request; + } + } + private class MyController : Controller { public bool Disposed { get; set; } @@ -53,5 +326,16 @@ namespace Microsoft.AspNet.Mvc.Core.Test Disposed = true; } } + + private class ControllerThatCannotBeActivated + { + [Activate] + public TestService Service { get; set; } + } + + private class TestService + { + + } } } diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/DefaultControllerTypeProviderTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/DefaultControllerTypeProviderTest.cs new file mode 100644 index 0000000000..04357a1a0a --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Core.Test/DefaultControllerTypeProviderTest.cs @@ -0,0 +1,217 @@ +// 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.Linq; +using System.Reflection; +using Microsoft.AspNet.Mvc.DefaultControllerTypeProviderControllers; +using Xunit; + +namespace Microsoft.AspNet.Mvc +{ + public class DefaultControllerTypeProviderTest + { + [Fact] + public void IsController_UserDefinedClass() + { + // Arrange + var typeInfo = typeof(StoreController).GetTypeInfo(); + var provider = GetControllerTypeProvider(); + + // Act + var isController = provider.IsController(typeInfo); + + // Assert + Assert.True(isController); + } + + [Fact] + public void IsController_FrameworkControllerClass() + { + // Arrange + var typeInfo = typeof(Controller).GetTypeInfo(); + var provider = GetControllerTypeProvider(); + + // Act + var isController = provider.IsController(typeInfo); + + // Assert + Assert.False(isController); + } + + [Fact] + public void IsController_UserDefinedControllerClass() + { + // Arrange + var typeInfo = typeof(DefaultControllerTypeProviderControllers.Controller).GetTypeInfo(); + var provider = GetControllerTypeProvider(); + + // Act + var isController = provider.IsController(typeInfo); + + // Assert + Assert.False(isController); + } + + [Fact] + public void IsController_Interface() + { + // Arrange + var typeInfo = typeof(IController).GetTypeInfo(); + var provider = GetControllerTypeProvider(); + + // Act + var isController = provider.IsController(typeInfo); + + // Assert + Assert.False(isController); + } + + [Fact] + public void IsController_AbstractClass() + { + // Arrange + var typeInfo = typeof(AbstractController).GetTypeInfo(); + var provider = GetControllerTypeProvider(); + + // Act + var isController = provider.IsController(typeInfo); + + // Assert + Assert.False(isController); + } + + [Fact] + public void IsController_DerivedAbstractClass() + { + // Arrange + var typeInfo = typeof(DerivedAbstractController).GetTypeInfo(); + var provider = GetControllerTypeProvider(); + + // Act + var isController = provider.IsController(typeInfo); + + // Assert + Assert.True(isController); + } + + [Fact] + public void IsController_OpenGenericClass() + { + // Arrange + var typeInfo = typeof(OpenGenericController<>).GetTypeInfo(); + var provider = GetControllerTypeProvider(); + + // Act + var isController = provider.IsController(typeInfo); + + // Assert + Assert.False(isController); + } + + [Fact] + public void IsController_ClosedGenericClass() + { + // Arrange + var typeInfo = typeof(OpenGenericController).GetTypeInfo(); + var provider = GetControllerTypeProvider(); + + // Act + var isController = provider.IsController(typeInfo); + + // Assert + Assert.True(isController); + } + + [Fact] + public void IsController_DerivedGenericClass() + { + // Arrange + var typeInfo = typeof(DerivedGenericController).GetTypeInfo(); + var provider = GetControllerTypeProvider(); + + // Act + var isController = provider.IsController(typeInfo); + + // Assert + Assert.True(isController); + } + + [Fact] + public void IsController_Poco_WithNamingConvention() + { + // Arrange + var typeInfo = typeof(PocoController).GetTypeInfo(); + var provider = GetControllerTypeProvider(); + + // Act + var isController = provider.IsController(typeInfo); + + // Assert + Assert.True(isController); + } + + [Fact] + public void IsController_NoControllerSuffix() + { + // Arrange + var typeInfo = typeof(NoSuffix).GetTypeInfo(); + var provider = GetControllerTypeProvider(); + + // Act + var isController = provider.IsController(typeInfo); + + // Assert + Assert.True(isController); + } + + private static DefaultControllerTypeProvider GetControllerTypeProvider() + { + var assemblyProvider = new FixedSetAssemblyProvider(); + return new DefaultControllerTypeProvider(assemblyProvider, NullLoggerFactory.Instance); + } + } +} + +// These controllers are used to test the DefaultControllerTypeProvider implementation +// which REQUIRES that they be public top-level classes. To avoid having to stub out the +// implementation of this class to test it, they are just top level classes. Don't reuse +// these outside this test - find a better way or use nested classes to keep the tests +// independent. +namespace Microsoft.AspNet.Mvc.DefaultControllerTypeProviderControllers +{ + public abstract class AbstractController : Mvc.Controller + { + } + + public class DerivedAbstractController : AbstractController + { + } + + public class StoreController : Mvc.Controller + { + } + + public class Controller + { + } + + public class OpenGenericController : Mvc.Controller + { + } + + public class DerivedGenericController : OpenGenericController + { + } + + public interface IController + { + } + + public class NoSuffix : Mvc.Controller + { + } + + public class PocoController + { + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ServiceBasedControllerActivatorTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ServiceBasedControllerActivatorTest.cs new file mode 100644 index 0000000000..bc2a5fc86e --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ServiceBasedControllerActivatorTest.cs @@ -0,0 +1,66 @@ +// 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.Http.Core; +using Microsoft.AspNet.Routing; +using Moq; +using Xunit; + +namespace Microsoft.AspNet.Mvc +{ + public class ServiceBasedControllerActivatorTest + { + [Fact] + public void Create_GetsServicesFromServiceProvider() + { + // Arrange + var controller = new DIController(); + var serviceProvider = new Mock(MockBehavior.Strict); + serviceProvider.Setup(s => s.GetService(typeof(DIController))) + .Returns(controller) + .Verifiable(); + var httpContext = new DefaultHttpContext + { + RequestServices = serviceProvider.Object + }; + var activator = new ServiceBasedControllerActivator(); + var actionContext = new ActionContext(httpContext, + new RouteData(), + new ActionDescriptor()); + + // Act + var instance = activator.Create(actionContext, typeof(DIController)); + + // Assert + Assert.Same(controller, instance); + serviceProvider.Verify(); + } + + [Fact] + public void Create_ThrowsIfControllerIsNotRegisteredInServiceProvider() + { + // Arrange + var expected = "No service for type '" + typeof(DIController) + "' has been registered."; + var controller = new DIController(); + var serviceProvider = new Mock(); + var httpContext = new DefaultHttpContext + { + RequestServices = serviceProvider.Object + }; + var activator = new ServiceBasedControllerActivator(); + var actionContext = new ActionContext(httpContext, + new RouteData(), + new ActionDescriptor()); + + // Act and Assert + var ex = Assert.Throws( + () => activator.Create(actionContext, typeof(DIController))); + Assert.Equal(expected, ex.Message); + } + + private class DIController : Controller + { + } + } +} diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/StaticAssemblyProvider.cs b/test/Microsoft.AspNet.Mvc.Core.Test/StaticAssemblyProvider.cs deleted file mode 100644 index 90547adeb8..0000000000 --- a/test/Microsoft.AspNet.Mvc.Core.Test/StaticAssemblyProvider.cs +++ /dev/null @@ -1,22 +0,0 @@ -// 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.Reflection; - -namespace Microsoft.AspNet.Mvc -{ - /// - /// An implementation of IAssemblyProvider that provides just this assembly. - /// - public class StaticAssemblyProvider : IAssemblyProvider - { - public IEnumerable CandidateAssemblies - { - get - { - yield return typeof(StaticAssemblyProvider).GetTypeInfo().Assembly; - } - } - } -} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/StaticControllerModelBuilder.cs b/test/Microsoft.AspNet.Mvc.Core.Test/StaticControllerModelBuilder.cs deleted file mode 100644 index 1ea00f6030..0000000000 --- a/test/Microsoft.AspNet.Mvc.Core.Test/StaticControllerModelBuilder.cs +++ /dev/null @@ -1,29 +0,0 @@ -// 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; - -namespace Microsoft.AspNet.Mvc.ApplicationModels -{ - /// - /// An implementation of StaticControllerModelBuilder that only allows controllers - /// from a fixed set of types. - /// - public class StaticControllerModelBuilder : DefaultControllerModelBuilder - { - public StaticControllerModelBuilder(params TypeInfo[] controllerTypes) - : base(new DefaultActionModelBuilder(), new NullLoggerFactory()) - { - ControllerTypes = new List(controllerTypes ?? Enumerable.Empty()); - } - - public List ControllerTypes { get; private set; } - - protected override bool IsController([NotNull] TypeInfo typeInfo) - { - return ControllerTypes.Contains(typeInfo); - } - } -} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ViewComponents/DefaultViewComponentSelectorTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ViewComponents/DefaultViewComponentSelectorTest.cs index 759cb88fd2..e8edbacf42 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/ViewComponents/DefaultViewComponentSelectorTest.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ViewComponents/DefaultViewComponentSelectorTest.cs @@ -177,7 +177,7 @@ namespace Microsoft.AspNet.Mvc private class FilteredViewComponentSelector : DefaultViewComponentSelector { public FilteredViewComponentSelector() - : base(new StaticAssemblyProvider()) + : base(GetAssemblyProvider()) { AllowedTypes = typeof(DefaultViewComponentSelectorTest).GetNestedTypes(BindingFlags.NonPublic); } @@ -188,6 +188,15 @@ namespace Microsoft.AspNet.Mvc { return AllowedTypes.Contains(typeInfo.AsType()); } + + private static IAssemblyProvider GetAssemblyProvider() + { + var assemblyProvider = new FixedSetAssemblyProvider(); + assemblyProvider.CandidateAssemblies.Add( + typeof(FilteredViewComponentSelector).GetTypeInfo().Assembly); + + return assemblyProvider; + } } } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/ActivatorTests.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/ActivatorTests.cs index dd18dca5ec..5b3190feea 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/ActivatorTests.cs +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/ActivatorTests.cs @@ -22,8 +22,8 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests var server = TestServer.Create(_provider, _app); var client = server.CreateClient(); - var expectedMessage = "No service for type 'ActivatorWebSite.CannotBeActivatedController+FakeType' " + - "has been registered."; + var expectedMessage = "The property 'Service' on controller 'ActivatorWebSite.CannotBeActivatedController' " + + "cannot be activated."; // Act & Assert var response = await client.GetAsync("http://localhost/CannotBeActivated/Index"); diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/ControllerFromServicesTests.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/ControllerFromServicesTests.cs new file mode 100644 index 0000000000..bf1c47962e --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/ControllerFromServicesTests.cs @@ -0,0 +1,103 @@ +// 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.Net; +using System.Net.Http; +using System.Threading.Tasks; +using ControllersFromServicesWebSite; +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.TestHost; +using Xunit; + +namespace Microsoft.AspNet.Mvc.FunctionalTests +{ + public class ControllerFromServicesTest + { + private readonly IServiceProvider _provider = TestHelper.CreateServices( + nameof(ControllersFromServicesWebSite)); + private readonly Action _app = new Startup().Configure; + + [Fact] + public async Task ControllersWithConstructorInjectionAreCreatedAndActivated() + { + // Arrange + var expected = "/constructorinjection 14 test-header-value"; + var server = TestServer.Create(_provider, _app); + var client = server.CreateClient(); + client.DefaultRequestHeaders.TryAddWithoutValidation("Test-Header", "test-header-value"); + + // Act + var response = await client.GetStringAsync("http://localhost/constructorinjection?value=14"); + + // Assert + Assert.Equal(expected, response); + } + + [Fact] + public async Task TypesDerivingFromControllerAreRegistered() + { + // Arrange + var expected = "No schedules available for 23"; + var server = TestServer.Create(_provider, _app); + var client = server.CreateClient(); + + // Act + var response = await client.GetStringAsync("http://localhost/schedule/23"); + + // Assert + Assert.Equal(expected, response); + } + + [Fact] + public async Task TypesWithControllerSuffixAreRegistered() + { + // Arrange + var expected = "Updated record employee303"; + var server = TestServer.Create(_provider, _app); + var client = server.CreateClient(); + + // Act + var response = await client.PutAsync("http://localhost/employee/update_records?recordId=employee303", + new StringContent(string.Empty)); + + // Assert + response.EnsureSuccessStatusCode(); + Assert.Equal(expected, await response.Content.ReadAsStringAsync()); + } + + [Fact] + public async Task TypesWithControllerSuffixAreConventionalRouted() + { + // Arrange + var expected = "Saved record employee #211"; + var server = TestServer.Create(_provider, _app); + var client = server.CreateClient(); + + // Act + var response = await client.PostAsync("http://localhost/employeerecords/save/211", + new StringContent(string.Empty)); + + // Assert + response.EnsureSuccessStatusCode(); + Assert.Equal(expected, await response.Content.ReadAsStringAsync()); + } + + [Theory] + [InlineData("generic")] + [InlineData("nested")] + [InlineData("not-in-services")] + public async Task AddControllersFromServices_UsesControllerDiscoveryContentions(string action) + { + // Arrange + var server = TestServer.Create(_provider, _app); + var client = server.CreateClient(); + + // Act + var response = await client.GetAsync("http://localhost/not-discovered/" + action); + + // Assert + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + } +} diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/Logging/LoggingStartupTest.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/Logging/LoggingStartupTest.cs index f4bfe81024..9d9e13528b 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/Logging/LoggingStartupTest.cs +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/Logging/LoggingStartupTest.cs @@ -40,31 +40,6 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests } } - [Fact] - public async Task IsControllerValues_LoggedAtStartup() - { - // Arrange and Act - var logs = await GetLogsByDataTypeAsync(); - - // Assert - Assert.NotEmpty(logs); - foreach (var log in logs) - { - dynamic isController = log.State; - if (string.Equals(typeof(HomeController).AssemblyQualifiedName, isController.Type.ToString())) - { - Assert.Equal( - ControllerStatus.IsController, - Enum.Parse(typeof(ControllerStatus), isController.Status.ToString())); - } - else - { - Assert.NotEqual(ControllerStatus.IsController, - Enum.Parse(typeof(ControllerStatus), isController.Status.ToString())); - } - } - } - [Fact] public async Task ControllerModelValues_LoggedAtStartup() { diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/project.json b/test/Microsoft.AspNet.Mvc.FunctionalTests/project.json index ead5aa95ad..00b56f537f 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/project.json +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/project.json @@ -17,6 +17,7 @@ "BasicWebSite": "1.0.0", "CompositeViewEngineWebSite": "1.0.0", "ConnegWebSite": "1.0.0", + "ControllersFromServicesWebSite": "1.0.0", "CustomRouteWebSite": "1.0.0", "ErrorPageMiddlewareWebSite": "1.0.0", "FilesWebSite": "1.0.0", diff --git a/test/Microsoft.AspNet.Mvc.Test/MvcServiceCollectionExtensionsTest.cs b/test/Microsoft.AspNet.Mvc.Test/MvcServiceCollectionExtensionsTest.cs new file mode 100644 index 0000000000..bde633b0ca --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Test/MvcServiceCollectionExtensionsTest.cs @@ -0,0 +1,153 @@ +// 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.Generic; +using System.Linq; +using System.Reflection; +using Microsoft.AspNet.Mvc.MvcServiceCollectionExtensionsTestControllers; +using Microsoft.Framework.ConfigurationModel; +using Microsoft.Framework.DependencyInjection; +using Xunit; + +namespace Microsoft.AspNet.Mvc +{ + public class MvcServiceCollectionExtensionsTest + { + [Fact] + public void WithControllersAsServices_AddsTypesToControllerTypeProviderAndServiceCollection() + { + // Arrange + var collection = new ServiceCollection(); + var controllerTypes = new[] { typeof(ControllerTypeA).GetTypeInfo(), typeof(TypeBController).GetTypeInfo() }; + + // Act + MvcServiceCollectionExtensions.WithControllersAsServices(collection, + controllerTypes); + + // Assert + var services = collection.ToList(); + Assert.Equal(4, services.Count); + Assert.Equal(typeof(ControllerTypeA), services[0].ServiceType); + Assert.Equal(typeof(ControllerTypeA), services[0].ImplementationType); + Assert.Equal(LifecycleKind.Transient, services[0].Lifecycle); + + Assert.Equal(typeof(TypeBController), services[1].ServiceType); + Assert.Equal(typeof(TypeBController), services[1].ImplementationType); + Assert.Equal(LifecycleKind.Transient, services[1].Lifecycle); + + Assert.Equal(typeof(IControllerActivator), services[2].ServiceType); + Assert.Equal(typeof(ServiceBasedControllerActivator), services[2].ImplementationType); + Assert.Equal(LifecycleKind.Transient, services[2].Lifecycle); + + Assert.Equal(typeof(IControllerTypeProvider), services[3].ServiceType); + var typeProvider = Assert.IsType(services[3].ImplementationInstance); + Assert.Equal(controllerTypes, typeProvider.ControllerTypes.OrderBy(c => c.Name)); + Assert.Equal(LifecycleKind.Singleton, services[3].Lifecycle); + } + + [Fact] + public void WithControllersAsServices_UsesConfigurationIfSpecified() + { + // Arrange + var collection = new ServiceCollection(); + var controllerTypes = new[] { typeof(ControllerTypeA), typeof(TypeBController) }; + var configuration = new Configuration(); + configuration.Add(new MemoryConfigurationSource()); + configuration.Set(typeof(IControllerActivator).FullName, + typeof(CustomActivator).AssemblyQualifiedName); + configuration.Set(typeof(IControllerTypeProvider).FullName, + typeof(CustomTypeProvider).AssemblyQualifiedName); + + // Act + MvcServiceCollectionExtensions.WithControllersAsServices(collection, + controllerTypes, + configuration); + + // Assert + var services = collection.ToList(); + Assert.Equal(4, services.Count); + Assert.Equal(typeof(ControllerTypeA), services[0].ServiceType); + Assert.Equal(typeof(ControllerTypeA), services[0].ImplementationType); + Assert.Equal(LifecycleKind.Transient, services[0].Lifecycle); + + Assert.Equal(typeof(TypeBController), services[1].ServiceType); + Assert.Equal(typeof(TypeBController), services[1].ImplementationType); + Assert.Equal(LifecycleKind.Transient, services[1].Lifecycle); + + Assert.Equal(typeof(IControllerActivator), services[2].ServiceType); + Assert.Equal(typeof(CustomActivator), services[2].ImplementationType); + Assert.Equal(LifecycleKind.Transient, services[2].Lifecycle); + + Assert.Equal(typeof(IControllerTypeProvider), services[3].ServiceType); + Assert.Equal(typeof(CustomTypeProvider), services[3].ImplementationType); + Assert.Equal(LifecycleKind.Singleton, services[3].Lifecycle); + } + + [Fact] + public void WithControllersAsServices_ScansControllersFromSpecifiedAssemblies() + { + // Arrange + var collection = new ServiceCollection(); + var assemblies = new[] { GetType().Assembly }; + var configuration = new Configuration(); + var controllerTypes = new[] { typeof(ControllerTypeA), typeof(TypeBController) }; + + // Act + MvcServiceCollectionExtensions.WithControllersAsServices(collection, + assemblies); + + // Assert + var services = collection.ToList(); + Assert.Equal(4, services.Count); + Assert.Equal(typeof(ControllerTypeA), services[0].ServiceType); + Assert.Equal(typeof(ControllerTypeA), services[0].ImplementationType); + Assert.Equal(LifecycleKind.Transient, services[0].Lifecycle); + + Assert.Equal(typeof(TypeBController), services[1].ServiceType); + Assert.Equal(typeof(TypeBController), services[1].ImplementationType); + Assert.Equal(LifecycleKind.Transient, services[1].Lifecycle); + + + Assert.Equal(typeof(IControllerActivator), services[2].ServiceType); + Assert.Equal(typeof(ServiceBasedControllerActivator), services[2].ImplementationType); + Assert.Equal(LifecycleKind.Transient, services[2].Lifecycle); + + Assert.Equal(typeof(IControllerTypeProvider), services[3].ServiceType); + var typeProvider = Assert.IsType(services[3].ImplementationInstance); + Assert.Equal(controllerTypes, typeProvider.ControllerTypes.OrderBy(c => c.Name)); + Assert.Equal(LifecycleKind.Singleton, services[3].Lifecycle); + } + + private class CustomActivator : IControllerActivator + { + public object Create(ActionContext context, Type controllerType) + { + throw new NotImplementedException(); + } + } + + public class CustomTypeProvider : IControllerTypeProvider + { + public IEnumerable ControllerTypes { get; set; } + } + } +} + +// These controllers are used to test the UseControllersAsServices implementation +// which REQUIRES that they be public top-level classes. To avoid having to stub out the +// implementation of this class to test it, they are just top level classes. Don't reuse +// these outside this test - find a better way or use nested classes to keep the tests +// independent. +namespace Microsoft.AspNet.Mvc.MvcServiceCollectionExtensionsTestControllers +{ + public class ControllerTypeA : Controller + { + + } + + public class TypeBController + { + + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/ApiControllerActionDiscoveryTest.cs b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/ApiControllerActionDiscoveryTest.cs index 68c1fb0e95..4836a1c585 100644 --- a/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/ApiControllerActionDiscoveryTest.cs +++ b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/ApiControllerActionDiscoveryTest.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Reflection; using Microsoft.AspNet.Mvc; using Microsoft.AspNet.Mvc.ApplicationModels; +using Microsoft.AspNet.Mvc.Core; using Microsoft.AspNet.Mvc.Filters; using Microsoft.AspNet.Mvc.WebApiCompatShim; using Microsoft.Framework.DependencyInjection; @@ -368,10 +369,11 @@ namespace System.Web.Http private INestedProviderManager CreateProvider() { - var assemblyProvider = new Mock(); - assemblyProvider - .SetupGet(ap => ap.CandidateAssemblies) - .Returns(new Assembly[] { typeof(ApiControllerActionDiscoveryTest).Assembly }); + var assemblyProvider = new FixedSetAssemblyProvider(); + assemblyProvider.CandidateAssemblies.Add(GetType().GetTypeInfo().Assembly); + var controllerTypeProvider = new NamespaceFilteredControllerTypeProvider(assemblyProvider); + var modelBuilder = new DefaultControllerModelBuilder(new DefaultActionModelBuilder(), + NullLoggerFactory.Instance); var filterProvider = new Mock(); filterProvider @@ -389,8 +391,8 @@ namespace System.Web.Http .Returns(options); var provider = new ControllerActionDescriptorProvider( - assemblyProvider.Object, - new NamespaceLimitedActionDiscoveryConventions(), + controllerTypeProvider, + modelBuilder, filterProvider.Object, optionsAccessor.Object, new NullLoggerFactory()); @@ -402,18 +404,21 @@ namespace System.Web.Http }); } - private class NamespaceLimitedActionDiscoveryConventions : DefaultControllerModelBuilder + private class NamespaceFilteredControllerTypeProvider : DefaultControllerTypeProvider { - public NamespaceLimitedActionDiscoveryConventions() - : base(new DefaultActionModelBuilder(), new NullLoggerFactory()) + public NamespaceFilteredControllerTypeProvider(IAssemblyProvider provider) + : base(provider, NullLoggerFactory.Instance) { + } - protected override bool IsController(TypeInfo typeInfo) + public override IEnumerable ControllerTypes { - return - typeInfo.Namespace == "System.Web.Http.TestControllers" && - base.IsController(typeInfo); + get + { + return base.ControllerTypes + .Where(typeInfo => typeInfo.Namespace == "System.Web.Http.TestControllers"); + } } } } diff --git a/test/WebSites/ActivatorWebSite/Controllers/PlainController.cs b/test/WebSites/ActivatorWebSite/Controllers/PlainController.cs index b634f33e33..94c52f33fb 100644 --- a/test/WebSites/ActivatorWebSite/Controllers/PlainController.cs +++ b/test/WebSites/ActivatorWebSite/Controllers/PlainController.cs @@ -8,7 +8,7 @@ namespace ActivatorWebSite { public class PlainController { - [Activate] + [FromServices] public MyService Service { get; set; } [Activate] diff --git a/test/WebSites/ControllersFromServicesClassLibrary/ControllerWithConstructorInjection.cs b/test/WebSites/ControllersFromServicesClassLibrary/ControllerWithConstructorInjection.cs new file mode 100644 index 0000000000..4aca4b7ea8 --- /dev/null +++ b/test/WebSites/ControllersFromServicesClassLibrary/ControllerWithConstructorInjection.cs @@ -0,0 +1,36 @@ +// 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 ControllersFromServicesClassLibrary +{ + public class ConstructorInjectionController + { + public ConstructorInjectionController(IUrlHelper urlHelper, + QueryValueService queryService) + { + UrlHelper = urlHelper; + QueryService = queryService; + } + + private IUrlHelper UrlHelper { get; } + + private QueryValueService QueryService { get; } + + [Activate] + public HttpRequest Request { get; set; } + + [HttpGet("/constructorinjection")] + public IActionResult Index() + { + var content = string.Join(" ", + UrlHelper.Action(), + QueryService.GetValue(), + Request.Headers["Test-Header"]); + + return new ContentResult { Content = content }; + } + } +} \ No newline at end of file diff --git a/test/WebSites/ControllersFromServicesClassLibrary/ControllersFromServicesClassLibrary.kproj b/test/WebSites/ControllersFromServicesClassLibrary/ControllersFromServicesClassLibrary.kproj new file mode 100644 index 0000000000..95b3103159 --- /dev/null +++ b/test/WebSites/ControllersFromServicesClassLibrary/ControllersFromServicesClassLibrary.kproj @@ -0,0 +1,20 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + 551dc89e-2a13-4cf2-83d7-1add802443d5 + + + + + + + + 2.0 + + + \ No newline at end of file diff --git a/test/WebSites/ControllersFromServicesClassLibrary/EmployeeRecords.cs b/test/WebSites/ControllersFromServicesClassLibrary/EmployeeRecords.cs new file mode 100644 index 0000000000..df505a0172 --- /dev/null +++ b/test/WebSites/ControllersFromServicesClassLibrary/EmployeeRecords.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 Microsoft.AspNet.Mvc; + +namespace ControllersFromServicesClassLibrary +{ + public class EmployeeRecords : Controller + { + [HttpPut("/employee/update_records")] + public IActionResult UpdateRecords(string recordId) + { + return Content("Updated record " + recordId); + } + + [HttpPost] + // This action uses conventional routing. + public IActionResult Save(string id) + { + return Content("Saved record employee #" + id); + } + + } +} diff --git a/test/WebSites/ControllersFromServicesClassLibrary/GenericController.cs b/test/WebSites/ControllersFromServicesClassLibrary/GenericController.cs new file mode 100644 index 0000000000..01111015cc --- /dev/null +++ b/test/WebSites/ControllersFromServicesClassLibrary/GenericController.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 ControllersFromServicesClassLibrary +{ + public class GenericController : Controller + { + [HttpGet("/not-discovered/generic")] + public IActionResult Index() + { + return new EmptyResult(); + } + } +} diff --git a/test/WebSites/ControllersFromServicesClassLibrary/NestedControllerOwner.cs b/test/WebSites/ControllersFromServicesClassLibrary/NestedControllerOwner.cs new file mode 100644 index 0000000000..858e2bfc61 --- /dev/null +++ b/test/WebSites/ControllersFromServicesClassLibrary/NestedControllerOwner.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 ControllersFromServicesClassLibrary +{ + public class NestedControllerOwner + { + public class NestedController : Controller + { + [HttpGet("/not-discovered/nested")] + public IActionResult Index() + { + return new EmptyResult(); + } + } + } +} diff --git a/test/WebSites/ControllersFromServicesClassLibrary/QueryValueService.cs b/test/WebSites/ControllersFromServicesClassLibrary/QueryValueService.cs new file mode 100644 index 0000000000..1a3b1e63b0 --- /dev/null +++ b/test/WebSites/ControllersFromServicesClassLibrary/QueryValueService.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.Hosting; +using Microsoft.AspNet.Http; + +namespace ControllersFromServicesClassLibrary +{ + public class QueryValueService + { + private readonly HttpContext _context; + + public QueryValueService(IHttpContextAccessor httpContextAccessor) + { + _context = httpContextAccessor.Value; + } + + public string GetValue() + { + return _context.Request.Query["value"]; + } + } +} \ No newline at end of file diff --git a/test/WebSites/ControllersFromServicesClassLibrary/TimeScheduleController.cs b/test/WebSites/ControllersFromServicesClassLibrary/TimeScheduleController.cs new file mode 100644 index 0000000000..5ce767eeb5 --- /dev/null +++ b/test/WebSites/ControllersFromServicesClassLibrary/TimeScheduleController.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 ControllersFromServicesClassLibrary +{ + public class TimeScheduleController + { + [HttpGet("/schedule/{id:int}")] + public IActionResult GetSchedule(int id) + { + return new ContentResult { Content = "No schedules available for " + id }; + } + } +} diff --git a/test/WebSites/ControllersFromServicesClassLibrary/project.json b/test/WebSites/ControllersFromServicesClassLibrary/project.json new file mode 100644 index 0000000000..1c8a133867 --- /dev/null +++ b/test/WebSites/ControllersFromServicesClassLibrary/project.json @@ -0,0 +1,9 @@ +{ + "dependencies": { + "Microsoft.AspNet.Mvc": "6.0.0-*", + }, + "frameworks": { + "aspnet50": { }, + "aspnetcore50": { } + } +} \ No newline at end of file diff --git a/test/WebSites/ControllersFromServicesWebSite/ControllersFromServicesWebSite.kproj b/test/WebSites/ControllersFromServicesWebSite/ControllersFromServicesWebSite.kproj new file mode 100644 index 0000000000..0e4d50e9d2 --- /dev/null +++ b/test/WebSites/ControllersFromServicesWebSite/ControllersFromServicesWebSite.kproj @@ -0,0 +1,20 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + 983741b2-4424-4ed1-9b03-7675a67230c8 + + + + + + + 2.0 + 42994 + + + \ No newline at end of file diff --git a/test/WebSites/ControllersFromServicesWebSite/NotInServicesController.cs b/test/WebSites/ControllersFromServicesWebSite/NotInServicesController.cs new file mode 100644 index 0000000000..96c27229b7 --- /dev/null +++ b/test/WebSites/ControllersFromServicesWebSite/NotInServicesController.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 ControllersFromServicesWebSite +{ + public class NotInServicesController : Controller + { + [HttpGet("/not-discovered/not-in-services")] + public IActionResult Index() + { + return View(); + } + } +} diff --git a/test/WebSites/ControllersFromServicesWebSite/Startup.cs b/test/WebSites/ControllersFromServicesWebSite/Startup.cs new file mode 100644 index 0000000000..95fb0b8c80 --- /dev/null +++ b/test/WebSites/ControllersFromServicesWebSite/Startup.cs @@ -0,0 +1,54 @@ +// 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.Reflection; +using Microsoft.AspNet.Builder; +using Microsoft.Framework.DependencyInjection; +using ControllersFromServicesClassLibrary; + +#if ASPNET50 +using Autofac; +using Microsoft.Framework.DependencyInjection.Autofac; +#endif + +namespace ControllersFromServicesWebSite +{ + public class Startup + { + public void Configure(IApplicationBuilder app) + { + var configuration = app.GetTestConfiguration(); + + app.UseServices(services => + { + services.AddMvc(configuration) + .WithControllersAsServices( + new[] + { + typeof(TimeScheduleController).GetTypeInfo().Assembly + }); + + services.AddTransient(); + +#if ASPNET50 + // Create the autofac container + var builder = new ContainerBuilder(); + + // Create the container and use the default application services as a fallback + AutofacRegistration.Populate( + builder, + services); + + return builder.Build() + .Resolve(); +#endif + }); + + app.UseMvc(routes => + { + routes.MapRoute("default", "{controller}/{action}/{id}"); + }); + } + } +} \ No newline at end of file diff --git a/test/WebSites/ControllersFromServicesWebSite/ViewData.cshtml b/test/WebSites/ControllersFromServicesWebSite/ViewData.cshtml new file mode 100644 index 0000000000..c864b370c8 --- /dev/null +++ b/test/WebSites/ControllersFromServicesWebSite/ViewData.cshtml @@ -0,0 +1 @@ +@ViewBag.Value \ No newline at end of file diff --git a/test/WebSites/ControllersFromServicesWebSite/project.json b/test/WebSites/ControllersFromServicesWebSite/project.json new file mode 100644 index 0000000000..5026f392c7 --- /dev/null +++ b/test/WebSites/ControllersFromServicesWebSite/project.json @@ -0,0 +1,24 @@ +{ + "commands": { + "web": "Microsoft.AspNet.Hosting server=Microsoft.AspNet.Server.WebListener server.urls=http://localhost:5001", + "kestrel": "Microsoft.AspNet.Hosting --server Kestrel --server.urls http://localhost:5000" + }, + "dependencies": { + "ControllersFromServicesClassLibrary": "1.0.0", + "Kestrel": "1.0.0-*", + "Microsoft.AspNet.Mvc": "6.0.0-*", + "Microsoft.AspNet.Mvc.TestConfiguration": "1.0.0", + "Microsoft.AspNet.Server.IIS": "1.0.0-*", + "Microsoft.AspNet.Server.WebListener": "1.0.0-*", + "Microsoft.AspNet.StaticFiles": "1.0.0-*" + }, + "frameworks": { + "aspnet50": { + "dependencies": { + "Microsoft.Framework.DependencyInjection.Autofac": "1.0.0-*" + } + }, + "aspnetcore50": { } + }, + "webroot": "wwwroot" +} \ No newline at end of file diff --git a/test/WebSites/ControllersFromServicesWebSite/wwwroot/readme.md b/test/WebSites/ControllersFromServicesWebSite/wwwroot/readme.md new file mode 100644 index 0000000000..ad8de8159e --- /dev/null +++ b/test/WebSites/ControllersFromServicesWebSite/wwwroot/readme.md @@ -0,0 +1 @@ +Functional test site for verifying that controllers registered as services can be consumed by DefaultControllerFactory. \ No newline at end of file diff --git a/test/WebSites/ModelBindingWebSite/Controllers/RoundtripController.cs b/test/WebSites/ModelBindingWebSite/Controllers/RoundtripController.cs index 8f30225bcf..7e8bdfcd2c 100644 --- a/test/WebSites/ModelBindingWebSite/Controllers/RoundtripController.cs +++ b/test/WebSites/ModelBindingWebSite/Controllers/RoundtripController.cs @@ -14,7 +14,7 @@ namespace ModelBindingWebSite.Controllers private IHtmlHelper _personHelper; private bool _activated; - [Activate] + [FromServices] public IHtmlHelper PersonHelper { get diff --git a/test/WebSites/MvcTagHelpersWebSite/Controllers/Catalog_CacheTagHelperController.cs b/test/WebSites/MvcTagHelpersWebSite/Controllers/Catalog_CacheTagHelperController.cs index 3fb0549183..51ba703cf4 100644 --- a/test/WebSites/MvcTagHelpersWebSite/Controllers/Catalog_CacheTagHelperController.cs +++ b/test/WebSites/MvcTagHelpersWebSite/Controllers/Catalog_CacheTagHelperController.cs @@ -8,7 +8,7 @@ namespace MvcTagHelpersWebSite.Controllers { public class Catalog_CacheTagHelperController : Controller { - [Activate] + [FromServices] public ProductsService ProductsService { get; set; } [HttpGet("/catalog")] diff --git a/test/WebSites/RequestServicesWebSite/Controllers/RequestScopedServiceController.cs b/test/WebSites/RequestServicesWebSite/Controllers/RequestScopedServiceController.cs index 2586890afa..dcaeb61d32 100644 --- a/test/WebSites/RequestServicesWebSite/Controllers/RequestScopedServiceController.cs +++ b/test/WebSites/RequestServicesWebSite/Controllers/RequestScopedServiceController.cs @@ -8,7 +8,7 @@ namespace RequestServicesWebSite [Route("RequestScoped/[action]")] public class RequestScopedServiceController { - [Activate] + [FromServices] public RequestIdService RequestIdService { get; set; } [HttpGet] diff --git a/test/WebSites/WebApiCompatShimWebSite/Controllers/BasicApiController.cs b/test/WebSites/WebApiCompatShimWebSite/Controllers/BasicApiController.cs index 16a04e36f9..141779a979 100644 --- a/test/WebSites/WebApiCompatShimWebSite/Controllers/BasicApiController.cs +++ b/test/WebSites/WebApiCompatShimWebSite/Controllers/BasicApiController.cs @@ -15,7 +15,7 @@ namespace WebApiCompatShimWebSite { public class BasicApiController : ApiController { - [Activate] + [FromServices] public IOptions OptionsAccessor { get; set; } // Verifies property activation