diff --git a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/ViewComponentTagHelperDescriptorFactory.cs b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/ViewComponentTagHelperDescriptorFactory.cs index cb64a4611a..a5181fd789 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/ViewComponentTagHelperDescriptorFactory.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/ViewComponentTagHelperDescriptorFactory.cs @@ -59,7 +59,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions var descriptorBuilder = TagHelperDescriptorBuilder.Create(ViewComponentTagHelperConventions.Kind, typeName, assemblyName); descriptorBuilder.SetTypeName(typeName); descriptorBuilder.DisplayName = displayName; - + if (TryFindInvokeMethod(type, out var method, out var diagnostic)) { var methodParameters = method.Parameters; @@ -84,21 +84,15 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions private bool TryFindInvokeMethod(INamedTypeSymbol type, out IMethodSymbol method, out RazorDiagnostic diagnostic) { - var methods = type.GetMembers() - .OfType() - .Where(m => - m.DeclaredAccessibility == Accessibility.Public && - (string.Equals(m.Name, ViewComponentTypes.AsyncMethodName, StringComparison.Ordinal) || - string.Equals(m.Name, ViewComponentTypes.SyncMethodName, StringComparison.Ordinal))) - .ToArray(); + var methods = GetInvokeMethods(type); - if (methods.Length == 0) + if (methods.Count == 0) { - diagnostic = RazorExtensionsDiagnosticFactory.CreateViewComponent_CannotFindMethod(type.ToDisplayString(FullNameTypeDisplayFormat)); + diagnostic = RazorExtensionsDiagnosticFactory.CreateViewComponent_CannotFindMethod(type.ToDisplayString(FullNameTypeDisplayFormat)); method = null; return false; } - else if (methods.Length > 1) + else if (methods.Count > 1) { diagnostic = RazorExtensionsDiagnosticFactory.CreateViewComponent_AmbiguousMethods(type.ToDisplayString(FullNameTypeDisplayFormat)); method = null; @@ -153,6 +147,27 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions return true; } + private static IReadOnlyList GetInvokeMethods(INamedTypeSymbol type) + { + var methods = new List(); + while (type != null) + { + var currentTypeMethods = type.GetMembers() + .OfType() + .Where(m => + m.DeclaredAccessibility == Accessibility.Public && + !m.IsStatic && + (string.Equals(m.Name, ViewComponentTypes.AsyncMethodName, StringComparison.Ordinal) || + string.Equals(m.Name, ViewComponentTypes.SyncMethodName, StringComparison.Ordinal))); + + methods.AddRange(currentTypeMethods); + + type = type.BaseType; + } + + return methods; + } + private void AddRequiredAttributes(ImmutableArray methodParameters, TagMatchingRuleDescriptorBuilder builder) { foreach (var parameter in methodParameters) @@ -164,7 +179,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions builder.Attribute(attributeBuilder => { var lowerKebabName = HtmlConventions.ToHtmlCase(parameter.Name); - attributeBuilder.Name =lowerKebabName; + attributeBuilder.Name = lowerKebabName; }); } } diff --git a/src/Microsoft.AspNetCore.Razor.Language/TagHelperDescriptorComparer.cs b/src/Microsoft.AspNetCore.Razor.Language/TagHelperDescriptorComparer.cs index 64f5206a7b..5ca9c0fe91 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/TagHelperDescriptorComparer.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/TagHelperDescriptorComparer.cs @@ -59,32 +59,81 @@ namespace Microsoft.AspNetCore.Razor.Language return false; } - return descriptorX != null && - string.Equals(descriptorX.Kind, descriptorY.Kind, StringComparison.Ordinal) && - string.Equals(descriptorX.AssemblyName, descriptorY.AssemblyName, StringComparison.Ordinal) && - string.Equals(descriptorX.Name, descriptorY.Name, StringComparison.Ordinal) && - Enumerable.SequenceEqual( - descriptorX.BoundAttributes.OrderBy(attribute => attribute.Name, _stringComparer), - descriptorY.BoundAttributes.OrderBy(attribute => attribute.Name, _stringComparer), - _boundAttributeComparer) && - Enumerable.SequenceEqual( - descriptorX.TagMatchingRules.OrderBy(rule => rule.TagName, _stringComparer), - descriptorY.TagMatchingRules.OrderBy(rule => rule.TagName, _stringComparer), - _tagMatchingRuleComparer) && - (descriptorX.AllowedChildTags == descriptorY.AllowedChildTags || + if (descriptorX == null) + { + return false; + } + + if (!string.Equals(descriptorX.Kind, descriptorY.Kind, StringComparison.Ordinal)) + { + return false; + } + + if (!string.Equals(descriptorX.AssemblyName, descriptorY.AssemblyName, StringComparison.Ordinal)) + { + return false; + } + + if (!string.Equals(descriptorX.Name, descriptorY.Name, StringComparison.Ordinal)) + { + return false; + } + + if (!Enumerable.SequenceEqual( + descriptorX.BoundAttributes.OrderBy(attribute => attribute.Name, _stringComparer), + descriptorY.BoundAttributes.OrderBy(attribute => attribute.Name, _stringComparer), + _boundAttributeComparer)) + { + return false; + } + + if (!Enumerable.SequenceEqual( + descriptorX.TagMatchingRules.OrderBy(rule => rule.TagName, _stringComparer), + descriptorY.TagMatchingRules.OrderBy(rule => rule.TagName, _stringComparer), + _tagMatchingRuleComparer)) + { + return false; + } + + if (!(descriptorX.AllowedChildTags == descriptorY.AllowedChildTags || (descriptorX.AllowedChildTags != null && descriptorY.AllowedChildTags != null && Enumerable.SequenceEqual( descriptorX.AllowedChildTags.OrderBy(childTag => childTag.Name, _stringComparer), descriptorY.AllowedChildTags.OrderBy(childTag => childTag.Name, _stringComparer), - _AllowedChildTagDescriptorComparer))) && - string.Equals(descriptorX.Documentation, descriptorY.Documentation, StringComparison.Ordinal) && - string.Equals(descriptorX.DisplayName, descriptorY.DisplayName, StringComparison.Ordinal) && - string.Equals(descriptorX.TagOutputHint, descriptorY.TagOutputHint, _stringComparison) && - Enumerable.SequenceEqual(descriptorX.Diagnostics, descriptorY.Diagnostics) && - Enumerable.SequenceEqual( - descriptorX.Metadata.OrderBy(metadataX => metadataX.Key, StringComparer.Ordinal), - descriptorY.Metadata.OrderBy(metadataY => metadataY.Key, StringComparer.Ordinal)); + _AllowedChildTagDescriptorComparer)))) + { + return false; + } + + if (!string.Equals(descriptorX.Documentation, descriptorY.Documentation, StringComparison.Ordinal)) + { + return false; + } + + if (!string.Equals(descriptorX.DisplayName, descriptorY.DisplayName, StringComparison.Ordinal)) + { + return false; + } + + if (!string.Equals(descriptorX.TagOutputHint, descriptorY.TagOutputHint, _stringComparison)) + { + return false; + } + + if (!Enumerable.SequenceEqual(descriptorX.Diagnostics, descriptorY.Diagnostics)) + { + return false; + } + + if (!Enumerable.SequenceEqual( + descriptorX.Metadata.OrderBy(metadataX => metadataX.Key, StringComparer.Ordinal), + descriptorY.Metadata.OrderBy(metadataY => metadataY.Key, StringComparer.Ordinal))) + { + return false; + } + + return true; } /// diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Extensions.Test/ViewComponentTagHelperDescriptorFactoryTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Extensions.Test/ViewComponentTagHelperDescriptorFactoryTest.cs index aa0065c770..d44f0169a1 100644 --- a/test/Microsoft.AspNetCore.Mvc.Razor.Extensions.Test/ViewComponentTagHelperDescriptorFactoryTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Razor.Extensions.Test/ViewComponentTagHelperDescriptorFactoryTest.cs @@ -145,6 +145,74 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions Assert.Equal(expectedDescriptor, descriptor, TagHelperDescriptorComparer.CaseSensitive); } + [Fact] + public void CreateDescriptor_ForSyncViewComponentWithInvokeInBaseType_Works() + { + // Arrange + var testCompilation = TestCompilation.Create(_assembly); + var factory = new ViewComponentTagHelperDescriptorFactory(testCompilation); + + var expectedDescriptor = TagHelperDescriptorBuilder.Create( + ViewComponentTagHelperConventions.Kind, + "__Generated__SyncDerivedViewComponentTagHelper", + typeof(SyncDerivedViewComponent).GetTypeInfo().Assembly.GetName().Name) + .TypeName("__Generated__SyncDerivedViewComponentTagHelper") + .DisplayName("SyncDerivedViewComponentTagHelper") + .TagMatchingRuleDescriptor(rule => + rule + .RequireTagName("vc:sync-derived") + .RequireAttributeDescriptor(attribute => attribute.Name("foo")) + .RequireAttributeDescriptor(attribute => attribute.Name("bar"))) + .BoundAttributeDescriptor(attribute => + attribute + .Name("foo") + .PropertyName("foo") + .TypeName(typeof(string).FullName) + .DisplayName("string SyncDerivedViewComponentTagHelper.foo")) + .BoundAttributeDescriptor(attribute => + attribute + .Name("bar") + .PropertyName("bar") + .TypeName(typeof(string).FullName) + .DisplayName("string SyncDerivedViewComponentTagHelper.bar")) + .AddMetadata(ViewComponentTagHelperMetadata.Name, "SyncDerived") + .Build(); + + var viewComponent = testCompilation.GetTypeByMetadataName(typeof(SyncDerivedViewComponent).FullName); + + // Act + var descriptor = factory.CreateDescriptor(viewComponent); + + // Assert + Assert.Equal(expectedDescriptor, descriptor, TagHelperDescriptorComparer.CaseSensitive); + } + + [Fact] + public void CreateDescriptor_ForAsyncViewComponentWithInvokeInBaseType_Works() + { + // Arrange + var testCompilation = TestCompilation.Create(_assembly); + var factory = new ViewComponentTagHelperDescriptorFactory(testCompilation); + + var expectedDescriptor = TagHelperDescriptorBuilder.Create( + ViewComponentTagHelperConventions.Kind, + "__Generated__AsyncDerivedViewComponentTagHelper", + typeof(AsyncDerivedViewComponent).Assembly.GetName().Name) + .TypeName("__Generated__AsyncDerivedViewComponentTagHelper") + .DisplayName("AsyncDerivedViewComponentTagHelper") + .TagMatchingRuleDescriptor(rule => rule.RequireTagName("vc:async-derived")) + .AddMetadata(ViewComponentTagHelperMetadata.Name, "AsyncDerived") + .Build(); + + var viewComponent = testCompilation.GetTypeByMetadataName(typeof(AsyncDerivedViewComponent).FullName); + + // Act + var descriptor = factory.CreateDescriptor(viewComponent); + + // Assert + Assert.Equal(expectedDescriptor, descriptor, TagHelperDescriptorComparer.CaseSensitive); + } + [Fact] public void CreateDescriptor_AddsDiagnostic_ForViewComponentWithNoInvokeMethod() { @@ -162,6 +230,40 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions Assert.Equal(RazorExtensionsDiagnosticFactory.ViewComponent_CannotFindMethod.Id, diagnostic.Id); } + [Fact] + public void CreateDescriptor_AddsDiagnostic_ForViewComponentWithNoInstanceInvokeMethod() + { + // Arrange + var testCompilation = TestCompilation.Create(_assembly); + var factory = new ViewComponentTagHelperDescriptorFactory(testCompilation); + + var viewComponent = testCompilation.GetTypeByMetadataName(typeof(StaticInvokeAsyncViewComponent).FullName); + + // Act + var descriptor = factory.CreateDescriptor(viewComponent); + + // Assert + var diagnostic = Assert.Single(descriptor.GetAllDiagnostics()); + Assert.Equal(RazorExtensionsDiagnosticFactory.ViewComponent_CannotFindMethod.Id, diagnostic.Id); + } + + [Fact] + public void CreateDescriptor_AddsDiagnostic_ForViewComponentWithNoPublicInvokeMethod() + { + // Arrange + var testCompilation = TestCompilation.Create(_assembly); + var factory = new ViewComponentTagHelperDescriptorFactory(testCompilation); + + var viewComponent = testCompilation.GetTypeByMetadataName(typeof(NonPublicInvokeAsyncViewComponent).FullName); + + // Act + var descriptor = factory.CreateDescriptor(viewComponent); + + // Assert + var diagnostic = Assert.Single(descriptor.GetAllDiagnostics()); + Assert.Equal(RazorExtensionsDiagnosticFactory.ViewComponent_CannotFindMethod.Id, diagnostic.Id); + } + [Fact] public void CreateDescriptor_ForViewComponentWithInvokeAsync_UnderstandsGenericTask() { @@ -278,6 +380,23 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions var diagnostic = Assert.Single(descriptor.GetAllDiagnostics()); Assert.Equal(RazorExtensionsDiagnosticFactory.ViewComponent_SyncMethod_CannotReturnTask.Id, diagnostic.Id); } + + [Fact] + public void CreateDescriptor_ForViewComponent_WithAmbiguousMethods() + { + // Arrange + var testCompilation = TestCompilation.Create(_assembly); + var factory = new ViewComponentTagHelperDescriptorFactory(testCompilation); + + var viewComponent = testCompilation.GetTypeByMetadataName(typeof(DerivedViewComponentWithAmbiguity).FullName); + + // Act + var descriptor = factory.CreateDescriptor(viewComponent); + + // Assert + var diagnostic = Assert.Single(descriptor.GetAllDiagnostics()); + Assert.Equal(RazorExtensionsDiagnosticFactory.ViewComponent_AmbiguousMethods.Id, diagnostic.Id); + } } public class StringParameterViewComponent @@ -340,4 +459,27 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions { public Task Invoke() => null; } + + public class SyncDerivedViewComponent : StringParameterViewComponent + { + } + + public class AsyncDerivedViewComponent : AsyncViewComponentWithNonGenericTask + { + } + + public class DerivedViewComponentWithAmbiguity : AsyncViewComponentWithNonGenericTask + { + public string Invoke() => null; + } + + public class StaticInvokeAsyncViewComponent + { + public static Task InvokeAsync() => null; + } + + public class NonPublicInvokeAsyncViewComponent + { + protected Task InvokeAsync() => null; + } } \ No newline at end of file