[Fixes #3752] Cleanup Controller invocation pipeline

* Added a Release method to IControllerActivator
* Changed Create in IControllerActivator to take in a ControllerActionContext
* Move the check to determine if a controller can be instantiated into the controller activator.
* Move logic for disposing controllers into the controller activator and make release on the
  controller factory delegate into the activator.
* Changed release methods to take in a controller context.
This commit is contained in:
jacalvar 2016-01-15 17:05:48 -08:00
parent e952009b09
commit 2b0bea675e
10 changed files with 246 additions and 121 deletions

View File

@ -2,6 +2,9 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Reflection;
using Microsoft.AspNetCore.Mvc.Core;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.Internal; using Microsoft.AspNetCore.Mvc.Internal;
namespace Microsoft.AspNetCore.Mvc.Controllers namespace Microsoft.AspNetCore.Mvc.Controllers
@ -19,23 +22,64 @@ namespace Microsoft.AspNetCore.Mvc.Controllers
/// <param name="typeActivatorCache">The <see cref="ITypeActivatorCache"/>.</param> /// <param name="typeActivatorCache">The <see cref="ITypeActivatorCache"/>.</param>
public DefaultControllerActivator(ITypeActivatorCache typeActivatorCache) public DefaultControllerActivator(ITypeActivatorCache typeActivatorCache)
{ {
if (typeActivatorCache == null)
{
throw new ArgumentNullException(nameof(typeActivatorCache));
}
_typeActivatorCache = typeActivatorCache; _typeActivatorCache = typeActivatorCache;
} }
/// <inheritdoc /> /// <inheritdoc />
public virtual object Create(ActionContext actionContext, Type controllerType) public virtual object Create(ControllerContext controllerContext)
{ {
if (actionContext == null) if (controllerContext == null)
{ {
throw new ArgumentNullException(nameof(actionContext)); throw new ArgumentNullException(nameof(controllerContext));
} }
if (controllerType == null) if (controllerContext.ActionDescriptor == null)
{ {
throw new ArgumentNullException(nameof(controllerType)); throw new ArgumentException(Resources.FormatPropertyOfTypeCannotBeNull(
nameof(ControllerContext.ActionDescriptor),
nameof(ControllerContext)));
} }
var serviceProvider = actionContext.HttpContext.RequestServices; var controllerTypeInfo = controllerContext.ActionDescriptor.ControllerTypeInfo;
return _typeActivatorCache.CreateInstance<object>(serviceProvider, controllerType); if (controllerTypeInfo.IsValueType ||
controllerTypeInfo.IsInterface ||
controllerTypeInfo.IsAbstract ||
(controllerTypeInfo.IsGenericType && controllerTypeInfo.IsGenericTypeDefinition))
{
var message = Resources.FormatValueInterfaceAbstractOrOpenGenericTypesCannotBeActivated(
controllerTypeInfo.FullName,
GetType().FullName);
throw new InvalidOperationException(message);
}
var serviceProvider = controllerContext.HttpContext.RequestServices;
return _typeActivatorCache.CreateInstance<object>(serviceProvider, controllerTypeInfo.AsType());
}
/// <inheritdoc />
public virtual void Release(ControllerContext context, object controller)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (controller == null)
{
throw new ArgumentNullException(nameof(controller));
}
var disposable = controller as IDisposable;
if (disposable != null)
{
disposable.Dispose();
}
} }
} }
} }

View File

