|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Reflection;
using Microsoft.AspNetCore.Components.RenderTree;
using Microsoft.AspNetCore.Components.Test.Helpers;
using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.AspNetCore.Components;
public class ComponentFactoryTest
{
[Fact]
public void InstantiateComponent_CreatesInstance()
{
// Arrange
var componentType = typeof(EmptyComponent);
var serviceProvider = GetServiceProvider();
var factory = new ComponentFactory(new DefaultComponentActivator(serviceProvider), new DefaultComponentPropertyActivator(), new TestRenderer());
// Act
var instance = factory.InstantiateComponent(GetServiceProvider(), componentType, null, null);
// Assert
Assert.NotNull(instance);
Assert.IsType<EmptyComponent>(instance);
}
[Fact]
public void InstantiateComponent_CreatesInstance_NonComponent()
{
// Arrange
var componentType = typeof(List<string>);
var serviceProvider = GetServiceProvider();
var factory = new ComponentFactory(new DefaultComponentActivator(serviceProvider), new DefaultComponentPropertyActivator(), new TestRenderer());
// Assert
var ex = Assert.Throws<ArgumentException>(() => factory.InstantiateComponent(GetServiceProvider(), componentType, null, null));
Assert.StartsWith($"The type {componentType.FullName} does not implement {nameof(IComponent)}.", ex.Message);
}
[Fact]
public void InstantiateComponent_CreatesInstance_WithCustomActivator()
{
// Arrange
var componentType = typeof(EmptyComponent);
var factory = new ComponentFactory(new CustomComponentActivator<ComponentWithInjectProperties>(), new DefaultComponentPropertyActivator(), new TestRenderer());
// Act
var instance = factory.InstantiateComponent(GetServiceProvider(), componentType, null, null);
// Assert
Assert.NotNull(instance);
var component = Assert.IsType<ComponentWithInjectProperties>(instance); // Custom activator returns a different type
// Public, and non-public properties, and properties with non-public setters should get assigned
Assert.NotNull(component.Property1);
Assert.NotNull(component.GetProperty2());
Assert.NotNull(component.Property3);
Assert.NotNull(component.Property4);
}
[Fact]
public void InstantiateComponent_ThrowsForNullInstance()
{
// Arrange
var componentType = typeof(EmptyComponent);
var factory = new ComponentFactory(new NullResultComponentActivator(), new DefaultComponentPropertyActivator(), new TestRenderer());
// Act
var ex = Assert.Throws<InvalidOperationException>(() => factory.InstantiateComponent(GetServiceProvider(), componentType, null, null));
Assert.Equal($"The component activator returned a null value for a component of type {componentType.FullName}.", ex.Message);
}
[Fact]
public void InstantiateComponent_AssignsPropertiesWithInjectAttributeOnBaseType()
{
// Arrange
var componentType = typeof(DerivedComponent);
var factory = new ComponentFactory(new CustomComponentActivator<DerivedComponent>(), new DefaultComponentPropertyActivator(), new TestRenderer());
// Act
var instance = factory.InstantiateComponent(GetServiceProvider(), componentType, null, null);
// Assert
Assert.NotNull(instance);
var component = Assert.IsType<DerivedComponent>(instance);
Assert.NotNull(component.Property1);
Assert.NotNull(component.GetProperty2());
Assert.NotNull(component.Property3);
Assert.NotNull(component.KeyedProperty);
// Property on derived type without [Inject] should not be assigned
Assert.Null(component.Property4);
// Property on the base type with the [Inject] attribute should
Assert.NotNull(((ComponentWithInjectProperties)component).Property4);
// Property on derived type with [Inject] should be assigned
Assert.NotNull(component.Property5);
}
[Fact]
public void InstantiateComponent_IgnoresPropertiesWithoutInjectAttribute()
{
// Arrange
var componentType = typeof(ComponentWithNonInjectableProperties);
var serviceProvider = GetServiceProvider();
var factory = new ComponentFactory(new DefaultComponentActivator(serviceProvider), new DefaultComponentPropertyActivator(), new TestRenderer());
// Act
var instance = factory.InstantiateComponent(serviceProvider, componentType, null, null);
// Assert
Assert.NotNull(instance);
var component = Assert.IsType<ComponentWithNonInjectableProperties>(instance);
// Public, and non-public properties, and properties with non-public setters should get assigned
Assert.NotNull(component.Property1);
Assert.Null(component.Property2);
}
[Fact]
public void InstantiateComponent_WithNoRenderMode_DoesNotUseRenderModeResolver()
{
// Arrange
var componentType = typeof(ComponentWithInjectProperties);
var renderer = new RendererWithResolveComponentForRenderMode(
/* won't be used */ new ComponentWithRenderMode());
var serviceProvider = GetServiceProvider();
var factory = new ComponentFactory(new DefaultComponentActivator(serviceProvider), new DefaultComponentPropertyActivator(), renderer);
// Act
var instance = factory.InstantiateComponent(serviceProvider, componentType, null, null);
// Assert
Assert.IsType<ComponentWithInjectProperties>(instance);
Assert.False(renderer.ResolverWasCalled);
}
[Fact]
public void InstantiateComponent_WithRenderModeOnComponent_UsesRenderModeResolver()
{
// Arrange
var resolvedComponent = new ComponentWithInjectProperties();
var componentType = typeof(ComponentWithRenderMode);
var renderer = new RendererWithResolveComponentForRenderMode(resolvedComponent);
var serviceProvider = GetServiceProvider();
var componentActivator = new DefaultComponentActivator(serviceProvider);
var factory = new ComponentFactory(componentActivator, new DefaultComponentPropertyActivator(), renderer);
// Act
var instance = (ComponentWithInjectProperties)factory.InstantiateComponent(serviceProvider, componentType, null, 1234);
// Assert
Assert.True(renderer.ResolverWasCalled);
Assert.Same(resolvedComponent, instance);
Assert.NotNull(instance.Property1);
Assert.Equal(componentType, renderer.RequestedComponentType);
Assert.Equal(1234, renderer.SuppliedParentComponentId);
Assert.Same(componentActivator, renderer.SuppliedActivator);
Assert.IsType<TestRenderMode>(renderer.SuppliedRenderMode);
}
[Fact]
public void InstantiateComponent_WithDerivedRenderModeOnDerivedComponent_CausesAmbiguousMatchException()
{
// We could allow derived components to override the rendermode, but:
// [1] It's unclear how that would be legitimate. If the base specifies a rendermode, it's saying
// it only works in that mode. It wouldn't be safe for a derived type to change that.
// [2] If we did want to implement this, we'd need to implement our own inheritance chain walking
// to make sure we find the rendermode from the *closest* ancestor type. GetCustomAttributes
// on its own isn't documented to return the results in any specific order.
// Since issue [1] makes it unclear we'd want to support this, for now we don't.
// Arrange
var resolvedComponent = new ComponentWithInjectProperties();
var componentType = typeof(DerivedComponentWithRenderMode);
var renderer = new RendererWithResolveComponentForRenderMode(resolvedComponent);
var serviceProvider = GetServiceProvider();
var componentActivator = new DefaultComponentActivator(serviceProvider);
var factory = new ComponentFactory(componentActivator, new DefaultComponentPropertyActivator(), renderer);
// Act/Assert
Assert.Throws<AmbiguousMatchException>(
() => factory.InstantiateComponent(serviceProvider, componentType, null, 1234));
}
[Fact]
public void InstantiateComponent_WithRenderModeOnCallSite_UsesRenderModeResolver()
{
// Arrange
// Notice that the requested component type is not the same as the resolved component type. This
// is intentional and shows that component factories are allowed to substitute other component types.
var resolvedComponent = new ComponentWithInjectProperties();
var componentType = typeof(ComponentWithNonInjectableProperties);
var callSiteRenderMode = new TestRenderMode();
var renderer = new RendererWithResolveComponentForRenderMode(resolvedComponent);
var serviceProvider = GetServiceProvider();
var componentActivator = new DefaultComponentActivator(serviceProvider);
var factory = new ComponentFactory(componentActivator, new DefaultComponentPropertyActivator(), renderer);
// Act
var instance = (ComponentWithInjectProperties)factory.InstantiateComponent(serviceProvider, componentType, callSiteRenderMode, 1234);
// Assert
Assert.Same(resolvedComponent, instance);
Assert.NotNull(instance.Property1);
Assert.Equal(componentType, renderer.RequestedComponentType);
Assert.Same(componentActivator, renderer.SuppliedActivator);
Assert.Same(callSiteRenderMode, renderer.SuppliedRenderMode);
Assert.Equal(1234, renderer.SuppliedParentComponentId);
}
[Fact]
public void InstantiateComponent_WithRenderModeOnComponentAndCallSite_Throws()
{
// Arrange
var resolvedComponent = new ComponentWithInjectProperties();
var componentType = typeof(ComponentWithRenderMode);
var renderer = new RendererWithResolveComponentForRenderMode(resolvedComponent);
var serviceProvider = GetServiceProvider();
var componentActivator = new DefaultComponentActivator(serviceProvider);
var factory = new ComponentFactory(componentActivator, new DefaultComponentPropertyActivator(), renderer);
// Even though the two rendermodes are literally the same object, we don't allow specifying any nonnull
// rendermode at the callsite if there's a nonnull fixed rendermode
var callsiteRenderMode = componentType.GetCustomAttribute<RenderModeAttribute>().Mode;
// Act/Assert
var ex = Assert.Throws<InvalidOperationException>(() =>
factory.InstantiateComponent(GetServiceProvider(), componentType, callsiteRenderMode, 1234));
Assert.Equal($"The component type '{componentType}' has a fixed rendermode of '{typeof(TestRenderMode)}', so it is not valid to specify any rendermode when using this component.", ex.Message);
}
[Fact]
public void InstantiateComponent_CreatesInstance_WithTypeActivation()
{
// Arrange
var serviceProvider = GetServiceProvider();
var componentType = typeof(ComponentWithConstructorInjection);
var resolvedComponent = new ComponentWithInjectProperties();
var renderer = new RendererWithResolveComponentForRenderMode(resolvedComponent);
var defaultComponentActivator = new DefaultComponentActivator(serviceProvider);
var factory = new ComponentFactory(defaultComponentActivator, new DefaultComponentPropertyActivator(), renderer);
// Act
var instance = factory.InstantiateComponent(serviceProvider, componentType, null, null);
// Assert
Assert.NotNull(instance);
var component = Assert.IsType<ComponentWithConstructorInjection>(instance);
Assert.NotNull(component.Property1);
Assert.NotNull(component.Property2);
Assert.NotNull(component.Property3); // Property injection should still work.
}
[Fact]
public void InstantiateComponent_WithCustomPropertyActivator_UsesCustomActivator()
{
// Arrange
var componentType = typeof(ComponentWithInjectProperties);
var serviceProvider = GetServiceProvider();
var customPropertyActivator = new CustomPropertyActivator();
var factory = new ComponentFactory(
new DefaultComponentActivator(serviceProvider),
customPropertyActivator,
new TestRenderer());
// Act
var instance = factory.InstantiateComponent(serviceProvider, componentType, null, null);
// Assert
Assert.NotNull(instance);
var component = Assert.IsType<ComponentWithInjectProperties>(instance);
Assert.True(customPropertyActivator.GetActivatorCalled);
Assert.Equal(componentType, customPropertyActivator.RequestedType);
Assert.True(customPropertyActivator.ActivatorInvoked);
}
[Fact]
public void InstantiateComponent_WithCustomPropertyActivator_ReceivesCorrectServiceProvider()
{
// Arrange
var componentType = typeof(ComponentWithInjectProperties);
var serviceProvider = GetServiceProvider();
var customPropertyActivator = new CustomPropertyActivator();
var factory = new ComponentFactory(
new DefaultComponentActivator(serviceProvider),
customPropertyActivator,
new TestRenderer());
// Act
factory.InstantiateComponent(serviceProvider, componentType, null, null);
// Assert
Assert.Same(serviceProvider, customPropertyActivator.ReceivedServiceProvider);
}
[Fact]
public void InstantiateComponent_WithCustomPropertyActivator_WhenActivatorReturnsDifferentType_StillUsesCustomActivator()
{
// Arrange
// The component activator returns a different type than requested
var requestedType = typeof(EmptyComponent);
var actualType = typeof(ComponentWithInjectProperties);
var serviceProvider = GetServiceProvider();
var customPropertyActivator = new CustomPropertyActivator();
var factory = new ComponentFactory(
new CustomComponentActivator<ComponentWithInjectProperties>(),
customPropertyActivator,
new TestRenderer());
// Act
var instance = factory.InstantiateComponent(serviceProvider, requestedType, null, null);
// Assert
Assert.IsType<ComponentWithInjectProperties>(instance);
// The property activator should be called with the actual type, not the requested type
Assert.True(customPropertyActivator.GetActivatorCalled);
// Since the types differ, GetActivator is called twice (once for requested, once for actual)
// But the activator should have been invoked for the actual component type
Assert.True(customPropertyActivator.ActivatorInvoked);
}
[Fact]
public void InstantiateComponent_WhenActivatorReturnsSubtype_InjectsAllSubtypeProperties()
{
// Arrange
// Request base type but activator returns derived type
var requestedType = typeof(ComponentWithInjectProperties);
var serviceProvider = GetServiceProvider();
var factory = new ComponentFactory(
new CustomComponentActivator<DerivedComponent>(),
new DefaultComponentPropertyActivator(),
new TestRenderer());
// Act
var instance = factory.InstantiateComponent(serviceProvider, requestedType, null, null);
// Assert
var component = Assert.IsType<DerivedComponent>(instance);
// All base type properties with [Inject] should be injected
Assert.NotNull(component.Property1);
Assert.NotNull(component.GetProperty2());
Assert.NotNull(component.Property3);
Assert.NotNull(((ComponentWithInjectProperties)component).Property4);
Assert.NotNull(component.KeyedProperty);
// Derived type properties with [Inject] should also be injected
Assert.NotNull(component.Property5);
// Derived type property without [Inject] should not be injected
Assert.Null(component.Property4);
}
[Fact]
public void InstantiateComponent_WhenActivatorReturnsExactType_InjectsPropertiesIdenticallyToSubtype()
{
// Arrange
// This test validates that there's no difference in property injection behavior
// whether the activator returns the exact type or a subtype
var requestedType = typeof(DerivedComponent);
var serviceProvider = GetServiceProvider();
var factory = new ComponentFactory(
new CustomComponentActivator<DerivedComponent>(),
new DefaultComponentPropertyActivator(),
new TestRenderer());
// Act
var instance = factory.InstantiateComponent(serviceProvider, requestedType, null, null);
// Assert
var component = Assert.IsType<DerivedComponent>(instance);
// All base type properties with [Inject] should be injected
Assert.NotNull(component.Property1);
Assert.NotNull(component.GetProperty2());
Assert.NotNull(component.Property3);
Assert.NotNull(((ComponentWithInjectProperties)component).Property4);
Assert.NotNull(component.KeyedProperty);
// Derived type properties with [Inject] should also be injected
Assert.NotNull(component.Property5);
// Derived type property without [Inject] should not be injected
Assert.Null(component.Property4);
}
[Fact]
public void DefaultComponentPropertyActivator_GetActivator_ReturnsActivatorThatInjectsProperties()
{
// Arrange
var componentType = typeof(ComponentWithInjectProperties);
var serviceProvider = GetServiceProvider();
var propertyActivator = new DefaultComponentPropertyActivator();
var component = new ComponentWithInjectProperties();
// Act
var activator = propertyActivator.GetActivator(componentType);
activator(serviceProvider, component);
// Assert
Assert.NotNull(component.Property1);
Assert.NotNull(component.GetProperty2());
Assert.NotNull(component.Property3);
Assert.NotNull(component.Property4);
Assert.NotNull(component.KeyedProperty);
}
[Fact]
public void DefaultComponentPropertyActivator_GetActivator_CachesActivator()
{
// Arrange
var componentType = typeof(ComponentWithInjectProperties);
var propertyActivator = new DefaultComponentPropertyActivator();
// Act
var activator1 = propertyActivator.GetActivator(componentType);
var activator2 = propertyActivator.GetActivator(componentType);
// Assert
Assert.Same(activator1, activator2);
}
[Fact]
public void DefaultComponentPropertyActivator_GetActivator_ReturnsNoOpForComponentWithoutInjectableProperties()
{
// Arrange
var componentType = typeof(EmptyComponent);
var serviceProvider = GetServiceProvider();
var propertyActivator = new DefaultComponentPropertyActivator();
var component = new EmptyComponent();
// Act
var activator = propertyActivator.GetActivator(componentType);
// Should not throw
activator(serviceProvider, component);
// Assert - nothing to verify, just ensuring no exception
}
[Fact]
public void DefaultComponentPropertyActivator_GetActivator_ThrowsForMissingService()
{
// Arrange
var componentType = typeof(ComponentWithInjectProperties);
var serviceProvider = new ServiceCollection().BuildServiceProvider(); // Empty provider
var propertyActivator = new DefaultComponentPropertyActivator();
var component = new ComponentWithInjectProperties();
// Act
var activator = propertyActivator.GetActivator(componentType);
var ex = Assert.Throws<InvalidOperationException>(() => activator(serviceProvider, component));
// Assert
Assert.Contains("There is no registered service of type", ex.Message);
}
[Fact]
public void DefaultComponentPropertyActivator_GetActivator_ThrowsForMissingKeyedService()
{
// Arrange
var componentType = typeof(ComponentWithOnlyKeyedProperty);
var serviceProvider = new ServiceCollection().BuildServiceProvider(); // Empty provider
var propertyActivator = new DefaultComponentPropertyActivator();
var component = new ComponentWithOnlyKeyedProperty();
// Act
var activator = propertyActivator.GetActivator(componentType);
var ex = Assert.Throws<InvalidOperationException>(() => activator(serviceProvider, component));
// Assert
Assert.Contains("registered keyed service", ex.Message);
}
private const string KeyedServiceKey = "my-keyed-service";
private static IServiceProvider GetServiceProvider()
{
return new ServiceCollection()
.AddTransient<TestService1>()
.AddTransient<TestService2>()
.AddKeyedTransient<TestService3>(KeyedServiceKey)
.BuildServiceProvider();
}
private class EmptyComponent : IComponent
{
public void Attach(RenderHandle renderHandle)
{
throw new NotImplementedException();
}
public Task SetParametersAsync(ParameterView parameters)
{
throw new NotImplementedException();
}
}
private class ComponentWithInjectProperties : IComponent
{
[Inject]
public TestService1 Property1 { get; set; }
[Inject]
private TestService2 Property2 { get; set; }
[Inject]
public TestService1 Property3 { get; private set; }
[Inject]
public TestService1 Property4 { get; set; }
[Inject(Key = KeyedServiceKey)]
public TestService3 KeyedProperty { get; set; }
public TestService2 GetProperty2() => Property2;
public void Attach(RenderHandle renderHandle)
{
throw new NotImplementedException();
}
public Task SetParametersAsync(ParameterView parameters)
{
throw new NotImplementedException();
}
}
private class ComponentWithNonInjectableProperties : IComponent
{
[Inject]
public TestService1 Property1 { get; set; }
public TestService1 Property2 { get; set; }
public void Attach(RenderHandle renderHandle)
{
throw new NotImplementedException();
}
public Task SetParametersAsync(ParameterView parameters)
{
throw new NotImplementedException();
}
}
public class ComponentWithConstructorInjection : IComponent
{
public ComponentWithConstructorInjection(TestService1 property1, TestService2 property2)
{
Property1 = property1;
Property2 = property2;
}
public TestService1 Property1 { get; }
public TestService2 Property2 { get; }
[Inject]
public TestService2 Property3 { get; set; }
public void Attach(RenderHandle renderHandle)
{
throw new NotImplementedException();
}
public Task SetParametersAsync(ParameterView parameters)
{
throw new NotImplementedException();
}
}
private class DerivedComponent : ComponentWithInjectProperties
{
public new TestService2 Property4 { get; set; }
[Inject]
public TestService2 Property5 { get; set; }
}
public class TestService1 { }
public class TestService2 { }
public class TestService3 { }
private class ComponentWithOnlyKeyedProperty : IComponent
{
[Inject(Key = KeyedServiceKey)]
public TestService3 KeyedProperty { get; set; }
public void Attach(RenderHandle renderHandle)
{
throw new NotImplementedException();
}
public Task SetParametersAsync(ParameterView parameters)
{
throw new NotImplementedException();
}
}
private class CustomComponentActivator<TResult> : IComponentActivator where TResult : IComponent, new()
{
public IComponent CreateInstance(Type componentType)
{
return new TResult();
}
}
private class NullResultComponentActivator : IComponentActivator
{
public IComponent CreateInstance(Type componentType)
{
return null;
}
}
private class CustomPropertyActivator : IComponentPropertyActivator
{
public bool GetActivatorCalled { get; private set; }
public Type RequestedType { get; private set; }
public bool ActivatorInvoked { get; private set; }
public IServiceProvider ReceivedServiceProvider { get; private set; }
public Action<IServiceProvider, IComponent> GetActivator(Type componentType)
{
GetActivatorCalled = true;
RequestedType = componentType;
// Return an activator that tracks invocation and delegates to the default
var defaultActivator = new DefaultComponentPropertyActivator();
var defaultAction = defaultActivator.GetActivator(componentType);
return (sp, component) =>
{
ActivatorInvoked = true;
ReceivedServiceProvider = sp;
defaultAction(sp, component);
};
}
}
private class TestRenderMode : IComponentRenderMode { }
private class DerivedComponentRenderMode : IComponentRenderMode { }
[DerivedComponentRenderMode]
private class DerivedComponentWithRenderMode : ComponentWithRenderMode
{
class DerivedComponentRenderModeAttribute : RenderModeAttribute
{
public override IComponentRenderMode Mode => new DerivedComponentRenderMode();
}
}
[OwnRenderMode]
private class ComponentWithRenderMode : IComponent
{
public void Attach(RenderHandle renderHandle)
{
throw new NotImplementedException();
}
public Task SetParametersAsync(ParameterView parameters)
{
throw new NotImplementedException();
}
class OwnRenderMode : RenderModeAttribute
{
public override IComponentRenderMode Mode => new TestRenderMode();
}
}
private class RendererWithResolveComponentForRenderMode : TestRenderer
{
private readonly IComponent _componentToReturn;
public RendererWithResolveComponentForRenderMode(IComponent componentToReturn) : base()
{
_componentToReturn = componentToReturn;
}
public bool ResolverWasCalled { get; private set; }
public Type RequestedComponentType { get; private set; }
public int? SuppliedParentComponentId { get; private set; }
public IComponentActivator SuppliedActivator { get; private set; }
public IComponentRenderMode SuppliedRenderMode { get; private set; }
public override Dispatcher Dispatcher => throw new NotImplementedException();
protected override void HandleException(Exception exception)
{
throw new NotImplementedException();
}
protected override Task UpdateDisplayAsync(in RenderBatch renderBatch)
{
throw new NotImplementedException();
}
protected internal override IComponent ResolveComponentForRenderMode(Type componentType, int? parentComponentId, IComponentActivator componentActivator, IComponentRenderMode renderMode)
{
ResolverWasCalled = true;
RequestedComponentType = componentType;
SuppliedParentComponentId = parentComponentId;
SuppliedActivator = componentActivator;
SuppliedRenderMode = renderMode;
return _componentToReturn;
}
}
}
|