Skip to content

Commit 53588b4

Browse files
Updates to IComponentActivator PR
1 parent dae55ed commit 53588b4

File tree

12 files changed

+119
-65
lines changed

12 files changed

+119
-65
lines changed

src/Components/Components/ref/Microsoft.AspNetCore.Components.netcoreapp.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,11 @@ protected virtual void OnParametersSet() { }
107107
protected virtual bool ShouldRender() { throw null; }
108108
protected void StateHasChanged() { }
109109
}
110+
public partial class DefaultComponentActivator : Microsoft.AspNetCore.Components.IComponentActivator
111+
{
112+
public DefaultComponentActivator() { }
113+
public Microsoft.AspNetCore.Components.IComponent CreateInstance(System.Type componentType) { throw null; }
114+
}
110115
public abstract partial class Dispatcher
111116
{
112117
protected Dispatcher() { }
@@ -234,6 +239,10 @@ public partial interface IComponent
234239
void Attach(Microsoft.AspNetCore.Components.RenderHandle renderHandle);
235240
System.Threading.Tasks.Task SetParametersAsync(Microsoft.AspNetCore.Components.ParameterView parameters);
236241
}
242+
public partial interface IComponentActivator
243+
{
244+
Microsoft.AspNetCore.Components.IComponent CreateInstance(System.Type componentType);
245+
}
237246
public partial interface IHandleAfterRender
238247
{
239248
System.Threading.Tasks.Task OnAfterRenderAsync();

src/Components/Components/src/ComponentFactory.cs

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,9 @@
66
using System.Linq;
77
using System.Reflection;
88
using Microsoft.AspNetCore.Components.Reflection;
9-
using Microsoft.Extensions.DependencyInjection;
109

1110
namespace Microsoft.AspNetCore.Components
1211
{
13-
/// <remarks>
14-
/// The <see cref="Instance"/> property on this type is used as a static global cache. Ensure any changes to this type
15-
/// are thread safe and can be safely cached statically.
16-
/// </remarks>
1712
internal class ComponentFactory
1813
{
1914
private static readonly BindingFlags _injectablePropertyBindingFlags
@@ -22,19 +17,20 @@ private static readonly BindingFlags _injectablePropertyBindingFlags
2217
private readonly ConcurrentDictionary<Type, Action<IServiceProvider, IComponent>> _cachedInitializers
2318
= new ConcurrentDictionary<Type, Action<IServiceProvider, IComponent>>();
2419

25-
public static readonly ComponentFactory Instance = new ComponentFactory();
20+
private readonly IComponentActivator _componentActivator;
2621

27-
public IComponent InstantiateComponent(IServiceProvider serviceProvider, Type componentType)
22+
public ComponentFactory(IComponentActivator componentActivator)
2823
{
29-
var activator = serviceProvider.GetService<IComponentActivator>();
30-
31-
var instance = activator != null
32-
? activator.CreateInstance(componentType)
33-
: Activator.CreateInstance(componentType);
24+
_componentActivator = componentActivator ?? throw new ArgumentNullException(nameof(componentActivator));
25+
}
3426

35-
if (!(instance is IComponent component))
27+
public IComponent InstantiateComponent(IServiceProvider serviceProvider, Type componentType)
28+
{
29+
var component = _componentActivator.CreateInstance(componentType);
30+
if (component is null)
3631
{
37-
throw new ArgumentException($"The type {componentType.FullName} does not implement {nameof(IComponent)}.", nameof(componentType));
32+
// The default activator will never do this, but an externally-supplied one might
33+
throw new InvalidOperationException($"The component activator returned a null value for a component of type {componentType.FullName}.");
3834
}
3935

4036
PerformPropertyInjection(serviceProvider, component);

src/Components/Components/src/DefaultComponentActivator.cs

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,25 @@
66
namespace Microsoft.AspNetCore.Components
77
{
88
/// <summary>
9-
/// Default implementation of component activator.
9+
/// Default implementation of <see cref="IComponentActivator"/>.
1010
/// </summary>
1111
public class DefaultComponentActivator : IComponentActivator
1212
{
13+
// If no IComponentActivator is supplied by DI, the renderer uses this instance.
14+
// It's internal because in the future, we might want to add per-scope state and then
15+
// it would no longer be applicable to have a shared instance.
16+
internal static IComponentActivator Instance { get; } = new DefaultComponentActivator();
17+
1318
/// <inheritdoc />
14-
public IComponent? CreateInstance(Type componentType)
19+
public IComponent CreateInstance(Type componentType)
1520
{
16-
return Activator.CreateInstance(componentType) as IComponent;
21+
var instance = Activator.CreateInstance(componentType);
22+
if (!(instance is IComponent component))
23+
{
24+
throw new ArgumentException($"The type {componentType.FullName} does not implement {nameof(IComponent)}.", nameof(componentType));
25+
}
26+
27+
return component;
1728
}
1829
}
1930
}

src/Components/Components/src/IComponentActivator.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,16 @@ namespace Microsoft.AspNetCore.Components
77
{
88
/// <summary>
99
/// Represents an activator that can be used to instantiate components.
10+
/// The activator is not responsible for dependency injection, since the framework
11+
/// performs dependency injection to the resulting instances separately.
1012
/// </summary>
1113
public interface IComponentActivator
1214
{
1315
/// <summary>
14-
/// Creates an component of the specified type using that type's default constructor.
16+
/// Creates a component of the specified type.
1517
/// </summary>
1618
/// <param name="componentType">The type of component to create.</param>
1719
/// <returns>A reference to the newly created component.</returns>
18-
IComponent? CreateInstance(Type componentType);
20+
IComponent CreateInstance(Type componentType);
1921
}
2022
}

src/Components/Components/src/RenderTree/Renderer.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
using System.Threading.Tasks;
1111
using Microsoft.AspNetCore.Components.Profiling;
1212
using Microsoft.AspNetCore.Components.Rendering;
13+
using Microsoft.Extensions.DependencyInjection;
1314
using Microsoft.Extensions.Logging;
1415

1516
namespace Microsoft.AspNetCore.Components.RenderTree
@@ -29,6 +30,7 @@ public abstract partial class Renderer : IDisposable
2930
private readonly Dictionary<ulong, EventCallback> _eventBindings = new Dictionary<ulong, EventCallback>();
3031
private readonly Dictionary<ulong, ulong> _eventHandlerIdReplacements = new Dictionary<ulong, ulong>();
3132
private readonly ILogger<Renderer> _logger;
33+
private readonly ComponentFactory _componentFactory;
3234

3335
private int _nextComponentId = 0; // TODO: change to 'long' when Mono .NET->JS interop supports it
3436
private bool _isBatchInProgress;
@@ -70,6 +72,10 @@ public Renderer(IServiceProvider serviceProvider, ILoggerFactory loggerFactory)
7072

7173
_serviceProvider = serviceProvider;
7274
_logger = loggerFactory.CreateLogger<Renderer>();
75+
76+
var componentActivator = serviceProvider.GetService<IComponentActivator>()
77+
?? DefaultComponentActivator.Instance;
78+
_componentFactory = new ComponentFactory(componentActivator);
7379
}
7480

7581
/// <summary>
@@ -89,7 +95,7 @@ public Renderer(IServiceProvider serviceProvider, ILoggerFactory loggerFactory)
8995
/// <param name="componentType">The type of the component to instantiate.</param>
9096
/// <returns>The component instance.</returns>
9197
protected IComponent InstantiateComponent(Type componentType)
92-
=> ComponentFactory.Instance.InstantiateComponent(_serviceProvider, componentType);
98+
=> _componentFactory.InstantiateComponent(_serviceProvider, componentType);
9399

94100
/// <summary>
95101
/// Associates the <see cref="IComponent"/> with the <see cref="Renderer"/>, assigning

src/Components/Components/test/ComponentFactoryTest.cs

Lines changed: 41 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
using System;
55
using System.Collections.Generic;
6-
using System.Text;
76
using System.Threading.Tasks;
87
using Microsoft.Extensions.DependencyInjection;
98
using Xunit;
@@ -17,7 +16,7 @@ public void InstantiateComponent_CreatesInstance()
1716
{
1817
// Arrange
1918
var componentType = typeof(EmptyComponent);
20-
var factory = new ComponentFactory();
19+
var factory = new ComponentFactory(new DefaultComponentActivator());
2120

2221
// Act
2322
var instance = factory.InstantiateComponent(GetServiceProvider(), componentType);
@@ -28,57 +27,56 @@ public void InstantiateComponent_CreatesInstance()
2827
}
2928

3029
[Fact]
31-
public void InstantiateComponent_CreatesInstance_WithActivator()
30+
public void InstantiateComponent_CreatesInstance_NonComponent()
3231
{
3332
// Arrange
34-
var componentType = typeof(EmptyComponent);
35-
var factory = new ComponentFactory();
36-
37-
// Act
38-
var instance = factory.InstantiateComponent(GetServiceProviderWithActivator(), componentType);
39-
40-
// Assert
41-
Assert.NotNull(instance);
42-
Assert.IsType<EmptyComponent>(instance);
43-
}
44-
45-
[Fact]
46-
public void InstantiateComponent_CreatesInstance_WithActivator_NonComponent()
47-
{
48-
// Arrange
49-
var componentType = typeof(NonComponent);
50-
var factory = new ComponentFactory();
33+
var componentType = typeof(List<string>);
34+
var factory = new ComponentFactory(new DefaultComponentActivator());
5135

5236
// Assert
53-
Assert.Throws<ArgumentException>(()=>factory.InstantiateComponent(GetServiceProviderWithActivator(), componentType));
37+
var ex = Assert.Throws<ArgumentException>(() => factory.InstantiateComponent(GetServiceProvider(), componentType));
38+
Assert.StartsWith($"The type {componentType.FullName} does not implement {nameof(IComponent)}.", ex.Message);
5439
}
5540

5641
[Fact]
57-
public void InstantiateComponent_AssignsPropertiesWithInjectAttribute()
42+
public void InstantiateComponent_CreatesInstance_WithCustomActivator()
5843
{
5944
// Arrange
60-
var componentType = typeof(ComponentWithInjectProperties);
61-
var factory = new ComponentFactory();
45+
var componentType = typeof(EmptyComponent);
46+
var factory = new ComponentFactory(new CustomComponentActivator<ComponentWithInjectProperties>());
6247

6348
// Act
6449
var instance = factory.InstantiateComponent(GetServiceProvider(), componentType);
6550

6651
// Assert
6752
Assert.NotNull(instance);
68-
var component = Assert.IsType<ComponentWithInjectProperties>(instance);
53+
var component = Assert.IsType<ComponentWithInjectProperties>(instance); // Custom activator returns a different type
54+
6955
// Public, and non-public properties, and properties with non-public setters should get assigned
7056
Assert.NotNull(component.Property1);
7157
Assert.NotNull(component.GetProperty2());
7258
Assert.NotNull(component.Property3);
7359
Assert.NotNull(component.Property4);
7460
}
7561

62+
[Fact]
63+
public void InstantiateComponent_ThrowsForNullInstance()
64+
{
65+
// Arrange
66+
var componentType = typeof(EmptyComponent);
67+
var factory = new ComponentFactory(new NullResultComponentActivator());
68+
69+
// Act
70+
var ex = Assert.Throws<InvalidOperationException>(() => factory.InstantiateComponent(GetServiceProvider(), componentType));
71+
Assert.Equal($"The component activator returned a null value for a component of type {componentType.FullName}.", ex.Message);
72+
}
73+
7674
[Fact]
7775
public void InstantiateComponent_AssignsPropertiesWithInjectAttributeOnBaseType()
7876
{
7977
// Arrange
8078
var componentType = typeof(DerivedComponent);
81-
var factory = new ComponentFactory();
79+
var factory = new ComponentFactory(new CustomComponentActivator<DerivedComponent>());
8280

8381
// Act
8482
var instance = factory.InstantiateComponent(GetServiceProvider(), componentType);
@@ -101,7 +99,7 @@ public void InstantiateComponent_IgnoresPropertiesWithoutInjectAttribute()
10199
{
102100
// Arrange
103101
var componentType = typeof(ComponentWithNonInjectableProperties);
104-
var factory = new ComponentFactory();
102+
var factory = new ComponentFactory(new DefaultComponentActivator());
105103

106104
// Act
107105
var instance = factory.InstantiateComponent(GetServiceProvider(), componentType);
@@ -122,15 +120,6 @@ private static IServiceProvider GetServiceProvider()
122120
.BuildServiceProvider();
123121
}
124122

125-
private static IServiceProvider GetServiceProviderWithActivator()
126-
{
127-
return new ServiceCollection()
128-
.AddTransient<TestService1>()
129-
.AddTransient<TestService2>()
130-
.AddSingleton<IComponentActivator, DefaultComponentActivator>()
131-
.BuildServiceProvider();
132-
}
133-
134123
private class EmptyComponent : IComponent
135124
{
136125
public void Attach(RenderHandle renderHandle)
@@ -197,9 +186,23 @@ private class DerivedComponent : ComponentWithInjectProperties
197186
public TestService2 Property5 { get; set; }
198187
}
199188

200-
private class NonComponent { }
201-
202189
public class TestService1 { }
203190
public class TestService2 { }
191+
192+
private class CustomComponentActivator<TResult> : IComponentActivator where TResult : IComponent, new()
193+
{
194+
public IComponent CreateInstance(Type componentType)
195+
{
196+
return new TResult();
197+
}
198+
}
199+
200+
private class NullResultComponentActivator : IComponentActivator
201+
{
202+
public IComponent CreateInstance(Type componentType)
203+
{
204+
return null;
205+
}
206+
}
204207
}
205208
}

src/Components/Components/test/DependencyInjectionTest.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ public void SetsPrivateInheritedInjectableProperties()
133133
}
134134

135135
private T InstantiateComponent<T>() where T: IComponent
136-
=> _renderer.InstantiateComponent<T>();
136+
=> (T)_renderer.InstantiateComponent<T>();
137137

138138
class HasPropertiesWithoutInjectAttribute : TestComponent
139139
{

src/Components/Components/test/OwningComponentBaseTest.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ public void CreatesScopeAndService()
2323

2424
var counter = serviceProvider.GetRequiredService<Counter>();
2525
var renderer = new TestRenderer(serviceProvider);
26-
var component1 = renderer.InstantiateComponent<MyOwningComponent>();
26+
var component1 = (MyOwningComponent)renderer.InstantiateComponent<MyOwningComponent>();
2727

2828
Assert.NotNull(component1.MyService);
2929
Assert.Equal(1, counter.CreatedCount);

src/Components/Components/test/RendererTest.cs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3733,6 +3733,35 @@ public void CannotAccessParameterViewAfterSynchronousReturn()
37333733
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);
37343734
}
37353735

