Skip to content

Commit a2f5be6

Browse files
authored
[Blazor] Introduce IComponentPropertyActivator for property injection (#64595)
1 parent 49a3054 commit a2f5be6

File tree

8 files changed

+442
-132
lines changed

8 files changed

+442
-132
lines changed

src/Components/Authorization/test/AuthorizeRouteViewTest.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ public AuthorizeRouteViewTest()
3535

3636
var services = serviceCollection.BuildServiceProvider();
3737
_renderer = new TestRenderer(services);
38-
var componentFactory = new ComponentFactory(new DefaultComponentActivator(services), _renderer);
38+
var componentFactory = new ComponentFactory(new DefaultComponentActivator(services), new DefaultComponentPropertyActivator(), _renderer);
3939
_authorizeRouteViewComponent = (AuthorizeRouteView)componentFactory.InstantiateComponent(services, typeof(AuthorizeRouteView), null, null);
4040
_authorizeRouteViewComponentId = _renderer.AssignRootComponentId(_authorizeRouteViewComponent);
4141
}

src/Components/Components/src/ComponentFactory.cs

Lines changed: 17 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,7 @@
55
using System.Diagnostics.CodeAnalysis;
66
using System.Reflection;
77
using Microsoft.AspNetCore.Components.HotReload;
8-
using Microsoft.AspNetCore.Components.Reflection;
98
using Microsoft.AspNetCore.Components.RenderTree;
10-
using Microsoft.Extensions.DependencyInjection;
119
using static Microsoft.AspNetCore.Internal.LinkerFlags;
1210

1311
namespace Microsoft.AspNetCore.Components;
@@ -19,10 +17,7 @@ internal sealed class ComponentFactory
1917
AppContext.TryGetSwitch("Microsoft.AspNetCore.Components.Unsupported.DisablePropertyInjection", out var isDisabled) &&
2018
isDisabled;
2119

22-
private const BindingFlags _injectablePropertyBindingFlags
23-
= BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
24-
25-
private static readonly ConcurrentDictionary<Type, ComponentTypeInfoCacheEntry> _cachedComponentTypeInfo = new();
20+
private static readonly ConcurrentDictionary<Type, IComponentRenderMode?> _cachedComponentTypeRenderModes = new();
2621

2722
static ComponentFactory()
2823
{
@@ -33,36 +28,35 @@ static ComponentFactory()
3328
}
3429

3530
private readonly IComponentActivator _componentActivator;
31+
private readonly IComponentPropertyActivator _propertyActivator;
3632
private readonly Renderer _renderer;
3733

38-
public ComponentFactory(IComponentActivator componentActivator, Renderer renderer)
34+
public ComponentFactory(IComponentActivator componentActivator, IComponentPropertyActivator propertyActivator, Renderer renderer)
3935
{
4036
_componentActivator = componentActivator ?? throw new ArgumentNullException(nameof(componentActivator));
37+
_propertyActivator = propertyActivator ?? throw new ArgumentNullException(nameof(propertyActivator));
4138
_renderer = renderer ?? throw new ArgumentNullException(nameof(renderer));
4239
}
4340

44-
public static void ClearCache() => _cachedComponentTypeInfo.Clear();
41+
public static void ClearCache() => _cachedComponentTypeRenderModes.Clear();
4542

46-
private static ComponentTypeInfoCacheEntry GetComponentTypeInfo([DynamicallyAccessedMembers(Component)] Type componentType)
43+
private static IComponentRenderMode? GetComponentTypeRenderMode([DynamicallyAccessedMembers(Component)] Type componentType)
4744
{
4845
// Unfortunately we can't use 'GetOrAdd' here because the DynamicallyAccessedMembers annotation doesn't flow through to the
4946
// callback, so it becomes an IL2111 warning. The following is equivalent and thread-safe because it's a ConcurrentDictionary
5047
// and it doesn't matter if we build a cache entry more than once.
51-
if (!_cachedComponentTypeInfo.TryGetValue(componentType, out var cacheEntry))
48+
if (!_cachedComponentTypeRenderModes.TryGetValue(componentType, out var renderMode))
5249
{
53-
var componentTypeRenderMode = componentType.GetCustomAttribute<RenderModeAttribute>()?.Mode;
54-
cacheEntry = new ComponentTypeInfoCacheEntry(
55-
componentTypeRenderMode,
56-
CreatePropertyInjector(componentType));
57-
_cachedComponentTypeInfo.TryAdd(componentType, cacheEntry);
50+
renderMode = componentType.GetCustomAttribute<RenderModeAttribute>()?.Mode;
51+
_cachedComponentTypeRenderModes.TryAdd(componentType, renderMode);
5852
}
5953

60-
return cacheEntry;
54+
return renderMode;
6155
}
6256

