Allow Invoke \ InvokeAsync methods for a ViewComponent to be defined in base types

Fixes https://github.com/aspnet/Mvc/issues/8397
This commit is contained in:
Pranav K 2018-09-17 17:06:41 -07:00
parent 060698a52f
commit fc86cc3ca1
3 changed files with 239 additions and 33 deletions

View File

@ -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<IMethodSymbol>()
.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<IMethodSymbol> GetInvokeMethods(INamedTypeSymbol type)
{
var methods = new List<IMethodSymbol>();
while (type != null)
{
var currentTypeMethods = type.GetMembers()
.OfType<IMethodSymbol>()
.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<IParameterSymbol> 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;
});
}
}

View File

@ -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;
}
/// <inheritdoc />

View File

@ -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<string> Invoke() => null;
}
public class SyncDerivedViewComponent : StringParameterViewComponent
{
}
public class AsyncDerivedViewComponent : AsyncViewComponentWithNonGenericTask
{
}
public class DerivedViewComponentWithAmbiguity : AsyncViewComponentWithNonGenericTask
{
public string Invoke() => null;
}
public class StaticInvokeAsyncViewComponent
{
public static Task<string> InvokeAsync() => null;
}
public class NonPublicInvokeAsyncViewComponent
{
protected Task<string> InvokeAsync() => null;
}
}