Updates to IComponentActivator PR

This commit is contained in:
Steve Sanderson 2020-07-02 13:31:22 +01:00
parent dae55edfec
commit 53588b45dc
12 changed files with 125 additions and 71 deletions

View File

@ -107,6 +107,11 @@ namespace Microsoft.AspNetCore.Components
protected virtual bool ShouldRender() { throw null; } protected virtual bool ShouldRender() { throw null; }
protected void StateHasChanged() { } protected void StateHasChanged() { }
} }
public partial class DefaultComponentActivator : Microsoft.AspNetCore.Components.IComponentActivator
{
public DefaultComponentActivator() { }
public Microsoft.AspNetCore.Components.IComponent CreateInstance(System.Type componentType) { throw null; }
}
public abstract partial class Dispatcher public abstract partial class Dispatcher
{ {
protected Dispatcher() { } protected Dispatcher() { }
@ -234,6 +239,10 @@ namespace Microsoft.AspNetCore.Components
void Attach(Microsoft.AspNetCore.Components.RenderHandle renderHandle); void Attach(Microsoft.AspNetCore.Components.RenderHandle renderHandle);
System.Threading.Tasks.Task SetParametersAsync(Microsoft.AspNetCore.Components.ParameterView parameters); System.Threading.Tasks.Task SetParametersAsync(Microsoft.AspNetCore.Components.ParameterView parameters);
} }
public partial interface IComponentActivator
{
Microsoft.AspNetCore.Components.IComponent CreateInstance(System.Type componentType);
}
public partial interface IHandleAfterRender public partial interface IHandleAfterRender
{ {
System.Threading.Tasks.Task OnAfterRenderAsync(); System.Threading.Tasks.Task OnAfterRenderAsync();

View File

@ -6,14 +6,9 @@ using System.Collections.Concurrent;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using Microsoft.AspNetCore.Components.Reflection; using Microsoft.AspNetCore.Components.Reflection;
using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.AspNetCore.Components namespace Microsoft.AspNetCore.Components
{ {
/// <remarks>
/// The <see cref="Instance"/> property on this type is used as a static global cache. Ensure any changes to this type
/// are thread safe and can be safely cached statically.
/// </remarks>
internal class ComponentFactory internal class ComponentFactory
{ {
private static readonly BindingFlags _injectablePropertyBindingFlags private static readonly BindingFlags _injectablePropertyBindingFlags
@ -22,19 +17,20 @@ namespace Microsoft.AspNetCore.Components
private readonly ConcurrentDictionary<Type, Action<IServiceProvider, IComponent>> _cachedInitializers private readonly ConcurrentDictionary<Type, Action<IServiceProvider, IComponent>> _cachedInitializers
= new ConcurrentDictionary<Type, Action<IServiceProvider, IComponent>>(); = new ConcurrentDictionary<Type, Action<IServiceProvider, IComponent>>();
public static readonly ComponentFactory Instance = new ComponentFactory(); private readonly IComponentActivator _componentActivator;
public ComponentFactory(IComponentActivator componentActivator)
{
_componentActivator = componentActivator ?? throw new ArgumentNullException(nameof(componentActivator));
}
public IComponent InstantiateComponent(IServiceProvider serviceProvider, Type componentType) public IComponent InstantiateComponent(IServiceProvider serviceProvider, Type componentType)
{ {
var activator = serviceProvider.GetService<IComponentActivator>(); var component = _componentActivator.CreateInstance(componentType);
if (component is null)
var instance = activator != null
? activator.CreateInstance(componentType)
: Activator.CreateInstance(componentType);
if (!(instance is IComponent component))
{ {
throw new ArgumentException($"The type {componentType.FullName} does not implement {nameof(IComponent)}.", nameof(componentType)); // The default activator will never do this, but an externally-supplied one might
throw new InvalidOperationException($"The component activator returned a null value for a component of type {componentType.FullName}.");
} }
PerformPropertyInjection(serviceProvider, component); PerformPropertyInjection(serviceProvider, component);

View File

@ -6,14 +6,25 @@ using System;
namespace Microsoft.AspNetCore.Components namespace Microsoft.AspNetCore.Components
{ {
/// <summary> /// <summary>
/// Default implementation of component activator. /// Default implementation of <see cref="IComponentActivator"/>.
/// </summary> /// </summary>
public class DefaultComponentActivator : IComponentActivator public class DefaultComponentActivator : IComponentActivator
{ {
// If no IComponentActivator is supplied by DI, the renderer uses this instance.
// It's internal because in the future, we might want to add per-scope state and then
// it would no longer be applicable to have a shared instance.
internal static IComponentActivator Instance { get; } = new DefaultComponentActivator();
/// <inheritdoc /> /// <inheritdoc />
public IComponent? CreateInstance(Type componentType) public IComponent CreateInstance(Type componentType)
{ {
return Activator.CreateInstance(componentType) as IComponent; var instance = Activator.CreateInstance(componentType);
if (!(instance is IComponent component))
{
throw new ArgumentException($"The type {componentType.FullName} does not implement {nameof(IComponent)}.", nameof(componentType));
}
return component;
} }
} }
} }

View File

@ -7,14 +7,16 @@ namespace Microsoft.AspNetCore.Components
{ {
/// <summary> /// <summary>
/// Represents an activator that can be used to instantiate components. /// Represents an activator that can be used to instantiate components.
/// The activator is not responsible for dependency injection, since the framework
/// performs dependency injection to the resulting instances separately.
/// </summary> /// </summary>
public interface IComponentActivator public interface IComponentActivator
{ {
/// <summary> /// <summary>
/// Creates an component of the specified type using that type's default constructor. /// Creates a component of the specified type.
/// </summary> /// </summary>
/// <param name="componentType">The type of component to create.</param> /// <param name="componentType">The type of component to create.</param>
/// <returns>A reference to the newly created component.</returns> /// <returns>A reference to the newly created component.</returns>
IComponent? CreateInstance(Type componentType); IComponent CreateInstance(Type componentType);
} }
} }

View File

@ -10,6 +10,7 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Profiling; using Microsoft.AspNetCore.Components.Profiling;
using Microsoft.AspNetCore.Components.Rendering; using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.Components.RenderTree namespace Microsoft.AspNetCore.Components.RenderTree
@ -29,6 +30,7 @@ namespace Microsoft.AspNetCore.Components.RenderTree
private readonly Dictionary<ulong, EventCallback> _eventBindings = new Dictionary<ulong, EventCallback>(); private readonly Dictionary<ulong, EventCallback> _eventBindings = new Dictionary<ulong, EventCallback>();
private readonly Dictionary<ulong, ulong> _eventHandlerIdReplacements = new Dictionary<ulong, ulong>(); private readonly Dictionary<ulong, ulong> _eventHandlerIdReplacements = new Dictionary<ulong, ulong>();
private readonly ILogger<Renderer> _logger; private readonly ILogger<Renderer> _logger;
private readonly ComponentFactory _componentFactory;
private int _nextComponentId = 0; // TODO: change to 'long' when Mono .NET->JS interop supports it private int _nextComponentId = 0; // TODO: change to 'long' when Mono .NET->JS interop supports it
private bool _isBatchInProgress; private bool _isBatchInProgress;
@ -70,6 +72,10 @@ namespace Microsoft.AspNetCore.Components.RenderTree
_serviceProvider = serviceProvider; _serviceProvider = serviceProvider;
_logger = loggerFactory.CreateLogger<Renderer>(); _logger = loggerFactory.CreateLogger<Renderer>();
var componentActivator = serviceProvider.GetService<IComponentActivator>()
?? DefaultComponentActivator.Instance;
_componentFactory = new ComponentFactory(componentActivator);
} }
/// <summary> /// <summary>
@ -89,7 +95,7 @@ namespace Microsoft.AspNetCore.Components.RenderTree
/// <param name="componentType">The type of the component to instantiate.</param> /// <param name="componentType">The type of the component to instantiate.</param>
/// <returns>The component instance.</returns> /// <returns>The component instance.</returns>
protected IComponent InstantiateComponent(Type componentType) protected IComponent InstantiateComponent(Type componentType)
=> ComponentFactory.Instance.InstantiateComponent(_serviceProvider, componentType); => _componentFactory.InstantiateComponent(_serviceProvider, componentType);
/// <summary> /// <summary>
/// Associates the <see cref="IComponent"/> with the <see cref="Renderer"/>, assigning /// Associates the <see cref="IComponent"/> with the <see cref="Renderer"/>, assigning

View File

@ -3,7 +3,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Xunit; using Xunit;
@ -17,7 +16,7 @@ namespace Microsoft.AspNetCore.Components
{ {
// Arrange // Arrange
var componentType = typeof(EmptyComponent); var componentType = typeof(EmptyComponent);
var factory = new ComponentFactory(); var factory = new ComponentFactory(new DefaultComponentActivator());
// Act // Act
var instance = factory.InstantiateComponent(GetServiceProvider(), componentType); var instance = factory.InstantiateComponent(GetServiceProvider(), componentType);
@ -28,44 +27,31 @@ namespace Microsoft.AspNetCore.Components
} }
[Fact] [Fact]
public void InstantiateComponent_CreatesInstance_WithActivator() public void InstantiateComponent_CreatesInstance_NonComponent()
{
// Arrange
var componentType = typeof(List<string>);
var factory = new ComponentFactory(new DefaultComponentActivator());
// Assert
var ex = Assert.Throws<ArgumentException>(() => factory.InstantiateComponent(GetServiceProvider(), componentType));
Assert.StartsWith($"The type {componentType.FullName} does not implement {nameof(IComponent)}.", ex.Message);
}
[Fact]
public void InstantiateComponent_CreatesInstance_WithCustomActivator()
{ {
// Arrange // Arrange
var componentType = typeof(EmptyComponent); var componentType = typeof(EmptyComponent);
var factory = new ComponentFactory(); var factory = new ComponentFactory(new CustomComponentActivator<ComponentWithInjectProperties>());
// Act
var instance = factory.InstantiateComponent(GetServiceProviderWithActivator(), componentType);
// Assert
Assert.NotNull(instance);
Assert.IsType<EmptyComponent>(instance);
}
[Fact]
public void InstantiateComponent_CreatesInstance_WithActivator_NonComponent()
{
// Arrange
var componentType = typeof(NonComponent);
var factory = new ComponentFactory();
// Assert
Assert.Throws<ArgumentException>(()=>factory.InstantiateComponent(GetServiceProviderWithActivator(), componentType));
}
[Fact]
public void InstantiateComponent_AssignsPropertiesWithInjectAttribute()
{
// Arrange
var componentType = typeof(ComponentWithInjectProperties);
var factory = new ComponentFactory();
// Act // Act
var instance = factory.InstantiateComponent(GetServiceProvider(), componentType); var instance = factory.InstantiateComponent(GetServiceProvider(), componentType);
// Assert // Assert
Assert.NotNull(instance); Assert.NotNull(instance);
var component = Assert.IsType<ComponentWithInjectProperties>(instance); var component = Assert.IsType<ComponentWithInjectProperties>(instance); // Custom activator returns a different type
// Public, and non-public properties, and properties with non-public setters should get assigned // Public, and non-public properties, and properties with non-public setters should get assigned
Assert.NotNull(component.Property1); Assert.NotNull(component.Property1);
Assert.NotNull(component.GetProperty2()); Assert.NotNull(component.GetProperty2());
@ -73,12 +59,24 @@ namespace Microsoft.AspNetCore.Components
Assert.NotNull(component.Property4); Assert.NotNull(component.Property4);
} }
[Fact]
public void InstantiateComponent_ThrowsForNullInstance()
{
// Arrange
var componentType = typeof(EmptyComponent);
var factory = new ComponentFactory(new NullResultComponentActivator());
// Act
var ex = Assert.Throws<InvalidOperationException>(() => factory.InstantiateComponent(GetServiceProvider(), componentType));
Assert.Equal($"The component activator returned a null value for a component of type {componentType.FullName}.", ex.Message);
}
[Fact] [Fact]
public void InstantiateComponent_AssignsPropertiesWithInjectAttributeOnBaseType() public void InstantiateComponent_AssignsPropertiesWithInjectAttributeOnBaseType()
{ {
// Arrange // Arrange
var componentType = typeof(DerivedComponent); var componentType = typeof(DerivedComponent);
var factory = new ComponentFactory(); var factory = new ComponentFactory(new CustomComponentActivator<DerivedComponent>());
// Act // Act
var instance = factory.InstantiateComponent(GetServiceProvider(), componentType); var instance = factory.InstantiateComponent(GetServiceProvider(), componentType);
@ -101,7 +99,7 @@ namespace Microsoft.AspNetCore.Components
{ {
// Arrange // Arrange
var componentType = typeof(ComponentWithNonInjectableProperties); var componentType = typeof(ComponentWithNonInjectableProperties);
var factory = new ComponentFactory(); var factory = new ComponentFactory(new DefaultComponentActivator());
// Act // Act
var instance = factory.InstantiateComponent(GetServiceProvider(), componentType); var instance = factory.InstantiateComponent(GetServiceProvider(), componentType);
@ -122,15 +120,6 @@ namespace Microsoft.AspNetCore.Components
.BuildServiceProvider(); .BuildServiceProvider();
} }
private static IServiceProvider GetServiceProviderWithActivator()
{
return new ServiceCollection()
.AddTransient<TestService1>()
.AddTransient<TestService2>()
.AddSingleton<IComponentActivator, DefaultComponentActivator>()
.BuildServiceProvider();
}
private class EmptyComponent : IComponent private class EmptyComponent : IComponent
{ {
public void Attach(RenderHandle renderHandle) public void Attach(RenderHandle renderHandle)
@ -197,9 +186,23 @@ namespace Microsoft.AspNetCore.Components
public TestService2 Property5 { get; set; } public TestService2 Property5 { get; set; }
} }
private class NonComponent { }
public class TestService1 { } public class TestService1 { }
public class TestService2 { } public class TestService2 { }
private class CustomComponentActivator<TResult> : IComponentActivator where TResult : IComponent, new()
{
public IComponent CreateInstance(Type componentType)
{
return new TResult();
}
}
private class NullResultComponentActivator : IComponentActivator
{
public IComponent CreateInstance(Type componentType)
{
return null;
}
}
} }
} }

View File

@ -133,7 +133,7 @@ namespace Microsoft.AspNetCore.Components.Test
} }
private T InstantiateComponent<T>() where T: IComponent private T InstantiateComponent<T>() where T: IComponent
=> _renderer.InstantiateComponent<T>(); => (T)_renderer.InstantiateComponent<T>();
class HasPropertiesWithoutInjectAttribute : TestComponent class HasPropertiesWithoutInjectAttribute : TestComponent
{ {

View File

@ -23,7 +23,7 @@ namespace Microsoft.AspNetCore.Components
var counter = serviceProvider.GetRequiredService<Counter>(); var counter = serviceProvider.GetRequiredService<Counter>();
var renderer = new TestRenderer(serviceProvider); var renderer = new TestRenderer(serviceProvider);
var component1 = renderer.InstantiateComponent<MyOwningComponent>(); var component1 = (MyOwningComponent)renderer.InstantiateComponent<MyOwningComponent>();
Assert.NotNull(component1.MyService); Assert.NotNull(component1.MyService);
Assert.Equal(1, counter.CreatedCount); Assert.Equal(1, counter.CreatedCount);

View File

@ -3733,6 +3733,35 @@ namespace Microsoft.AspNetCore.Components.Test
Assert.Equal($"The {nameof(ParameterView)} instance can no longer be read because it has expired. {nameof(ParameterView)} can only be read synchronously and must not be stored for later use.", ex.Message); Assert.Equal($"The {nameof(ParameterView)} instance can no longer be read because it has expired. {nameof(ParameterView)} can only be read synchronously and must not be stored for later use.", ex.Message);
} }
[Fact]
public void CanUseCustomComponentActivator()
{
// Arrange
var serviceProvider = new TestServiceProvider();
var componentActivator = new TestComponentActivator<MessageComponent>();
serviceProvider.AddService<IComponentActivator>(componentActivator);
var renderer = new TestRenderer(serviceProvider);
// Act: Ask for TestComponent
var suppliedComponent = renderer.InstantiateComponent<TestComponent>();
// Assert: We actually receive MessageComponent
Assert.IsType<MessageComponent>(suppliedComponent);
Assert.Collection(componentActivator.RequestedComponentTypes,
requestedType => Assert.Equal(typeof(TestComponent), requestedType));
}
private class TestComponentActivator<TResult> : IComponentActivator where TResult: IComponent, new()
{
public List<Type> RequestedComponentTypes { get; } = new List<Type>();
public IComponent CreateInstance(Type componentType)
{
RequestedComponentTypes.Add(componentType);
return new TResult();
}
}
private class NoOpRenderer : Renderer private class NoOpRenderer : Renderer
{ {
public NoOpRenderer() : base(new TestServiceProvider(), NullLoggerFactory.Instance) public NoOpRenderer() : base(new TestServiceProvider(), NullLoggerFactory.Instance)

View File

@ -74,7 +74,6 @@ namespace Microsoft.Extensions.DependencyInjection
services.AddScoped<IJSRuntime, RemoteJSRuntime>(); services.AddScoped<IJSRuntime, RemoteJSRuntime>();
services.AddScoped<INavigationInterception, RemoteNavigationInterception>(); services.AddScoped<INavigationInterception, RemoteNavigationInterception>();
services.AddScoped<AuthenticationStateProvider, ServerAuthenticationStateProvider>(); services.AddScoped<AuthenticationStateProvider, ServerAuthenticationStateProvider>();
services.AddScoped<IComponentActivator, DefaultComponentActivator>();
services.TryAddEnumerable(ServiceDescriptor.Singleton<IConfigureOptions<CircuitOptions>, CircuitOptionsJSInteropDetailedErrorsConfiguration>()); services.TryAddEnumerable(ServiceDescriptor.Singleton<IConfigureOptions<CircuitOptions>, CircuitOptionsJSInteropDetailedErrorsConfiguration>());

View File

@ -88,8 +88,8 @@ namespace Microsoft.AspNetCore.Components.Test.Helpers
return task; return task;
} }
public T InstantiateComponent<T>() where T : IComponent public IComponent InstantiateComponent<T>()
=> (T)InstantiateComponent(typeof(T)); => InstantiateComponent(typeof(T));
protected override void HandleException(Exception exception) protected override void HandleException(Exception exception)
{ {

View File

@ -212,13 +212,12 @@ namespace Microsoft.Extensions.DependencyInjection
services.TryAddScoped<NavigationManager, HttpNavigationManager>(); services.TryAddScoped<NavigationManager, HttpNavigationManager>();
services.TryAddScoped<IJSRuntime, UnsupportedJavaScriptRuntime>(); services.TryAddScoped<IJSRuntime, UnsupportedJavaScriptRuntime>();
services.TryAddScoped<INavigationInterception, UnsupportedNavigationInterception>(); services.TryAddScoped<INavigationInterception, UnsupportedNavigationInterception>();
services.TryAddTransient<ControllerSaveTempDataPropertyFilter>(); services.TryAddTransient<ControllerSaveTempDataPropertyFilter>();
// This does caching so it should stay singleton // This does caching so it should stay singleton
services.TryAddSingleton<ITempDataProvider, CookieTempDataProvider>(); services.TryAddSingleton<ITempDataProvider, CookieTempDataProvider>();
services.TryAddSingleton<TempDataSerializer, DefaultTempDataSerializer>(); services.TryAddSingleton<TempDataSerializer, DefaultTempDataSerializer>();
services.TryAddSingleton<IComponentActivator, DefaultComponentActivator>();
// //
// Antiforgery // Antiforgery