File: IntegrationTests\ComponentGenericTypeIntegrationTest.cs
Web Access
Project: src\src\Razor\src\Compiler\Microsoft.AspNetCore.Razor.Language\test\Microsoft.AspNetCore.Razor.Language.UnitTests.csproj (Microsoft.AspNetCore.Razor.Language.UnitTests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Globalization;
using System.Linq;
using Microsoft.AspNetCore.Razor.Language.Components;
using Microsoft.CodeAnalysis.CSharp;
using Xunit;
 
namespace Microsoft.AspNetCore.Razor.Language.IntegrationTests;
 
public class ComponentGenericTypeIntegrationTest : RazorIntegrationTestBase
{
    private readonly CSharpSyntaxTree GenericContextComponent = Parse(@"
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
namespace Test
{
    public class GenericContext<TItem> : ComponentBase
    {
        protected override void BuildRenderTree(RenderTreeBuilder builder)
        {
            var items = (IReadOnlyList<TItem>)Items ?? Array.Empty<TItem>();
            for (var i = 0; i < items.Count; i++)
            {
                if (ChildContent == null)
                {
                    builder.AddContent(i, Items[i]);
                }
                else
                {
                    builder.AddContent(i, ChildContent, new Context() { Index = i, Item = items[i], });
                }
            }
        }
 
        [Parameter]
        public List<TItem> Items { get; set; }
 
        [Parameter]
        public RenderFragment<Context> ChildContent { get; set; }
 
        public class Context
        {
            public int Index { get; set; }
            public TItem Item { get; set; }
        }
    }
}
");
 
    private readonly CSharpSyntaxTree MultipleGenericParameterComponent = Parse(@"
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
namespace Test
{
    public class MultipleGenericParameter<TItem1, TItem2, TItem3> : ComponentBase
    {
        protected override void BuildRenderTree(RenderTreeBuilder builder)
        {
            builder.AddContent(0, Item1);
            builder.AddContent(1, Item2);
            builder.AddContent(2, Item3);
        }
 
        [Parameter]
        public TItem1 Item1 { get; set; }
 
        [Parameter]
        public TItem2 Item2 { get; set; }
 
        [Parameter]
        public TItem3 Item3 { get; set; }
    }
}
");
 
    internal override RazorFileKind? FileKind => RazorFileKind.Component;
 
    internal override bool UseTwoPhaseCompilation => true;
 
    [Fact]
    public void GenericComponent_WithoutAnyTypeParameters_TriggersDiagnostic()
    {
        // Arrange
        AdditionalSyntaxTrees.Add(GenericContextComponent);
 
        // Act
        var generated = CompileToCSharp(@"
<GenericContext />");
 
        // Assert
        var diagnostic = Assert.Single(generated.RazorDiagnostics);
        Assert.Same(ComponentDiagnosticFactory.GenericComponentTypeInferenceUnderspecified.Id, diagnostic.Id);
        Assert.Equal("""
            The type of component 'GenericContext' cannot be inferred based on the values provided. Consider specifying the type arguments directly using the following attributes: 'TItem'.
            """,
            diagnostic.GetMessage(CultureInfo.CurrentCulture));
    }
 
    [Fact]
    public void GenericComponent_WithMissingTypeParameters_TriggersDiagnostic()
    {
        // Arrange
        AdditionalSyntaxTrees.Add(MultipleGenericParameterComponent);
 
        // Act
        var generated = CompileToCSharp(@"
<MultipleGenericParameter TItem1=int />");
 
        // Assert
        var diagnostic = Assert.Single(generated.RazorDiagnostics);
        Assert.Same(ComponentDiagnosticFactory.GenericComponentMissingTypeArgument.Id, diagnostic.Id);
        Assert.Equal("""
            The component 'MultipleGenericParameter' is missing required type arguments. Specify the missing types using the attributes: 'TItem2', 'TItem3'.
            """,
            diagnostic.GetMessage(CultureInfo.CurrentCulture));
    }
 
    [Fact]
    public void GenericAndNonGenericComponents_WithSameName_CanCoexist()
    {
        // Arrange
        // Create a non-generic component named SomeComponent
        var nonGenericComponent = Parse(@"
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
namespace Test
{
    public class SomeComponent : ComponentBase
    {
        [Parameter]
        public RenderFragment ChildContent { get; set; }
 
        protected override void BuildRenderTree(RenderTreeBuilder builder)
        {
            builder.OpenElement(0, ""div"");
            builder.AddContent(1, ChildContent);
            builder.CloseElement();
        }
    }
}
");
 
        // Create a generic component named SomeComponent<TItem>
        var genericComponent = Parse(@"
using System.Collections.Generic;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
namespace Test
{
    public class SomeComponent<TItem> : ComponentBase
    {
        [Parameter]
        public RenderFragment<TItem> ChildContent { get; set; }
        
        [Parameter]
        public IReadOnlyCollection<TItem> Items { get; set; }
 
        protected override void BuildRenderTree(RenderTreeBuilder builder)
        {
            builder.OpenElement(0, ""div"");
            foreach (var item in Items)
            {
                builder.AddContent(1, ChildContent(item));
            }
            builder.CloseElement();
        }
    }
}
");
 
        AdditionalSyntaxTrees.Add(nonGenericComponent);
        AdditionalSyntaxTrees.Add(genericComponent);
 
        // Act - Use the non-generic component (no type parameters provided)
        var nonGenericGenerated = CompileToCSharp(@"
@using Test
<SomeComponent>
    <p>Non-generic content</p>
</SomeComponent>");
 
        // Assert - No diagnostics should be generated for non-generic usage
        Assert.Empty(nonGenericGenerated.RazorDiagnostics);
 
        // Act - Use the generic component (type parameter provided)
        var genericGenerated = CompileToCSharp(@"
@using Test
<SomeComponent TItem=""string"" Items=""@(new[] { ""a"", ""b"", ""c"" })"">
    <p>Item: @context</p>
</SomeComponent>");
 
        // Assert - No diagnostics should be generated for generic usage
        Assert.Empty(genericGenerated.RazorDiagnostics);
    }
 
    [Fact]
    public void GenericAndNonGenericComponents_WithSameName_NonGenericUsage()
    {
        // Arrange
        var nonGenericComponent = Parse(@"
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
namespace Test
{
    public class MyComponent : ComponentBase
    {
        [Parameter]
        public string Message { get; set; }
 
        protected override void BuildRenderTree(RenderTreeBuilder builder)
        {
            builder.OpenElement(0, ""div"");
            builder.AddContent(1, Message);
            builder.CloseElement();
        }
    }
}
");
 
        var genericComponent = Parse(@"
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
namespace Test
{
    public class MyComponent<TItem> : ComponentBase
    {
        [Parameter]
        public TItem Item { get; set; }
 
        protected override void BuildRenderTree(RenderTreeBuilder builder)
        {
            builder.OpenElement(0, ""div"");
            builder.AddContent(1, Item);
            builder.CloseElement();
        }
    }
}
");
 
        AdditionalSyntaxTrees.Add(nonGenericComponent);
        AdditionalSyntaxTrees.Add(genericComponent);
 
        // Act
        var generated = CompileToCSharp(@"
@using Test
<MyComponent Message=""Hello"" />");
 
        // Assert - Should use non-generic component without errors
        Assert.Empty(generated.RazorDiagnostics);
        Assert.Contains("global::Test.MyComponent", generated.Code);
        Assert.DoesNotContain("global::Test.MyComponent<", generated.Code);
    }
 
    [Fact]
    public void GenericAndNonGenericComponents_WithSameName_GenericUsage()
    {
        // Arrange
        var nonGenericComponent = Parse(@"
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
namespace Test
{
    public class MyComponent : ComponentBase
    {
        [Parameter]
        public string Message { get; set; }
 
        protected override void BuildRenderTree(RenderTreeBuilder builder)
        {
            builder.OpenElement(0, ""div"");
            builder.AddContent(1, Message);
            builder.CloseElement();
        }
    }
}
");
 
        var genericComponent = Parse(@"
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
namespace Test
{
    public class MyComponent<TItem> : ComponentBase
    {
        [Parameter]
        public TItem Item { get; set; }
 
        protected override void BuildRenderTree(RenderTreeBuilder builder)
        {
            builder.OpenElement(0, ""div"");
            builder.AddContent(1, Item);
            builder.CloseElement();
        }
    }
}
");
 
        AdditionalSyntaxTrees.Add(nonGenericComponent);
        AdditionalSyntaxTrees.Add(genericComponent);
 
        // Act
        var generated = CompileToCSharp(@"
@using Test
<MyComponent TItem=""int"" Item=""42"" />");
 
        // Assert - Should use generic component without errors
        Assert.Empty(generated.RazorDiagnostics);
        Assert.Contains("global::Test.MyComponent<", generated.Code);
    }
 
    [Fact]
    public void GenericAndNonGenericComponents_WithSameMessageParameter_StringValue()
    {
        // Arrange - Both components have a Message parameter
        // Non-generic: Message is string
        // Generic: Message uses TItem
        var nonGenericComponent = Parse(@"
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
namespace Test
{
    public class MyComponent : ComponentBase
    {
        [Parameter]
        public string Message { get; set; }
 
        protected override void BuildRenderTree(RenderTreeBuilder builder)
        {
            builder.OpenElement(0, ""div"");
            builder.AddContent(1, Message);
            builder.CloseElement();
        }
    }
}
");
 
        var genericComponent = Parse(@"
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
namespace Test
{
    public class MyComponent<TItem> : ComponentBase
    {
        [Parameter]
        public TItem Message { get; set; }
 
        protected override void BuildRenderTree(RenderTreeBuilder builder)
        {
            builder.OpenElement(0, ""div"");
            builder.AddContent(1, Message);
            builder.CloseElement();
        }
    }
}
");
 
        AdditionalSyntaxTrees.Add(nonGenericComponent);
        AdditionalSyntaxTrees.Add(genericComponent);
 
        // Act - Pass a string to Message without providing generic parameter
        var generated = CompileToCSharp(@"
@using Test
<MyComponent Message=""Hello World"" />");
 
        // Assert - When both components have a parameter with the same name,
        // the system cannot disambiguate and reports an ambiguous component selection error
        Assert.Single(generated.RazorDiagnostics);
        var diagnostic = generated.RazorDiagnostics.Single();
        Assert.Equal("RZ10012", diagnostic.Id);
        Assert.Contains("cannot be disambiguated between generic and non-generic versions", 
            diagnostic.GetMessage(CultureInfo.InvariantCulture));
    }
 
    [Fact]
    public void GenericAndNonGenericComponents_WithSameMessageParameter_NonStringValue()
    {
        // Arrange - Both components have a Message parameter
        // Non-generic: Message is string
        // Generic: Message uses TItem
        var nonGenericComponent = Parse(@"
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
namespace Test
{
    public class MyComponent : ComponentBase
    {
        [Parameter]
        public string Message { get; set; }
 
        protected override void BuildRenderTree(RenderTreeBuilder builder)
        {
            builder.OpenElement(0, ""div"");
            builder.AddContent(1, Message);
            builder.CloseElement();
        }
    }
}
");
 
        var genericComponent = Parse(@"
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
namespace Test
{
    public class MyComponent<TItem> : ComponentBase
    {
        [Parameter]
        public TItem Message { get; set; }
 
        protected override void BuildRenderTree(RenderTreeBuilder builder)
        {
            builder.OpenElement(0, ""div"");
            builder.AddContent(1, Message);
            builder.CloseElement();
        }
    }
}
");
 
        AdditionalSyntaxTrees.Add(nonGenericComponent);
        AdditionalSyntaxTrees.Add(genericComponent);
 
        // Act - Pass an expression that evaluates to non-string (int) to Message
        var generated = CompileToCSharp(@"
@using Test
<MyComponent Message=""@42"" />");
 
        // Assert - When both components have a parameter with the same name,
        // the system cannot disambiguate and reports an ambiguous component selection error.
        // The @42 syntax also causes additional parsing errors.
        Assert.Contains(generated.RazorDiagnostics, d => d.Id == "RZ10012");
        var ambiguityDiagnostic = generated.RazorDiagnostics.Single(d => d.Id == "RZ10012");
        Assert.Contains("cannot be disambiguated between generic and non-generic versions",
            ambiguityDiagnostic.GetMessage(CultureInfo.InvariantCulture));
    }
 
    [Fact]
    public void MultipleGenericComponents_DifferentTypeParameterCounts_NonGenericUsage()
    {
        // Arrange - Multiple generic components with different type parameter counts
        var nonGenericComponent = Parse(@"
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
namespace Test
{
    public class MyComponent : ComponentBase
    {
        [Parameter]
        public string Value { get; set; }
 
        protected override void BuildRenderTree(RenderTreeBuilder builder)
        {
            builder.OpenElement(0, ""div"");
            builder.AddContent(1, Value);
            builder.CloseElement();
        }
    }
}
");
 
        var singleParamComponent = Parse(@"
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
namespace Test
{
    public class MyComponent<T> : ComponentBase
    {
        [Parameter]
        public T Item { get; set; }
 
        protected override void BuildRenderTree(RenderTreeBuilder builder)
        {
            builder.OpenElement(0, ""div"");
            builder.AddContent(1, Item);
            builder.CloseElement();
        }
    }
}
");
 
        var twoParamComponent = Parse(@"
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
namespace Test
{
    public class MyComponent<T1, T2> : ComponentBase
    {
        [Parameter]
        public T1 First { get; set; }
        
        [Parameter]
        public T2 Second { get; set; }
 
        protected override void BuildRenderTree(RenderTreeBuilder builder)
        {
            builder.OpenElement(0, ""div"");
            builder.AddContent(1, First);
            builder.AddContent(2, Second);
            builder.CloseElement();
        }
    }
}
");
 
        AdditionalSyntaxTrees.Add(nonGenericComponent);
        AdditionalSyntaxTrees.Add(singleParamComponent);
        AdditionalSyntaxTrees.Add(twoParamComponent);
 
        // Act - Use without type parameters
        var generated = CompileToCSharp(@"
@using Test
<MyComponent Value=""test"" />");
 
        // Assert - Should select non-generic component
        Assert.Empty(generated.RazorDiagnostics);
        Assert.Contains("global::Test.MyComponent", generated.Code);
        Assert.DoesNotContain("global::Test.MyComponent<", generated.Code);
    }
 
    [Fact]
    public void MultipleGenericComponents_DifferentTypeParameterCounts_SingleTypeParameter()
    {
        // Arrange
        var nonGenericComponent = Parse(@"
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
namespace Test
{
    public class MyComponent : ComponentBase
    {
        [Parameter]
        public string Value { get; set; }
    }
}
");
 
        var singleParamComponent = Parse(@"
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
namespace Test
{
    public class MyComponent<T> : ComponentBase
    {
        [Parameter]
        public T Item { get; set; }
    }
}
");
 
        var twoParamComponent = Parse(@"
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
namespace Test
{
    public class MyComponent<T1, T2> : ComponentBase
    {
        [Parameter]
        public T1 First { get; set; }
        
        [Parameter]
        public T2 Second { get; set; }
    }
}
");
 
        AdditionalSyntaxTrees.Add(nonGenericComponent);
        AdditionalSyntaxTrees.Add(singleParamComponent);
        AdditionalSyntaxTrees.Add(twoParamComponent);
 
        // Act - Provide one type parameter
        var generated = CompileToCSharp(@"
@using Test
<MyComponent T=""string"" Item=""test"" />");
 
        // Assert - Should select MyComponent<T>
        Assert.Empty(generated.RazorDiagnostics);
        Assert.Contains("global::Test.MyComponent<", generated.Code);
    }
 
    [Fact]
    public void MultipleGenericComponents_DifferentTypeParameterCounts_TwoTypeParameters()
    {
        // Arrange
        var nonGenericComponent = Parse(@"
using Microsoft.AspNetCore.Components;
namespace Test
{
    public class MyComponent : ComponentBase
    {
        [Parameter]
        public string Value { get; set; }
    }
}
");
 
        var singleParamComponent = Parse(@"
using Microsoft.AspNetCore.Components;
namespace Test
{
    public class MyComponent<T> : ComponentBase
    {
        [Parameter]
        public T Item { get; set; }
    }
}
");
 
        var twoParamComponent = Parse(@"
using Microsoft.AspNetCore.Components;
namespace Test
{
    public class MyComponent<T1, T2> : ComponentBase
    {
        [Parameter]
        public T1 First { get; set; }
        
        [Parameter]
        public T2 Second { get; set; }
    }
}
");
 
        AdditionalSyntaxTrees.Add(nonGenericComponent);
        AdditionalSyntaxTrees.Add(singleParamComponent);
        AdditionalSyntaxTrees.Add(twoParamComponent);
 
        // Act - Provide two type parameters
        var generated = CompileToCSharp(@"
@using Test
<MyComponent T1=""string"" T2=""int"" First=""hello"" Second=""42"" />");
 
        // Assert - Should select MyComponent<T1, T2>
        Assert.Empty(generated.RazorDiagnostics);
        Assert.Contains("global::Test.MyComponent<", generated.Code);
    }
}