[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:
jacalvar 2016-01-15 17:06:21 -08:00
parent 2b0bea675e
commit 354400f12b
12 changed files with 416 additions and 86 deletions

View File

@ -118,8 +118,10 @@ namespace Microsoft.Extensions.DependencyInjection
// //
// View Components // View Components
// //
// These do caching so they should stay singleton // These do caching so they should stay singleton
services.TryAddSingleton<IViewComponentSelector, DefaultViewComponentSelector>(); services.TryAddSingleton<IViewComponentSelector, DefaultViewComponentSelector>();
services.TryAddSingleton<IViewComponentFactory, DefaultViewComponentFactory>();
services.TryAddSingleton<IViewComponentActivator, DefaultViewComponentActivator>(); services.TryAddSingleton<IViewComponentActivator, DefaultViewComponentActivator>();
services.TryAddSingleton< services.TryAddSingleton<
IViewComponentDescriptorCollectionProvider, IViewComponentDescriptorCollectionProvider,

View File

@ -858,6 +858,22 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
return string.Format(CultureInfo.CurrentCulture, GetString("ViewComponent_AmbiguousMethods"), p0, p1, p2); 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) private static string GetString(string name, params string[] formatterNames)
{ {
var value = _resourceManager.GetString(name); var value = _resourceManager.GetString(name);

View File

@ -277,4 +277,7 @@
<data name="ViewComponent_AmbiguousMethods" xml:space="preserve"> <data name="ViewComponent_AmbiguousMethods" xml:space="preserve">
<value>View component '{0}' must have exactly one public method named '{1}' or '{2}'.</value> <value>View component '{0}' must have exactly one public method named '{1}' or '{2}'.</value>
</data> </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> </root>

View File

@ -2,9 +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.Collections.Concurrent;
using System.Reflection; using System.Reflection;
using Microsoft.Extensions.Internal; using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
namespace Microsoft.AspNetCore.Mvc.ViewComponents namespace Microsoft.AspNetCore.Mvc.ViewComponents
{ {
@ -18,49 +18,71 @@ namespace Microsoft.AspNetCore.Mvc.ViewComponents
/// </remarks> /// </remarks>
public class DefaultViewComponentActivator : IViewComponentActivator public class DefaultViewComponentActivator : IViewComponentActivator
{ {
private readonly Func<Type, PropertyActivator<ViewComponentContext>[]> _getPropertiesToActivate; private readonly ITypeActivatorCache _typeActivatorCache;
private readonly ConcurrentDictionary<Type, PropertyActivator<ViewComponentContext>[]> _injectActions;
/// <summary> /// <summary>
/// Initializes a new instance of <see cref="DefaultViewComponentActivator"/> class. /// Initializes a new instance of <see cref="DefaultViewComponentActivator"/> class.
/// </summary> /// </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>[]>(); if (typeActivatorCache == null)
_getPropertiesToActivate = type => {
PropertyActivator<ViewComponentContext>.GetPropertiesToActivate( throw new ArgumentNullException(nameof(typeActivatorCache));
type, }
typeof(ViewComponentContextAttribute),
CreateActivateInfo); _typeActivatorCache = typeActivatorCache;
} }
/// <inheritdoc /> /// <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) if (context == null)
{ {
throw new ArgumentNullException(nameof(context)); throw new ArgumentNullException(nameof(context));
} }
var propertiesToActivate = _injectActions.GetOrAdd( var componentType = context.ViewComponentDescriptor.Type.GetTypeInfo();
viewComponent.GetType(),
_getPropertiesToActivate);
for (var i = 0; i < propertiesToActivate.Length; i++) if (componentType.IsValueType ||
componentType.IsInterface ||
componentType.IsAbstract ||
(componentType.IsGenericType && componentType.IsGenericTypeDefinition))
{ {
var activateInfo = propertiesToActivate[i]; var message = Resources.FormatValueInterfaceAbstractOrOpenGenericTypesCannotBeActivated(
activateInfo.Activate(viewComponent, context); 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();
}
} }
} }
} }

View File

@ -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);
}
}
}

View File