@ -31,6 +31,16 @@ namespace Microsoft.AspNetCore.Mvc.Controllers
IControllerActivator controllerActivator, IControllerActivator controllerActivator,
IEnumerable<IControllerPropertyActivator> propertyActivators) IEnumerable<IControllerPropertyActivator> propertyActivators)
{ {
if (controllerActivator == null)
{
throw new ArgumentNullException(nameof(controllerActivator));
}
if (propertyActivators == null)
{
throw new ArgumentNullException(nameof(propertyActivators));
}
_controllerActivator = controllerActivator; _controllerActivator = controllerActivator;
_propertyActivators = propertyActivators.ToArray(); _propertyActivators = propertyActivators.ToArray();
} }
@ -61,20 +71,7 @@ namespace Microsoft.AspNetCore.Mvc.Controllers
nameof(ControllerContext))); nameof(ControllerContext)));
} }
var controllerType = context.ActionDescriptor.ControllerTypeInfo.AsType(); var controller = _controllerActivator.Create(context);
var controllerTypeInfo = controllerType.GetTypeInfo();
if (controllerTypeInfo.IsValueType ||
controllerTypeInfo.IsInterface ||
controllerTypeInfo.IsAbstract ||
(controllerTypeInfo.IsGenericType && controllerTypeInfo.IsGenericTypeDefinition))
{
var message = Resources.FormatValueInterfaceAbstractOrOpenGenericTypesCannotBeActivated(
controllerType.FullName,
GetType().FullName);
throw new InvalidOperationException(message);
}
var controller = _controllerActivator.Create(context, controllerType);
foreach (var propertyActivator in _propertyActivators) foreach (var propertyActivator in _propertyActivators)
{ {
propertyActivator.Activate(context, controller); propertyActivator.Activate(context, controller);
@ -84,14 +81,19 @@ namespace Microsoft.AspNetCore.Mvc.Controllers
} }
/// <inheritdoc /> /// <inheritdoc />
public virtual void ReleaseController(object controller) public virtual void ReleaseController(ControllerContext context, object controller)
{ {
var disposableController = controller as IDisposable; if (context == null)
if (disposableController != null)
{ {
disposableController.Dispose(); throw new ArgumentNullException(nameof(context));
} }
if (controller == null)
{
throw new ArgumentNullException(nameof(controller));
}
_controllerActivator.Release(context, controller);
} }
} }
} }

View File

@ -13,7 +13,13 @@ namespace Microsoft.AspNetCore.Mvc.Controllers
/// <summary> /// <summary>
/// Creates a controller. /// Creates a controller.
/// </summary> /// </summary>
/// <param name="context">The <see cref="ActionContext"/> for the executing action.</param> /// <param name="context">The <see cref="ControllerContext"/> for the executing action.</param>
object Create(ActionContext context, Type controllerType); object Create(ControllerContext context);
/// <summary>
/// Releases a controller.
/// </summary>
/// <param name="controller">The controller to release.</param>
void Release(ControllerContext context, object controller);
} }
} }

View File

@ -19,6 +19,6 @@ namespace Microsoft.AspNetCore.Mvc.Controllers
/// Releases a controller instance. /// Releases a controller instance.
/// </summary> /// </summary>
/// <param name="controller">The controller.</param> /// <param name="controller">The controller.</param>
void ReleaseController(object controller); void ReleaseController(ControllerContext context, object controller);
} }
} }

View File

@ -13,19 +13,21 @@ namespace Microsoft.AspNetCore.Mvc.Controllers
public class ServiceBasedControllerActivator : IControllerActivator public class ServiceBasedControllerActivator : IControllerActivator
{ {
/// <inheritdoc /> /// <inheritdoc />
public object Create(ActionContext actionContext, Type controllerType) public object Create(ControllerContext actionContext)
{ {
if (actionContext == null) if (actionContext == null)
{ {
throw new ArgumentNullException(nameof(actionContext)); throw new ArgumentNullException(nameof(actionContext));
} }
if (controllerType == null) var controllerType = actionContext.ActionDescriptor.ControllerTypeInfo.AsType();
{
throw new ArgumentNullException(nameof(controllerType));
}
return actionContext.HttpContext.RequestServices.GetRequiredService(controllerType); return actionContext.HttpContext.RequestServices.GetRequiredService(controllerType);
} }
/// <inheritdoc />
public virtual void Release(ControllerContext context, object controller)
{
}
} }
} }

View File

