Skip to content

Commit

Permalink
Activate components by default
Browse files Browse the repository at this point in the history
  • Loading branch information
pranavkm committed Feb 8, 2022
1 parent 4310f2c commit 197d527
Show file tree
Hide file tree
Showing 10 changed files with 112 additions and 72 deletions.
65 changes: 48 additions & 17 deletions src/Components/Components/src/ComponentFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using Microsoft.AspNetCore.Components.Reflection;
using Microsoft.Extensions.DependencyInjection;
using static Microsoft.AspNetCore.Internal.LinkerFlags;

namespace Microsoft.AspNetCore.Components;
Expand All @@ -14,46 +15,76 @@ internal sealed class ComponentFactory
private const BindingFlags _injectablePropertyBindingFlags
= BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;

private static readonly ConcurrentDictionary<Type, Action<IServiceProvider, IComponent>> _cachedInitializers = new();
private static readonly ConcurrentDictionary<Type, (Action<IServiceProvider, IComponent> propertyInitializers, ObjectFactory? componentInitializer)> _cachedInitializers = new();
private readonly IComponentActivator? _componentActivator;

private readonly IComponentActivator _componentActivator;

public ComponentFactory(IComponentActivator componentActivator)
public ComponentFactory(IComponentActivator? componentActivator)
{
_componentActivator = componentActivator ?? throw new ArgumentNullException(nameof(componentActivator));
_componentActivator = componentActivator;
}

public static void ClearCache() => _cachedInitializers.Clear();

public IComponent InstantiateComponent(IServiceProvider serviceProvider, [DynamicallyAccessedMembers(Component)] Type componentType)
{
var component = _componentActivator.CreateInstance(componentType);
if (component is null)
if (_componentActivator is not null)
{
// 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}.");
return InstantiateWithActivator(_componentActivator, serviceProvider, componentType);
}

PerformPropertyInjection(serviceProvider, component);
return InstantiateDefault(serviceProvider, componentType);
}

private static IComponent InstantiateDefault(IServiceProvider serviceProvider, [DynamicallyAccessedMembers(Component)] Type componentType)
{
// This is thread-safe because _cachedInitializers is a ConcurrentDictionary.
// We might generate the initializer more than once for a given type, but would
// still produce the correct result.
if (!_cachedInitializers.TryGetValue(componentType, out var initializer))
{
if (!typeof(IComponent).IsAssignableFrom(componentType))
{
throw new ArgumentException($"The type {componentType.FullName} does not implement {nameof(IComponent)}.", nameof(componentType));
}

initializer = (CreatePropertyInitializer(componentType), ActivatorUtilities.CreateFactory(componentType, Type.EmptyTypes));
_cachedInitializers.TryAdd(componentType, initializer);
}

var (propertyInitializer, componentInitializer) = initializer;
var component = (IComponent)componentInitializer!.Invoke(serviceProvider, Array.Empty<object?>());
propertyInitializer(serviceProvider, component);
return component;
}

private static void PerformPropertyInjection(IServiceProvider serviceProvider, IComponent instance)
private static IComponent InstantiateWithActivator(IComponentActivator componentActivator, IServiceProvider serviceProvider, [DynamicallyAccessedMembers(Component)] Type componentType)
{
var component = componentActivator.CreateInstance(componentType);
if (component is null)
{
// A user implemented IComponentActivator might return null.
throw new InvalidOperationException($"The component activator returned a null value for a component of type {componentType.FullName}.");
}

// Use the activated type instead of specified type since the activator may return different/ derived instances.
componentType = component.GetType();

// This is thread-safe because _cachedInitializers is a ConcurrentDictionary.
// We might generate the initializer more than once for a given type, but would
// still produce the correct result.
var instanceType = instance.GetType();
if (!_cachedInitializers.TryGetValue(instanceType, out var initializer))
if (!_cachedInitializers.TryGetValue(componentType, out var initializer))
{
initializer = CreateInitializer(instanceType);
_cachedInitializers.TryAdd(instanceType, initializer);
initializer = (CreatePropertyInitializer(componentType), componentInitializer: null);
_cachedInitializers.TryAdd(componentType, initializer);
}

initializer(serviceProvider, instance);
var (propertyInitializer, _) = initializer;

propertyInitializer(serviceProvider, component);
return component;
}

