diff --git a/src/Microsoft.AspNetCore.Razor.Language/src/BoundAttributeDescriptorBuilderExtensions.cs b/src/Microsoft.AspNetCore.Razor.Language/src/BoundAttributeDescriptorBuilderExtensions.cs index e73cf067..0cac5593 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/src/BoundAttributeDescriptorBuilderExtensions.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/src/BoundAttributeDescriptorBuilderExtensions.cs @@ -96,4 +96,14 @@ public static class BoundAttributeDescriptorBuilderExtensions return null; } + + public static void SetGloballyQualifiedTypeName(this BoundAttributeDescriptorBuilder builder, string globallyQualifiedTypeName) + { + builder.Metadata[TagHelperMetadata.Common.GloballyQualifiedTypeName] = globallyQualifiedTypeName; + } + + public static string GetGloballyQualifiedTypeName(this BoundAttributeDescriptor descriptor) + { + return descriptor?.Metadata[TagHelperMetadata.Common.GloballyQualifiedTypeName]; + } } diff --git a/src/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentDesignTimeNodeWriter.cs b/src/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentDesignTimeNodeWriter.cs index b4aa4c91..36cd5898 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentDesignTimeNodeWriter.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentDesignTimeNodeWriter.cs @@ -714,7 +714,15 @@ internal class ComponentDesignTimeNodeWriter : ComponentNodeWriter { context.CodeWriter.Write(ComponentsApi.RuntimeHelpers.TypeCheck); context.CodeWriter.Write("<"); - TypeNameHelper.WriteGloballyQualifiedName(context.CodeWriter, node.TypeName); + var explicitType = (bool?)node.Annotations[ComponentMetadata.Component.ExplicitTypeNameKey]; + if (explicitType == true) + { + context.CodeWriter.Write(node.TypeName); + } + else + { + TypeNameHelper.WriteGloballyQualifiedName(context.CodeWriter, node.TypeName); + } context.CodeWriter.Write(">"); context.CodeWriter.Write("("); } diff --git a/src/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentGenericTypePass.cs b/src/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentGenericTypePass.cs index 62d0a884..bd0aa09e 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentGenericTypePass.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentGenericTypePass.cs @@ -337,11 +337,11 @@ internal class ComponentGenericTypePass : ComponentIntermediateNodePassBase, IRa foreach (var attribute in node.Attributes) { - string globallyQualifiedTypeName = null; + var globallyQualifiedTypeName = attribute.BoundAttribute.GetGloballyQualifiedTypeName(); if (attribute.TypeName != null) { - globallyQualifiedTypeName = rewriter.Rewrite(attribute.TypeName); + globallyQualifiedTypeName = rewriter.Rewrite(globallyQualifiedTypeName ?? attribute.TypeName); attribute.GloballyQualifiedTypeName = globallyQualifiedTypeName; } @@ -350,6 +350,18 @@ internal class ComponentGenericTypePass : ComponentIntermediateNodePassBase, IRa // If we know the type name, then replace any generic type parameter inside it with // the known types. attribute.TypeName = globallyQualifiedTypeName; + // This is a special case in which we are dealing with a property TItem. + // Given that TItem can have been defined explicitly by the user to a partially + // qualified type, (like MyType), we check if the globally qualified type name + // contains "global::" which will be the case in all cases as we've computed + // this information from the Roslyn symbol except for when the symbol is a generic + // type parameter. In which case, we mark it with an additional annotation to + // acount for that during code generation and avoid trying to fully qualify + // the type name. + if (!globallyQualifiedTypeName.StartsWith("global::", StringComparison.Ordinal)) + { + attribute.Annotations.Add(ComponentMetadata.Component.ExplicitTypeNameKey, true); + } } else if (attribute.TypeName == null && (attribute.BoundAttribute?.IsDelegateProperty() ?? false)) { diff --git a/src/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentMetadata.cs b/src/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentMetadata.cs index 2372494b..56fdff25 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentMetadata.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentMetadata.cs @@ -101,6 +101,8 @@ internal static class ComponentMetadata public const string GenericTypedKey = "Components.GenericTyped"; + public const string ExplicitTypeNameKey = "Components.ExplicitTypeName"; + public const string TypeParameterKey = "Components.TypeParameter"; public const string TypeParameterIsCascadingKey = "Components.TypeParameterIsCascading"; diff --git a/src/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentNodeWriter.cs b/src/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentNodeWriter.cs index f463caf9..5b246a55 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentNodeWriter.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentNodeWriter.cs @@ -294,7 +294,7 @@ internal abstract class ComponentNodeWriter : IntermediateNodeWriter, ITemplateT typeName = attribute.TypeName; if (attribute.BoundAttribute != null && !attribute.BoundAttribute.IsGenericTypedProperty()) { - typeName = "global::" + typeName; + typeName = typeName.StartsWith("global::", StringComparison.Ordinal) ? typeName : $"global::{typeName}"; } } diff --git a/src/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentRuntimeNodeWriter.cs b/src/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentRuntimeNodeWriter.cs index 730e02a2..0a8cf89c 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentRuntimeNodeWriter.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentRuntimeNodeWriter.cs @@ -653,7 +653,15 @@ internal class ComponentRuntimeNodeWriter : ComponentNodeWriter { context.CodeWriter.Write(ComponentsApi.RuntimeHelpers.TypeCheck); context.CodeWriter.Write("<"); - TypeNameHelper.WriteGloballyQualifiedName(context.CodeWriter, node.TypeName); + var explicitType = (bool?)node.Annotations[ComponentMetadata.Component.ExplicitTypeNameKey]; + if (explicitType == true) + { + context.CodeWriter.Write(node.TypeName); + } + else + { + TypeNameHelper.WriteGloballyQualifiedName(context.CodeWriter, node.TypeName); + } context.CodeWriter.Write(">"); context.CodeWriter.Write("("); } diff --git a/src/Microsoft.AspNetCore.Razor.Language/src/TagHelperMetadata.cs b/src/Microsoft.AspNetCore.Razor.Language/src/TagHelperMetadata.cs index 63233479..2e9797f5 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/src/TagHelperMetadata.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/src/TagHelperMetadata.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. #nullable disable @@ -13,6 +13,8 @@ public static class TagHelperMetadata public static readonly string TypeName = "Common.TypeName"; + public static readonly string GloballyQualifiedTypeName = "Common.GloballyQualifiedTypeName"; + public static readonly string ClassifyAttributesOnly = "Common.ClassifyAttributesOnly"; } diff --git a/src/Microsoft.AspNetCore.Razor.Language/test/IntegrationTests/ComponentCodeGenerationTestBase.cs b/src/Microsoft.AspNetCore.Razor.Language/test/IntegrationTests/ComponentCodeGenerationTestBase.cs index 9f759c60..82b00a23 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/test/IntegrationTests/ComponentCodeGenerationTestBase.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/test/IntegrationTests/ComponentCodeGenerationTestBase.cs @@ -3710,6 +3710,36 @@ namespace Test CompileToAssembly(generated); } + [Fact] + public void GenericComponent_NonPrimitiveType() + { + // Arrange + AdditionalSyntaxTrees.Add(Parse(@" +using Microsoft.AspNetCore.Components; + +namespace Test +{ + public class MyComponent : ComponentBase + { + [Parameter] public TItem Item { get; set; } + } + + public class CustomType + { + } +} +")); + + // Act + var generated = CompileToCSharp(@" +"); + + // Assert + AssertDocumentNodeMatchesBaseline(generated.CodeDocument); + AssertCSharpDocumentMatchesBaseline(generated.CodeDocument); + CompileToAssembly(generated); + } + [Fact] public void ChildComponent_Generic_TypeInference() { diff --git a/src/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentDesignTimeCodeGenerationTest/GenericComponent_NonPrimitiveType/TestComponent.codegen.cs b/src/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentDesignTimeCodeGenerationTest/GenericComponent_NonPrimitiveType/TestComponent.codegen.cs new file mode 100644 index 00000000..813eaedd --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentDesignTimeCodeGenerationTest/GenericComponent_NonPrimitiveType/TestComponent.codegen.cs @@ -0,0 +1,55 @@ +// +#pragma warning disable 1591 +namespace Test +{ + #line hidden + using System; + using System.Collections.Generic; + using System.Linq; + using System.Threading.Tasks; + using Microsoft.AspNetCore.Components; + public partial class TestComponent : global::Microsoft.AspNetCore.Components.ComponentBase + { + #pragma warning disable 219 + private void __RazorDirectiveTokenHelpers__() { + } + #pragma warning restore 219 + #pragma warning disable 0414 + private static System.Object __o = null; + #pragma warning restore 0414 + #pragma warning disable 1998 + protected override void BuildRenderTree(global::Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder __builder) + { + __o = typeof( +#nullable restore +#line 1 "x:\dir\subdir\Test\TestComponent.cshtml" + CustomType + +#line default +#line hidden +#nullable disable + ); + __o = global::Microsoft.AspNetCore.Components.CompilerServices.RuntimeHelpers.TypeCheck( +#nullable restore +#line 1 "x:\dir\subdir\Test\TestComponent.cshtml" + new CustomType() + +#line default +#line hidden +#nullable disable + ); + __builder.AddAttribute(-1, "ChildContent", (global::Microsoft.AspNetCore.Components.RenderFragment)((__builder2) => { + } + )); +#nullable restore +#line 1 "x:\dir\subdir\Test\TestComponent.cshtml" +__o = typeof(global::Test.MyComponent<>); + +#line default +#line hidden +#nullable disable + } + #pragma warning restore 1998 + } +} +#pragma warning restore 1591 diff --git a/src/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentDesignTimeCodeGenerationTest/GenericComponent_NonPrimitiveType/TestComponent.ir.txt b/src/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentDesignTimeCodeGenerationTest/GenericComponent_NonPrimitiveType/TestComponent.ir.txt new file mode 100644 index 00000000..17e38c81 --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentDesignTimeCodeGenerationTest/GenericComponent_NonPrimitiveType/TestComponent.ir.txt @@ -0,0 +1,21 @@ +Document - + NamespaceDeclaration - - Test + UsingDirective - (3:1,1 [12] ) - System + UsingDirective - (18:2,1 [32] ) - System.Collections.Generic + UsingDirective - (53:3,1 [17] ) - System.Linq + UsingDirective - (73:4,1 [28] ) - System.Threading.Tasks + UsingDirective - (104:5,1 [37] ) - Microsoft.AspNetCore.Components + ClassDeclaration - - public partial - TestComponent - global::Microsoft.AspNetCore.Components.ComponentBase - + DesignTimeDirective - + CSharpCode - + IntermediateToken - - CSharp - #pragma warning disable 0414 + CSharpCode - + IntermediateToken - - CSharp - private static System.Object __o = null; + CSharpCode - + IntermediateToken - - CSharp - #pragma warning restore 0414 + MethodDeclaration - - protected override - void - BuildRenderTree + Component - (0:0,0 [57] x:\dir\subdir\Test\TestComponent.cshtml) - MyComponent + ComponentTypeArgument - (20:0,20 [10] x:\dir\subdir\Test\TestComponent.cshtml) - TItem + LazyIntermediateToken - (20:0,20 [10] x:\dir\subdir\Test\TestComponent.cshtml) - CSharp - CustomType + ComponentAttribute - (38:0,38 [16] x:\dir\subdir\Test\TestComponent.cshtml) - Item - Item - AttributeStructure.DoubleQuotes + LazyIntermediateToken - (38:0,38 [16] x:\dir\subdir\Test\TestComponent.cshtml) - CSharp - new CustomType() diff --git a/src/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentDesignTimeCodeGenerationTest/GenericComponent_NonPrimitiveType/TestComponent.mappings.txt b/src/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentDesignTimeCodeGenerationTest/GenericComponent_NonPrimitiveType/TestComponent.mappings.txt new file mode 100644 index 00000000..abd7c89e --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentDesignTimeCodeGenerationTest/GenericComponent_NonPrimitiveType/TestComponent.mappings.txt @@ -0,0 +1,10 @@ +Source Location: (20:0,20 [10] x:\dir\subdir\Test\TestComponent.cshtml) +|CustomType| +Generated Location: (915:25,20 [10] ) +|CustomType| + +Source Location: (38:0,38 [16] x:\dir\subdir\Test\TestComponent.cshtml) +|new CustomType()| +Generated Location: (1215:34,38 [16] ) +|new CustomType()| + diff --git a/src/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentRuntimeCodeGenerationTest/GenericComponent_NonPrimitiveType/TestComponent.codegen.cs b/src/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentRuntimeCodeGenerationTest/GenericComponent_NonPrimitiveType/TestComponent.codegen.cs new file mode 100644 index 00000000..200b5aea --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentRuntimeCodeGenerationTest/GenericComponent_NonPrimitiveType/TestComponent.codegen.cs @@ -0,0 +1,31 @@ +// +#pragma warning disable 1591 +namespace Test +{ + #line hidden + using System; + using System.Collections.Generic; + using System.Linq; + using System.Threading.Tasks; + using Microsoft.AspNetCore.Components; + public partial class TestComponent : global::Microsoft.AspNetCore.Components.ComponentBase + { + #pragma warning disable 1998 + protected override void BuildRenderTree(global::Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder __builder) + { + __builder.OpenComponent>(0); + __builder.AddAttribute(1, "Item", global::Microsoft.AspNetCore.Components.CompilerServices.RuntimeHelpers.TypeCheck( +#nullable restore +#line 1 "x:\dir\subdir\Test\TestComponent.cshtml" + new CustomType() + +#line default +#line hidden +#nullable disable + )); + __builder.CloseComponent(); + } + #pragma warning restore 1998 + } +} +#pragma warning restore 1591 diff --git a/src/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentRuntimeCodeGenerationTest/GenericComponent_NonPrimitiveType/TestComponent.ir.txt b/src/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentRuntimeCodeGenerationTest/GenericComponent_NonPrimitiveType/TestComponent.ir.txt new file mode 100644 index 00000000..040ab5d7 --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentRuntimeCodeGenerationTest/GenericComponent_NonPrimitiveType/TestComponent.ir.txt @@ -0,0 +1,14 @@ +Document - + NamespaceDeclaration - - Test + UsingDirective - (3:1,1 [14] ) - System + UsingDirective - (18:2,1 [34] ) - System.Collections.Generic + UsingDirective - (53:3,1 [19] ) - System.Linq + UsingDirective - (73:4,1 [30] ) - System.Threading.Tasks + UsingDirective - (104:5,1 [39] ) - Microsoft.AspNetCore.Components + ClassDeclaration - - public partial - TestComponent - global::Microsoft.AspNetCore.Components.ComponentBase - + MethodDeclaration - - protected override - void - BuildRenderTree + Component - (0:0,0 [57] x:\dir\subdir\Test\TestComponent.cshtml) - MyComponent + ComponentTypeArgument - (20:0,20 [10] x:\dir\subdir\Test\TestComponent.cshtml) - TItem + LazyIntermediateToken - (20:0,20 [10] x:\dir\subdir\Test\TestComponent.cshtml) - CSharp - CustomType + ComponentAttribute - (38:0,38 [16] x:\dir\subdir\Test\TestComponent.cshtml) - Item - Item - AttributeStructure.DoubleQuotes + LazyIntermediateToken - (38:0,38 [16] x:\dir\subdir\Test\TestComponent.cshtml) - CSharp - new CustomType() diff --git a/src/Microsoft.CodeAnalysis.Razor/src/ComponentTagHelperDescriptorProvider.cs b/src/Microsoft.CodeAnalysis.Razor/src/ComponentTagHelperDescriptorProvider.cs index 77819e00..a1ea2769 100644 --- a/src/Microsoft.CodeAnalysis.Razor/src/ComponentTagHelperDescriptorProvider.cs +++ b/src/Microsoft.CodeAnalysis.Razor/src/ComponentTagHelperDescriptorProvider.cs @@ -181,7 +181,7 @@ internal class ComponentTagHelperDescriptorProvider : RazorEngineFeatureBase, IT pb.TypeName = property.Type.ToDisplayString(FullNameTypeDisplayFormat); pb.SetPropertyName(property.Name); pb.IsEditorRequired = property.GetAttributes().Any(a => a.AttributeClass.ToDisplayString() == "Microsoft.AspNetCore.Components.EditorRequiredAttribute"); - + pb.SetGloballyQualifiedTypeName(property.Type.ToDisplayString(GloballyQualifiedFullNameTypeDisplayFormat)); if (kind == PropertyKind.Enum) { pb.IsEnum = true; diff --git a/src/Microsoft.CodeAnalysis.Razor/src/GlobalQualifiedTypeNameRewriter.cs b/src/Microsoft.CodeAnalysis.Razor/src/GlobalQualifiedTypeNameRewriter.cs index 56c42b80..00c4ddb9 100644 --- a/src/Microsoft.CodeAnalysis.Razor/src/GlobalQualifiedTypeNameRewriter.cs +++ b/src/Microsoft.CodeAnalysis.Razor/src/GlobalQualifiedTypeNameRewriter.cs @@ -59,7 +59,21 @@ internal class GlobalQualifiedTypeNameRewriter : TypeNameRewriter node = (QualifiedNameSyntax)base.VisitQualifiedName(node); // Rewriting these is complicated, best to just tostring and parse again. - return SyntaxFactory.ParseTypeName("global::" + node.ToString()); + return SyntaxFactory.ParseTypeName(IsGloballyQualified(node) ? node.ToString() : "global::" + node.ToString()); + + static bool IsGloballyQualified(QualifiedNameSyntax node) + { + var candidate = node; + while (candidate != null) + { + if (candidate.Left is AliasQualifiedNameSyntax) + { + return true; + } + candidate = candidate.Left as QualifiedNameSyntax; + } + return false; + } } public override SyntaxNode VisitIdentifierName(IdentifierNameSyntax node) diff --git a/src/Microsoft.CodeAnalysis.Razor/test/ComponentTagHelperDescriptorProviderTest.cs b/src/Microsoft.CodeAnalysis.Razor/test/ComponentTagHelperDescriptorProviderTest.cs index 4a0b2a4e..61f7aaa5 100644 --- a/src/Microsoft.CodeAnalysis.Razor/test/ComponentTagHelperDescriptorProviderTest.cs +++ b/src/Microsoft.CodeAnalysis.Razor/test/ComponentTagHelperDescriptorProviderTest.cs @@ -131,6 +131,7 @@ namespace Test // which is trivial. Verifying it once in detail and then ignoring it. Assert.Collection( attribute.Metadata.OrderBy(kvp => kvp.Key), + kvp => { Assert.Equal(TagHelperMetadata.Common.GloballyQualifiedTypeName, kvp.Key); Assert.Equal("global::System.String", kvp.Value); }, kvp => { Assert.Equal(TagHelperMetadata.Common.PropertyName, kvp.Key); Assert.Equal("MyProperty", kvp.Value); }); }