@ -83,11 +83,16 @@ namespace Microsoft.AspNetCore.Mvc.Internal
protected override void ReleaseInstance(object instance) protected override void ReleaseInstance(object instance)
{ {
_controllerFactory.ReleaseController(instance); _controllerFactory.ReleaseController(Context, instance);
} }
protected override async Task<IActionResult> InvokeActionAsync(ActionExecutingContext actionExecutingContext) protected override async Task<IActionResult> InvokeActionAsync(ActionExecutingContext actionExecutingContext)
{ {
if (actionExecutingContext == null)
{
throw new ArgumentNullException(nameof(actionExecutingContext));
}
var actionMethodInfo = _descriptor.MethodInfo; var actionMethodInfo = _descriptor.MethodInfo;
var arguments = ControllerActionExecutor.PrepareArguments( var arguments = ControllerActionExecutor.PrepareArguments(
actionExecutingContext.ActionArguments, actionExecutingContext.ActionArguments,

View File

@ -2,9 +2,13 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Reflection;
using Microsoft.AspNetCore.Http.Internal; using Microsoft.AspNetCore.Http.Internal;
using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.Internal; using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing;
using Moq; using Moq;
using Xunit; using Xunit;
@ -26,16 +30,79 @@ namespace Microsoft.AspNetCore.Mvc.Controllers
{ {
RequestServices = serviceProvider.Object RequestServices = serviceProvider.Object
}; };
var actionContext = new ActionContext(httpContext,
new RouteData(), var context = new ControllerContext(
new ActionDescriptor()); new ActionContext(
httpContext,
new RouteData(),
new ControllerActionDescriptor
{
ControllerTypeInfo = type.GetTypeInfo()
}));
// Act // Act
var instance = activator.Create(actionContext, type); var instance = activator.Create(context);
// Assert // Assert
Assert.IsType(type, instance); Assert.IsType(type, instance);
} }
[Fact]
public void Release_DisposesController_IfDisposable()
{
// Arrange
var controller = new MyController();
var activator = new DefaultControllerActivator(Mock.Of<ITypeActivatorCache>());
// Act
activator.Release(new ControllerContext(), controller);
// Assert
Assert.Equal(true, controller.Disposed);
}
[Theory]
[InlineData(typeof(int))]
[InlineData(typeof(OpenGenericType<>))]
[InlineData(typeof(AbstractType))]
[InlineData(typeof(InterfaceType))]
public void CreateController_ThrowsIfControllerCannotBeActivated(Type type)
{
// Arrange
var actionDescriptor = new ControllerActionDescriptor
{
ControllerTypeInfo = type.GetTypeInfo()
};
var context = new ControllerContext()
{
ActionDescriptor = actionDescriptor,
HttpContext = new DefaultHttpContext()
{
RequestServices = GetServices(),
},
};
var factory = new DefaultControllerActivator(new TypeActivatorCache());
// Act and Assert
var exception = Assert.Throws<InvalidOperationException>(() => factory.Create(context));
Assert.Equal(
$"The type '{type.FullName}' cannot be activated by '{typeof(DefaultControllerActivator).FullName}' " +
"because it is either a value type, an interface, an abstract class or an open generic type.",
exception.Message);
}
[Fact]
public void DefaultControllerActivator_ReleasesNonIDisposableController()
{
// Arrange
var activator = new DefaultControllerActivator(Mock.Of<ITypeActivatorCache>());
var controller = new object();
// Act + Assert (does not throw)
activator.Release(Mock.Of<ControllerContext>(), controller);
}
[Fact] [Fact]
public void Create_TypeActivatesTypesWithServices() public void Create_TypeActivatesTypesWithServices()
{ {
@ -51,11 +118,18 @@ namespace Microsoft.AspNetCore.Mvc.Controllers
{ {
RequestServices = serviceProvider.Object RequestServices = serviceProvider.Object
}; };
var actionContext = new ActionContext(httpContext,
new RouteData(), var context = new ControllerContext(
new ActionDescriptor()); new ActionContext(
httpContext,
new RouteData(),
new ControllerActionDescriptor
{
ControllerTypeInfo = typeof(TypeDerivingFromControllerWithServices).GetTypeInfo()
}));
// Act // Act
var instance = activator.Create(actionContext, typeof(TypeDerivingFromControllerWithServices)); var instance = activator.Create(context);
// Assert // Assert
var controller = Assert.IsType<TypeDerivingFromControllerWithServices>(instance); var controller = Assert.IsType<TypeDerivingFromControllerWithServices>(instance);
@ -81,6 +155,19 @@ namespace Microsoft.AspNetCore.Mvc.Controllers
public TestService TestService { get; } public TestService TestService { get; }
} }
private IServiceProvider GetServices()
{
var metadataProvider = new EmptyModelMetadataProvider();
var services = new Mock<IServiceProvider>();
services.Setup(s => s.GetService(typeof(IUrlHelper)))
.Returns(Mock.Of<IUrlHelper>());
services.Setup(s => s.GetService(typeof(IModelMetadataProvider)))
.Returns(metadataProvider);
services.Setup(s => s.GetService(typeof(IObjectModelValidator)))
.Returns(new DefaultObjectValidator(metadataProvider));
return services.Object;
}
private class PocoType private class PocoType
{ {
} }
@ -88,5 +175,27 @@ namespace Microsoft.AspNetCore.Mvc.Controllers
private class TestService private class TestService
{ {
} }
private class OpenGenericType<T> : Controller
{
}
private abstract class AbstractType : Controller
{
}
private interface InterfaceType
{
}
private class MyController : IDisposable
{
public bool Disposed { get; set; }
public void Dispose()
{
Disposed = true;
}
}
} }
} }

