From e524994734c6aab69d45d7f41da4b4689a2d39e0 Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Thu, 22 Feb 2018 18:45:14 +0000 Subject: [PATCH] Make ComponentFactory do all the reflection up-front and cache the resulting delegates --- .../Components/ComponentFactory.cs | 67 ++++++++++++++++--- .../DependencyInjectionTest.cs | 17 ++++- 2 files changed, 73 insertions(+), 11 deletions(-) diff --git a/src/Microsoft.AspNetCore.Blazor/Components/ComponentFactory.cs b/src/Microsoft.AspNetCore.Blazor/Components/ComponentFactory.cs index 1302bd1c34..76be3fdadd 100644 --- a/src/Microsoft.AspNetCore.Blazor/Components/ComponentFactory.cs +++ b/src/Microsoft.AspNetCore.Blazor/Components/ComponentFactory.cs @@ -2,6 +2,8 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Collections.Generic; +using System.Linq; using System.Reflection; namespace Microsoft.AspNetCore.Blazor.Components @@ -11,6 +13,8 @@ namespace Microsoft.AspNetCore.Blazor.Components private readonly IServiceProvider _serviceProvider; private readonly static BindingFlags _injectablePropertyBindingFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; + private readonly IDictionary> _cachedInitializers + = new Dictionary>(); public ComponentFactory(IServiceProvider serviceProvider) { @@ -33,25 +37,68 @@ namespace Microsoft.AspNetCore.Blazor.Components private void PerformPropertyInjection(IComponent instance) { - // TODO: Cache delegates, etc - var type = instance.GetType(); - var properties = type.GetTypeInfo().GetProperties(_injectablePropertyBindingFlags); - foreach (var property in properties) + var instanceType = instance.GetType(); + if (!_cachedInitializers.TryGetValue(instanceType, out var initializer)) { - var injectAttribute = property.GetCustomAttribute(); - if (injectAttribute != null) + initializer = CreateInitializer(instanceType); + _cachedInitializers[instanceType] = initializer; + } + + initializer(instance); + } + + private Action CreateInitializer(Type type) + { + // Do all the reflection up front + var injectableProperties = type.GetTypeInfo() + .GetProperties(_injectablePropertyBindingFlags) + .Where(p => p.GetCustomAttribute() != null) + .Where(p => p.SetMethod != null); + var injectables = injectableProperties.Select(property => + ( + propertyName: property.Name, + propertyType: property.PropertyType, + setter: (IPropertySetter)Activator.CreateInstance( + typeof(PropertySetter<,>).MakeGenericType(type, property.PropertyType), + property.SetMethod) + )).ToArray(); + + // Return an action whose closure can write all the injected properties + // without any further reflection calls (just typecasts) + return instance => + { + foreach (var injectable in injectables) { - var serviceInstance = _serviceProvider.GetService(property.PropertyType); + var serviceInstance = _serviceProvider.GetService(injectable.propertyType); if (serviceInstance == null) { throw new InvalidOperationException($"Cannot provide value for property " + - $"'{property.Name}' on type '{type.FullName}'. There is no registered " + - $"service of type '{property.PropertyType.FullName}'."); + $"'{injectable.propertyName}' on type '{type.FullName}'. There is no " + + $"registered service of type '{injectable.propertyType}'."); } - property.SetValue(instance, serviceInstance); + injectable.setter.SetValue(instance, serviceInstance); } + }; + } + + private interface IPropertySetter + { + void SetValue(object target, object value); + } + + private class PropertySetter : IPropertySetter + { + private readonly Action _setterDelegate; + + public PropertySetter(MethodInfo setMethod) + { + _setterDelegate = (Action)Delegate.CreateDelegate( + typeof(Action), setMethod); } + + public void SetValue(object target, object value) + => _setterDelegate((TTarget)target, (TValue)value); } } } diff --git a/test/Microsoft.AspNetCore.Blazor.Test/DependencyInjectionTest.cs b/test/Microsoft.AspNetCore.Blazor.Test/DependencyInjectionTest.cs index 99b19aae4f..957a3b37f0 100644 --- a/test/Microsoft.AspNetCore.Blazor.Test/DependencyInjectionTest.cs +++ b/test/Microsoft.AspNetCore.Blazor.Test/DependencyInjectionTest.cs @@ -41,6 +41,16 @@ namespace Microsoft.AspNetCore.Blazor.Test Assert.Null(HasStaticProperties.StaticPropertyWithoutInject); } + [Fact] + public void IgnoresGetOnlyProperties() + { + // Arrange/Act + var component = InstantiateComponent(); + + // Assert + Assert.Null(component.MyService); + } + [Fact] public void ThrowsIfNoSuchServiceIsRegistered() { @@ -119,7 +129,12 @@ namespace Microsoft.AspNetCore.Blazor.Test [Inject] public static IMyService StaticPropertyWithInject { get; set; } public static IMyService StaticPropertyWithoutInject { get; set; } } - + + class HasGetOnlyProperty : TestComponent + { + [Inject] public IMyService MyService { get; } + } + class HasInjectableProperty : TestComponent { [Inject] public IMyService MyService { get; set; }