Skip to content

Commit

Permalink
Construction injection
Browse files Browse the repository at this point in the history
  • Loading branch information
javiercn committed Feb 9, 2024
1 parent e07adfe commit a404424
Show file tree
Hide file tree
Showing 6 changed files with 99 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public AuthorizeRouteViewTest()

var services = serviceCollection.BuildServiceProvider();
_renderer = new TestRenderer(services);
var componentFactory = new ComponentFactory(new DefaultComponentActivator(), _renderer);
var componentFactory = new ComponentFactory(new DefaultComponentActivator(services), _renderer);
_authorizeRouteViewComponent = (AuthorizeRouteView)componentFactory.InstantiateComponent(services, typeof(AuthorizeRouteView), null, null);
_authorizeRouteViewComponentId = _renderer.AssignRootComponentId(_authorizeRouteViewComponent);
}
Expand Down
4 changes: 2 additions & 2 deletions src/Components/Components/src/ComponentFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,9 @@ public IComponent InstantiateComponent(IServiceProvider serviceProvider, [Dynami
private static void PerformPropertyInjection(IServiceProvider serviceProvider, IComponent instance)
{
// Suppressed with "pragma warning disable" so ILLink Roslyn Anayzer doesn't report the warning.
#pragma warning disable IL2072 // 'componentType' argument does not satisfy 'DynamicallyAccessedMemberTypes.All' in call to 'Microsoft.AspNetCore.Components.ComponentFactory.GetComponentTypeInfo(Type)'.
#pragma warning disable IL2072 // 'componentType' argument does not satisfy 'DynamicallyAccessedMemberTypes.All' in call to 'Microsoft.AspNetCore.Components.ComponentFactory.GetComponentTypeInfo(Type)'.
var componentTypeInfo = GetComponentTypeInfo(instance.GetType());
#pragma warning restore IL2072 // 'componentType' argument does not satisfy 'DynamicallyAccessedMemberTypes.All' in call to 'Microsoft.AspNetCore.Components.ComponentFactory.GetComponentTypeInfo(Type)'.
#pragma warning restore IL2072 // 'componentType' argument does not satisfy 'DynamicallyAccessedMemberTypes.All' in call to 'Microsoft.AspNetCore.Components.ComponentFactory.GetComponentTypeInfo(Type)'.

componentTypeInfo.PerformPropertyInjection(serviceProvider, instance);
}
Expand Down
26 changes: 23 additions & 3 deletions src/Components/Components/src/DefaultComponentActivator.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.DependencyInjection;

namespace Microsoft.AspNetCore.Components;

internal sealed class DefaultComponentActivator : IComponentActivator
internal sealed class DefaultComponentActivator(IServiceProvider serviceProvider) : IComponentActivator
{
public static IComponentActivator Instance { get; } = new DefaultComponentActivator();
private static readonly ConcurrentDictionary<Type, ObjectFactory> _cachedComponentTypeInfo = new();

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

/// <inheritdoc />
public IComponent CreateInstance([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type componentType)
Expand All @@ -17,6 +21,22 @@ public IComponent CreateInstance([DynamicallyAccessedMembers(DynamicallyAccessed
throw new ArgumentException($"The type {componentType.FullName} does not implement {nameof(IComponent)}.", nameof(componentType));
}

return (IComponent)Activator.CreateInstance(componentType)!;
var factory = GetObjectFactory(componentType);

return (IComponent)factory(serviceProvider, []);
}

private static ObjectFactory GetObjectFactory([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type componentType)
{
// Unfortunately we can't use 'GetOrAdd' here because the DynamicallyAccessedMembers annotation doesn't flow through to the
// callback, so it becomes an IL2111 warning. The following is equivalent and thread-safe because it's a ConcurrentDictionary
// and it doesn't matter if we build a cache entry more than once.
if (!_cachedComponentTypeInfo.TryGetValue(componentType, out var factory))
{
factory = ActivatorUtilities.CreateFactory(componentType, Type.EmptyTypes);
_cachedComponentTypeInfo.TryAdd(componentType, factory);
}

return factory;
}
}
3 changes: 2 additions & 1 deletion src/Components/Components/src/RenderTree/Renderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ public Renderer(IServiceProvider serviceProvider, ILoggerFactory loggerFactory,
private static IComponentActivator GetComponentActivatorOrDefault(IServiceProvider serviceProvider)
{
return serviceProvider.GetService<IComponentActivator>()
?? DefaultComponentActivator.Instance;
?? new DefaultComponentActivator(serviceProvider);
}

/// <summary>
Expand Down Expand Up @@ -155,6 +155,7 @@ private async void RenderRootComponentsOnHotReload()
// Before re-rendering the root component, also clear any well-known caches in the framework
ComponentFactory.ClearCache();
ComponentProperties.ClearCache();
DefaultComponentActivator.ClearCache();

await Dispatcher.InvokeAsync(() =>
{
Expand Down
85 changes: 70 additions & 15 deletions src/Components/Components/test/ComponentFactoryTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ public void InstantiateComponent_CreatesInstance()
{
// Arrange
var componentType = typeof(EmptyComponent);
var factory = new ComponentFactory(new DefaultComponentActivator(), new TestRenderer());

var serviceProvider = GetServiceProvider();
var factory = new ComponentFactory(new DefaultComponentActivator(serviceProvider), new TestRenderer());

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

Expand All @@ -30,8 +31,9 @@ public void InstantiateComponent_CreatesInstance_NonComponent()
{
// Arrange
var componentType = typeof(List<string>);
var factory = new ComponentFactory(new DefaultComponentActivator(), new TestRenderer());

var serviceProvider = GetServiceProvider();
var factory = new ComponentFactory(new DefaultComponentActivator(serviceProvider), new TestRenderer());

// Assert
var ex = Assert.Throws<ArgumentException>(() => factory.InstantiateComponent(GetServiceProvider(), componentType, null, null));
Assert.StartsWith($"The type {componentType.FullName} does not implement {nameof(IComponent)}.", ex.Message);
Expand Down Expand Up @@ -99,10 +101,11 @@ public void InstantiateComponent_IgnoresPropertiesWithoutInjectAttribute()
{
// Arrange
var componentType = typeof(ComponentWithNonInjectableProperties);
var factory = new ComponentFactory(new DefaultComponentActivator(), new TestRenderer());
var serviceProvider = GetServiceProvider();
var factory = new ComponentFactory(new DefaultComponentActivator(serviceProvider), new TestRenderer());

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

// Assert
Assert.NotNull(instance);
Expand All @@ -119,10 +122,11 @@ public void InstantiateComponent_WithNoRenderMode_DoesNotUseRenderModeResolver()
var componentType = typeof(ComponentWithInjectProperties);
var renderer = new RendererWithResolveComponentForRenderMode(
/* won't be used */ new ComponentWithRenderMode());
var factory = new ComponentFactory(new DefaultComponentActivator(), renderer);
var serviceProvider = GetServiceProvider();
var factory = new ComponentFactory(new DefaultComponentActivator(serviceProvider), renderer);

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

// Assert
Assert.IsType<ComponentWithInjectProperties>(instance);
Expand All @@ -136,11 +140,12 @@ public void InstantiateComponent_WithRenderModeOnComponent_UsesRenderModeResolve
var resolvedComponent = new ComponentWithInjectProperties();
var componentType = typeof(ComponentWithRenderMode);
var renderer = new RendererWithResolveComponentForRenderMode(resolvedComponent);
var componentActivator = new DefaultComponentActivator();
var serviceProvider = GetServiceProvider();
var componentActivator = new DefaultComponentActivator(serviceProvider);
var factory = new ComponentFactory(componentActivator, renderer);

// Act
var instance = (ComponentWithInjectProperties)factory.InstantiateComponent(GetServiceProvider(), componentType, null, 1234);
var instance = (ComponentWithInjectProperties)factory.InstantiateComponent(serviceProvider, componentType, null, 1234);

// Assert
Assert.True(renderer.ResolverWasCalled);
Expand All @@ -167,12 +172,13 @@ public void InstantiateComponent_WithDerivedRenderModeOnDerivedComponent_CausesA
var resolvedComponent = new ComponentWithInjectProperties();
var componentType = typeof(DerivedComponentWithRenderMode);
var renderer = new RendererWithResolveComponentForRenderMode(resolvedComponent);
var componentActivator = new DefaultComponentActivator();
var serviceProvider = GetServiceProvider();
var componentActivator = new DefaultComponentActivator(serviceProvider);
var factory = new ComponentFactory(componentActivator, renderer);

// Act/Assert
Assert.Throws<AmbiguousMatchException>(
() => factory.InstantiateComponent(GetServiceProvider(), componentType, null, 1234));
() => factory.InstantiateComponent(serviceProvider, componentType, null, 1234));
}

[Fact]
Expand All @@ -185,11 +191,12 @@ public void InstantiateComponent_WithRenderModeOnCallSite_UsesRenderModeResolver
var componentType = typeof(ComponentWithNonInjectableProperties);
var callSiteRenderMode = new TestRenderMode();
var renderer = new RendererWithResolveComponentForRenderMode(resolvedComponent);
var componentActivator = new DefaultComponentActivator();
var serviceProvider = GetServiceProvider();
var componentActivator = new DefaultComponentActivator(serviceProvider);
var factory = new ComponentFactory(componentActivator, renderer);

// Act
var instance = (ComponentWithInjectProperties)factory.InstantiateComponent(GetServiceProvider(), componentType, callSiteRenderMode, 1234);
var instance = (ComponentWithInjectProperties)factory.InstantiateComponent(serviceProvider, componentType, callSiteRenderMode, 1234);

// Assert
Assert.Same(resolvedComponent, instance);
Expand All @@ -207,7 +214,8 @@ public void InstantiateComponent_WithRenderModeOnComponentAndCallSite_Throws()
var resolvedComponent = new ComponentWithInjectProperties();
var componentType = typeof(ComponentWithRenderMode);
var renderer = new RendererWithResolveComponentForRenderMode(resolvedComponent);
var componentActivator = new DefaultComponentActivator();
var serviceProvider = GetServiceProvider();
var componentActivator = new DefaultComponentActivator(serviceProvider);
var factory = new ComponentFactory(componentActivator, renderer);

// Even though the two rendermodes are literally the same object, we don't allow specifying any nonnull
Expand All @@ -220,6 +228,28 @@ public void InstantiateComponent_WithRenderModeOnComponentAndCallSite_Throws()
Assert.Equal($"The component type '{componentType}' has a fixed rendermode of '{typeof(TestRenderMode)}', so it is not valid to specify any rendermode when using this component.", ex.Message);
}

[Fact]
public void InstantiateComponent_CreatesInstance_WithTypeActivation()
{
// Arrange
var serviceProvider = GetServiceProvider();
var componentType = typeof(ComponentWithConstructorInjection);
var resolvedComponent = new ComponentWithInjectProperties();
var renderer = new RendererWithResolveComponentForRenderMode(resolvedComponent);
var defaultComponentActivator = new DefaultComponentActivator(serviceProvider);
var factory = new ComponentFactory(defaultComponentActivator, renderer);

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

// 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.
}

private const string KeyedServiceKey = "my-keyed-service";

private static IServiceProvider GetServiceProvider()
Expand Down Expand Up @@ -292,6 +322,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 DerivedComponent : ComponentWithInjectProperties
{
public new TestService2 Property4 { get; set; }
Expand Down
2 changes: 1 addition & 1 deletion src/Components/Components/test/RouteViewTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public RouteViewTest()
var services = serviceCollection.BuildServiceProvider();
_renderer = new TestRenderer(services);

var componentFactory = new ComponentFactory(new DefaultComponentActivator(), _renderer);
var componentFactory = new ComponentFactory(new DefaultComponentActivator(services), _renderer);
_routeViewComponent = (RouteView)componentFactory.InstantiateComponent(services, typeof(RouteView), null, null);

_routeViewComponentId = _renderer.AssignRootComponentId(_routeViewComponent);
Expand Down

0 comments on commit a404424

Please sign in to comment.