6357
public IComponent InstantiateComponent(IServiceProvider serviceProvider, [DynamicallyAccessedMembers(Component)] Type componentType, IComponentRenderMode? callerSpecifiedRenderMode, int? parentComponentId)
6458
{
65-
var (componentTypeRenderMode, propertyInjector) = GetComponentTypeInfo(componentType);
59+
var componentTypeRenderMode = GetComponentTypeRenderMode(componentType);
6660
IComponent component;
6761

6862
if (componentTypeRenderMode is null && callerSpecifiedRenderMode is null)
@@ -89,111 +83,19 @@ public IComponent InstantiateComponent(IServiceProvider serviceProvider, [Dynami
8983

9084
if (!_propertyInjectionDisabled)
9185
{
92-
if (component.GetType() == componentType)
93-
{
94-
// Fast, common case: use the cached data we already looked up
95-
propertyInjector(serviceProvider, component);
96-
}
97-
else
98-
{
99-
// Uncommon case where the activator/resolver returned a different type. Needs an extra cache lookup.
100-
PerformPropertyInjection(serviceProvider, component);
101-
}
86+
PerformPropertyInjection(serviceProvider, component);
10287
}
10388

10489
return component;
10590
}
10691

107-
private static void PerformPropertyInjection(IServiceProvider serviceProvider, IComponent instance)
92+
private void PerformPropertyInjection(IServiceProvider serviceProvider, IComponent instance)
10893
{
10994
// Suppressed with "pragma warning disable" so ILLink Roslyn Anayzer doesn't report the warning.
110-
#pragma warning disable IL2072 // 'componentType' argument does not satisfy 'DynamicallyAccessedMemberTypes.All' in call to 'Microsoft.AspNetCore.Components.ComponentFactory.GetComponentTypeInfo(Type)'.
111-
var componentTypeInfo = GetComponentTypeInfo(instance.GetType());
112-
#pragma warning restore IL2072 // 'componentType' argument does not satisfy 'DynamicallyAccessedMemberTypes.All' in call to 'Microsoft.AspNetCore.Components.ComponentFactory.GetComponentTypeInfo(Type)'.
113-
114-
componentTypeInfo.PerformPropertyInjection(serviceProvider, instance);
115-
}
116-
117-
private static Action<IServiceProvider, IComponent> CreatePropertyInjector([DynamicallyAccessedMembers(Component)] Type type)
118-
{
119-
// Do all the reflection up front
120-
List<(string name, Type propertyType, PropertySetter setter, object? serviceKey)>? injectables = null;
121-
foreach (var property in MemberAssignment.GetPropertiesIncludingInherited(type, _injectablePropertyBindingFlags))
122-
{
123-
var injectAttribute = property.GetCustomAttribute<InjectAttribute>();
124-
if (injectAttribute is null)
125-
{
126-
continue;
127-
}
128-
129-
injectables ??= new();
130-
injectables.Add((property.Name, property.PropertyType, new PropertySetter(type, property), injectAttribute.Key));
131-
}
132-
133-
if (injectables is null)
134-
{
135-
return static (_, _) => { };
136-
}
137-
138-
return Initialize;
95+
#pragma warning disable IL2072 // 'componentType' argument does not satisfy 'DynamicallyAccessedMemberTypes.All' in call to 'IComponentPropertyActivator.GetActivator(Type)'.
96+
var propertyActivator = _propertyActivator.GetActivator(instance.GetType());
97+
#pragma warning restore IL2072
13998

140-
// Return an action whose closure can write all the injected properties
141-
// without any further reflection calls (just typecasts)
142-
void Initialize(IServiceProvider serviceProvider, IComponent component)
143-
{
144-
foreach (var (propertyName, propertyType, setter, serviceKey) in injectables)
145-
{
146-
object? serviceInstance;
147-
148-
if (serviceKey is not null)
149-
{
150-
if (serviceProvider is not IKeyedServiceProvider keyedServiceProvider)
151-
{
152-
throw new InvalidOperationException($"Cannot provide a value for property " +
153-
$"'{propertyName}' on type '{type.FullName}'. The service provider " +
154-
$"does not implement '{nameof(IKeyedServiceProvider)}' and therefore " +
155-
$"cannot provide keyed services.");
156-
}
157-
158-
serviceInstance = keyedServiceProvider.GetKeyedService(propertyType, serviceKey)
159-
?? throw new InvalidOperationException($"Cannot provide a value for property " +
160-
$"'{propertyName}' on type '{type.FullName}'. There is no " +
161-
$"registered keyed service of type '{propertyType}' with key '{serviceKey}'.");
162-
}
163-
else
164-
{
165-
serviceInstance = serviceProvider.GetService(propertyType)
166-
?? throw new InvalidOperationException($"Cannot provide a value for property " +
167-
$"'{propertyName}' on type '{type.FullName}'. There is no " +
168-
$"registered service of type '{propertyType}'.");
169-
}
170-
171-
setter.SetValue(component, serviceInstance);
172-
}
173-
}
174-
}
175-
176-
// Tracks information about a specific component type that ComponentFactory uses
177-
private sealed class ComponentTypeInfoCacheEntry
178-
{
179-
public IComponentRenderMode? ComponentTypeRenderMode { get; }
180-
181-
public Action<IServiceProvider, IComponent> PerformPropertyInjection { get; }
182-
183-
public ComponentTypeInfoCacheEntry(
184-
IComponentRenderMode? componentTypeRenderMode,
185-
Action<IServiceProvider, IComponent> performPropertyInjection)
186-
{
187-
ComponentTypeRenderMode = componentTypeRenderMode;
188-
PerformPropertyInjection = performPropertyInjection;
189-
}
190-
191-
public void Deconstruct(
192-
out IComponentRenderMode? componentTypeRenderMode,
193-
out Action<IServiceProvider, IComponent> performPropertyInjection)
194-
{
195-
componentTypeRenderMode = ComponentTypeRenderMode;
196-
performPropertyInjection = PerformPropertyInjection;
197-
}
99+
propertyActivator(serviceProvider, instance);
198100
}
199101
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Collections.Concurrent;
5+
using System.Diagnostics.CodeAnalysis;
6+
using System.Reflection;
7+
using Microsoft.AspNetCore.Components.HotReload;
8+
using Microsoft.AspNetCore.Components.Reflection;
9+
using Microsoft.Extensions.DependencyInjection;
10+
using static Microsoft.AspNetCore.Internal.LinkerFlags;
11+
12+
namespace Microsoft.AspNetCore.Components;
13+
14+
internal sealed class DefaultComponentPropertyActivator : IComponentPropertyActivator
15+
{
16+
private const BindingFlags InjectablePropertyBindingFlags
17+
= BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
18+
19+
private static readonly ConcurrentDictionary<Type, Action<IServiceProvider, IComponent>> _cachedPropertyActivators = new();
20+
21+
static DefaultComponentPropertyActivator()
22+
{
23+
if (HotReloadManager.Default.MetadataUpdateSupported)
24+
{
25+
HotReloadManager.Default.OnDeltaApplied += ClearCache;
26+
}
27+
}
28+
29+
public static void ClearCache() => _cachedPropertyActivators.Clear();
30+
31+
/// <inheritdoc />
32+
public Action<IServiceProvider, IComponent> GetActivator(
33+
[DynamicallyAccessedMembers(Component)] Type componentType)
34+
{
35+
// Unfortunately we can't use 'GetOrAdd' here because the DynamicallyAccessedMembers annotation doesn't flow through to the
36+
// callback, so it becomes an IL2111 warning. The following is equivalent and thread-safe because it's a ConcurrentDictionary
37+
// and it doesn't matter if we build a cache entry more than once.
38+
if (!_cachedPropertyActivators.TryGetValue(componentType, out var activator))
39+
{
40+
activator = CreatePropertyActivator(componentType);
41+
_cachedPropertyActivators.TryAdd(componentType, activator);
42+
}
43+
44+
return activator;
45+
}
46+
47+
private static Action<IServiceProvider, IComponent> CreatePropertyActivator(
48+
[DynamicallyAccessedMembers(Component)] Type type)
49+
{
50+
// Do all the reflection up front
51+
List<(string name, Type propertyType, PropertySetter setter, object? serviceKey)>? injectables = null;
52+
foreach (var property in MemberAssignment.GetPropertiesIncludingInherited(type, InjectablePropertyBindingFlags))
53+
{
54+
var injectAttribute = property.GetCustomAttribute<InjectAttribute>();
55+
if (injectAttribute is null)
56+
{
57+
continue;
58+
}
59+
60+
injectables ??= new();
61+
injectables.Add((property.Name, property.PropertyType, new PropertySetter(type, property), injectAttribute.Key));
62+
}
63+
64+
if (injectables is null)
65+
{
66+
return static (_, _) => { };
67+
}
68+
69+
return Initialize;
70+
71+
// Return an action whose closure can write all the injected properties
72+
// without any further reflection calls (just typecasts)
73+
void Initialize(IServiceProvider serviceProvider, IComponent component)
74+
{
75+
foreach (var (propertyName, propertyType, setter, serviceKey) in injectables)
76+
{
77+
object? serviceInstance;
78+
79+
if (serviceKey is not null)
80+
{
81+
if (serviceProvider is not IKeyedServiceProvider keyedServiceProvider)
82+
{
83+
throw new InvalidOperationException($"Cannot provide a value for property " +
84+
$"'{propertyName}' on type '{type.FullName}'. The service provider " +
85+
$"does not implement '{nameof(IKeyedServiceProvider)}' and therefore " +
86+
$"cannot provide keyed services.");
87+
}
88+
89+
serviceInstance = keyedServiceProvider.GetKeyedService(propertyType, serviceKey)
90+
?? throw new InvalidOperationException($"Cannot provide a value for property " +
91+
$"'{propertyName}' on type '{type.FullName}'. There is no " +
92+
$"registered keyed service of type '{propertyType}' with key '{serviceKey}'.");
93+
}
94+
else
95+
{
96+
serviceInstance = serviceProvider.GetService(propertyType)
97+
?? throw new InvalidOperationException($"Cannot provide a value for property " +
98+
$"'{propertyName}' on type '{type.FullName}'. There is no " +
99+
$"registered service of type '{propertyType}'.");
100+
}
101+
102+
setter.SetValue(component, serviceInstance);
103+
}
104+
}
105+
}
106+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Diagnostics.CodeAnalysis;
5+
using static Microsoft.AspNetCore.Internal.LinkerFlags;
6+
7+
namespace Microsoft.AspNetCore.Components;
8+
9+
/// <summary>
10+
/// Provides a mechanism for activating properties on Blazor component instances.
11+
/// </summary>
12+
/// <remarks>
13+
/// This interface allows customization of how properties marked with <see cref="InjectAttribute"/>
14+
/// are populated on component instances. The default implementation uses the <see cref="IServiceProvider"/>
15+
/// to resolve services for injection.
16+
/// </remarks>
17+
public interface IComponentPropertyActivator
18+
{
19+
/// <summary>
20+
/// Gets a delegate that activates properties on a component of the specified type.
21+
/// </summary>
22+
/// <param name="componentType">The type of component to create an activator for.</param>
23+
/// <returns>
24+
/// A delegate that takes an <see cref="IServiceProvider"/> and an <see cref="IComponent"/>
25+
/// instance, and populates the component's injectable properties.
26+
/// </returns>
27+
Action<IServiceProvider, IComponent> GetActivator(
28+
[DynamicallyAccessedMembers(Component)] Type componentType);
29+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
#nullable enable
2+
Microsoft.AspNetCore.Components.IComponentPropertyActivator
3+
Microsoft.AspNetCore.Components.IComponentPropertyActivator.GetActivator(System.Type! componentType) -> System.Action<System.IServiceProvider!, Microsoft.AspNetCore.Components.IComponent!>!
24
*REMOVED*Microsoft.AspNetCore.Components.ResourceAsset.ResourceAsset(string! url, System.Collections.Generic.IReadOnlyList<Microsoft.AspNetCore.Components.ResourceAssetProperty!>? properties) -> void
35
*REMOVED*Microsoft.AspNetCore.Components.Routing.Router.PreferExactMatches.get -> bool
46
*REMOVED*Microsoft.AspNetCore.Components.Routing.Router.PreferExactMatches.set -> void

src/Components/Components/src/RenderTree/Renderer.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ public Renderer(IServiceProvider serviceProvider, ILoggerFactory loggerFactory,
9999
// has always taken ILoggerFactory so to avoid the per-instance string allocation of the logger name we just pass the
100100
// logger name in here as a string literal.
101101
_logger = loggerFactory.CreateLogger("Microsoft.AspNetCore.Components.RenderTree.Renderer");
102-
_componentFactory = new ComponentFactory(componentActivator, this);
102+
_componentFactory = new ComponentFactory(componentActivator, GetComponentPropertyActivatorOrDefault(serviceProvider), this);
103103
_componentsMetrics = serviceProvider.GetService<ComponentsMetrics>();
104104
_componentsActivitySource = serviceProvider.GetService<ComponentsActivitySource>();
105105
_componentsActivitySource?.Init(new ComponentsActivityLinkStore(this));
@@ -122,6 +122,12 @@ private static IComponentActivator GetComponentActivatorOrDefault(IServiceProvid
122122
?? new DefaultComponentActivator(serviceProvider);
123123
}
124124

125+
private static IComponentPropertyActivator GetComponentPropertyActivatorOrDefault(IServiceProvider serviceProvider)
126+
{
127+
return serviceProvider.GetService<IComponentPropertyActivator>()
128+
?? new DefaultComponentPropertyActivator();
129+
}
130+
125131
/// <summary>
126132
/// Gets the <see cref="Components.Dispatcher" /> associated with this <see cref="Renderer" />.
127133
/// </summary>
@@ -186,6 +192,7 @@ private async void RenderRootComponentsOnHotReload()
186192
ComponentFactory.ClearCache();
187193
ComponentProperties.ClearCache();
188194
DefaultComponentActivator.ClearCache();
195+
DefaultComponentPropertyActivator.ClearCache();
189196

190197
await Dispatcher.InvokeAsync(() =>
191198
{

0 commit comments

Comments
 (0)