private static Action<IServiceProvider, IComponent> CreateInitializer([DynamicallyAccessedMembers(Component)] Type type)
private static Action<IServiceProvider, IComponent> CreatePropertyInitializer([DynamicallyAccessedMembers(Component)] Type type)
{
// Do all the reflection up front
List<(string name, Type propertyType, PropertySetter setter)>? injectables = null;
Expand Down
22 changes: 0 additions & 22 deletions src/Components/Components/src/DefaultComponentActivator.cs

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<linker>
<assembly fullname="Microsoft.AspNetCore.Components, Version=6.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60">
<assembly fullname="Microsoft.AspNetCore.Components, Version=7.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60">
<attribute fullname="System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessageAttribute">
<argument>ILLink</argument>
<argument>IL2026</argument>
Expand Down Expand Up @@ -41,7 +41,7 @@
<argument>ILLink</argument>
<argument>IL2072</argument>
<property name="Scope">member</property>
<property name="Target">M:Microsoft.AspNetCore.Components.ComponentFactory.PerformPropertyInjection(System.IServiceProvider,Microsoft.AspNetCore.Components.IComponent)</property>
<property name="Target">M:Microsoft.AspNetCore.Components.ComponentFactory.InstantiateWithActivator(Microsoft.AspNetCore.Components.IComponentActivator,System.IServiceProvider,System.Type)</property>
</attribute>
<attribute fullname="System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessageAttribute">
<argument>ILLink</argument>
Expand All @@ -59,7 +59,7 @@
<argument>ILLink</argument>
<argument>IL2077</argument>
<property name="Scope">member</property>
<property name="Target">M:Microsoft.AspNetCore.Components.ComponentFactory.CreateInitializer(System.Type)</property>
<property name="Target">M:Microsoft.AspNetCore.Components.ComponentFactory.CreatePropertyInitializer(System.Type)</property>
</attribute>
<attribute fullname="System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessageAttribute">
<argument>ILLink</argument>
Expand Down
12 changes: 3 additions & 9 deletions src/Components/Components/src/RenderTree/Renderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ public event UnhandledExceptionEventHandler UnhandledSynchronizationException
/// <param name="serviceProvider">The <see cref="IServiceProvider"/> to be used when initializing components.</param>
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
public Renderer(IServiceProvider serviceProvider, ILoggerFactory loggerFactory)
: this(serviceProvider, loggerFactory, GetComponentActivatorOrDefault(serviceProvider))
: this(serviceProvider, loggerFactory, GetComponentActivatorOrDefault(serviceProvider)!)
{
// This overload is provided for back-compatibility
}
Expand All @@ -87,22 +87,16 @@ public Renderer(IServiceProvider serviceProvider, ILoggerFactory loggerFactory,
throw new ArgumentNullException(nameof(loggerFactory));
}

if (componentActivator is null)
{
throw new ArgumentNullException(nameof(componentActivator));
}

_serviceProvider = serviceProvider;
_logger = loggerFactory.CreateLogger<Renderer>();
_componentFactory = new ComponentFactory(componentActivator);
}

internal HotReloadManager HotReloadManager { get; set; } = HotReloadManager.Default;

private static IComponentActivator GetComponentActivatorOrDefault(IServiceProvider serviceProvider)
private static IComponentActivator? GetComponentActivatorOrDefault(IServiceProvider serviceProvider)
{
return serviceProvider.GetService<IComponentActivator>()
?? DefaultComponentActivator.Instance;
return serviceProvider.GetService<IComponentActivator>();
}

/// <summary>
Expand Down
49 changes: 46 additions & 3 deletions src/Components/Components/test/ComponentFactoryTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public void InstantiateComponent_CreatesInstance()
{
// Arrange
var componentType = typeof(EmptyComponent);
var factory = new ComponentFactory(new DefaultComponentActivator());
var factory = new ComponentFactory(null);

// Act
var instance = factory.InstantiateComponent(GetServiceProvider(), componentType);
Expand All @@ -22,12 +22,30 @@ public void InstantiateComponent_CreatesInstance()
Assert.IsType<EmptyComponent>(instance);
}

[Fact]
public void InstantiateComponent_CreatesInstance_WithTypeActivation()
{
// Arrange
var componentType = typeof(ComponentWithConstructorInjection);
var factory = new ComponentFactory(null);

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

// Assert
Assert.NotNull(instance);
var component = Assert.IsType<ComponentWithConstructorInjection>(instance);
Assert.NotNull(component.Property1);
Assert.NotNull(component.Property2);
Assert.NotNull(component.Property3); // Property injection should still work.
}

