diff --git a/src/Components/Authorization/test/AuthorizeRouteViewTest.cs b/src/Components/Authorization/test/AuthorizeRouteViewTest.cs index 9a310fe441c1..912301e8b893 100644 --- a/src/Components/Authorization/test/AuthorizeRouteViewTest.cs +++ b/src/Components/Authorization/test/AuthorizeRouteViewTest.cs @@ -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); } diff --git a/src/Components/Components/src/ComponentFactory.cs b/src/Components/Components/src/ComponentFactory.cs index 9f56b156d3c8..12ad8ba03dea 100644 --- a/src/Components/Components/src/ComponentFactory.cs +++ b/src/Components/Components/src/ComponentFactory.cs @@ -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); } diff --git a/src/Components/Components/src/DefaultComponentActivator.cs b/src/Components/Components/src/DefaultComponentActivator.cs index d57ff19e2a62..716591e4a660 100644 --- a/src/Components/Components/src/DefaultComponentActivator.cs +++ b/src/Components/Components/src/DefaultComponentActivator.cs @@ -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 _cachedComponentTypeInfo = new(); + + public static void ClearCache() => _cachedComponentTypeInfo.Clear(); /// public IComponent CreateInstance([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type componentType) @@ -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; } } diff --git a/src/Components/Components/src/RenderTree/Renderer.cs b/src/Components/Components/src/RenderTree/Renderer.cs index 231425bd56f4..2d2065cb0c95 100644 --- a/src/Components/Components/src/RenderTree/Renderer.cs +++ b/src/Components/Components/src/RenderTree/Renderer.cs @@ -102,7 +102,7 @@ public Renderer(IServiceProvider serviceProvider, ILoggerFactory loggerFactory, private static IComponentActivator GetComponentActivatorOrDefault(IServiceProvider serviceProvider) { return serviceProvider.GetService() - ?? DefaultComponentActivator.Instance; + ?? new DefaultComponentActivator(serviceProvider); } /// @@ -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(() => { diff --git a/src/Components/Components/test/ComponentFactoryTest.cs b/src/Components/Components/test/ComponentFactoryTest.cs index da0109a21115..5b0b32bc7535 100644 --- a/src/Components/Components/test/ComponentFactoryTest.cs +++ b/src/Components/Components/test/ComponentFactoryTest.cs @@ -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); @@ -30,8 +31,9 @@ public void InstantiateComponent_CreatesInstance_NonComponent() { // Arrange var componentType = typeof(List); - 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(() => factory.InstantiateComponent(GetServiceProvider(), componentType, null, null)); Assert.StartsWith($"The type {componentType.FullName} does not implement {nameof(IComponent)}.", ex.Message); @@ -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); @@ -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(instance); @@ -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); @@ -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( - () => factory.InstantiateComponent(GetServiceProvider(), componentType, null, 1234)); + () => factory.InstantiateComponent(serviceProvider, componentType, null, 1234)); } [Fact] @@ -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); @@ -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 @@ -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(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() @@ -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; } diff --git a/src/Components/Components/test/RouteViewTest.cs b/src/Components/Components/test/RouteViewTest.cs index 2ee99f135146..09715b18d239 100644 --- a/src/Components/Components/test/RouteViewTest.cs +++ b/src/Components/Components/test/RouteViewTest.cs @@ -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); diff --git a/src/Components/Server/test/Circuits/RemoteRendererTest.cs b/src/Components/Server/test/Circuits/RemoteRendererTest.cs index c25fada6bc61..9ca641979610 100644 --- a/src/Components/Server/test/Circuits/RemoteRendererTest.cs +++ b/src/Components/Server/test/Circuits/RemoteRendererTest.cs @@ -738,7 +738,7 @@ public TestComponent() { } - public TestComponent(RenderFragment renderFragment) + internal TestComponent(RenderFragment renderFragment) { _renderFragment = renderFragment; }