File: ViewComponents\DefaultViewComponentSelectorTest.cs
Web Access
Project: src\src\Mvc\Mvc.ViewFeatures\test\Microsoft.AspNetCore.Mvc.ViewFeatures.Test.csproj (Microsoft.AspNetCore.Mvc.ViewFeatures.Test)
// 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.Mvc.ApplicationParts;
 
namespace Microsoft.AspNetCore.Mvc.ViewComponents;
 
public class DefaultViewComponentSelectorTest
{
    private static readonly string Namespace = typeof(DefaultViewComponentSelectorTest).Namespace;
 
    [Fact]
    public void SelectComponent_ByShortNameWithSuffix()
    {
        // Arrange
        var selector = CreateSelector();
 
        // Act
        var result = selector.SelectComponent("Suffix");
 
        // Assert
        Assert.Same(typeof(ViewComponentContainer.SuffixViewComponent).GetTypeInfo(), result.TypeInfo);
    }
 
    [Fact]
    public void SelectComponent_ByLongNameWithSuffix()
    {
        // Arrange
        var selector = CreateSelector();
 
        // Act
        var result = selector.SelectComponent($"{Namespace}.Suffix");
 
        // Assert
        Assert.Same(typeof(ViewComponentContainer.SuffixViewComponent).GetTypeInfo(), result.TypeInfo);
    }
 
    [Fact]
    public void SelectComponent_ByShortNameWithoutSuffix()
    {
        // Arrange
        var selector = CreateSelector();
 
        // Act
        var result = selector.SelectComponent("WithoutSuffix");
 
        // Assert
        Assert.Same(typeof(ViewComponentContainer.WithoutSuffix).GetTypeInfo(), result.TypeInfo);
    }
 
    [Fact]
    public void SelectComponent_ByLongNameWithoutSuffix()
    {
        // Arrange
        var selector = CreateSelector();
 
        // Act
        var result = selector.SelectComponent($"{Namespace}.WithoutSuffix");
 
        // Assert
        Assert.Same(typeof(ViewComponentContainer.WithoutSuffix).GetTypeInfo(), result.TypeInfo);
    }
 
    [Fact]
    public void SelectComponent_ByAttribute()
    {
        // Arrange
        var selector = CreateSelector();
 
        // Act
        var result = selector.SelectComponent("ByAttribute");
 
        // Assert
        Assert.Same(typeof(ViewComponentContainer.ByAttribute).GetTypeInfo(), result.TypeInfo);
    }
 
    [Fact]
    public void SelectComponent_ByNamingConvention()
    {
        // Arrange
        var selector = CreateSelector();
 
        // Act
        var result = selector.SelectComponent("ByNamingConvention");
 
        // Assert
        Assert.Same(typeof(ViewComponentContainer.ByNamingConventionViewComponent).GetTypeInfo(), result.TypeInfo);
    }
 
    [Fact]
    public void SelectComponent_Ambiguity()
    {
        // Arrange
        var selector = CreateSelector();
        var expected =
            "The view component name 'Ambiguous' matched multiple types:" + Environment.NewLine +
            $"Type: '{typeof(ViewComponentContainer.Ambiguous1)}' - " +
            "Name: 'Namespace1.Ambiguous'" + Environment.NewLine +
            $"Type: '{typeof(ViewComponentContainer.Ambiguous2)}' - " +
            "Name: 'Namespace2.Ambiguous'";
 
        // Act
        var ex = Assert.Throws<InvalidOperationException>(() => selector.SelectComponent("Ambiguous"));
 
        // Assert
        Assert.Equal(expected, ex.Message);
    }
 
    [Theory]
    [InlineData("Name")]
    [InlineData("Ambiguous.Name")]
    public void SelectComponent_AmbiguityDueToDerivation(string name)
    {
        // Arrange
        var selector = CreateSelector();
        var expected =
            $"The view component name '{name}' matched multiple types:" + Environment.NewLine +
            $"Type: '{typeof(ViewComponentContainer.AmbiguousBase)}' - " +
            "Name: 'Ambiguous.Name'" + Environment.NewLine +
            $"Type: '{typeof(ViewComponentContainer.DerivedAmbiguous)}' - " +
            "Name: 'Ambiguous.Name'";
 
        // Act
        var ex = Assert.Throws<InvalidOperationException>(() => selector.SelectComponent(name));
 
        // Assert
        Assert.Equal(expected, ex.Message);
    }
 