3736+
[Fact]
3737+
public void CanUseCustomComponentActivator()
3738+
{
3739+
// Arrange
3740+
var serviceProvider = new TestServiceProvider();
3741+
var componentActivator = new TestComponentActivator<MessageComponent>();
3742+
serviceProvider.AddService<IComponentActivator>(componentActivator);
3743+
var renderer = new TestRenderer(serviceProvider);
3744+
3745+
// Act: Ask for TestComponent
3746+
var suppliedComponent = renderer.InstantiateComponent<TestComponent>();
3747+
3748+
// Assert: We actually receive MessageComponent
3749+
Assert.IsType<MessageComponent>(suppliedComponent);
3750+
Assert.Collection(componentActivator.RequestedComponentTypes,
3751+
requestedType => Assert.Equal(typeof(TestComponent), requestedType));
3752+
}
3753+
3754+
private class TestComponentActivator<TResult> : IComponentActivator where TResult: IComponent, new()
3755+
{
3756+
public List<Type> RequestedComponentTypes { get; } = new List<Type>();
3757+
3758+
public IComponent CreateInstance(Type componentType)
3759+
{
3760+
RequestedComponentTypes.Add(componentType);
3761+
return new TResult();
3762+
}
3763+
}
3764+
37363765
private class NoOpRenderer : Renderer
37373766
{
37383767
public NoOpRenderer() : base(new TestServiceProvider(), NullLoggerFactory.Instance)

src/Components/Server/src/DependencyInjection/ComponentServiceCollectionExtensions.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,6 @@ public static IServerSideBlazorBuilder AddServerSideBlazor(this IServiceCollecti
7474
services.AddScoped<IJSRuntime, RemoteJSRuntime>();
7575
services.AddScoped<INavigationInterception, RemoteNavigationInterception>();
7676
services.AddScoped<AuthenticationStateProvider, ServerAuthenticationStateProvider>();
77-
services.AddScoped<IComponentActivator, DefaultComponentActivator>();
7877

7978
services.TryAddEnumerable(ServiceDescriptor.Singleton<IConfigureOptions<CircuitOptions>, CircuitOptionsJSInteropDetailedErrorsConfiguration>());
8079

0 commit comments

Comments
 (0)