@ -19,8 +19,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewComponents
/// </summary> /// </summary>
public class DefaultViewComponentInvoker : IViewComponentInvoker public class DefaultViewComponentInvoker : IViewComponentInvoker
{ {
private readonly ITypeActivatorCache _typeActivatorCache; private readonly IViewComponentFactory _viewComponentFactory;
private readonly IViewComponentActivator _viewComponentActivator;
private readonly DiagnosticSource _diagnosticSource; private readonly DiagnosticSource _diagnosticSource;
private readonly ILogger _logger; private readonly ILogger _logger;
@ -28,23 +27,17 @@ namespace Microsoft.AspNetCore.Mvc.ViewComponents
/// Initializes a new instance of <see cref="DefaultViewComponentInvoker"/>. /// Initializes a new instance of <see cref="DefaultViewComponentInvoker"/>.
/// </summary> /// </summary>
/// <param name="typeActivatorCache">Caches factories for instantiating view component instances.</param> /// <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="diagnosticSource">The <see cref="DiagnosticSource"/>.</param>
/// <param name="logger">The <see cref="ILogger"/>.</param> /// <param name="logger">The <see cref="ILogger"/>.</param>
public DefaultViewComponentInvoker( public DefaultViewComponentInvoker(
ITypeActivatorCache typeActivatorCache, IViewComponentFactory viewComponentFactory,
IViewComponentActivator viewComponentActivator,
DiagnosticSource diagnosticSource, DiagnosticSource diagnosticSource,
ILogger logger) ILogger logger)
{ {
if (typeActivatorCache == null) if (viewComponentFactory == null)
{ {
throw new ArgumentNullException(nameof(typeActivatorCache)); throw new ArgumentNullException(nameof(viewComponentFactory));
}
if (viewComponentActivator == null)
{
throw new ArgumentNullException(nameof(viewComponentActivator));
} }
if (diagnosticSource == null) if (diagnosticSource == null)
@ -57,8 +50,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewComponents
throw new ArgumentNullException(nameof(logger)); throw new ArgumentNullException(nameof(logger));
} }
_typeActivatorCache = typeActivatorCache; _viewComponentFactory = viewComponentFactory;
_viewComponentActivator = viewComponentActivator;
_diagnosticSource = diagnosticSource; _diagnosticSource = diagnosticSource;
_logger = logger; _logger = logger;
} }
@ -95,24 +87,9 @@ namespace Microsoft.AspNetCore.Mvc.ViewComponents
await result.ExecuteAsync(context); 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) private async Task<IViewComponentResult> InvokeAsyncCore(ViewComponentContext context)
{ {
var component = CreateComponent(context); var component = _viewComponentFactory.CreateViewComponent(context);
using (_logger.ViewComponentScope(context)) using (_logger.ViewComponentScope(context))
{ {
@ -129,13 +106,15 @@ namespace Microsoft.AspNetCore.Mvc.ViewComponents
_logger.ViewComponentExecuted(context, startTimestamp, viewComponentResult); _logger.ViewComponentExecuted(context, startTimestamp, viewComponentResult);
_diagnosticSource.AfterViewComponent(context, viewComponentResult, component); _diagnosticSource.AfterViewComponent(context, viewComponentResult, component);
_viewComponentFactory.ReleaseViewComponent(context, component);
return viewComponentResult; return viewComponentResult;
} }
} }
private IViewComponentResult InvokeSyncCore(ViewComponentContext context) private IViewComponentResult InvokeSyncCore(ViewComponentContext context)
{ {
var component = CreateComponent(context); var component = _viewComponentFactory.CreateViewComponent(context);
using (_logger.ViewComponentScope(context)) using (_logger.ViewComponentScope(context))
{ {
@ -155,6 +134,8 @@ namespace Microsoft.AspNetCore.Mvc.ViewComponents
} }
catch (TargetInvocationException ex) catch (TargetInvocationException ex)
{ {
_viewComponentFactory.ReleaseViewComponent(context, component);
// Preserve callstack of any user-thrown exceptions. // Preserve callstack of any user-thrown exceptions.
var exceptionInfo = ExceptionDispatchInfo.Capture(ex.InnerException); var exceptionInfo = ExceptionDispatchInfo.Capture(ex.InnerException);
exceptionInfo.Throw(); exceptionInfo.Throw();
@ -165,6 +146,8 @@ namespace Microsoft.AspNetCore.Mvc.ViewComponents
_logger.ViewComponentExecuted(context, startTimestamp, viewComponentResult); _logger.ViewComponentExecuted(context, startTimestamp, viewComponentResult);
_diagnosticSource.AfterViewComponent(context, viewComponentResult, component); _diagnosticSource.AfterViewComponent(context, viewComponentResult, component);
_viewComponentFactory.ReleaseViewComponent(context, component);
return viewComponentResult; return viewComponentResult;
} }
} }

View File