    [Fact]
    public void SelectComponent_FullNameToAvoidAmbiguity()
    {
        // Arrange
        var selector = CreateSelector();
 
        // Act
        var result = selector.SelectComponent("Namespace1.Ambiguous");
 
        // Assert
        Assert.Same(typeof(ViewComponentContainer.Ambiguous1).GetTypeInfo(), result.TypeInfo);
    }
 
    [Fact]
    public void SelectComponent_OverrideNameToAvoidAmbiguity()
    {
        // Arrange
        var selector = CreateSelector();
 
        // Act
        var result = selector.SelectComponent("NonAmbiguousName");
 
        // Assert
        Assert.Same(typeof(ViewComponentContainer.DerivedAmbiguousWithOverriddenName).GetTypeInfo(), result.TypeInfo);
    }
 
    [Theory]
    [InlineData("FullNameInAttribute")]
    [InlineData("CoolNameSpace.FullNameInAttribute")]
    public void SelectComponent_FullNameInAttribute(string name)
    {
        // Arrange
        var selector = CreateSelector();
 
        // Act
        var result = selector.SelectComponent(name);
 
        // Assert
        Assert.Same(typeof(ViewComponentContainer.FullNameInAttribute).GetTypeInfo(), result.TypeInfo);
    }
 
    private IViewComponentSelector CreateSelector()
    {
        var provider = new DefaultViewComponentDescriptorCollectionProvider(
            new FilteredViewComponentDescriptorProvider());
 
        return new DefaultViewComponentSelector(provider);
    }
 
    private class ViewComponentContainer
    {
        public class SuffixViewComponent : ViewComponent
        {
            public string Invoke() => "Hello";
        }
 
        public class WithoutSuffix : ViewComponent
        {
            public string Invoke() => "Hello";
        }
 
        public class ByNamingConventionViewComponent
        {
            public string Invoke() => "Hello";
        }
 
        [ViewComponent]
        public class ByAttribute
        {
            public string Invoke() => "Hello";
        }
 
        [ViewComponent(Name = "Namespace1.Ambiguous")]
        public class Ambiguous1
        {
            public string Invoke() => "Hello";
        }
 
        [ViewComponent(Name = "Namespace2.Ambiguous")]
        public class Ambiguous2
        {
            public string Invoke() => "Hello";
        }
 
        [ViewComponent(Name = "CoolNameSpace.FullNameInAttribute")]
        public class FullNameInAttribute
        {
            public string Invoke() => "Hello";
        }
 
        [ViewComponent(Name = "Ambiguous.Name")]
        public class AmbiguousBase
        {
            public string Invoke() => "Hello";
        }
 
        public class DerivedAmbiguous : AmbiguousBase
        {
        }
 
        [ViewComponent(Name = "NonAmbiguousName")]
        public class DerivedAmbiguousWithOverriddenName : AmbiguousBase
        {
        }
    }
    // This will only consider types nested inside this class as ViewComponent classes
    private class FilteredViewComponentDescriptorProvider : DefaultViewComponentDescriptorProvider
    {
        public FilteredViewComponentDescriptorProvider()
            : this(typeof(ViewComponentContainer).GetNestedTypes(bindingAttr: BindingFlags.Public))
        {
        }
 
        // For error messages in tests above, ensure the TestApplicationPart returns types in a consistent order.
        public FilteredViewComponentDescriptorProvider(params Type[] allowedTypes)
            : base(GetApplicationPartManager(allowedTypes.OrderBy(type => type.Name, StringComparer.Ordinal)))
        {
        }
 
        private static ApplicationPartManager GetApplicationPartManager(IEnumerable<Type> types)
        {
            var manager = new ApplicationPartManager();
            manager.ApplicationParts.Add(new TestApplicationPart(types));
            manager.FeatureProviders.Add(new TestFeatureProvider());
            return manager;
        }
 
        private class TestFeatureProvider : IApplicationFeatureProvider<ViewComponentFeature>
        {
            public void PopulateFeature(IEnumerable<ApplicationPart> parts, ViewComponentFeature feature)
            {
                foreach (var type in parts.OfType<IApplicationPartTypeProvider>().SelectMany(p => p.Types))
                {
                    feature.ViewComponents.Add(type);
                }
            }
        }
    }
}