Skip to content

[Blazor] Constructor injection #53915

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Feb 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Original file line number Diff line number Diff line change
Expand Up @@ -738,7 +738,7 @@ public TestComponent()
{
}

public TestComponent(RenderFragment renderFragment)
internal TestComponent(RenderFragment renderFragment)
{
_renderFragment = renderFragment;
}
Expand Down