@ -10,19 +10,31 @@ namespace Microsoft.AspNetCore.Mvc.ViewComponents
{ {
public class DefaultViewComponentInvokerFactory : IViewComponentInvokerFactory public class DefaultViewComponentInvokerFactory : IViewComponentInvokerFactory
{ {
private readonly ITypeActivatorCache _typeActivatorCache; private readonly IViewComponentFactory _viewComponentFactory;
private readonly IViewComponentActivator _viewComponentActivator;
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly DiagnosticSource _diagnosticSource; private readonly DiagnosticSource _diagnosticSource;
public DefaultViewComponentInvokerFactory( public DefaultViewComponentInvokerFactory(
ITypeActivatorCache typeActivatorCache, IViewComponentFactory viewComponentFactory,
IViewComponentActivator viewComponentActivator,
DiagnosticSource diagnosticSource, DiagnosticSource diagnosticSource,
ILoggerFactory loggerFactory) ILoggerFactory loggerFactory)
{ {
_typeActivatorCache = typeActivatorCache; if (viewComponentFactory == null)
_viewComponentActivator = viewComponentActivator; {
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; _diagnosticSource = diagnosticSource;
_logger = loggerFactory.CreateLogger<DefaultViewComponentInvoker>(); _logger = loggerFactory.CreateLogger<DefaultViewComponentInvoker>();
@ -40,8 +52,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewComponents
} }
return new DefaultViewComponentInvoker( return new DefaultViewComponentInvoker(
_typeActivatorCache, _viewComponentFactory,
_viewComponentActivator,
_diagnosticSource, _diagnosticSource,
_logger); _logger);
} }

View File

@ -4,17 +4,25 @@
namespace Microsoft.AspNetCore.Mvc.ViewComponents namespace Microsoft.AspNetCore.Mvc.ViewComponents
{ {
/// <summary> /// <summary>
/// Provides methods to activate an instantiated ViewComponent /// Provides methods to instantiate and release a ViewComponent.
/// </summary> /// </summary>
public interface IViewComponentActivator public interface IViewComponentActivator
{ {
/// <summary> /// <summary>
/// When implemented in a type, activates an instantiated ViewComponent. /// Instantiates a ViewComponent.
/// </summary> /// </summary>
/// <param name="viewComponent">The ViewComponent to activate.</param>
/// <param name="context"> /// <param name="context">
/// The <see cref="ViewComponentContext"/> for the executing <see cref="ViewComponent"/>. /// The <see cref="ViewComponentContext"/> for the executing <see cref="ViewComponent"/>.
/// </param> /// </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);
} }
} }

View File

@ -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);
}
}

View File

@ -525,6 +525,7 @@ namespace Microsoft.AspNetCore.Mvc
services.AddSingleton<IViewComponentInvokerFactory, DefaultViewComponentInvokerFactory>(); services.AddSingleton<IViewComponentInvokerFactory, DefaultViewComponentInvokerFactory>();
services.AddSingleton<ITypeActivatorCache, TypeActivatorCache>(); services.AddSingleton<ITypeActivatorCache, TypeActivatorCache>();
services.AddSingleton<IViewComponentActivator, DefaultViewComponentActivator>(); services.AddSingleton<IViewComponentActivator, DefaultViewComponentActivator>();
services.AddSingleton<IViewComponentFactory, DefaultViewComponentFactory>();
services.AddSingleton<IViewComponentDescriptorProvider>(new FixedSetViewComponentDescriptorProvider(descriptors)); services.AddSingleton<IViewComponentDescriptorProvider>(new FixedSetViewComponentDescriptorProvider(descriptors));
services.AddSingleton<IModelMetadataProvider, EmptyModelMetadataProvider>(); services.AddSingleton<IModelMetadataProvider, EmptyModelMetadataProvider>();
services.AddSingleton<ILoggerFactory>(NullLoggerFactory.Instance); 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 private class TextViewComponent : ViewComponent
{ {
public HtmlString Invoke(string name) public HtmlString Invoke(string name)
@ -588,15 +599,5 @@ namespace Microsoft.AspNetCore.Mvc
return Task.FromResult(new HtmlString("Hello-Async, " + name)); 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();
}
}
} }
} }

View File

@ -3,6 +3,10 @@
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Http.Internal;
using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Mvc.Rendering;
using Moq;
using Xunit; using Xunit;
namespace Microsoft.AspNetCore.Mvc.ViewComponents namespace Microsoft.AspNetCore.Mvc.ViewComponents
@ -13,35 +17,116 @@ namespace Microsoft.AspNetCore.Mvc.ViewComponents
public void DefaultViewComponentActivator_ActivatesViewComponentContext() public void DefaultViewComponentActivator_ActivatesViewComponentContext()
{ {
// Arrange // Arrange
var activator = new DefaultViewComponentActivator(); var expectedInstance = new TestViewComponent();
var context = new ViewComponentContext(); var typeActivator = new Mock<ITypeActivatorCache>();
var instance = new TestViewComponent(); 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 // Act
activator.Activate(instance, context); var instance = activator.Create(context) as ViewComponent;
// Assert // Assert
Assert.NotNull(instance);
Assert.Same(context, instance.ViewComponentContext); 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] [Fact]
public void DefaultViewComponentActivator_ActivatesViewComponentContext_IgnoresNonPublic() public void DefaultViewComponentActivator_ActivatesViewComponentContext_IgnoresNonPublic()
{ {
// Arrange // Arrange
var activator = new DefaultViewComponentActivator(); var expectedInstance = new VisibilityViewComponent();
var context = new ViewComponentContext(); var typeActivator = new Mock<ITypeActivatorCache>();
var instance = new VisibilityViewComponent(); 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 // Act
activator.Activate(instance, context); var instance = activator.Create(context) as VisibilityViewComponent;
// Assert // Assert
Assert.NotNull(instance);
Assert.Same(context, instance.ViewComponentContext); Assert.Same(context, instance.ViewComponentContext);
Assert.Null(instance.C); 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 private class TestViewComponent : ViewComponent
{ {
public Task ExecuteAsync() public Task ExecuteAsync()

View File

@ -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";
}
}
}