[Fixes #3752] Refactor and cleanup view components
* Moved instantiation of view components into DefaultViewComponentActivator * Introduced IViewComponentFactory to handling setup of new view component instances. * Added a release method on IViewComponentActivator for handling tear down of view component instances.
This commit is contained in:
parent
2b0bea675e
commit
354400f12b
|
|
@ -118,8 +118,10 @@ namespace Microsoft.Extensions.DependencyInjection
|
|||
//
|
||||
// View Components
|
||||
//
|
||||
|
||||
// These do caching so they should stay singleton
|
||||
services.TryAddSingleton<IViewComponentSelector, DefaultViewComponentSelector>();
|
||||
services.TryAddSingleton<IViewComponentFactory, DefaultViewComponentFactory>();
|
||||
services.TryAddSingleton<IViewComponentActivator, DefaultViewComponentActivator>();
|
||||
services.TryAddSingleton<
|
||||
IViewComponentDescriptorCollectionProvider,
|
||||
|
|
|
|||
|
|
@ -858,6 +858,22 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
|||
return string.Format(CultureInfo.CurrentCulture, GetString("ViewComponent_AmbiguousMethods"), p0, p1, p2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The type '{0}' cannot be activated by '{1}' because it is either a value type, an interface, an abstract class or an open generic type.
|
||||
/// </summary>
|
||||
internal static string ValueInterfaceAbstractOrOpenGenericTypesCannotBeActivated
|
||||
{
|
||||
get { return GetString("ValueInterfaceAbstractOrOpenGenericTypesCannotBeActivated"); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The type '{0}' cannot be activated by '{1}' because it is either a value type, an interface, an abstract class or an open generic type.
|
||||
/// </summary>
|
||||
internal static string FormatValueInterfaceAbstractOrOpenGenericTypesCannotBeActivated(object p0, object p1)
|
||||
{
|
||||
return string.Format(CultureInfo.CurrentCulture, GetString("ValueInterfaceAbstractOrOpenGenericTypesCannotBeActivated"), p0, p1);
|
||||
}
|
||||
|
||||
private static string GetString(string name, params string[] formatterNames)
|
||||
{
|
||||
var value = _resourceManager.GetString(name);
|
||||
|
|
|
|||
|
|
@ -277,4 +277,7 @@
|
|||
<data name="ViewComponent_AmbiguousMethods" xml:space="preserve">
|
||||
<value>View component '{0}' must have exactly one public method named '{1}' or '{2}'.</value>
|
||||
</data>
|
||||
<data name="ValueInterfaceAbstractOrOpenGenericTypesCannotBeActivated" xml:space="preserve">
|
||||
<value>The type '{0}' cannot be activated by '{1}' because it is either a value type, an interface, an abstract class or an open generic type.</value>
|
||||
</data>
|
||||
</root>
|
||||
|
|
@ -2,9 +2,9 @@
|
|||
// 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.Reflection;
|
||||
using Microsoft.Extensions.Internal;
|
||||
using Microsoft.AspNetCore.Mvc.Internal;
|
||||
using Microsoft.AspNetCore.Mvc.ViewFeatures;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.ViewComponents
|
||||
{
|
||||
|
|
@ -18,49 +18,71 @@ namespace Microsoft.AspNetCore.Mvc.ViewComponents
|
|||
/// </remarks>
|
||||
public class DefaultViewComponentActivator : IViewComponentActivator
|
||||
{
|
||||
private readonly Func<Type, PropertyActivator<ViewComponentContext>[]> _getPropertiesToActivate;
|
||||
private readonly ConcurrentDictionary<Type, PropertyActivator<ViewComponentContext>[]> _injectActions;
|
||||
private readonly ITypeActivatorCache _typeActivatorCache;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of <see cref="DefaultViewComponentActivator"/> class.
|
||||
/// </summary>
|
||||
public DefaultViewComponentActivator()
|
||||
/// <param name="typeActivatorCache">
|
||||
/// The <see cref="ITypeActivatorCache"/> used to create new view component instances.
|
||||
/// </param>
|
||||
public DefaultViewComponentActivator(ITypeActivatorCache typeActivatorCache)
|
||||
{
|
||||
_injectActions = new ConcurrentDictionary<Type, PropertyActivator<ViewComponentContext>[]>();
|
||||
_getPropertiesToActivate = type =>
|
||||
PropertyActivator<ViewComponentContext>.GetPropertiesToActivate(
|
||||
type,
|
||||
typeof(ViewComponentContextAttribute),
|
||||
CreateActivateInfo);
|
||||
if (typeActivatorCache == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(typeActivatorCache));
|
||||
}
|
||||
|
||||
_typeActivatorCache = typeActivatorCache;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual void Activate(object viewComponent, ViewComponentContext context)
|
||||
public virtual object Create(ViewComponentContext context)
|
||||
{
|
||||
if (viewComponent == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(viewComponent));
|
||||
}
|
||||
|
||||
if (context == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
var propertiesToActivate = _injectActions.GetOrAdd(
|
||||
viewComponent.GetType(),
|
||||
_getPropertiesToActivate);
|
||||
var componentType = context.ViewComponentDescriptor.Type.GetTypeInfo();
|
||||
|
||||
for (var i = 0; i < propertiesToActivate.Length; i++)
|
||||
if (componentType.IsValueType ||
|
||||
componentType.IsInterface ||
|
||||
componentType.IsAbstract ||
|
||||
(componentType.IsGenericType && componentType.IsGenericTypeDefinition))
|
||||
{
|
||||
var activateInfo = propertiesToActivate[i];
|
||||
activateInfo.Activate(viewComponent, context);
|
||||
var message = Resources.FormatValueInterfaceAbstractOrOpenGenericTypesCannotBeActivated(
|
||||
componentType.FullName,
|
||||
GetType().FullName);
|
||||
|
||||
throw new InvalidOperationException(message);
|
||||
}
|
||||
|
||||
var viewComponent = _typeActivatorCache.CreateInstance<object>(
|
||||
context.ViewContext.HttpContext.RequestServices,
|
||||
context.ViewComponentDescriptor.Type);
|
||||
|
||||
return viewComponent;
|
||||
}
|
||||
|
||||
private PropertyActivator<ViewComponentContext> CreateActivateInfo(PropertyInfo property)
|
||||
/// <inheritdoc />
|
||||
public virtual void Release(ViewComponentContext context, object viewComponent)
|
||||
{
|
||||
return new PropertyActivator<ViewComponentContext>(property, context => context);
|
||||
if (context == null)
|
||||
{
|
||||
throw new InvalidOperationException(nameof(context));
|
||||
}
|
||||
|
||||
if (viewComponent == null)
|
||||
{
|
||||
throw new InvalidOperationException(nameof(viewComponent));
|
||||
}
|
||||
|
||||
var disposable = viewComponent as IDisposable;
|
||||
if (disposable != null)
|
||||
{
|
||||
disposable.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Reflection;
|
||||
using Microsoft.Extensions.Internal;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.ViewComponents
|
||||
{
|
||||
/// <summary>
|
||||
/// Default implementation for <see cref="IViewComponentFactory"/>.
|
||||
/// </summary>
|
||||
public class DefaultViewComponentFactory : IViewComponentFactory
|
||||
{
|
||||
private readonly IViewComponentActivator _activator;
|
||||
private readonly Func<Type, PropertyActivator<ViewComponentContext>[]> _getPropertiesToActivate;
|
||||
private readonly ConcurrentDictionary<Type, PropertyActivator<ViewComponentContext>[]> _injectActions;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of <see cref="DefaultViewComponentFactory"/>
|
||||
/// </summary>
|
||||
/// <param name="activator">
|
||||
/// The <see cref="IViewComponentActivator"/> used to create new view component instances.
|
||||
/// </param>
|
||||
public DefaultViewComponentFactory(IViewComponentActivator activator)
|
||||
{
|
||||
if (activator == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(activator));
|
||||
}
|
||||
|
||||
_activator = activator;
|
||||
|
||||
_getPropertiesToActivate = type => PropertyActivator<ViewComponentContext>.GetPropertiesToActivate(
|
||||
type,
|
||||
typeof(ViewComponentContextAttribute),
|
||||
CreateActivateInfo);
|
||||
|
||||
_injectActions = new ConcurrentDictionary<Type, PropertyActivator<ViewComponentContext>[]>();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public object CreateViewComponent(ViewComponentContext context)
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
var component = _activator.Create(context);
|
||||
|
||||
InjectProperties(context, component);
|
||||
|
||||
return component;
|
||||
}
|
||||
|
||||
private void InjectProperties(ViewComponentContext context, object viewComponent)
|
||||
{
|
||||
var propertiesToActivate = _injectActions.GetOrAdd(
|
||||
viewComponent.GetType(),
|
||||
type =>
|
||||
PropertyActivator<ViewComponentContext>.GetPropertiesToActivate(
|
||||
type,
|
||||
typeof(ViewComponentContextAttribute),
|
||||
CreateActivateInfo));
|
||||
|
||||
for (var i = 0; i < propertiesToActivate.Length; i++)
|
||||
{
|
||||
var activateInfo = propertiesToActivate[i];
|
||||
activateInfo.Activate(viewComponent, context);
|
||||
}
|
||||
}
|
||||
|
||||
private static PropertyActivator<ViewComponentContext> CreateActivateInfo(PropertyInfo property)
|
||||
{
|
||||
return new PropertyActivator<ViewComponentContext>(property, context => context);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ReleaseViewComponent(ViewComponentContext context, object component)
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
if (component == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(component));
|
||||
}
|
||||
|
||||
_activator.Release(context, component);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -19,8 +19,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewComponents
|
|||
/// </summary>
|
||||
public class DefaultViewComponentInvoker : IViewComponentInvoker
|
||||
{
|
||||
private readonly ITypeActivatorCache _typeActivatorCache;
|
||||
private readonly IViewComponentActivator _viewComponentActivator;
|
||||
private readonly IViewComponentFactory _viewComponentFactory;
|
||||
private readonly DiagnosticSource _diagnosticSource;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
|
|
@ -28,23 +27,17 @@ namespace Microsoft.AspNetCore.Mvc.ViewComponents
|
|||
/// Initializes a new instance of <see cref="DefaultViewComponentInvoker"/>.
|
||||
/// </summary>
|
||||
/// <param name="typeActivatorCache">Caches factories for instantiating view component instances.</param>
|
||||
/// <param name="viewComponentActivator">The <see cref="IViewComponentActivator"/>.</param>
|
||||
/// <param name="viewComponentFactory">The <see cref="IViewComponentFactory"/>.</param>
|
||||
/// <param name="diagnosticSource">The <see cref="DiagnosticSource"/>.</param>
|
||||
/// <param name="logger">The <see cref="ILogger"/>.</param>
|
||||
public DefaultViewComponentInvoker(
|
||||
ITypeActivatorCache typeActivatorCache,
|
||||
IViewComponentActivator viewComponentActivator,
|
||||
IViewComponentFactory viewComponentFactory,
|
||||
DiagnosticSource diagnosticSource,
|
||||
ILogger logger)
|
||||
{
|
||||
if (typeActivatorCache == null)
|
||||
if (viewComponentFactory == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(typeActivatorCache));
|
||||
}
|
||||
|
||||
if (viewComponentActivator == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(viewComponentActivator));
|
||||
throw new ArgumentNullException(nameof(viewComponentFactory));
|
||||
}
|
||||
|
||||
if (diagnosticSource == null)
|
||||
|
|
@ -57,8 +50,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewComponents
|
|||
throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
_typeActivatorCache = typeActivatorCache;
|
||||
_viewComponentActivator = viewComponentActivator;
|
||||
_viewComponentFactory = viewComponentFactory;
|
||||
_diagnosticSource = diagnosticSource;
|
||||
_logger = logger;
|
||||
}
|
||||
|
|
@ -95,24 +87,9 @@ namespace Microsoft.AspNetCore.Mvc.ViewComponents
|
|||
await result.ExecuteAsync(context);
|
||||
}
|
||||
|
||||
private object CreateComponent(ViewComponentContext context)
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
var services = context.ViewContext.HttpContext.RequestServices;
|
||||
var component = _typeActivatorCache.CreateInstance<object>(
|
||||
services,
|
||||
context.ViewComponentDescriptor.Type);
|
||||
_viewComponentActivator.Activate(component, context);
|
||||
return component;
|
||||
}
|
||||
|
||||
private async Task<IViewComponentResult> InvokeAsyncCore(ViewComponentContext context)
|
||||
{
|
||||
var component = CreateComponent(context);
|
||||
var component = _viewComponentFactory.CreateViewComponent(context);
|
||||
|
||||
using (_logger.ViewComponentScope(context))
|
||||
{
|
||||
|
|
@ -129,13 +106,15 @@ namespace Microsoft.AspNetCore.Mvc.ViewComponents
|
|||
_logger.ViewComponentExecuted(context, startTimestamp, viewComponentResult);
|
||||
_diagnosticSource.AfterViewComponent(context, viewComponentResult, component);
|
||||
|
||||
_viewComponentFactory.ReleaseViewComponent(context, component);
|
||||
|
||||
return viewComponentResult;
|
||||
}
|
||||
}
|
||||
|
||||
private IViewComponentResult InvokeSyncCore(ViewComponentContext context)
|
||||
{
|
||||
var component = CreateComponent(context);
|
||||
var component = _viewComponentFactory.CreateViewComponent(context);
|
||||
|
||||
using (_logger.ViewComponentScope(context))
|
||||
{
|
||||
|
|
@ -155,6 +134,8 @@ namespace Microsoft.AspNetCore.Mvc.ViewComponents
|
|||
}
|
||||
catch (TargetInvocationException ex)
|
||||
{
|
||||
_viewComponentFactory.ReleaseViewComponent(context, component);
|
||||
|
||||
// Preserve callstack of any user-thrown exceptions.
|
||||
var exceptionInfo = ExceptionDispatchInfo.Capture(ex.InnerException);
|
||||
exceptionInfo.Throw();
|
||||
|
|
@ -165,6 +146,8 @@ namespace Microsoft.AspNetCore.Mvc.ViewComponents
|
|||
_logger.ViewComponentExecuted(context, startTimestamp, viewComponentResult);
|
||||
_diagnosticSource.AfterViewComponent(context, viewComponentResult, component);
|
||||
|
||||
_viewComponentFactory.ReleaseViewComponent(context, component);
|
||||
|
||||
return viewComponentResult;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,19 +10,31 @@ namespace Microsoft.AspNetCore.Mvc.ViewComponents
|
|||
{
|
||||
public class DefaultViewComponentInvokerFactory : IViewComponentInvokerFactory
|
||||
{
|
||||
private readonly ITypeActivatorCache _typeActivatorCache;
|
||||
private readonly IViewComponentActivator _viewComponentActivator;
|
||||
private readonly IViewComponentFactory _viewComponentFactory;
|
||||
private readonly ILogger _logger;
|
||||
private readonly DiagnosticSource _diagnosticSource;
|
||||
|
||||
public DefaultViewComponentInvokerFactory(
|
||||
ITypeActivatorCache typeActivatorCache,
|
||||
IViewComponentActivator viewComponentActivator,
|
||||
IViewComponentFactory viewComponentFactory,
|
||||
DiagnosticSource diagnosticSource,
|
||||
ILoggerFactory loggerFactory)
|
||||
{
|
||||
_typeActivatorCache = typeActivatorCache;
|
||||
_viewComponentActivator = viewComponentActivator;
|
||||
if (viewComponentFactory == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(viewComponentFactory));
|
||||
}
|
||||
|
||||
if (diagnosticSource == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(diagnosticSource));
|
||||
}
|
||||
|
||||
if (loggerFactory == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(loggerFactory));
|
||||
}
|
||||
|
||||
_viewComponentFactory = viewComponentFactory;
|
||||
_diagnosticSource = diagnosticSource;
|
||||
|
||||
_logger = loggerFactory.CreateLogger<DefaultViewComponentInvoker>();
|
||||
|
|
@ -40,8 +52,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewComponents
|
|||
}
|
||||
|
||||
return new DefaultViewComponentInvoker(
|
||||
_typeActivatorCache,
|
||||
_viewComponentActivator,
|
||||
_viewComponentFactory,
|
||||
_diagnosticSource,
|
||||
_logger);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,17 +4,25 @@
|
|||
namespace Microsoft.AspNetCore.Mvc.ViewComponents
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides methods to activate an instantiated ViewComponent
|
||||
/// Provides methods to instantiate and release a ViewComponent.
|
||||
/// </summary>
|
||||
public interface IViewComponentActivator
|
||||
{
|
||||
/// <summary>
|
||||
/// When implemented in a type, activates an instantiated ViewComponent.
|
||||
/// Instantiates a ViewComponent.
|
||||
/// </summary>
|
||||
/// <param name="viewComponent">The ViewComponent to activate.</param>
|
||||
/// <param name="context">
|
||||
/// The <see cref="ViewComponentContext"/> for the executing <see cref="ViewComponent"/>.
|
||||
/// </param>
|
||||
void Activate(object viewComponent, ViewComponentContext context);
|
||||
object Create(ViewComponentContext context);
|
||||
|
||||
/// <summary>
|
||||
/// Releases a ViewComponent instance.
|
||||
/// </summary>
|
||||
/// <param name="context">
|
||||
/// The <see cref="ViewComponentContext"/> associated with the <paramref name="component"/>.
|
||||
/// </param>
|
||||
/// <param name="viewComponent">The <see cref="ViewComponent"/> to release.</param>
|
||||
void Release(ViewComponentContext context, object viewComponent);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.ViewComponents
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides methods for creation and disposal of view components.
|
||||
/// </summary>
|
||||
public interface IViewComponentFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new controller for the specified <paramref name="context"/>.
|
||||
/// </summary>
|
||||
/// <param name="context"><see cref="ViewComponentContext"/> for the view component.</param>
|
||||
/// <returns>The view component.</returns>
|
||||
object CreateViewComponent(ViewComponentContext context);
|
||||
|
||||
/// <summary>
|
||||
/// Releases a view component instance.
|
||||
/// </summary>
|
||||
/// <param name="context">The context associated with the <paramref name="component"/>.</param>
|
||||
/// <param name="component">The view component.</param>
|
||||
void ReleaseViewComponent(ViewComponentContext context, object component);
|
||||
}
|
||||
}
|
||||
|
|
@ -525,6 +525,7 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
services.AddSingleton<IViewComponentInvokerFactory, DefaultViewComponentInvokerFactory>();
|
||||
services.AddSingleton<ITypeActivatorCache, TypeActivatorCache>();
|
||||
services.AddSingleton<IViewComponentActivator, DefaultViewComponentActivator>();
|
||||
services.AddSingleton<IViewComponentFactory, DefaultViewComponentFactory>();
|
||||
services.AddSingleton<IViewComponentDescriptorProvider>(new FixedSetViewComponentDescriptorProvider(descriptors));
|
||||
services.AddSingleton<IModelMetadataProvider, EmptyModelMetadataProvider>();
|
||||
services.AddSingleton<ILoggerFactory>(NullLoggerFactory.Instance);
|
||||
|
|
@ -573,6 +574,16 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
}
|
||||
}
|
||||
|
||||
private static string ReadBody(HttpResponse response)
|
||||
{
|
||||
response.Body.Seek(0, SeekOrigin.Begin);
|
||||
|
||||
using (var reader = new StreamReader(response.Body))
|
||||
{
|
||||
return reader.ReadToEnd();
|
||||
}
|
||||
}
|
||||
|
||||
private class TextViewComponent : ViewComponent
|
||||
{
|
||||
public HtmlString Invoke(string name)
|
||||
|
|
@ -588,15 +599,5 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
return Task.FromResult(new HtmlString("Hello-Async, " + name));
|
||||
}
|
||||
}
|
||||
|
||||
private static string ReadBody(HttpResponse response)
|
||||
{
|
||||
response.Body.Seek(0, SeekOrigin.Begin);
|
||||
|
||||
using (var reader = new StreamReader(response.Body))
|
||||
{
|
||||
return reader.ReadToEnd();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,10 @@
|
|||
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http.Internal;
|
||||
using Microsoft.AspNetCore.Mvc.Internal;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.ViewComponents
|
||||
|
|
@ -13,35 +17,116 @@ namespace Microsoft.AspNetCore.Mvc.ViewComponents
|
|||
public void DefaultViewComponentActivator_ActivatesViewComponentContext()
|
||||
{
|
||||
// Arrange
|
||||
var activator = new DefaultViewComponentActivator();
|
||||
var expectedInstance = new TestViewComponent();
|
||||
|
||||
var context = new ViewComponentContext();
|
||||
var instance = new TestViewComponent();
|
||||
var typeActivator = new Mock<ITypeActivatorCache>();
|
||||
typeActivator
|
||||
.Setup(ta => ta.CreateInstance<object>(It.IsAny<IServiceProvider>(), It.IsAny<Type>()))
|
||||
.Returns(expectedInstance);
|
||||
|
||||
var activator = new DefaultViewComponentActivator(typeActivator.Object);
|
||||
|
||||
var context = CreateContext(typeof(TestViewComponent));
|
||||
expectedInstance.ViewComponentContext = context;
|
||||
|
||||
// Act
|
||||
activator.Activate(instance, context);
|
||||
var instance = activator.Create(context) as ViewComponent;
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(instance);
|
||||
Assert.Same(context, instance.ViewComponentContext);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(typeof(int))]
|
||||
[InlineData(typeof(OpenGenericType<>))]
|
||||
[InlineData(typeof(AbstractType))]
|
||||
[InlineData(typeof(InterfaceType))]
|
||||
public void Create_ThrowsIfControllerCannotBeActivated(Type type)
|
||||
{
|
||||
// Arrange
|
||||
var actionDescriptor = new ViewComponentDescriptor
|
||||
{
|
||||
Type = type
|
||||
};
|
||||
|
||||
var context = new ViewComponentContext
|
||||
{
|
||||
ViewComponentDescriptor = actionDescriptor,
|
||||
ViewContext = new ViewContext
|
||||
{
|
||||
HttpContext = new DefaultHttpContext()
|
||||
{
|
||||
RequestServices = Mock.Of<IServiceProvider>()
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
var activator = new DefaultViewComponentActivator(new TypeActivatorCache());
|
||||
|
||||
// Act and Assert
|
||||
var exception = Assert.Throws<InvalidOperationException>(() => activator.Create(context));
|
||||
Assert.Equal(
|
||||
$"The type '{type.FullName}' cannot be activated by '{typeof(DefaultViewComponentActivator).FullName}' " +
|
||||
"because it is either a value type, an interface, an abstract class or an open generic type.",
|
||||
exception.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DefaultViewComponentActivator_ActivatesViewComponentContext_IgnoresNonPublic()
|
||||
{
|
||||
// Arrange
|
||||
var activator = new DefaultViewComponentActivator();
|
||||
var expectedInstance = new VisibilityViewComponent();
|
||||
|
||||
var context = new ViewComponentContext();
|
||||
var instance = new VisibilityViewComponent();
|
||||
var typeActivator = new Mock<ITypeActivatorCache>();
|
||||
typeActivator
|
||||
.Setup(ta => ta.CreateInstance<object>(It.IsAny<IServiceProvider>(), It.IsAny<Type>()))
|
||||
.Returns(expectedInstance);
|
||||
|
||||
var activator = new DefaultViewComponentActivator(typeActivator.Object);
|
||||
|
||||
var context = CreateContext(typeof(VisibilityViewComponent));
|
||||
expectedInstance.ViewComponentContext = context;
|
||||
|
||||
// Act
|
||||
activator.Activate(instance, context);
|
||||
var instance = activator.Create(context) as VisibilityViewComponent;
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(instance);
|
||||
Assert.Same(context, instance.ViewComponentContext);
|
||||
Assert.Null(instance.C);
|
||||
}
|
||||
|
||||
private static ViewComponentContext CreateContext(Type componentType)
|
||||
{
|
||||
return new ViewComponentContext
|
||||
{
|
||||
ViewComponentDescriptor = new ViewComponentDescriptor
|
||||
{
|
||||
Type = componentType
|
||||
},
|
||||
ViewContext = new ViewContext
|
||||
{
|
||||
HttpContext = new DefaultHttpContext
|
||||
{
|
||||
RequestServices = Mock.Of<IServiceProvider>()
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private class OpenGenericType<T> : Controller
|
||||
{
|
||||
}
|
||||
|
||||
private abstract class AbstractType : Controller
|
||||
{
|
||||
}
|
||||
|
||||
private interface InterfaceType
|
||||
{
|
||||
}
|
||||
|
||||
private class TestViewComponent : ViewComponent
|
||||
{
|
||||
public Task ExecuteAsync()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,78 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.ViewComponents
|
||||
{
|
||||
public class DefaultViewComponentFactoryTest
|
||||
{
|
||||
[Fact]
|
||||
public void CreateViewComponent_ActivatesProperties_OnTheInstance()
|
||||
{
|
||||
// Arrange
|
||||
var context = new ViewComponentContext
|
||||
{
|
||||
};
|
||||
|
||||
var component = new ActivablePropertiesViewComponent();
|
||||
var activator = new Mock<IViewComponentActivator>();
|
||||
activator.Setup(a => a.Create(context))
|
||||
.Returns(component);
|
||||
|
||||
var factory = new DefaultViewComponentFactory(activator.Object);
|
||||
|
||||
// Act
|
||||
var result = factory.CreateViewComponent(context);
|
||||
|
||||
// Assert
|
||||
var activablePropertiesComponent = Assert.IsType<ActivablePropertiesViewComponent>(result);
|
||||
|
||||
Assert.Same(component, activablePropertiesComponent);
|
||||
Assert.Same(component.Context, activablePropertiesComponent.Context);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReleaseViewComponent_CallsDispose_OnTheInstance()
|
||||
{
|
||||
// Arrange
|
||||
var context = new ViewComponentContext
|
||||
{
|
||||
};
|
||||
|
||||
var component = new ActivablePropertiesViewComponent();
|
||||
|
||||
var viewComponentActivator = new Mock<IViewComponentActivator>();
|
||||
viewComponentActivator.Setup(vca => vca.Release(context, component))
|
||||
.Callback<ViewComponentContext, object>((c, o) => (o as IDisposable)?.Dispose());
|
||||
|
||||
var factory = new DefaultViewComponentFactory(viewComponentActivator.Object);
|
||||
|
||||
// Act
|
||||
factory.ReleaseViewComponent(context, component);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(true, component.Disposed);
|
||||
}
|
||||
}
|
||||
|
||||
public class ActivablePropertiesViewComponent : IDisposable
|
||||
{
|
||||
[ViewComponentContext]
|
||||
public ViewComponentContext Context { get; set; }
|
||||
|
||||
public bool Disposed { get; private set; }
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Disposed = true;
|
||||
}
|
||||
|
||||
public string Invoke()
|
||||
{
|
||||
return "something";
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue