From ceacd489aa254c4fdbf58bb7512058f597a03530 Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Thu, 22 Feb 2018 17:50:28 +0000 Subject: [PATCH] Simplistic implementation of property injection --- .../Components/ComponentFactory.cs | 23 ++- .../Components/InjectAttribute.cs | 16 ++ .../DependencyInjectionTest.cs | 162 ++++++++++++++++++ test/shared/TestRenderer.cs | 10 +- test/shared/TestServiceProvider.cs | 12 +- 5 files changed, 220 insertions(+), 3 deletions(-) create mode 100644 src/Microsoft.AspNetCore.Blazor/Components/InjectAttribute.cs create mode 100644 test/Microsoft.AspNetCore.Blazor.Test/DependencyInjectionTest.cs diff --git a/src/Microsoft.AspNetCore.Blazor/Components/ComponentFactory.cs b/src/Microsoft.AspNetCore.Blazor/Components/ComponentFactory.cs index 780a510b9e..1302bd1c34 100644 --- a/src/Microsoft.AspNetCore.Blazor/Components/ComponentFactory.cs +++ b/src/Microsoft.AspNetCore.Blazor/Components/ComponentFactory.cs @@ -2,12 +2,15 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Reflection; namespace Microsoft.AspNetCore.Blazor.Components { internal class ComponentFactory { private readonly IServiceProvider _serviceProvider; + private readonly static BindingFlags _injectablePropertyBindingFlags + = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; public ComponentFactory(IServiceProvider serviceProvider) { @@ -30,7 +33,25 @@ namespace Microsoft.AspNetCore.Blazor.Components private void PerformPropertyInjection(IComponent instance) { - // TODO + // TODO: Cache delegates, etc + var type = instance.GetType(); + var properties = type.GetTypeInfo().GetProperties(_injectablePropertyBindingFlags); + foreach (var property in properties) + { + var injectAttribute = property.GetCustomAttribute(); + if (injectAttribute != null) + { + var serviceInstance = _serviceProvider.GetService(property.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}'."); + } + + property.SetValue(instance, serviceInstance); + } + } } } } diff --git a/src/Microsoft.AspNetCore.Blazor/Components/InjectAttribute.cs b/src/Microsoft.AspNetCore.Blazor/Components/InjectAttribute.cs new file mode 100644 index 0000000000..1fd08ae43f --- /dev/null +++ b/src/Microsoft.AspNetCore.Blazor/Components/InjectAttribute.cs @@ -0,0 +1,16 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.AspNetCore.Blazor.Components +{ + /// + /// Indicates that the associated property should have a value injected from the + /// service provider during initialization. + /// + [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] + public class InjectAttribute : Attribute + { + } +} diff --git a/test/Microsoft.AspNetCore.Blazor.Test/DependencyInjectionTest.cs b/test/Microsoft.AspNetCore.Blazor.Test/DependencyInjectionTest.cs new file mode 100644 index 0000000000..99b19aae4f --- /dev/null +++ b/test/Microsoft.AspNetCore.Blazor.Test/DependencyInjectionTest.cs @@ -0,0 +1,162 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Blazor.Components; +using Microsoft.AspNetCore.Blazor.Test.Helpers; +using System; +using Xunit; + +namespace Microsoft.AspNetCore.Blazor.Test +{ + public class DependencyInjectionTest + { + private readonly TestRenderer _renderer; + private readonly TestServiceProvider _serviceProvider; + + public DependencyInjectionTest() + { + _serviceProvider = new TestServiceProvider(); + _renderer = new TestRenderer(_serviceProvider); + } + + [Fact] + public void IgnoresPropertiesWithoutInjectAttribute() + { + // Arrange/Act + var component = InstantiateComponent(); + + // Assert + Assert.Null(component.SomeProperty); + Assert.Null(component.PrivatePropertyValue); + } + + [Fact] + public void IgnoresStaticProperties() + { + // Arrange/Act + var component = InstantiateComponent(); + + // Assert + Assert.Null(HasStaticProperties.StaticPropertyWithInject); + Assert.Null(HasStaticProperties.StaticPropertyWithoutInject); + } + + [Fact] + public void ThrowsIfNoSuchServiceIsRegistered() + { + var ex = Assert.Throws(() => + { + InstantiateComponent(); + }); + + Assert.Equal($"Cannot provide value for property '{nameof(HasInjectableProperty.MyService)}' " + + $"on type '{typeof(HasInjectableProperty).FullName}'. There is no registered service " + + $"of type '{typeof(IMyService).FullName}'.", ex.Message); + } + + [Fact] + public void SetsInjectablePropertyValueIfServiceIsRegistered() + { + // Arrange + var serviceInstance = new MyServiceImplementation(); + _serviceProvider.AddService(serviceInstance); + + // Act + var instance = InstantiateComponent(); + + // Assert + Assert.Same(serviceInstance, instance.MyService); + } + + [Fact] + public void HandlesInjectablePropertyScenarios() + { + // Arrange + var serviceInstance = new MyServiceImplementation(); + var otherServiceInstance = new MyOtherServiceImplementation(); + var concreteServiceInstance = new MyConcreteService(); + _serviceProvider.AddService(serviceInstance); + _serviceProvider.AddService(otherServiceInstance); + _serviceProvider.AddService(concreteServiceInstance); + + // Act + var instance = InstantiateComponent(); + + // Assert + Assert.Same(serviceInstance, instance.PublicReadWrite); + Assert.Same(serviceInstance, instance.PublicReadOnly); + Assert.Same(serviceInstance, instance.PrivateValue); + Assert.Same(otherServiceInstance, instance.DifferentServiceType); + Assert.Same(concreteServiceInstance, instance.ConcreteServiceType); + } + + [Fact] + public void SetsInheritedInjectableProperties() + { + // Arrange + var serviceInstance = new MyServiceImplementation(); + _serviceProvider.AddService(serviceInstance); + + // Act + var instance = InstantiateComponent(); + + // Assert + Assert.Same(serviceInstance, instance.MyService); + } + + private T InstantiateComponent() where T: IComponent + => _renderer.InstantiateComponent(); + + class HasPropertiesWithoutInjectAttribute : TestComponent + { + public IMyService SomeProperty { get; set; } + public IMyService PrivatePropertyValue => PrivateProperty; + private IMyService PrivateProperty { get; set; } + } + + class HasStaticProperties : TestComponent + { + [Inject] public static IMyService StaticPropertyWithInject { get; set; } + public static IMyService StaticPropertyWithoutInject { get; set; } + } + + class HasInjectableProperty : TestComponent + { + [Inject] public IMyService MyService { get; set; } + } + + class HasManyInjectableProperties : TestComponent + { + [Inject] public IMyService PublicReadWrite { get; set; } + [Inject] public IMyService PublicReadOnly { get; private set; } + [Inject] private IMyService Private { get; set; } + [Inject] public IMyOtherService DifferentServiceType { get; set; } + [Inject] public MyConcreteService ConcreteServiceType { get; set; } + + public IMyService PrivateValue => Private; + } + + class HasInheritedInjectedProperty : HasInjectableProperty { } + + interface IMyService { } + interface IMyOtherService { } + + class MyServiceImplementation : IMyService { } + class MyOtherServiceImplementation : IMyOtherService { } + class MyConcreteService { } + + class TestComponent : IComponent + { + // IMPORTANT: The fact that these throw demonstrates that the injection + // happens before any of the lifecycle methods. If you change these to + // not throw, then be sure also to add a test to verify that injection + // occurs before lifecycle methods. + + public void Init(RenderHandle renderHandle) + => throw new NotImplementedException(); + + public void SetParameters(ParameterCollection parameters) + => throw new NotImplementedException(); + } + } +} diff --git a/test/shared/TestRenderer.cs b/test/shared/TestRenderer.cs index 41e334d6b3..6d1bdc9ff9 100644 --- a/test/shared/TestRenderer.cs +++ b/test/shared/TestRenderer.cs @@ -1,6 +1,7 @@ // Copyright (c) .NET Foundation. All rights reserved. // 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 Microsoft.AspNetCore.Blazor.Components; @@ -11,7 +12,11 @@ namespace Microsoft.AspNetCore.Blazor.Test.Helpers { public class TestRenderer : Renderer { - public TestRenderer(): base(new TestServiceProvider()) + public TestRenderer(): this(new TestServiceProvider()) + { + } + + public TestRenderer(IServiceProvider serviceProvider) : base(serviceProvider) { } @@ -24,6 +29,9 @@ namespace Microsoft.AspNetCore.Blazor.Test.Helpers public new void DispatchEvent(int componentId, int eventHandlerId, UIEventArgs args) => base.DispatchEvent(componentId, eventHandlerId, args); + public T InstantiateComponent() where T : IComponent + => (T)InstantiateComponent(typeof(T)); + protected override void UpdateDisplay(RenderBatch renderBatch) { var capturedBatch = new CapturedBatch(); diff --git a/test/shared/TestServiceProvider.cs b/test/shared/TestServiceProvider.cs index 7903a45cf0..0e862009c3 100644 --- a/test/shared/TestServiceProvider.cs +++ b/test/shared/TestServiceProvider.cs @@ -2,11 +2,21 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Collections.Generic; namespace Microsoft.AspNetCore.Blazor.Test.Helpers { public class TestServiceProvider : IServiceProvider { - public object GetService(Type serviceType) => throw new NotImplementedException(); + private readonly Dictionary> _factories + = new Dictionary>(); + + public object GetService(Type serviceType) + => _factories.TryGetValue(serviceType, out var factory) + ? factory() + : null; + + internal void AddService(T value) + => _factories.Add(typeof(T), () => value); } }