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:
parent
060698a52f
commit
fc86cc3ca1
|
|
@ -59,7 +59,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions
|
||||||
var descriptorBuilder = TagHelperDescriptorBuilder.Create(ViewComponentTagHelperConventions.Kind, typeName, assemblyName);
|
var descriptorBuilder = TagHelperDescriptorBuilder.Create(ViewComponentTagHelperConventions.Kind, typeName, assemblyName);
|
||||||
descriptorBuilder.SetTypeName(typeName);
|
descriptorBuilder.SetTypeName(typeName);
|
||||||
descriptorBuilder.DisplayName = displayName;
|
descriptorBuilder.DisplayName = displayName;
|
||||||
|
|
||||||
if (TryFindInvokeMethod(type, out var method, out var diagnostic))
|
if (TryFindInvokeMethod(type, out var method, out var diagnostic))
|
||||||
{
|
{
|
||||||
var methodParameters = method.Parameters;
|
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)
|
private bool TryFindInvokeMethod(INamedTypeSymbol type, out IMethodSymbol method, out RazorDiagnostic diagnostic)
|
||||||
{
|
{
|
||||||
var methods = type.GetMembers()
|
var methods = GetInvokeMethods(type);
|
||||||
.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();
|
|
||||||
|
|
||||||
if (methods.Length == 0)
|
if (methods.Count == 0)
|
||||||
{
|
{
|
||||||
diagnostic = RazorExtensionsDiagnosticFactory.CreateViewComponent_CannotFindMethod(type.ToDisplayString(FullNameTypeDisplayFormat));
|
diagnostic = RazorExtensionsDiagnosticFactory.CreateViewComponent_CannotFindMethod(type.ToDisplayString(FullNameTypeDisplayFormat));
|
||||||
method = null;
|
method = null;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
else if (methods.Length > 1)
|
else if (methods.Count > 1)
|
||||||
{
|
{
|
||||||
diagnostic = RazorExtensionsDiagnosticFactory.CreateViewComponent_AmbiguousMethods(type.ToDisplayString(FullNameTypeDisplayFormat));
|
diagnostic = RazorExtensionsDiagnosticFactory.CreateViewComponent_AmbiguousMethods(type.ToDisplayString(FullNameTypeDisplayFormat));
|
||||||
method = null;
|
method = null;
|
||||||
|
|
@ -153,6 +147,27 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions
|
||||||
return true;
|
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)
|
private void AddRequiredAttributes(ImmutableArray<IParameterSymbol> methodParameters, TagMatchingRuleDescriptorBuilder builder)
|
||||||
{
|
{
|
||||||
foreach (var parameter in methodParameters)
|
foreach (var parameter in methodParameters)
|
||||||
|
|
@ -164,7 +179,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions
|
||||||
builder.Attribute(attributeBuilder =>
|
builder.Attribute(attributeBuilder =>
|
||||||
{
|
{
|
||||||
var lowerKebabName = HtmlConventions.ToHtmlCase(parameter.Name);
|
var lowerKebabName = HtmlConventions.ToHtmlCase(parameter.Name);
|
||||||
attributeBuilder.Name =lowerKebabName;
|
attributeBuilder.Name = lowerKebabName;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -59,32 +59,81 @@ namespace Microsoft.AspNetCore.Razor.Language
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return descriptorX != null &&
|
if (descriptorX == null)
|
||||||
string.Equals(descriptorX.Kind, descriptorY.Kind, StringComparison.Ordinal) &&
|
{
|
||||||
string.Equals(descriptorX.AssemblyName, descriptorY.AssemblyName, StringComparison.Ordinal) &&
|
return false;
|
||||||
string.Equals(descriptorX.Name, descriptorY.Name, StringComparison.Ordinal) &&
|
}
|
||||||
Enumerable.SequenceEqual(
|
|
||||||
descriptorX.BoundAttributes.OrderBy(attribute => attribute.Name, _stringComparer),
|
if (!string.Equals(descriptorX.Kind, descriptorY.Kind, StringComparison.Ordinal))
|
||||||
descriptorY.BoundAttributes.OrderBy(attribute => attribute.Name, _stringComparer),
|
{
|
||||||
_boundAttributeComparer) &&
|
return false;
|
||||||
Enumerable.SequenceEqual(
|
}
|
||||||
descriptorX.TagMatchingRules.OrderBy(rule => rule.TagName, _stringComparer),
|
|
||||||
descriptorY.TagMatchingRules.OrderBy(rule => rule.TagName, _stringComparer),
|
if (!string.Equals(descriptorX.AssemblyName, descriptorY.AssemblyName, StringComparison.Ordinal))
|
||||||
_tagMatchingRuleComparer) &&
|
{
|
||||||
(descriptorX.AllowedChildTags == descriptorY.AllowedChildTags ||
|
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 &&
|
(descriptorX.AllowedChildTags != null &&
|
||||||
descriptorY.AllowedChildTags != null &&
|
descriptorY.AllowedChildTags != null &&
|
||||||
Enumerable.SequenceEqual(
|
Enumerable.SequenceEqual(
|
||||||
descriptorX.AllowedChildTags.OrderBy(childTag => childTag.Name, _stringComparer),
|
descriptorX.AllowedChildTags.OrderBy(childTag => childTag.Name, _stringComparer),
|
||||||
descriptorY.AllowedChildTags.OrderBy(childTag => childTag.Name, _stringComparer),
|
descriptorY.AllowedChildTags.OrderBy(childTag => childTag.Name, _stringComparer),
|
||||||
_AllowedChildTagDescriptorComparer))) &&
|
_AllowedChildTagDescriptorComparer))))
|
||||||
string.Equals(descriptorX.Documentation, descriptorY.Documentation, StringComparison.Ordinal) &&
|
{
|
||||||
string.Equals(descriptorX.DisplayName, descriptorY.DisplayName, StringComparison.Ordinal) &&
|
return false;
|
||||||
string.Equals(descriptorX.TagOutputHint, descriptorY.TagOutputHint, _stringComparison) &&
|
}
|
||||||
Enumerable.SequenceEqual(descriptorX.Diagnostics, descriptorY.Diagnostics) &&
|
|
||||||
Enumerable.SequenceEqual(
|
if (!string.Equals(descriptorX.Documentation, descriptorY.Documentation, StringComparison.Ordinal))
|
||||||
descriptorX.Metadata.OrderBy(metadataX => metadataX.Key, StringComparer.Ordinal),
|
{
|
||||||
descriptorY.Metadata.OrderBy(metadataY => metadataY.Key, StringComparer.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 />
|
/// <inheritdoc />
|
||||||
|
|
|
||||||
|
|
@ -145,6 +145,74 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions
|
||||||
Assert.Equal(expectedDescriptor, descriptor, TagHelperDescriptorComparer.CaseSensitive);
|
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]
|
[Fact]
|
||||||
public void CreateDescriptor_AddsDiagnostic_ForViewComponentWithNoInvokeMethod()
|
public void CreateDescriptor_AddsDiagnostic_ForViewComponentWithNoInvokeMethod()
|
||||||
{
|
{
|
||||||
|
|
@ -162,6 +230,40 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions
|
||||||
Assert.Equal(RazorExtensionsDiagnosticFactory.ViewComponent_CannotFindMethod.Id, diagnostic.Id);
|
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]
|
[Fact]
|
||||||
public void CreateDescriptor_ForViewComponentWithInvokeAsync_UnderstandsGenericTask()
|
public void CreateDescriptor_ForViewComponentWithInvokeAsync_UnderstandsGenericTask()
|
||||||
{
|
{
|
||||||
|
|
@ -278,6 +380,23 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions
|
||||||
var diagnostic = Assert.Single(descriptor.GetAllDiagnostics());
|
var diagnostic = Assert.Single(descriptor.GetAllDiagnostics());
|
||||||
Assert.Equal(RazorExtensionsDiagnosticFactory.ViewComponent_SyncMethod_CannotReturnTask.Id, diagnostic.Id);
|
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
|
public class StringParameterViewComponent
|
||||||
|
|
@ -340,4 +459,27 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions
|
||||||
{
|
{
|
||||||
public Task<string> Invoke() => null;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue