Skip to content
Open
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(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);
}
Expand Down
124 changes: 18 additions & 106 deletions src/Components/Components/src/ComponentFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<Type, ComponentTypeInfoCacheEntry> _cachedComponentTypeInfo = new();
private static readonly ConcurrentDictionary<Type, IComponentRenderMode?> _cachedComponentTypeRenderModes = new();

static ComponentFactory()
{
Expand All @@ -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<RenderModeAttribute>()?.Mode;
cacheEntry = new ComponentTypeInfoCacheEntry(
componentTypeRenderMode,
CreatePropertyInjector(componentType));
_cachedComponentTypeInfo.TryAdd(componentType, cacheEntry);
renderMode = componentType.GetCustomAttribute<RenderModeAttribute>()?.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)
Expand Down Expand Up @@ -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
{
Expand All @@ -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<IServiceProvider, IComponent> 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<InjectAttribute>();
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<IServiceProvider, IComponent> PerformPropertyInjection { get; }

public ComponentTypeInfoCacheEntry(
IComponentRenderMode? componentTypeRenderMode,
Action<IServiceProvider, IComponent> performPropertyInjection)
{
ComponentTypeRenderMode = componentTypeRenderMode;
PerformPropertyInjection = performPropertyInjection;
}

public void Deconstruct(
out IComponentRenderMode? componentTypeRenderMode,
out Action<IServiceProvider, IComponent> performPropertyInjection)
{
componentTypeRenderMode = ComponentTypeRenderMode;
performPropertyInjection = PerformPropertyInjection;
}
propertyActivator(serviceProvider, instance);
}
}
106 changes: 106 additions & 0 deletions src/Components/Components/src/DefaultComponentPropertyActivator.cs
Original file line number Diff line number Diff line change
@@ -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<Type, Action<IServiceProvider, IComponent>> _cachedPropertyActivators = new();

static DefaultComponentPropertyActivator()
{
if (HotReloadManager.Default.MetadataUpdateSupported)
{
HotReloadManager.Default.OnDeltaApplied += ClearCache;
}
}

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

/// <inheritdoc />
public Action<IServiceProvider, IComponent> 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<IServiceProvider, IComponent> 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<InjectAttribute>();
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);
}
}
}
}
29 changes: 29 additions & 0 deletions src/Components/Components/src/IComponentPropertyActivator.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Provides a mechanism for activating properties on Blazor component instances.
/// </summary>
/// <remarks>
/// This interface allows customization of how properties marked with <see cref="InjectAttribute"/>
/// are populated on component instances. The default implementation uses the <see cref="IServiceProvider"/>
/// to resolve services for injection.
/// </remarks>
public interface IComponentPropertyActivator
{
/// <summary>
/// Gets a delegate that activates properties on a component of the specified type.
/// </summary>
/// <param name="componentType">The type of component to create an activator for.</param>
/// <returns>
/// A delegate that takes an <see cref="IServiceProvider"/> and an <see cref="IComponent"/>
/// instance, and populates the component's injectable properties.
/// </returns>
Action<IServiceProvider, IComponent> GetActivator(
[DynamicallyAccessedMembers(Component)] Type componentType);
}
2 changes: 2 additions & 0 deletions src/Components/Components/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
#nullable enable
Microsoft.AspNetCore.Components.IComponentPropertyActivator
Microsoft.AspNetCore.Components.IComponentPropertyActivator.GetActivator(System.Type! componentType) -> System.Action<System.IServiceProvider!, Microsoft.AspNetCore.Components.IComponent!>!
9 changes: 8 additions & 1 deletion src/Components/Components/src/RenderTree/Renderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ComponentsMetrics>();
_componentsActivitySource = serviceProvider.GetService<ComponentsActivitySource>();
_componentsActivitySource?.Init(new ComponentsActivityLinkStore(this));
Expand All @@ -122,6 +122,12 @@ private static IComponentActivator GetComponentActivatorOrDefault(IServiceProvid
?? new DefaultComponentActivator(serviceProvider);
}

private static IComponentPropertyActivator GetComponentPropertyActivatorOrDefault(IServiceProvider serviceProvider)
{
return serviceProvider.GetService<IComponentPropertyActivator>()
?? new DefaultComponentPropertyActivator();
}

/// <summary>
/// Gets the <see cref="Components.Dispatcher" /> associated with this <see cref="Renderer" />.
/// </summary>
Expand Down Expand Up @@ -186,6 +192,7 @@ private async void RenderRootComponentsOnHotReload()
ComponentFactory.ClearCache();
ComponentProperties.ClearCache();
DefaultComponentActivator.ClearCache();
DefaultComponentPropertyActivator.ClearCache();

await Dispatcher.InvokeAsync(() =>
{
Expand Down
Loading
Loading