View File

@ -35,7 +35,7 @@ namespace Microsoft.AspNetCore.Mvc.Controllers
}; };
var activator = new Mock<IControllerActivator>(); var activator = new Mock<IControllerActivator>();
activator.Setup(a => a.Create(context, typeof(MyController))) activator.Setup(a => a.Create(context))
.Returns(expected) .Returns(expected)
.Verifiable(); .Verifiable();
@ -186,61 +186,20 @@ namespace Microsoft.AspNetCore.Mvc.Controllers
exception.Message); exception.Message);
} }
[Theory]
[InlineData(typeof(int))]
[InlineData(typeof(OpenGenericType<>))]
[InlineData(typeof(AbstractType))]
[InlineData(typeof(InterfaceType))]
public void CreateController_ThrowsIfControllerCannotBeActivated(Type type)
{
// Arrange
var actionDescriptor = new ControllerActionDescriptor
{
ControllerTypeInfo = type.GetTypeInfo()
};
var context = new ControllerContext()
{
ActionDescriptor = actionDescriptor,
HttpContext = new DefaultHttpContext()
{
RequestServices = GetServices(),
},
};
var factory = CreateControllerFactory(new DefaultControllerActivator(new TypeActivatorCache()));
// Act and Assert
var exception = Assert.Throws<InvalidOperationException>(() => factory.CreateController(context));
Assert.Equal(
$"The type '{type.FullName}' cannot be activated by '{typeof(DefaultControllerFactory).FullName}' " +
"because it is either a value type, an interface, an abstract class or an open generic type.",
exception.Message);
}
[Fact] [Fact]
public void DefaultControllerFactory_DisposesIDisposableController() public void DefaultControllerFactory_DelegatesDisposalToControllerActivator()
{ {
// Arrange // Arrange
var factory = CreateControllerFactory(); var activatorMock = new Mock<IControllerActivator>();
activatorMock.Setup(s => s.Release(It.IsAny<ControllerContext>(), It.IsAny<object>()));
var factory = CreateControllerFactory(activatorMock.Object);
var controller = new MyController(); var controller = new MyController();
// Act + Assert // Act + Assert
Assert.False(controller.Disposed); factory.ReleaseController(new ControllerContext(), controller);
factory.ReleaseController(controller); activatorMock.Verify();
Assert.True(controller.Disposed);
}
[Fact]
public void DefaultControllerFactory_ReleasesNonIDisposableController()
{
// Arrange
var factory = CreateControllerFactory();
var controller = new object();
// Act + Assert (does not throw)
factory.ReleaseController(controller);
} }
private IServiceProvider GetServices() private IServiceProvider GetServices()
@ -258,7 +217,9 @@ namespace Microsoft.AspNetCore.Mvc.Controllers
private static DefaultControllerFactory CreateControllerFactory(IControllerActivator controllerActivator = null) private static DefaultControllerFactory CreateControllerFactory(IControllerActivator controllerActivator = null)
{ {
controllerActivator = controllerActivator ?? Mock.Of<IControllerActivator>(); var activatorMock = new Mock<IControllerActivator>();
controllerActivator = controllerActivator ?? activatorMock.Object;
var propertyActivators = new IControllerPropertyActivator[] var propertyActivators = new IControllerPropertyActivator[]
{ {
new DefaultControllerPropertyActivator(), new DefaultControllerPropertyActivator(),
@ -313,21 +274,5 @@ namespace Microsoft.AspNetCore.Mvc.Controllers
private class TestService private class TestService
{ {
} }
private class Controller
{
}
private class OpenGenericType<T> : Controller
{
}
private abstract class AbstractType : Controller
{
}
private interface InterfaceType
{
}
} }
} }

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Reflection;
using Microsoft.AspNetCore.Http.Internal; using Microsoft.AspNetCore.Http.Internal;
using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing;
@ -26,12 +27,16 @@ namespace Microsoft.AspNetCore.Mvc.Controllers
RequestServices = serviceProvider.Object RequestServices = serviceProvider.Object
}; };
var activator = new ServiceBasedControllerActivator(); var activator = new ServiceBasedControllerActivator();
var actionContext = new ActionContext(httpContext, var context = new ControllerContext(new ActionContext(
new RouteData(), httpContext,
new ActionDescriptor()); new RouteData(),
new ControllerActionDescriptor
{
ControllerTypeInfo = typeof(DIController).GetTypeInfo()
}));
// Act // Act
var instance = activator.Create(actionContext, typeof(DIController)); var instance = activator.Create(context);
// Assert // Assert
Assert.Same(controller, instance); Assert.Same(controller, instance);
@ -44,19 +49,26 @@ namespace Microsoft.AspNetCore.Mvc.Controllers
// Arrange // Arrange
var expected = "No service for type '" + typeof(DIController) + "' has been registered."; var expected = "No service for type '" + typeof(DIController) + "' has been registered.";
var controller = new DIController(); var controller = new DIController();
var serviceProvider = new Mock<IServiceProvider>();
var httpContext = new DefaultHttpContext var httpContext = new DefaultHttpContext
{ {
RequestServices = serviceProvider.Object RequestServices = Mock.Of<IServiceProvider>()
}; };
var activator = new ServiceBasedControllerActivator(); var activator = new ServiceBasedControllerActivator();
var actionContext = new ActionContext(httpContext, var context = new ControllerContext(
new RouteData(), new ActionContext(
new ActionDescriptor()); httpContext,
new RouteData(),
new ControllerActionDescriptor
{
ControllerTypeInfo = typeof(DIController).GetTypeInfo()
}));
// Act and Assert // Act and Assert
var ex = Assert.Throws<InvalidOperationException>( var ex = Assert.Throws<InvalidOperationException>(
() => activator.Create(actionContext, typeof(DIController))); () => activator.Create(context));
Assert.Equal(expected, ex.Message); Assert.Equal(expected, ex.Message);
} }

View File

@ -2208,7 +2208,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
return _controller; return _controller;
} }
public void ReleaseController(object controller) public void ReleaseController(ControllerContext context, object controller)
{ {
Assert.NotNull(controller); Assert.NotNull(controller);
Assert.Same(_controller, controller); Assert.Same(_controller, controller);