From ceb56cbb2636e6c4ae8ab35ff1b1e78177a4b2b9 Mon Sep 17 00:00:00 2001 From: Javier Calvarro Nelson Date: Tue, 2 Dec 2025 16:24:57 +0100 Subject: [PATCH] Introduce IComponentPropertyActivator for Blazor property injection This PR introduces a new public abstraction IComponentPropertyActivator for Blazor property injection, addressing #63451. - Add IComponentPropertyActivator interface for customizing property injection - Add DefaultComponentPropertyActivator internal implementation - Refactor ComponentFactory to use the new abstraction - Update Renderer to resolve IComponentPropertyActivator from DI - Add comprehensive unit tests for the new functionality --- .../test/AuthorizeRouteViewTest.cs | 2 +- .../Components/src/ComponentFactory.cs | 124 ++-------- .../src/DefaultComponentPropertyActivator.cs | 106 +++++++++ .../src/IComponentPropertyActivator.cs | 29 +++ .../Components/src/PublicAPI.Unshipped.txt | 2 + .../Components/src/RenderTree/Renderer.cs | 9 +- .../Components/test/ComponentFactoryTest.cs | 225 ++++++++++++++++-- .../Components/test/RouteViewTest.cs | 2 +- 8 files changed, 376 insertions(+), 123 deletions(-) create mode 100644 src/Components/Components/src/DefaultComponentPropertyActivator.cs create mode 100644 src/Components/Components/src/IComponentPropertyActivator.cs diff --git a/src/Components/Authorization/test/AuthorizeRouteViewTest.cs b/src/Components/Authorization/test/AuthorizeRouteViewTest.cs index 912301e8b893..b3ec1ca1a7a4 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(services), _renderer); + var componentFactory = new ComponentFactory(new DefaultComponentActivator(services), new DefaultComponentPropertyActivator(), _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 0618c9a4d20a..06c9e8e952e6 100644 --- a/src/Components/Components/src/ComponentFactory.cs +++ b/src/Components/Components/src/ComponentFactory.cs @@ -5,9 +5,7 @@ using System.Diagnostics.CodeAnalysis; using System.Reflection; using Microsoft.AspNetCore.Components.HotReload; -using Microsoft.AspNetCore.Components.Reflection; using Microsoft.AspNetCore.Components.RenderTree; -using Microsoft.Extensions.DependencyInjection; using static Microsoft.AspNetCore.Internal.LinkerFlags; namespace Microsoft.AspNetCore.Components; @@ -19,10 +17,7 @@ internal sealed class ComponentFactory AppContext.TryGetSwitch("Microsoft.AspNetCore.Components.Unsupported.DisablePropertyInjection", out var isDisabled) && isDisabled; - private const BindingFlags _injectablePropertyBindingFlags - = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; - - private static readonly ConcurrentDictionary _cachedComponentTypeInfo = new(); + private static readonly ConcurrentDictionary _cachedComponentTypeRenderModes = new(); static ComponentFactory() { @@ -33,36 +28,35 @@ static ComponentFactory() } private readonly IComponentActivator _componentActivator; + private readonly IComponentPropertyActivator _propertyActivator; private readonly Renderer _renderer; - public ComponentFactory(IComponentActivator componentActivator, Renderer renderer) + public ComponentFactory(IComponentActivator componentActivator, IComponentPropertyActivator propertyActivator, Renderer renderer) { _componentActivator = componentActivator ?? throw new ArgumentNullException(nameof(componentActivator)); + _propertyActivator = propertyActivator ?? throw new ArgumentNullException(nameof(propertyActivator)); _renderer = renderer ?? throw new ArgumentNullException(nameof(renderer)); } - public static void ClearCache() => _cachedComponentTypeInfo.Clear(); + public static void ClearCache() => _cachedComponentTypeRenderModes.Clear(); - private static ComponentTypeInfoCacheEntry GetComponentTypeInfo([DynamicallyAccessedMembers(Component)] Type componentType) + private static IComponentRenderMode? GetComponentTypeRenderMode([DynamicallyAccessedMembers(Component)] 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 cacheEntry)) + if (!_cachedComponentTypeRenderModes.TryGetValue(componentType, out var renderMode)) { - var componentTypeRenderMode = componentType.GetCustomAttribute()?.Mode; - cacheEntry = new ComponentTypeInfoCacheEntry( - componentTypeRenderMode, - CreatePropertyInjector(componentType)); - _cachedComponentTypeInfo.TryAdd(componentType, cacheEntry); + renderMode = componentType.GetCustomAttribute()?.Mode; + _cachedComponentTypeRenderModes.TryAdd(componentType, renderMode); } - return cacheEntry; + return renderMode; } public IComponent InstantiateComponent(IServiceProvider serviceProvider, [DynamicallyAccessedMembers(Component)] Type componentType, IComponentRenderMode? callerSpecifiedRenderMode, int? parentComponentId) { - var (componentTypeRenderMode, propertyInjector) = GetComponentTypeInfo(componentType); + var componentTypeRenderMode = GetComponentTypeRenderMode(componentType); IComponent component; if (componentTypeRenderMode is null && callerSpecifiedRenderMode is null) @@ -92,7 +86,8 @@ public IComponent InstantiateComponent(IServiceProvider serviceProvider, [Dynami if (component.GetType() == componentType) { // Fast, common case: use the cached data we already looked up - propertyInjector(serviceProvider, component); + var propertyActivator = _propertyActivator.GetActivator(componentType); + propertyActivator(serviceProvider, component); } else { @@ -104,96 +99,13 @@ public IComponent InstantiateComponent(IServiceProvider serviceProvider, [Dynami return component; } - private static void PerformPropertyInjection(IServiceProvider serviceProvider, IComponent instance) + private 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)'. - 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)'. - - componentTypeInfo.PerformPropertyInjection(serviceProvider, instance); - } - - private static Action CreatePropertyInjector([DynamicallyAccessedMembers(Component)] Type type) - { - // Do all the reflection up front - List<(string name, Type propertyType, PropertySetter setter, object? serviceKey)>? injectables = null; - foreach (var property in MemberAssignment.GetPropertiesIncludingInherited(type, _injectablePropertyBindingFlags)) - { - var injectAttribute = property.GetCustomAttribute(); - if (injectAttribute is null) - { - continue; - } - - injectables ??= new(); - injectables.Add((property.Name, property.PropertyType, new PropertySetter(type, property), injectAttribute.Key)); - } - - if (injectables is null) - { - return static (_, _) => { }; - } - - return Initialize; +#pragma warning disable IL2072 // 'componentType' argument does not satisfy 'DynamicallyAccessedMemberTypes.All' in call to 'IComponentPropertyActivator.GetActivator(Type)'. + var propertyActivator = _propertyActivator.GetActivator(instance.GetType()); +#pragma warning restore IL2072 - // Return an action whose closure can write all the injected properties - // without any further reflection calls (just typecasts) - void Initialize(IServiceProvider serviceProvider, IComponent component) - { - foreach (var (propertyName, propertyType, setter, serviceKey) in injectables) - { - object? serviceInstance; - - if (serviceKey is not null) - { - if (serviceProvider is not IKeyedServiceProvider keyedServiceProvider) - { - throw new InvalidOperationException($"Cannot provide a value for property " + - $"'{propertyName}' on type '{type.FullName}'. The service provider " + - $"does not implement '{nameof(IKeyedServiceProvider)}' and therefore " + - $"cannot provide keyed services."); - } - - serviceInstance = keyedServiceProvider.GetKeyedService(propertyType, serviceKey) - ?? throw new InvalidOperationException($"Cannot provide a value for property " + - $"'{propertyName}' on type '{type.FullName}'. There is no " + - $"registered keyed service of type '{propertyType}' with key '{serviceKey}'."); - } - else - { - serviceInstance = serviceProvider.GetService(propertyType) - ?? throw new InvalidOperationException($"Cannot provide a value for property " + - $"'{propertyName}' on type '{type.FullName}'. There is no " + - $"registered service of type '{propertyType}'."); - } - - setter.SetValue(component, serviceInstance); - } - } - } - - // Tracks information about a specific component type that ComponentFactory uses - private sealed class ComponentTypeInfoCacheEntry - { - public IComponentRenderMode? ComponentTypeRenderMode { get; } - - public Action PerformPropertyInjection { get; } - - public ComponentTypeInfoCacheEntry( - IComponentRenderMode? componentTypeRenderMode, - Action performPropertyInjection) - { - ComponentTypeRenderMode = componentTypeRenderMode; - PerformPropertyInjection = performPropertyInjection; - } - - public void Deconstruct( - out IComponentRenderMode? componentTypeRenderMode, - out Action performPropertyInjection) - { - componentTypeRenderMode = ComponentTypeRenderMode; - performPropertyInjection = PerformPropertyInjection; - } + propertyActivator(serviceProvider, instance); } } diff --git a/src/Components/Components/src/DefaultComponentPropertyActivator.cs b/src/Components/Components/src/DefaultComponentPropertyActivator.cs new file mode 100644 index 000000000000..3fa29d33903c --- /dev/null +++ b/src/Components/Components/src/DefaultComponentPropertyActivator.cs @@ -0,0 +1,106 @@ +// 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 System.Reflection; +using Microsoft.AspNetCore.Components.HotReload; +using Microsoft.AspNetCore.Components.Reflection; +using Microsoft.Extensions.DependencyInjection; +using static Microsoft.AspNetCore.Internal.LinkerFlags; + +namespace Microsoft.AspNetCore.Components; + +internal sealed class DefaultComponentPropertyActivator : IComponentPropertyActivator +{ + private const BindingFlags InjectablePropertyBindingFlags + = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; + + private static readonly ConcurrentDictionary> _cachedPropertyActivators = new(); + + static DefaultComponentPropertyActivator() + { + if (HotReloadManager.Default.MetadataUpdateSupported) + { + HotReloadManager.Default.OnDeltaApplied += ClearCache; + } + } + + public static void ClearCache() => _cachedPropertyActivators.Clear(); + + /// + public Action GetActivator( + [DynamicallyAccessedMembers(Component)] 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 (!_cachedPropertyActivators.TryGetValue(componentType, out var activator)) + { + activator = CreatePropertyActivator(componentType); + _cachedPropertyActivators.TryAdd(componentType, activator); + } + + return activator; + } + + private static Action CreatePropertyActivator( + [DynamicallyAccessedMembers(Component)] Type type) + { + // Do all the reflection up front + List<(string name, Type propertyType, PropertySetter setter, object? serviceKey)>? injectables = null; + foreach (var property in MemberAssignment.GetPropertiesIncludingInherited(type, InjectablePropertyBindingFlags)) + { + var injectAttribute = property.GetCustomAttribute(); + if (injectAttribute is null) + { + continue; + } + + injectables ??= new(); + injectables.Add((property.Name, property.PropertyType, new PropertySetter(type, property), injectAttribute.Key)); + } + + if (injectables is null) + { + return static (_, _) => { }; + } + + return Initialize; + + // Return an action whose closure can write all the injected properties + // without any further reflection calls (just typecasts) + void Initialize(IServiceProvider serviceProvider, IComponent component) + { + foreach (var (propertyName, propertyType, setter, serviceKey) in injectables) + { + object? serviceInstance; + + if (serviceKey is not null) + { + if (serviceProvider is not IKeyedServiceProvider keyedServiceProvider) + { + throw new InvalidOperationException($"Cannot provide a value for property " + + $"'{propertyName}' on type '{type.FullName}'. The service provider " + + $"does not implement '{nameof(IKeyedServiceProvider)}' and therefore " + + $"cannot provide keyed services."); + } + + serviceInstance = keyedServiceProvider.GetKeyedService(propertyType, serviceKey) + ?? throw new InvalidOperationException($"Cannot provide a value for property " + + $"'{propertyName}' on type '{type.FullName}'. There is no " + + $"registered keyed service of type '{propertyType}' with key '{serviceKey}'."); + } + else + { + serviceInstance = serviceProvider.GetService(propertyType) + ?? throw new InvalidOperationException($"Cannot provide a value for property " + + $"'{propertyName}' on type '{type.FullName}'. There is no " + + $"registered service of type '{propertyType}'."); + } + + setter.SetValue(component, serviceInstance); + } + } + } +} diff --git a/src/Components/Components/src/IComponentPropertyActivator.cs b/src/Components/Components/src/IComponentPropertyActivator.cs new file mode 100644 index 000000000000..c3bd40c3ec8e --- /dev/null +++ b/src/Components/Components/src/IComponentPropertyActivator.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using static Microsoft.AspNetCore.Internal.LinkerFlags; + +namespace Microsoft.AspNetCore.Components; + +/// +/// Provides a mechanism for activating properties on Blazor component instances. +/// +/// +/// This interface allows customization of how properties marked with +/// are populated on component instances. The default implementation uses the +/// to resolve services for injection. +/// +public interface IComponentPropertyActivator +{ + /// + /// Gets a delegate that activates properties on a component of the specified type. + /// + /// The type of component to create an activator for. + /// + /// A delegate that takes an and an + /// instance, and populates the component's injectable properties. + /// + Action GetActivator( + [DynamicallyAccessedMembers(Component)] Type componentType); +} diff --git a/src/Components/Components/src/PublicAPI.Unshipped.txt b/src/Components/Components/src/PublicAPI.Unshipped.txt index 7dc5c58110bf..ef29dd2ca874 100644 --- a/src/Components/Components/src/PublicAPI.Unshipped.txt +++ b/src/Components/Components/src/PublicAPI.Unshipped.txt @@ -1 +1,3 @@ #nullable enable +Microsoft.AspNetCore.Components.IComponentPropertyActivator +Microsoft.AspNetCore.Components.IComponentPropertyActivator.GetActivator(System.Type! componentType) -> System.Action! diff --git a/src/Components/Components/src/RenderTree/Renderer.cs b/src/Components/Components/src/RenderTree/Renderer.cs index ad0864443da4..6b6b9df93f8b 100644 --- a/src/Components/Components/src/RenderTree/Renderer.cs +++ b/src/Components/Components/src/RenderTree/Renderer.cs @@ -99,7 +99,7 @@ public Renderer(IServiceProvider serviceProvider, ILoggerFactory loggerFactory, // has always taken ILoggerFactory so to avoid the per-instance string allocation of the logger name we just pass the // logger name in here as a string literal. _logger = loggerFactory.CreateLogger("Microsoft.AspNetCore.Components.RenderTree.Renderer"); - _componentFactory = new ComponentFactory(componentActivator, this); + _componentFactory = new ComponentFactory(componentActivator, GetComponentPropertyActivatorOrDefault(serviceProvider), this); _componentsMetrics = serviceProvider.GetService(); _componentsActivitySource = serviceProvider.GetService(); _componentsActivitySource?.Init(new ComponentsActivityLinkStore(this)); @@ -122,6 +122,12 @@ private static IComponentActivator GetComponentActivatorOrDefault(IServiceProvid ?? new DefaultComponentActivator(serviceProvider); } + private static IComponentPropertyActivator GetComponentPropertyActivatorOrDefault(IServiceProvider serviceProvider) + { + return serviceProvider.GetService() + ?? new DefaultComponentPropertyActivator(); + } + /// /// Gets the associated with this . /// @@ -186,6 +192,7 @@ private async void RenderRootComponentsOnHotReload() ComponentFactory.ClearCache(); ComponentProperties.ClearCache(); DefaultComponentActivator.ClearCache(); + DefaultComponentPropertyActivator.ClearCache(); await Dispatcher.InvokeAsync(() => { diff --git a/src/Components/Components/test/ComponentFactoryTest.cs b/src/Components/Components/test/ComponentFactoryTest.cs index 5b0b32bc7535..c6a3c623452d 100644 --- a/src/Components/Components/test/ComponentFactoryTest.cs +++ b/src/Components/Components/test/ComponentFactoryTest.cs @@ -16,8 +16,8 @@ public void InstantiateComponent_CreatesInstance() // Arrange var componentType = typeof(EmptyComponent); var serviceProvider = GetServiceProvider(); - var factory = new ComponentFactory(new DefaultComponentActivator(serviceProvider), new TestRenderer()); - + var factory = new ComponentFactory(new DefaultComponentActivator(serviceProvider), new DefaultComponentPropertyActivator(), new TestRenderer()); + // Act var instance = factory.InstantiateComponent(GetServiceProvider(), componentType, null, null); @@ -32,8 +32,8 @@ public void InstantiateComponent_CreatesInstance_NonComponent() // Arrange var componentType = typeof(List); var serviceProvider = GetServiceProvider(); - var factory = new ComponentFactory(new DefaultComponentActivator(serviceProvider), new TestRenderer()); - + var factory = new ComponentFactory(new DefaultComponentActivator(serviceProvider), new DefaultComponentPropertyActivator(), 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); @@ -44,7 +44,7 @@ public void InstantiateComponent_CreatesInstance_WithCustomActivator() { // Arrange var componentType = typeof(EmptyComponent); - var factory = new ComponentFactory(new CustomComponentActivator(), new TestRenderer()); + var factory = new ComponentFactory(new CustomComponentActivator(), new DefaultComponentPropertyActivator(), new TestRenderer()); // Act var instance = factory.InstantiateComponent(GetServiceProvider(), componentType, null, null); @@ -65,7 +65,7 @@ public void InstantiateComponent_ThrowsForNullInstance() { // Arrange var componentType = typeof(EmptyComponent); - var factory = new ComponentFactory(new NullResultComponentActivator(), new TestRenderer()); + var factory = new ComponentFactory(new NullResultComponentActivator(), new DefaultComponentPropertyActivator(), new TestRenderer()); // Act var ex = Assert.Throws(() => factory.InstantiateComponent(GetServiceProvider(), componentType, null, null)); @@ -77,7 +77,7 @@ public void InstantiateComponent_AssignsPropertiesWithInjectAttributeOnBaseType( { // Arrange var componentType = typeof(DerivedComponent); - var factory = new ComponentFactory(new CustomComponentActivator(), new TestRenderer()); + var factory = new ComponentFactory(new CustomComponentActivator(), new DefaultComponentPropertyActivator(), new TestRenderer()); // Act var instance = factory.InstantiateComponent(GetServiceProvider(), componentType, null, null); @@ -102,7 +102,7 @@ public void InstantiateComponent_IgnoresPropertiesWithoutInjectAttribute() // Arrange var componentType = typeof(ComponentWithNonInjectableProperties); var serviceProvider = GetServiceProvider(); - var factory = new ComponentFactory(new DefaultComponentActivator(serviceProvider), new TestRenderer()); + var factory = new ComponentFactory(new DefaultComponentActivator(serviceProvider), new DefaultComponentPropertyActivator(), new TestRenderer()); // Act var instance = factory.InstantiateComponent(serviceProvider, componentType, null, null); @@ -123,7 +123,7 @@ public void InstantiateComponent_WithNoRenderMode_DoesNotUseRenderModeResolver() var renderer = new RendererWithResolveComponentForRenderMode( /* won't be used */ new ComponentWithRenderMode()); var serviceProvider = GetServiceProvider(); - var factory = new ComponentFactory(new DefaultComponentActivator(serviceProvider), renderer); + var factory = new ComponentFactory(new DefaultComponentActivator(serviceProvider), new DefaultComponentPropertyActivator(), renderer); // Act var instance = factory.InstantiateComponent(serviceProvider, componentType, null, null); @@ -142,7 +142,7 @@ public void InstantiateComponent_WithRenderModeOnComponent_UsesRenderModeResolve var renderer = new RendererWithResolveComponentForRenderMode(resolvedComponent); var serviceProvider = GetServiceProvider(); var componentActivator = new DefaultComponentActivator(serviceProvider); - var factory = new ComponentFactory(componentActivator, renderer); + var factory = new ComponentFactory(componentActivator, new DefaultComponentPropertyActivator(), renderer); // Act var instance = (ComponentWithInjectProperties)factory.InstantiateComponent(serviceProvider, componentType, null, 1234); @@ -174,7 +174,7 @@ public void InstantiateComponent_WithDerivedRenderModeOnDerivedComponent_CausesA var renderer = new RendererWithResolveComponentForRenderMode(resolvedComponent); var serviceProvider = GetServiceProvider(); var componentActivator = new DefaultComponentActivator(serviceProvider); - var factory = new ComponentFactory(componentActivator, renderer); + var factory = new ComponentFactory(componentActivator, new DefaultComponentPropertyActivator(), renderer); // Act/Assert Assert.Throws( @@ -193,7 +193,7 @@ public void InstantiateComponent_WithRenderModeOnCallSite_UsesRenderModeResolver var renderer = new RendererWithResolveComponentForRenderMode(resolvedComponent); var serviceProvider = GetServiceProvider(); var componentActivator = new DefaultComponentActivator(serviceProvider); - var factory = new ComponentFactory(componentActivator, renderer); + var factory = new ComponentFactory(componentActivator, new DefaultComponentPropertyActivator(), renderer); // Act var instance = (ComponentWithInjectProperties)factory.InstantiateComponent(serviceProvider, componentType, callSiteRenderMode, 1234); @@ -216,7 +216,7 @@ public void InstantiateComponent_WithRenderModeOnComponentAndCallSite_Throws() var renderer = new RendererWithResolveComponentForRenderMode(resolvedComponent); var serviceProvider = GetServiceProvider(); var componentActivator = new DefaultComponentActivator(serviceProvider); - var factory = new ComponentFactory(componentActivator, renderer); + var factory = new ComponentFactory(componentActivator, new DefaultComponentPropertyActivator(), renderer); // Even though the two rendermodes are literally the same object, we don't allow specifying any nonnull // rendermode at the callsite if there's a nonnull fixed rendermode @@ -237,7 +237,7 @@ public void InstantiateComponent_CreatesInstance_WithTypeActivation() var resolvedComponent = new ComponentWithInjectProperties(); var renderer = new RendererWithResolveComponentForRenderMode(resolvedComponent); var defaultComponentActivator = new DefaultComponentActivator(serviceProvider); - var factory = new ComponentFactory(defaultComponentActivator, renderer); + var factory = new ComponentFactory(defaultComponentActivator, new DefaultComponentPropertyActivator(), renderer); // Act var instance = factory.InstantiateComponent(serviceProvider, componentType, null, null); @@ -250,6 +250,162 @@ public void InstantiateComponent_CreatesInstance_WithTypeActivation() Assert.NotNull(component.Property3); // Property injection should still work. } + [Fact] + public void InstantiateComponent_WithCustomPropertyActivator_UsesCustomActivator() + { + // Arrange + var componentType = typeof(ComponentWithInjectProperties); + var serviceProvider = GetServiceProvider(); + var customPropertyActivator = new CustomPropertyActivator(); + var factory = new ComponentFactory( + new DefaultComponentActivator(serviceProvider), + customPropertyActivator, + new TestRenderer()); + + // Act + var instance = factory.InstantiateComponent(serviceProvider, componentType, null, null); + + // Assert + Assert.NotNull(instance); + var component = Assert.IsType(instance); + Assert.True(customPropertyActivator.GetActivatorCalled); + Assert.Equal(componentType, customPropertyActivator.RequestedType); + Assert.True(customPropertyActivator.ActivatorInvoked); + } + + [Fact] + public void InstantiateComponent_WithCustomPropertyActivator_ReceivesCorrectServiceProvider() + { + // Arrange + var componentType = typeof(ComponentWithInjectProperties); + var serviceProvider = GetServiceProvider(); + var customPropertyActivator = new CustomPropertyActivator(); + var factory = new ComponentFactory( + new DefaultComponentActivator(serviceProvider), + customPropertyActivator, + new TestRenderer()); + + // Act + factory.InstantiateComponent(serviceProvider, componentType, null, null); + + // Assert + Assert.Same(serviceProvider, customPropertyActivator.ReceivedServiceProvider); + } + + [Fact] + public void InstantiateComponent_WithCustomPropertyActivator_WhenActivatorReturnsDifferentType_StillUsesCustomActivator() + { + // Arrange + // The component activator returns a different type than requested + var requestedType = typeof(EmptyComponent); + var actualType = typeof(ComponentWithInjectProperties); + var serviceProvider = GetServiceProvider(); + var customPropertyActivator = new CustomPropertyActivator(); + var factory = new ComponentFactory( + new CustomComponentActivator(), + customPropertyActivator, + new TestRenderer()); + + // Act + var instance = factory.InstantiateComponent(serviceProvider, requestedType, null, null); + + // Assert + Assert.IsType(instance); + // The property activator should be called with the actual type, not the requested type + Assert.True(customPropertyActivator.GetActivatorCalled); + // Since the types differ, GetActivator is called twice (once for requested, once for actual) + // But the activator should have been invoked for the actual component type + Assert.True(customPropertyActivator.ActivatorInvoked); + } + + [Fact] + public void DefaultComponentPropertyActivator_GetActivator_ReturnsActivatorThatInjectsProperties() + { + // Arrange + var componentType = typeof(ComponentWithInjectProperties); + var serviceProvider = GetServiceProvider(); + var propertyActivator = new DefaultComponentPropertyActivator(); + var component = new ComponentWithInjectProperties(); + + // Act + var activator = propertyActivator.GetActivator(componentType); + activator(serviceProvider, component); + + // Assert + Assert.NotNull(component.Property1); + Assert.NotNull(component.GetProperty2()); + Assert.NotNull(component.Property3); + Assert.NotNull(component.Property4); + Assert.NotNull(component.KeyedProperty); + } + + [Fact] + public void DefaultComponentPropertyActivator_GetActivator_CachesActivator() + { + // Arrange + var componentType = typeof(ComponentWithInjectProperties); + var propertyActivator = new DefaultComponentPropertyActivator(); + + // Act + var activator1 = propertyActivator.GetActivator(componentType); + var activator2 = propertyActivator.GetActivator(componentType); + + // Assert + Assert.Same(activator1, activator2); + } + + [Fact] + public void DefaultComponentPropertyActivator_GetActivator_ReturnsNoOpForComponentWithoutInjectableProperties() + { + // Arrange + var componentType = typeof(EmptyComponent); + var serviceProvider = GetServiceProvider(); + var propertyActivator = new DefaultComponentPropertyActivator(); + var component = new EmptyComponent(); + + // Act + var activator = propertyActivator.GetActivator(componentType); + + // Should not throw + activator(serviceProvider, component); + + // Assert - nothing to verify, just ensuring no exception + } + + [Fact] + public void DefaultComponentPropertyActivator_GetActivator_ThrowsForMissingService() + { + // Arrange + var componentType = typeof(ComponentWithInjectProperties); + var serviceProvider = new ServiceCollection().BuildServiceProvider(); // Empty provider + var propertyActivator = new DefaultComponentPropertyActivator(); + var component = new ComponentWithInjectProperties(); + + // Act + var activator = propertyActivator.GetActivator(componentType); + var ex = Assert.Throws(() => activator(serviceProvider, component)); + + // Assert + Assert.Contains("There is no registered service of type", ex.Message); + } + + [Fact] + public void DefaultComponentPropertyActivator_GetActivator_ThrowsForMissingKeyedService() + { + // Arrange + var componentType = typeof(ComponentWithOnlyKeyedProperty); + var serviceProvider = new ServiceCollection().BuildServiceProvider(); // Empty provider + var propertyActivator = new DefaultComponentPropertyActivator(); + var component = new ComponentWithOnlyKeyedProperty(); + + // Act + var activator = propertyActivator.GetActivator(componentType); + var ex = Assert.Throws(() => activator(serviceProvider, component)); + + // Assert + Assert.Contains("registered keyed service", ex.Message); + } + private const string KeyedServiceKey = "my-keyed-service"; private static IServiceProvider GetServiceProvider() @@ -359,6 +515,22 @@ public class TestService1 { } public class TestService2 { } public class TestService3 { } + private class ComponentWithOnlyKeyedProperty : IComponent + { + [Inject(Key = KeyedServiceKey)] + public TestService3 KeyedProperty { get; set; } + + public void Attach(RenderHandle renderHandle) + { + throw new NotImplementedException(); + } + + public Task SetParametersAsync(ParameterView parameters) + { + throw new NotImplementedException(); + } + } + private class CustomComponentActivator : IComponentActivator where TResult : IComponent, new() { public IComponent CreateInstance(Type componentType) @@ -375,6 +547,31 @@ public IComponent CreateInstance(Type componentType) } } + private class CustomPropertyActivator : IComponentPropertyActivator + { + public bool GetActivatorCalled { get; private set; } + public Type RequestedType { get; private set; } + public bool ActivatorInvoked { get; private set; } + public IServiceProvider ReceivedServiceProvider { get; private set; } + + public Action GetActivator(Type componentType) + { + GetActivatorCalled = true; + RequestedType = componentType; + + // Return an activator that tracks invocation and delegates to the default + var defaultActivator = new DefaultComponentPropertyActivator(); + var defaultAction = defaultActivator.GetActivator(componentType); + + return (sp, component) => + { + ActivatorInvoked = true; + ReceivedServiceProvider = sp; + defaultAction(sp, component); + }; + } + } + private class TestRenderMode : IComponentRenderMode { } private class DerivedComponentRenderMode : IComponentRenderMode { } diff --git a/src/Components/Components/test/RouteViewTest.cs b/src/Components/Components/test/RouteViewTest.cs index 09715b18d239..7a1fc359ae0d 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(services), _renderer); + var componentFactory = new ComponentFactory(new DefaultComponentActivator(services), new DefaultComponentPropertyActivator(), _renderer); _routeViewComponent = (RouteView)componentFactory.InstantiateComponent(services, typeof(RouteView), null, null); _routeViewComponentId = _renderer.AssignRootComponentId(_routeViewComponent);