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 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
{
protected Dispatcher() { }
@ -234,6 +239,10 @@ namespace Microsoft.AspNetCore.Components
void Attach(Microsoft.AspNetCore.Components.RenderHandle renderHandle);
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
{
System.Threading.Tasks.Task OnAfterRenderAsync();

View File

@ -6,14 +6,9 @@ using System.Collections.Concurrent;
using System.Linq;
using System.Reflection;
using Microsoft.AspNetCore.Components.Reflection;
using Microsoft.Extensions.DependencyInjection;
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
{
private static readonly BindingFlags _injectablePropertyBindingFlags
@ -22,19 +17,20 @@ namespace Microsoft.AspNetCore.Components
private readonly ConcurrentDictionary<Type, Action<IServiceProvider, IComponent>> _cachedInitializers
= 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)
{
var activator = serviceProvider.GetService<IComponentActivator>();
var instance = activator != null
? activator.CreateInstance(componentType)
: Activator.CreateInstance(componentType);
if (!(instance is IComponent component))
var component = _componentActivator.CreateInstance(componentType);
if (component is null)
{
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);

View File

@ -6,14 +6,25 @@ using System;
namespace Microsoft.AspNetCore.Components
{
/// <summary>
/// Default implementation of component activator.
/// Default implementation of <see cref="IComponentActivator"/>.
/// </summary>
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 />
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>
/// 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>
public interface IComponentActivator
{
/// <summary>
/// Creates an component of the specified type using that type's default constructor.
/// Creates a component of the specified type.
/// </summary>
/// <param name="componentType">The type of component to create.</param>
/// <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 Microsoft.AspNetCore.Components.Profiling;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
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, ulong> _eventHandlerIdReplacements = new Dictionary<ulong, ulong>();
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 bool _isBatchInProgress;
@ -70,6 +72,10 @@ namespace Microsoft.AspNetCore.Components.RenderTree
_serviceProvider = serviceProvider;
_logger = loggerFactory.CreateLogger<Renderer>();
var componentActivator = serviceProvider.GetService<IComponentActivator>()
?? DefaultComponentActivator.Instance;
_componentFactory = new ComponentFactory(componentActivator);
}
/// <summary>
@ -89,7 +95,7 @@ namespace Microsoft.AspNetCore.Components.RenderTree
/// <param name="componentType">The type of the component to instantiate.</param>
/// <returns>The component instance.</returns>
protected IComponent InstantiateComponent(Type componentType)
=> ComponentFactory.Instance.InstantiateComponent(_serviceProvider, componentType);
=> _componentFactory.InstantiateComponent(_serviceProvider, componentType);
/// <summary>
/// Associates the <see cref="IComponent"/> with the <see cref="Renderer"/>, assigning

View File

@ -3,7 +3,6 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Xunit;
@ -17,7 +16,7 @@ namespace Microsoft.AspNetCore.Components
{
// Arrange
var componentType = typeof(EmptyComponent);
var factory = new ComponentFactory();
var factory = new ComponentFactory(new DefaultComponentActivator());
// Act
var instance = factory.InstantiateComponent(GetServiceProvider(), componentType);
@ -28,44 +27,31 @@ namespace Microsoft.AspNetCore.Components
}
[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
var componentType = typeof(EmptyComponent);
var factory = new ComponentFactory();
// 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();
var factory = new ComponentFactory(new CustomComponentActivator<ComponentWithInjectProperties>());
// Act
var instance = factory.InstantiateComponent(GetServiceProvider(), componentType);
// Assert
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
Assert.NotNull(component.Property1);
Assert.NotNull(component.GetProperty2());
@ -73,12 +59,24 @@ namespace Microsoft.AspNetCore.Components
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]
public void InstantiateComponent_AssignsPropertiesWithInjectAttributeOnBaseType()
{
// Arrange
var componentType = typeof(DerivedComponent);
var factory = new ComponentFactory();
var factory = new ComponentFactory(new CustomComponentActivator<DerivedComponent>());
// Act
var instance = factory.InstantiateComponent(GetServiceProvider(), componentType);
@ -101,7 +99,7 @@ namespace Microsoft.AspNetCore.Components
{
// Arrange
var componentType = typeof(ComponentWithNonInjectableProperties);
var factory = new ComponentFactory();
var factory = new ComponentFactory(new DefaultComponentActivator());
// Act
var instance = factory.InstantiateComponent(GetServiceProvider(), componentType);
@ -122,15 +120,6 @@ namespace Microsoft.AspNetCore.Components
.BuildServiceProvider();
}
private static IServiceProvider GetServiceProviderWithActivator()
{
return new ServiceCollection()
.AddTransient<TestService1>()
.AddTransient<TestService2>()
.AddSingleton<IComponentActivator, DefaultComponentActivator>()
.BuildServiceProvider();
}
private class EmptyComponent : IComponent
{
public void Attach(RenderHandle renderHandle)
@ -197,9 +186,23 @@ namespace Microsoft.AspNetCore.Components
public TestService2 Property5 { get; set; }
}
private class NonComponent { }
public class TestService1 { }
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
=> _renderer.InstantiateComponent<T>();
=> (T)_renderer.InstantiateComponent<T>();
class HasPropertiesWithoutInjectAttribute : TestComponent
{

View File

@ -23,7 +23,7 @@ namespace Microsoft.AspNetCore.Components
var counter = serviceProvider.GetRequiredService<Counter>();
var renderer = new TestRenderer(serviceProvider);
var component1 = renderer.InstantiateComponent<MyOwningComponent>();
var component1 = (MyOwningComponent)renderer.InstantiateComponent<MyOwningComponent>();
Assert.NotNull(component1.MyService);
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);
}
[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
{
public NoOpRenderer() : base(new TestServiceProvider(), NullLoggerFactory.Instance)

View File

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

View File

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

View File

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