diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/DependencyInjection/MvcViewFeaturesMvcCoreBuilderExtensions.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/DependencyInjection/MvcViewFeaturesMvcCoreBuilderExtensions.cs index c1529f45a6..cc9a3b30e4 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/DependencyInjection/MvcViewFeaturesMvcCoreBuilderExtensions.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/DependencyInjection/MvcViewFeaturesMvcCoreBuilderExtensions.cs @@ -118,8 +118,10 @@ namespace Microsoft.Extensions.DependencyInjection // // View Components // + // These do caching so they should stay singleton services.TryAddSingleton(); + services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton< IViewComponentDescriptorCollectionProvider, diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Properties/Resources.Designer.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Properties/Resources.Designer.cs index 6c84e49f5a..7557de5a97 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Properties/Resources.Designer.cs @@ -858,6 +858,22 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures return string.Format(CultureInfo.CurrentCulture, GetString("ViewComponent_AmbiguousMethods"), p0, p1, p2); } + /// + /// 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. + /// + internal static string ValueInterfaceAbstractOrOpenGenericTypesCannotBeActivated + { + get { return GetString("ValueInterfaceAbstractOrOpenGenericTypesCannotBeActivated"); } + } + + /// + /// 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. + /// + 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); diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Resources.resx b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Resources.resx index 53e8d0d193..d1f2131bc6 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Resources.resx +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Resources.resx @@ -277,4 +277,7 @@ View component '{0}' must have exactly one public method named '{1}' or '{2}'. + + 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. + \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewComponents/DefaultViewComponentActivator.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewComponents/DefaultViewComponentActivator.cs index a3a0a0fca2..2f13caae1c 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewComponents/DefaultViewComponentActivator.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewComponents/DefaultViewComponentActivator.cs @@ -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 /// public class DefaultViewComponentActivator : IViewComponentActivator { - private readonly Func[]> _getPropertiesToActivate; - private readonly ConcurrentDictionary[]> _injectActions; + private readonly ITypeActivatorCache _typeActivatorCache; /// /// Initializes a new instance of class. /// - public DefaultViewComponentActivator() + /// + /// The used to create new view component instances. + /// + public DefaultViewComponentActivator(ITypeActivatorCache typeActivatorCache) { - _injectActions = new ConcurrentDictionary[]>(); - _getPropertiesToActivate = type => - PropertyActivator.GetPropertiesToActivate( - type, - typeof(ViewComponentContextAttribute), - CreateActivateInfo); + if (typeActivatorCache == null) + { + throw new ArgumentNullException(nameof(typeActivatorCache)); + } + + _typeActivatorCache = typeActivatorCache; } /// - 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( + context.ViewContext.HttpContext.RequestServices, + context.ViewComponentDescriptor.Type); + + return viewComponent; } - private PropertyActivator CreateActivateInfo(PropertyInfo property) + /// + public virtual void Release(ViewComponentContext context, object viewComponent) { - return new PropertyActivator(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(); + } } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewComponents/DefaultViewComponentFactory.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewComponents/DefaultViewComponentFactory.cs new file mode 100644 index 0000000000..5c3e23315a --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewComponents/DefaultViewComponentFactory.cs @@ -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 +{ + /// + /// Default implementation for . + /// + public class DefaultViewComponentFactory : IViewComponentFactory + { + private readonly IViewComponentActivator _activator; + private readonly Func[]> _getPropertiesToActivate; + private readonly ConcurrentDictionary[]> _injectActions; + + /// + /// Creates a new instance of + /// + /// + /// The used to create new view component instances. + /// + public DefaultViewComponentFactory(IViewComponentActivator activator) + { + if (activator == null) + { + throw new ArgumentNullException(nameof(activator)); + } + + _activator = activator; + + _getPropertiesToActivate = type => PropertyActivator.GetPropertiesToActivate( + type, + typeof(ViewComponentContextAttribute), + CreateActivateInfo); + + _injectActions = new ConcurrentDictionary[]>(); + } + + /// + 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.GetPropertiesToActivate( + type, + typeof(ViewComponentContextAttribute), + CreateActivateInfo)); + + for (var i = 0; i < propertiesToActivate.Length; i++) + { + var activateInfo = propertiesToActivate[i]; + activateInfo.Activate(viewComponent, context); + } + } + + private static PropertyActivator CreateActivateInfo(PropertyInfo property) + { + return new PropertyActivator(property, context => context); + } + + /// + 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); + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewComponents/DefaultViewComponentInvoker.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewComponents/DefaultViewComponentInvoker.cs index 853298f95b..8df665be83 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewComponents/DefaultViewComponentInvoker.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewComponents/DefaultViewComponentInvoker.cs @@ -19,8 +19,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewComponents /// 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 . /// /// Caches factories for instantiating view component instances. - /// The . + /// The . /// The . /// The . 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( - services, - context.ViewComponentDescriptor.Type); - _viewComponentActivator.Activate(component, context); - return component; - } - private async Task 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; } } diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewComponents/DefaultViewComponentInvokerFactory.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewComponents/DefaultViewComponentInvokerFactory.cs index 8eccf8172f..24ea02c26d 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewComponents/DefaultViewComponentInvokerFactory.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewComponents/DefaultViewComponentInvokerFactory.cs @@ -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(); @@ -40,8 +52,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewComponents } return new DefaultViewComponentInvoker( - _typeActivatorCache, - _viewComponentActivator, + _viewComponentFactory, _diagnosticSource, _logger); } diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewComponents/IViewComponentActivator.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewComponents/IViewComponentActivator.cs index d113a357ad..9f42143c48 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewComponents/IViewComponentActivator.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewComponents/IViewComponentActivator.cs @@ -4,17 +4,25 @@ namespace Microsoft.AspNetCore.Mvc.ViewComponents { /// - /// Provides methods to activate an instantiated ViewComponent + /// Provides methods to instantiate and release a ViewComponent. /// public interface IViewComponentActivator { /// - /// When implemented in a type, activates an instantiated ViewComponent. + /// Instantiates a ViewComponent. /// - /// The ViewComponent to activate. /// /// The for the executing . /// - void Activate(object viewComponent, ViewComponentContext context); + object Create(ViewComponentContext context); + + /// + /// Releases a ViewComponent instance. + /// + /// + /// The associated with the . + /// + /// The to release. + void Release(ViewComponentContext context, object viewComponent); } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewComponents/IViewComponentFactory.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewComponents/IViewComponentFactory.cs new file mode 100644 index 0000000000..e3ff201f70 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewComponents/IViewComponentFactory.cs @@ -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 +{ + /// + /// Provides methods for creation and disposal of view components. + /// + public interface IViewComponentFactory + { + /// + /// Creates a new controller for the specified . + /// + /// for the view component. + /// The view component. + object CreateViewComponent(ViewComponentContext context); + + /// + /// Releases a view component instance. + /// + /// The context associated with the . + /// The view component. + void ReleaseViewComponent(ViewComponentContext context, object component); + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ViewComponentResultTest.cs b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ViewComponentResultTest.cs index b278c4ab1c..19b5d5c78e 100644 --- a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ViewComponentResultTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ViewComponentResultTest.cs @@ -525,6 +525,7 @@ namespace Microsoft.AspNetCore.Mvc services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(new FixedSetViewComponentDescriptorProvider(descriptors)); services.AddSingleton(); services.AddSingleton(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(); - } - } } } diff --git a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ViewComponents/DefaultViewComponentActivatorTests.cs b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ViewComponents/DefaultViewComponentActivatorTests.cs index 9af87ed450..11cf47823e 100644 --- a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ViewComponents/DefaultViewComponentActivatorTests.cs +++ b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ViewComponents/DefaultViewComponentActivatorTests.cs @@ -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(); + typeActivator + .Setup(ta => ta.CreateInstance(It.IsAny(), It.IsAny())) + .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() + }, + } + }; + + var activator = new DefaultViewComponentActivator(new TypeActivatorCache()); + + // Act and Assert + var exception = Assert.Throws(() => 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(); + typeActivator + .Setup(ta => ta.CreateInstance(It.IsAny(), It.IsAny())) + .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() + } + } + }; + } + + private class OpenGenericType : Controller + { + } + + private abstract class AbstractType : Controller + { + } + + private interface InterfaceType + { + } + private class TestViewComponent : ViewComponent { public Task ExecuteAsync() diff --git a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ViewComponents/DefaultViewComponentFactoryTest.cs b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ViewComponents/DefaultViewComponentFactoryTest.cs new file mode 100644 index 0000000000..83f54e504c --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ViewComponents/DefaultViewComponentFactoryTest.cs @@ -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(); + 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(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(); + viewComponentActivator.Setup(vca => vca.Release(context, component)) + .Callback((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"; + } + } +}