File: DependencyInjectionTest.cs
Web Access
Project: src\src\Components\Components\test\Microsoft.AspNetCore.Components.Tests.csproj (Microsoft.AspNetCore.Components.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using Microsoft.AspNetCore.Components.Test.Helpers;
 
namespace Microsoft.AspNetCore.Components.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<HasPropertiesWithoutInjectAttribute>();
 
        // Assert
        Assert.Null(component.SomeProperty);
        Assert.Null(component.PrivatePropertyValue);
    }
 
    [Fact]
    public void IgnoresStaticProperties()
    {
        // Arrange/Act
        var component = InstantiateComponent<HasStaticProperties>();
 
        // Assert
        Assert.Null(HasStaticProperties.StaticPropertyWithInject);
        Assert.Null(HasStaticProperties.StaticPropertyWithoutInject);
    }
 
    [Fact]
    public void ThrowsForInjectablePropertiesWithoutSetter()
    {
        var ex = Assert.Throws<InvalidOperationException>(() =>
        {
            InstantiateComponent<HasGetOnlyPropertyWithInject>();
        });
 
        Assert.Equal($"Cannot provide a value for property '{nameof(HasInjectableProperty.MyService)}' " +
            $"on type '{typeof(HasGetOnlyPropertyWithInject).FullName}' because the property " +
            $"has no setter.", ex.Message);
    }
 
    [Fact]
    public void ThrowsIfNoSuchServiceIsRegistered()
    {
        var ex = Assert.Throws<InvalidOperationException>(() =>
        {
            InstantiateComponent<HasInjectableProperty>();
        });
 
        Assert.Equal($"Cannot provide a 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 ThrowsIfNoSuchKeyedServiceIsRegistered()
    {
        var serviceInstance = new MyServiceImplementation();
        _serviceProvider.AddKeyedService<IMyService>(serviceInstance, "mismatched-key");
 
        var ex = Assert.Throws<InvalidOperationException>(() =>
        {
            InstantiateComponent<HasKeyedInjectableProperty>();
        });
 
        Assert.Equal($"Cannot provide a value for property '{nameof(HasKeyedInjectableProperty.MyService)}' " +
            $"on type '{typeof(HasKeyedInjectableProperty).FullName}'. There is no registered keyed service " +
            $"of type '{typeof(IMyService).FullName}' with key '{HasKeyedInjectableProperty.ServiceKey}'.", ex.Message);
    }
 
    [Fact]
    public void SetsInjectablePropertyValueIfServiceIsRegistered()
    {
        // Arrange
        var serviceInstance = new MyServiceImplementation();
        _serviceProvider.AddService<IMyService>(serviceInstance);
 
        // Act
        var instance = InstantiateComponent<HasInjectableProperty>();
 
        // Assert
        Assert.Same(serviceInstance, instance.MyService);
    }
 
    [Fact]
    public void SetsKeyedInjectablePropertyValueIfKeyedServiceIsRegistered()
    {
        // Arrange
        var serviceInstance = new MyServiceImplementation();
        _serviceProvider.AddKeyedService<IMyService>(serviceInstance, HasKeyedInjectableProperty.ServiceKey);
 
        // Act
        var instance = InstantiateComponent<HasKeyedInjectableProperty>();
 
        // 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<IMyService>(serviceInstance);
        _serviceProvider.AddService<IMyOtherService>(otherServiceInstance);
        _serviceProvider.AddService(concreteServiceInstance);
        _serviceProvider.AddKeyedService<IMyService>(serviceInstance, HasManyInjectableProperties.ServiceKey);
 
        // Act
        var instance = InstantiateComponent<HasManyInjectableProperties>();
 
        // 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);
        Assert.Same(serviceInstance, instance.KeyedService);
    }
 
    [Fact]
    public void SetsInheritedInjectableProperties()
    {
        // Arrange
        var serviceInstance = new MyServiceImplementation();
        _serviceProvider.AddService<IMyService>(serviceInstance);
 
        // Act
        var instance = InstantiateComponent<HasInheritedInjectedProperty>();
 
        // Assert
        Assert.Same(serviceInstance, instance.MyService);
    }
 
    [Fact]
    public void SetsPrivateInheritedInjectableProperties()
    {
        // Arrange
        var serviceInstance = new MyServiceImplementation();
        _serviceProvider.AddService<IMyService>(serviceInstance);
 
        // Act
        var instance = InstantiateComponent<HasInheritedPrivateInjectableProperty>();
 
        // Assert
        Assert.Same(serviceInstance, instance.PrivateMyService);
    }
 
    private T InstantiateComponent<T>() where T : IComponent
        => (T)_renderer.InstantiateComponent<T>();
 
    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 HasGetOnlyPropertyWithInject : TestComponent
    {
        [Inject] public IMyService MyService { get; }
    }
 
    class HasInjectableProperty : TestComponent
    {
        [Inject] public IMyService MyService { get; set; }
    }
 
    class HasKeyedInjectableProperty : TestComponent
    {
        public const string ServiceKey = "my-service";
 
        [Inject(Key = ServiceKey)] public IMyService MyService { get; set; }
    }
 
    class HasPrivateInjectableProperty : TestComponent
    {
        [Inject] private IMyService MyService { get; set; }
 
        public IMyService PrivateMyService => MyService;
    }
 
    class HasInheritedPrivateInjectableProperty : HasPrivateInjectableProperty { }
 
    class HasManyInjectableProperties : TestComponent
    {
        public const string ServiceKey = "my-service";
 
        [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; }
        [Inject(Key = ServiceKey)] public IMyService KeyedService { 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 Attach(RenderHandle renderHandle)
            => throw new NotImplementedException();
 
        public Task SetParametersAsync(ParameterView parameters)
            => throw new NotImplementedException();
    }
}