[Fact]
public void InstantiateComponent_CreatesInstance_NonComponent()
{
// Arrange
var componentType = typeof(List<string>);
var factory = new ComponentFactory(new DefaultComponentActivator());
var factory = new ComponentFactory(null);

// Assert
var ex = Assert.Throws<ArgumentException>(() => factory.InstantiateComponent(GetServiceProvider(), componentType));
Expand Down Expand Up @@ -95,7 +113,7 @@ public void InstantiateComponent_IgnoresPropertiesWithoutInjectAttribute()
{
// Arrange
var componentType = typeof(ComponentWithNonInjectableProperties);
var factory = new ComponentFactory(new DefaultComponentActivator());
var factory = new ComponentFactory(null);

// Act
var instance = factory.InstantiateComponent(GetServiceProvider(), componentType);
Expand Down Expand Up @@ -129,6 +147,31 @@ public Task SetParametersAsync(ParameterView parameters)
}
}

public class ComponentWithConstructorInjection : IComponent
{
public ComponentWithConstructorInjection(TestService1 property1, TestService2 property2)
{
Property1 = property1;
Property2 = property2;
}

public TestService1 Property1 { get; }
public TestService2 Property2 { get; }

[Inject]
public TestService2 Property3 { get; set; }

public void Attach(RenderHandle renderHandle)
{
throw new NotImplementedException();
}

public Task SetParametersAsync(ParameterView parameters)
{
throw new NotImplementedException();
}
}

private class ComponentWithInjectProperties : IComponent
{
[Inject]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<linker>
<assembly fullname="Microsoft.AspNetCore.Components.Forms, Version=6.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60">
<assembly fullname="Microsoft.AspNetCore.Components.Forms, Version=7.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60">
<attribute fullname="System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessageAttribute">
<argument>ILLink</argument>
<argument>IL2026</argument>
Expand Down
8 changes: 7 additions & 1 deletion src/Components/Samples/BlazorServerApp/Pages/FetchData.razor
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
<PageTitle>Weather forecast</PageTitle>

@using BlazorServerApp.Data
@inject WeatherForecastService ForecastService

<h1>Weather forecast</h1>

Expand Down Expand Up @@ -41,6 +40,13 @@ else
@code {
WeatherForecast[]? forecasts;

public FetchData(WeatherForecastService forecastService)
{
ForecastService = forecastService;
}

private WeatherForecastService ForecastService { get; }

protected override async Task OnInitializedAsync()
{
forecasts = await ForecastService.GetForecastAsync(DateTime.Now);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<linker>
<assembly fullname="Microsoft.AspNetCore.Components.Web, Version=6.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60">
<attribute fullname="System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessageAttribute">
<argument>ILLink</argument>
<argument>IL2026</argument>
<property name="Scope">member</property>
<property name="Target">M:Microsoft.AspNetCore.Components.Web.Infrastructure.JSComponentInterop.SetRootComponentParameters(System.Int32,System.Int32,System.Text.Json.JsonElement,System.Text.Json.JsonSerializerOptions)</property>
</attribute>
<attribute fullname="System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessageAttribute">
<argument>ILLink</argument>
<argument>IL2026</argument>
<property name="Scope">member</property>
<property name="Target">M:Microsoft.AspNetCore.Components.Web.WebEventData.ParseEventArgsJson(Microsoft.AspNetCore.Components.RenderTree.Renderer,System.Text.Json.JsonSerializerOptions,System.UInt64,System.String,System.Text.Json.JsonElement)</property>
</attribute>
<assembly fullname="Microsoft.AspNetCore.Components.Web, Version=7.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60">
<attribute fullname="System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessageAttribute">
<argument>ILLink</argument>
<argument>IL2062</argument>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<linker>
<assembly fullname="Microsoft.AspNetCore.Components.WebAssembly.Authentication, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60">
<attribute fullname="System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessageAttribute">
Expand All @@ -14,4 +14,4 @@
<property name="Target">M:Microsoft.Extensions.DependencyInjection.WebAssemblyAuthenticationServiceCollectionExtensions.&lt;&gt;c__1`1.&lt;AddAuthenticationStateProvider&gt;b__1_0(System.IServiceProvider)</property>
</attribute>
</assembly>
</linker>
</linker>
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<linker>
<assembly fullname="Microsoft.JSInterop, Version=6.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60">
<assembly fullname="Microsoft.JSInterop, Version=7.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60">
<attribute fullname="System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessageAttribute">
<argument>ILLink</argument>
<argument>IL2026</argument>
Expand Down

0 comments on commit 197d527

Please sign in to comment.