Add support for `ViewComponentTagHelpers`.

- Hardcoded `ViewComponent` discovery.
- Hardcoded `ViewComponentTagHelperDescriptor` creation.
- Added test to validate that ViewComponents are discovered and transitioned into TagHelpers properly.
- Avoided adding a reference to MVC to prevent circular references. This resulted in custom marker attributes to represent `ViewComponent`s. Also made a lot of use of `ViewComponent` conventions (ending in "ViewComponent").

#932
This commit is contained in:
N. Taylor Mullen 2017-01-17 17:02:15 -08:00
parent a84f35022e
commit 6e647854fa
7 changed files with 1079 additions and 24 deletions

View File

@ -1,7 +1,9 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Razor.Evolution;
using Microsoft.AspNetCore.Razor.Evolution.Legacy;
@ -9,9 +11,18 @@ namespace Microsoft.CodeAnalysis.Razor
{
internal class DefaultTagHelperResolver : TagHelperResolver
{
public DefaultTagHelperResolver(bool designTime)
private static readonly Version SupportedVCTHMvcVersion = new Version(1, 1);
private readonly string ViewComponentAssembly;
public DefaultTagHelperResolver(bool designTime) : this(designTime, ViewComponentTypes.Assembly)
{
}
// Internal for testing
internal DefaultTagHelperResolver(bool designTime, string viewComponentAssembly)
{
DesignTime = designTime;
ViewComponentAssembly = viewComponentAssembly;
}
public bool DesignTime { get; }
@ -19,17 +30,75 @@ namespace Microsoft.CodeAnalysis.Razor
public override IReadOnlyList<TagHelperDescriptor> GetTagHelpers(Compilation compilation)
{
var results = new List<TagHelperDescriptor>();
var errors = new ErrorSink();
// If ITagHelper isn't defined, then we couldn't possibly find anything.
VisitTagHelpers(compilation, results, errors);
VisitViewComponents(compilation, results, errors);
return results;
}
private void VisitTagHelpers(Compilation compilation, List<TagHelperDescriptor> results, ErrorSink errors)
{
var @interface = compilation.GetTypeByMetadataName(TagHelperTypes.ITagHelper);
if (@interface == null)
{
return results;
// If ITagHelper isn't defined, then we couldn't possibly find anything.
return;
}
var types = new List<INamedTypeSymbol>();
var visitor = new Visitor(@interface, types);
var visitor = new TagHelperVisitor(@interface, types);
VisitCompilation(visitor, compilation);
var factory = new DefaultTagHelperDescriptorFactory(compilation, DesignTime);
foreach (var type in types)
{
var descriptors = factory.CreateDescriptors(type, errors);
results.AddRange(descriptors);
}
}
private void VisitViewComponents(Compilation compilation, List<TagHelperDescriptor> results, ErrorSink errors)
{
var mvcViewFeaturesAssembly = compilation.References
.Select(reference => compilation.GetAssemblyOrModuleSymbol(reference))
.OfType<IAssemblySymbol>()
.FirstOrDefault(assembly => string.Equals(assembly.Identity.Name, ViewComponentAssembly, StringComparison.Ordinal));
if (mvcViewFeaturesAssembly == null || mvcViewFeaturesAssembly.Identity.Version < SupportedVCTHMvcVersion)
{
return;
}
var viewComponentAttributeSymbol = compilation.GetTypeByMetadataName(ViewComponentTypes.ViewComponentAttribute);
var nonViewComponentAttributeSymbol = compilation.GetTypeByMetadataName(ViewComponentTypes.NonViewComponentAttribute);
var types = new List<INamedTypeSymbol>();
var visitor = new ViewComponentVisitor(viewComponentAttributeSymbol, viewComponentAttributeSymbol, types);
VisitCompilation(visitor, compilation);
var factory = new ViewComponentTagHelperDescriptorFactory(compilation);
foreach (var type in types)
{
try
{
var descriptor = factory.CreateDescriptor(type);
results.Add(descriptor);
}
catch (Exception ex)
{
errors.OnError(SourceLocation.Zero, ex.Message, length: 0);
}
}
}
private static void VisitCompilation(SymbolVisitor visitor, Compilation compilation)
{
visitor.Visit(compilation.Assembly.GlobalNamespace);
foreach (var reference in compilation.References)
@ -39,26 +108,15 @@ namespace Microsoft.CodeAnalysis.Razor
visitor.Visit(assembly.GlobalNamespace);
}
}
var errors = new ErrorSink();
var factory = new DefaultTagHelperDescriptorFactory(compilation, DesignTime);
foreach (var type in types)
{
var descriptors = factory.CreateDescriptors(type, errors);
results.AddRange(descriptors);
}
return results;
}
// Visits top-level types and finds interface implementations.
internal class Visitor : SymbolVisitor
internal class TagHelperVisitor : SymbolVisitor
{
private INamedTypeSymbol _interface;
private List<INamedTypeSymbol> _results;
public Visitor(INamedTypeSymbol @interface, List<INamedTypeSymbol> results)
public TagHelperVisitor(INamedTypeSymbol @interface, List<INamedTypeSymbol> results)
{
_interface = @interface;
_results = results;
@ -88,5 +146,79 @@ namespace Microsoft.CodeAnalysis.Razor
symbol.AllInterfaces.Contains(_interface);
}
}
internal class ViewComponentVisitor : SymbolVisitor
{
private INamedTypeSymbol _viewComponentAttribute;
private INamedTypeSymbol _nonViewComponentAttribute;
private List<INamedTypeSymbol> _results;
public ViewComponentVisitor(
INamedTypeSymbol viewComponentAttribute,
INamedTypeSymbol nonViewComponentAttribute,
List<INamedTypeSymbol> results)
{
_viewComponentAttribute = viewComponentAttribute;
_nonViewComponentAttribute = nonViewComponentAttribute;
_results = results;
}
public override void VisitNamedType(INamedTypeSymbol symbol)
{
if (IsViewComponent(symbol))
{
_results.Add(symbol);
}
if (symbol.DeclaredAccessibility != Accessibility.Public)
{
return;
}
foreach (var member in symbol.GetTypeMembers())
{
Visit(member);
}
}
public override void VisitNamespace(INamespaceSymbol symbol)
{
foreach (var member in symbol.GetMembers())
{
Visit(member);
}
}
internal bool IsViewComponent(INamedTypeSymbol symbol)
{
if (symbol.DeclaredAccessibility != Accessibility.Public ||
symbol.IsAbstract ||
symbol.IsGenericType ||
AttributeIsDefined(symbol, _nonViewComponentAttribute))
{
return false;
}
return symbol.Name.EndsWith(ViewComponentTypes.ViewComponentSuffix) ||
AttributeIsDefined(symbol, _viewComponentAttribute);
}
private static bool AttributeIsDefined(INamedTypeSymbol type, INamedTypeSymbol queryAttribute)
{
if (type == null)
{
return false;
}
var attribute = type.GetAttributes().Where(a => a.AttributeClass == queryAttribute).FirstOrDefault();
if (attribute != null)
{
return true;
}
return AttributeIsDefined(type.BaseType, queryAttribute);
}
}
}
}

View File

@ -0,0 +1,110 @@
// <auto-generated />
namespace Microsoft.CodeAnalysis.Razor.Workspaces
{
using System.Globalization;
using System.Reflection;
using System.Resources;
internal static class ViewComponentResources
{
private static readonly ResourceManager _resourceManager
= new ResourceManager("Microsoft.CodeAnalysis.Razor.Workspaces.ViewComponentResources", typeof(ViewComponentResources).GetTypeInfo().Assembly);
/// <summary>
/// View component '{0}' must have exactly one public method named '{1}' or '{2}'.
/// </summary>
internal static string ViewComponent_AmbiguousMethods
{
get { return GetString("ViewComponent_AmbiguousMethods"); }
}
/// <summary>
/// View component '{0}' must have exactly one public method named '{1}' or '{2}'.
/// </summary>
internal static string FormatViewComponent_AmbiguousMethods(object p0, object p1, object p2)
{
return string.Format(CultureInfo.CurrentCulture, GetString("ViewComponent_AmbiguousMethods"), p0, p1, p2);
}
/// <summary>
/// Method '{0}' of view component '{1}' should be declared to return {2}&amp;lt;T&amp;gt;.
/// </summary>
internal static string ViewComponent_AsyncMethod_ShouldReturnTask
{
get { return GetString("ViewComponent_AsyncMethod_ShouldReturnTask"); }
}
/// <summary>
/// Method '{0}' of view component '{1}' should be declared to return {2}&amp;lt;T&amp;gt;.
/// </summary>
internal static string FormatViewComponent_AsyncMethod_ShouldReturnTask(object p0, object p1, object p2)
{
return string.Format(CultureInfo.CurrentCulture, GetString("ViewComponent_AsyncMethod_ShouldReturnTask"), p0, p1, p2);
}
/// <summary>
/// Could not find an '{0}' or '{1}' method for the view component '{2}'.
/// </summary>
internal static string ViewComponent_CannotFindMethod
{
get { return GetString("ViewComponent_CannotFindMethod"); }
}
/// <summary>
/// Could not find an '{0}' or '{1}' method for the view component '{2}'.
/// </summary>
internal static string FormatViewComponent_CannotFindMethod(object p0, object p1, object p2)
{
return string.Format(CultureInfo.CurrentCulture, GetString("ViewComponent_CannotFindMethod"), p0, p1, p2);
}
/// <summary>
/// Method '{0}' of view component '{1}' cannot return a {2}.
/// </summary>
internal static string ViewComponent_SyncMethod_CannotReturnTask
{
get { return GetString("ViewComponent_SyncMethod_CannotReturnTask"); }
}
/// <summary>
/// Method '{0}' of view component '{1}' cannot return a {2}.
/// </summary>
internal static string FormatViewComponent_SyncMethod_CannotReturnTask(object p0, object p1, object p2)
{
return string.Format(CultureInfo.CurrentCulture, GetString("ViewComponent_SyncMethod_CannotReturnTask"), p0, p1, p2);
}
/// <summary>
/// Method '{0}' of view component '{1}' should be declared to return a value.
/// </summary>
internal static string ViewComponent_SyncMethod_ShouldReturnValue
{
get { return GetString("ViewComponent_SyncMethod_ShouldReturnValue"); }
}
/// <summary>
/// Method '{0}' of view component '{1}' should be declared to return a value.
/// </summary>
internal static string FormatViewComponent_SyncMethod_ShouldReturnValue(object p0, object p1)
{
return string.Format(CultureInfo.CurrentCulture, GetString("ViewComponent_SyncMethod_ShouldReturnValue"), p0, p1);
}
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);
System.Diagnostics.Debug.Assert(value != null);
if (formatterNames != null)
{
for (var i = 0; i < formatterNames.Length; i++)
{
value = value.Replace("{" + formatterNames[i] + "}", "{" + i + "}");
}
}
return value;
}
}
}

View File

@ -0,0 +1,135 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="ViewComponent_AmbiguousMethods" xml:space="preserve">
<value>View component '{0}' must have exactly one public method named '{1}' or '{2}'.</value>
</data>
<data name="ViewComponent_AsyncMethod_ShouldReturnTask" xml:space="preserve">
<value>Method '{0}' of view component '{1}' should be declared to return {2}&amp;lt;T&amp;gt;.</value>
</data>
<data name="ViewComponent_CannotFindMethod" xml:space="preserve">
<value>Could not find an '{0}' or '{1}' method for the view component '{2}'.</value>
</data>
<data name="ViewComponent_SyncMethod_CannotReturnTask" xml:space="preserve">
<value>Method '{0}' of view component '{1}' cannot return a {2}.</value>
</data>
<data name="ViewComponent_SyncMethod_ShouldReturnValue" xml:space="preserve">
<value>Method '{0}' of view component '{1}' should be declared to return a value.</value>
</data>
</root>

View File

@ -0,0 +1,238 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Evolution;
using Microsoft.CodeAnalysis.Razor.Workspaces;
namespace Microsoft.CodeAnalysis.Razor
{
internal class ViewComponentTagHelperDescriptorFactory
{
private readonly INamedTypeSymbol _viewComponentAttributeSymbol;
private readonly INamedTypeSymbol _genericTaskSymbol;
private readonly INamedTypeSymbol _taskSymbol;
private readonly INamedTypeSymbol _iDictionarySymbol;
private static readonly SymbolDisplayFormat FullNameTypeDisplayFormat =
SymbolDisplayFormat.FullyQualifiedFormat
.WithGlobalNamespaceStyle(SymbolDisplayGlobalNamespaceStyle.Omitted)
.WithMiscellaneousOptions(SymbolDisplayFormat.FullyQualifiedFormat.MiscellaneousOptions & (~SymbolDisplayMiscellaneousOptions.UseSpecialTypes));
public ViewComponentTagHelperDescriptorFactory(Compilation compilation)
{
_viewComponentAttributeSymbol = compilation.GetTypeByMetadataName(ViewComponentTypes.ViewComponentAttribute);
_genericTaskSymbol = compilation.GetTypeByMetadataName(ViewComponentTypes.GenericTask);
_taskSymbol = compilation.GetTypeByMetadataName(ViewComponentTypes.Task);
_iDictionarySymbol = compilation.GetTypeByMetadataName(TagHelperTypes.IDictionary);
}
public virtual TagHelperDescriptor CreateDescriptor(INamedTypeSymbol type)
{
var assemblyName = type.ContainingAssembly.Name;
var shortName = GetShortName(type);
var tagName = $"vc:{DefaultTagHelperDescriptorFactory.ToHtmlCase(shortName)}";
var typeName = $"__Generated__{shortName}ViewComponentTagHelper";
var descriptor = new TagHelperDescriptor
{
TagName = tagName,
TypeName = typeName,
AssemblyName = assemblyName
};
SetAttributeDescriptors(type, descriptor);
descriptor.PropertyBag.Add(ViewComponentTypes.ViewComponentNameKey, shortName);
return descriptor;
}
private void SetAttributeDescriptors(INamedTypeSymbol type, TagHelperDescriptor descriptor)
{
var methodParameters = GetInvokeMethodParameters(type);
var attributeDescriptors = new List<TagHelperAttributeDescriptor>();
var indexerDescriptors = new List<TagHelperAttributeDescriptor>();
var requiredAttributeDescriptors = new List<TagHelperRequiredAttributeDescriptor>();
foreach (var parameter in methodParameters)
{
var lowerKebabName = DefaultTagHelperDescriptorFactory.ToHtmlCase(parameter.Name);
var typeName = parameter.Type.ToDisplayString(FullNameTypeDisplayFormat);
var attributeDescriptor = new TagHelperAttributeDescriptor
{
Name = lowerKebabName,
PropertyName = parameter.Name,
TypeName = typeName
};
attributeDescriptor.IsEnum = parameter.Type.TypeKind == TypeKind.Enum;
attributeDescriptor.IsIndexer = false;
attributeDescriptors.Add(attributeDescriptor);
var indexerDescriptor = GetIndexerAttributeDescriptor(parameter, lowerKebabName);
if (indexerDescriptor != null)
{
indexerDescriptors.Add(indexerDescriptor);
}
else
{
// Set required attributes only for non-indexer attributes. Indexer attributes can't be required attributes
// because there are two ways of setting values for the attribute.
requiredAttributeDescriptors.Add(new TagHelperRequiredAttributeDescriptor
{
Name = lowerKebabName
});
}
}
attributeDescriptors.AddRange(indexerDescriptors);
descriptor.Attributes = attributeDescriptors;
descriptor.RequiredAttributes = requiredAttributeDescriptors;
}
private TagHelperAttributeDescriptor GetIndexerAttributeDescriptor(IParameterSymbol parameter, string name)
{
INamedTypeSymbol dictionaryType;
if ((parameter.Type as INamedTypeSymbol)?.ConstructedFrom == _iDictionarySymbol)
{
dictionaryType = (INamedTypeSymbol)parameter.Type;
}
else if (parameter.Type.AllInterfaces.Any(s => s.ConstructedFrom == _iDictionarySymbol))
{
dictionaryType = parameter.Type.AllInterfaces.First(s => s.ConstructedFrom == _iDictionarySymbol);
}
else
{
dictionaryType = null;
}
if (dictionaryType == null || dictionaryType.TypeArguments[0].SpecialType != SpecialType.System_String)
{
return null;
}
var type = dictionaryType.TypeArguments[1];
var descriptor = new TagHelperAttributeDescriptor
{
Name = name + "-",
PropertyName = parameter.Name,
TypeName = type.ToDisplayString(FullNameTypeDisplayFormat),
IsEnum = type.TypeKind == TypeKind.Enum,
IsIndexer = true
};
return descriptor;
}
private ImmutableArray<IParameterSymbol> GetInvokeMethodParameters(INamedTypeSymbol componentType)
{
var methods = componentType.GetMembers()
.OfType<IMethodSymbol>()
.Where(method =>
method.DeclaredAccessibility == Accessibility.Public &&
(string.Equals(method.Name, ViewComponentTypes.AsyncMethodName, StringComparison.Ordinal) ||
string.Equals(method.Name, ViewComponentTypes.SyncMethodName, StringComparison.Ordinal)))
.ToArray();
if (methods.Length == 0)
{
throw new InvalidOperationException(
ViewComponentResources.FormatViewComponent_CannotFindMethod(ViewComponentTypes.SyncMethodName, ViewComponentTypes.AsyncMethodName, componentType.ToDisplayString(FullNameTypeDisplayFormat)));
}
else if (methods.Length > 1)
{
throw new InvalidOperationException(
ViewComponentResources.FormatViewComponent_AmbiguousMethods(componentType.ToDisplayString(FullNameTypeDisplayFormat), ViewComponentTypes.AsyncMethodName, ViewComponentTypes.SyncMethodName));
}
var selectedMethod = methods[0];
var returnType = selectedMethod.ReturnType as INamedTypeSymbol;
if (string.Equals(selectedMethod.Name, ViewComponentTypes.AsyncMethodName, StringComparison.Ordinal) && returnType != null)
{
if (!returnType.IsGenericType == true ||
returnType.ConstructedFrom == _genericTaskSymbol)
{
throw new InvalidOperationException(ViewComponentResources.FormatViewComponent_AsyncMethod_ShouldReturnTask(
ViewComponentTypes.AsyncMethodName,
componentType.ToDisplayString(FullNameTypeDisplayFormat),
nameof(Task)));
}
}
else if (returnType != null)
{
// Will invoke synchronously. Method must not return void, Task or Task<T>.
if (returnType.SpecialType == SpecialType.System_Void)
{
throw new InvalidOperationException(ViewComponentResources.FormatViewComponent_SyncMethod_ShouldReturnValue(
ViewComponentTypes.SyncMethodName,
componentType.ToDisplayString(FullNameTypeDisplayFormat)));
}
var inheritsFromTask = false;
var currentType = returnType;
while (currentType != null)
{
if (currentType == _taskSymbol)
{
inheritsFromTask = true;
break;
}
currentType = currentType.BaseType;
}
if (inheritsFromTask)
{
throw new InvalidOperationException(ViewComponentResources.FormatViewComponent_SyncMethod_CannotReturnTask(
ViewComponentTypes.SyncMethodName,
componentType.ToDisplayString(FullNameTypeDisplayFormat),
nameof(Task)));
}
}
var methodParameters = selectedMethod.Parameters;
return methodParameters;
}
private string GetShortName(INamedTypeSymbol componentType)
{
var viewComponentAttribute = componentType.GetAttributes().Where(a => a.AttributeClass == _viewComponentAttributeSymbol).FirstOrDefault();
var name = viewComponentAttribute
?.NamedArguments
.Where(namedArgument => string.Equals(namedArgument.Key, ViewComponentTypes.ViewComponent.Name, StringComparison.Ordinal))
.FirstOrDefault()
.Value
.Value as string;
if (!string.IsNullOrEmpty(name))
{
var separatorIndex = name.LastIndexOf('.');
if (separatorIndex >= 0)
{
return name.Substring(separatorIndex + 1);
}
else
{
return name;
}
}
// Get name by convention
if (componentType.Name.EndsWith(ViewComponentTypes.ViewComponentSuffix, StringComparison.OrdinalIgnoreCase))
{
return componentType.Name.Substring(0, componentType.Name.Length - ViewComponentTypes.ViewComponentSuffix.Length);
}
else
{
return componentType.Name;
}
}
}
}

View File

@ -0,0 +1,31 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.CodeAnalysis.Razor
{
internal static class ViewComponentTypes
{
public const string Assembly = "Microsoft.AspNetCore.Mvc.ViewFeatures";
public const string ViewComponentSuffix = "ViewComponent";
public const string ViewComponentAttribute = "Microsoft.AspNetCore.Mvc.ViewComponentAttribute";
public const string NonViewComponentAttribute = "Microsoft.AspNetCore.Mvc.NonViewComponentAttribute";
public const string GenericTask = "System.Threading.Tasks.Task`1";
public const string Task = "System.Threading.Tasks.Task";
public const string ViewComponentNameKey = "ViewComponentName";
public const string AsyncMethodName = "InvokeAsync";
public const string SyncMethodName = "Invoke";
public static class ViewComponent
{
public const string Name = "Name";
}
}
}

View File

@ -16,16 +16,20 @@ namespace Microsoft.CodeAnalysis.Razor.Workspaces
private static INamedTypeSymbol ITagHelperSymbol { get; } = Compilation.GetTypeByMetadataName(TagHelperTypes.ITagHelper);
private DefaultTagHelperResolver.Visitor TestVisitor => new DefaultTagHelperResolver.Visitor(ITagHelperSymbol, new List<INamedTypeSymbol>());
// In practice MVC will provide a marker attribute for ViewComponents. To prevent a circular reference between MVC and Razor
// we can use a test class as a marker.
private static INamedTypeSymbol TestViewComponentAttributeSymbol { get; } = Compilation.GetTypeByMetadataName(typeof(TestViewComponentAttribute).FullName);
private static INamedTypeSymbol TestNonViewComponentAttributeSymbol { get; } = Compilation.GetTypeByMetadataName(typeof(TestNonViewComponentAttribute).FullName);
[Fact]
public void IsTagHelper_PlainTagHelper_ReturnsTrue()
{
// Arrange
var testVisitor = new DefaultTagHelperResolver.TagHelperVisitor(ITagHelperSymbol, new List<INamedTypeSymbol>());
var tagHelperSymbol = Compilation.GetTypeByMetadataName(typeof(Valid_PlainTagHelper).FullName);
// Act
var isTagHelper = TestVisitor.IsTagHelper(tagHelperSymbol);
var isTagHelper = testVisitor.IsTagHelper(tagHelperSymbol);
// Assert
Assert.True(isTagHelper);
@ -35,10 +39,11 @@ namespace Microsoft.CodeAnalysis.Razor.Workspaces
public void IsTagHelper_InheritedTagHelper_ReturnsTrue()
{
// Arrange
var testVisitor = new DefaultTagHelperResolver.TagHelperVisitor(ITagHelperSymbol, new List<INamedTypeSymbol>());
var tagHelperSymbol = Compilation.GetTypeByMetadataName(typeof(Valid_InheritedTagHelper).FullName);
// Act
var isTagHelper = TestVisitor.IsTagHelper(tagHelperSymbol);
var isTagHelper = testVisitor.IsTagHelper(tagHelperSymbol);
// Assert
Assert.True(isTagHelper);
@ -48,10 +53,11 @@ namespace Microsoft.CodeAnalysis.Razor.Workspaces
public void IsTagHelper_AbstractTagHelper_ReturnsFalse()
{
// Arrange
var testVisitor = new DefaultTagHelperResolver.TagHelperVisitor(ITagHelperSymbol, new List<INamedTypeSymbol>());
var tagHelperSymbol = Compilation.GetTypeByMetadataName(typeof(Invalid_AbstractTagHelper).FullName);
// Act
var isTagHelper = TestVisitor.IsTagHelper(tagHelperSymbol);
var isTagHelper = testVisitor.IsTagHelper(tagHelperSymbol);
// Assert
Assert.False(isTagHelper);
@ -61,10 +67,11 @@ namespace Microsoft.CodeAnalysis.Razor.Workspaces
public void IsTagHelper_GenericTagHelper_ReturnsFalse()
{
// Arrange
var testVisitor = new DefaultTagHelperResolver.TagHelperVisitor(ITagHelperSymbol, new List<INamedTypeSymbol>());
var tagHelperSymbol = Compilation.GetTypeByMetadataName(typeof(Invalid_GenericTagHelper<>).FullName);
// Act
var isTagHelper = TestVisitor.IsTagHelper(tagHelperSymbol);
var isTagHelper = testVisitor.IsTagHelper(tagHelperSymbol);
// Assert
Assert.False(isTagHelper);
@ -74,10 +81,11 @@ namespace Microsoft.CodeAnalysis.Razor.Workspaces
public void IsTagHelper_InternalTagHelper_ReturnsFalse()
{
// Arrange
var testVisitor = new DefaultTagHelperResolver.TagHelperVisitor(ITagHelperSymbol, new List<INamedTypeSymbol>());
var tagHelperSymbol = Compilation.GetTypeByMetadataName(typeof(Invalid_InternalTagHelper).FullName);
// Act
var isTagHelper = TestVisitor.IsTagHelper(tagHelperSymbol);
var isTagHelper = testVisitor.IsTagHelper(tagHelperSymbol);
// Assert
Assert.False(isTagHelper);
@ -88,19 +96,177 @@ namespace Microsoft.CodeAnalysis.Razor.Workspaces
{
// Arrange
var resolver = new DefaultTagHelperResolver(designTime: false);
var expectedTypeName = typeof(DefaultTagHelperResolver).FullName + "." + nameof(Invalid_NestedPublicTagHelper);
// Act
var descriptors = resolver.GetTagHelpers(Compilation);
// Assert
var matchingDescriptors = descriptors
.Where(descriptor => string.Equals(descriptor.TypeName, typeof(Invalid_NestedPublicTagHelper).FullName, StringComparison.Ordinal));
.Where(descriptor => string.Equals(descriptor.TypeName, expectedTypeName, StringComparison.Ordinal));
Assert.Empty(matchingDescriptors);
}
[Fact]
public void IsViewComponent_PlainViewComponent_ReturnsTrue()
{
// Arrange
var testVisitor = new DefaultTagHelperResolver.ViewComponentVisitor(
TestViewComponentAttributeSymbol,
TestNonViewComponentAttributeSymbol,
new List<INamedTypeSymbol>());
var tagHelperSymbol = Compilation.GetTypeByMetadataName(typeof(Valid_PlainViewComponent).FullName);
// Act
var isViewComponent = testVisitor.IsViewComponent(tagHelperSymbol);
// Assert
Assert.True(isViewComponent);
}
[Fact]
public void IsViewComponent_DecoratedViewComponent_ReturnsTrue()
{
// Arrange
var testVisitor = new DefaultTagHelperResolver.ViewComponentVisitor(
TestViewComponentAttributeSymbol,
TestNonViewComponentAttributeSymbol,
new List<INamedTypeSymbol>());
var tagHelperSymbol = Compilation.GetTypeByMetadataName(typeof(Valid_DecoratedVC).FullName);
// Act
var isViewComponent = testVisitor.IsViewComponent(tagHelperSymbol);
// Assert
Assert.True(isViewComponent);
}
[Fact]
public void IsViewComponent_InheritedViewComponent_ReturnsTrue()
{
// Arrange
var testVisitor = new DefaultTagHelperResolver.ViewComponentVisitor(
TestViewComponentAttributeSymbol,
TestNonViewComponentAttributeSymbol,
new List<INamedTypeSymbol>());
var tagHelperSymbol = Compilation.GetTypeByMetadataName(typeof(Valid_InheritedVC).FullName);
// Act
var isViewComponent = testVisitor.IsViewComponent(tagHelperSymbol);
// Assert
Assert.True(isViewComponent);
}
[Fact]
public void IsViewComponent_AbstractViewComponent_ReturnsFalse()
{
// Arrange
var testVisitor = new DefaultTagHelperResolver.ViewComponentVisitor(
TestViewComponentAttributeSymbol,
TestNonViewComponentAttributeSymbol,
new List<INamedTypeSymbol>());
var tagHelperSymbol = Compilation.GetTypeByMetadataName(typeof(Invalid_AbstractViewComponent).FullName);
// Act
var isViewComponent = testVisitor.IsViewComponent(tagHelperSymbol);
// Assert
Assert.False(isViewComponent);
}
[Fact]
public void IsViewComponent_GenericViewComponent_ReturnsFalse()
{
// Arrange
var testVisitor = new DefaultTagHelperResolver.ViewComponentVisitor(
TestViewComponentAttributeSymbol,
TestNonViewComponentAttributeSymbol,
new List<INamedTypeSymbol>());
var tagHelperSymbol = Compilation.GetTypeByMetadataName(typeof(Invalid_GenericViewComponent<>).FullName);
// Act
var isViewComponent = testVisitor.IsViewComponent(tagHelperSymbol);
// Assert
Assert.False(isViewComponent);
}
[Fact]
public void IsViewComponent_InternalViewComponent_ReturnsFalse()
{
// Arrange
var testVisitor = new DefaultTagHelperResolver.ViewComponentVisitor(
TestViewComponentAttributeSymbol,
TestNonViewComponentAttributeSymbol,
new List<INamedTypeSymbol>());
var tagHelperSymbol = Compilation.GetTypeByMetadataName(typeof(Invalid_InternalViewComponent).FullName);
// Act
var isViewComponent = testVisitor.IsViewComponent(tagHelperSymbol);
// Assert
Assert.False(isViewComponent);
}
[Fact]
public void GetTagHelpers_NestedViewComponentTagHelpersAreFound()
{
// Arrange
var resolver = new DefaultTagHelperResolver(
designTime: false,
viewComponentAssembly: typeof(DefaultTagHelperResolverTest).Assembly.GetName().Name);
var expectedTypeName = "__Generated__" + nameof(Valid_NestedPublicViewComponent) + "TagHelper";
// Act
var descriptors = resolver.GetTagHelpers(Compilation);
// Assert
Assert.Single(descriptors, descriptor => string.Equals(descriptor.TypeName, expectedTypeName, StringComparison.Ordinal));
}
[Fact]
public void IsViewComponent_DecoratedNonViewComponent_ReturnsFalse()
{
// Arrange
var testVisitor = new DefaultTagHelperResolver.ViewComponentVisitor(
TestViewComponentAttributeSymbol,
TestNonViewComponentAttributeSymbol,
new List<INamedTypeSymbol>());
var tagHelperSymbol = Compilation.GetTypeByMetadataName(typeof(Invalid_DecoratedViewComponent).FullName);
// Act
var isViewComponent = testVisitor.IsViewComponent(tagHelperSymbol);
// Assert
Assert.False(isViewComponent);
}
[Fact]
public void IsViewComponent_InheritedNonViewComponent_ReturnsFalse()
{
// Arrange
var testVisitor = new DefaultTagHelperResolver.ViewComponentVisitor(
TestViewComponentAttributeSymbol,
TestNonViewComponentAttributeSymbol,
new List<INamedTypeSymbol>());
var tagHelperSymbol = Compilation.GetTypeByMetadataName(typeof(Invalid_InheritedViewComponent).FullName);
// Act
var isViewComponent = testVisitor.IsViewComponent(tagHelperSymbol);
// Assert
Assert.False(isViewComponent);
}
public class Invalid_NestedPublicTagHelper : TagHelper
{
}
public class Valid_NestedPublicViewComponent
{
public string Invoke(string foo) => null;
}
}
public abstract class Invalid_AbstractTagHelper : TagHelper
@ -122,4 +288,47 @@ namespace Microsoft.CodeAnalysis.Razor.Workspaces
public class Valid_InheritedTagHelper : Valid_PlainTagHelper
{
}
public abstract class Invalid_AbstractViewComponent
{
}
public class Invalid_GenericViewComponent<T>
{
}
internal class Invalid_InternalViewComponent
{
}
public class Valid_PlainViewComponent
{
}
[TestViewComponent]
public class Valid_DecoratedVC
{
}
public class Valid_InheritedVC : Valid_DecoratedVC
{
}
[TestNonViewComponent]
public class Invalid_DecoratedViewComponent
{
}
[TestViewComponent]
public class Invalid_InheritedViewComponent : Invalid_DecoratedViewComponent
{
}
public class TestViewComponentAttribute : Attribute
{
}
public class TestNonViewComponentAttribute : Attribute
{
}
}

View File

@ -0,0 +1,200 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using Microsoft.AspNetCore.Razor.Evolution;
using Microsoft.CodeAnalysis.Razor.Workspaces.Test;
using Microsoft.CodeAnalysis.Razor.Workspaces.Test.Comparers;
using Xunit;
namespace Microsoft.CodeAnalysis.Razor.Workspaces
{
public class ViewComponentTagHelperDescriptorFactoryTest
{
[Fact]
public void CreateDescriptor_UnderstandsStringParameters()
{
// Arrange
var testCompilation = TestCompilation.Create();
var viewComponent = testCompilation.GetTypeByMetadataName(typeof(StringParameterViewComponent).FullName);
var factory = new ViewComponentTagHelperDescriptorFactory(testCompilation);
var expectedDescriptor = new TagHelperDescriptor
{
TagName = "vc:string-parameter",
TypeName = "__Generated__StringParameterViewComponentTagHelper",
AssemblyName = typeof(StringParameterViewComponent).Assembly.GetName().Name,
Attributes = new List<TagHelperAttributeDescriptor>
{
new TagHelperAttributeDescriptor
{
Name = "foo",
PropertyName = "foo",
TypeName = typeof(string).FullName
},
new TagHelperAttributeDescriptor
{
Name = "bar",
PropertyName = "bar",
TypeName = typeof(string).FullName
}
},
RequiredAttributes = new List<TagHelperRequiredAttributeDescriptor>
{
new TagHelperRequiredAttributeDescriptor
{
Name = "foo"
},
new TagHelperRequiredAttributeDescriptor
{
Name = "bar"
}
}
};
expectedDescriptor.PropertyBag.Add(ViewComponentTypes.ViewComponentNameKey, "StringParameter");
// Act
var descriptor = factory.CreateDescriptor(viewComponent);
// Assert
Assert.Equal(expectedDescriptor, descriptor, CaseSensitiveTagHelperDescriptorComparer.Default);
}
[Fact]
public void CreateDescriptor_UnderstandsVariousParameterTypes()
{
// Arrange
var testCompilation = TestCompilation.Create();
var viewComponent = testCompilation.GetTypeByMetadataName(typeof(VariousParameterViewComponent).FullName);
var factory = new ViewComponentTagHelperDescriptorFactory(testCompilation);
var expectedDescriptor = new TagHelperDescriptor
{
TagName = "vc:various-parameter",
TypeName = "__Generated__VariousParameterViewComponentTagHelper",
AssemblyName = typeof(VariousParameterViewComponent).Assembly.GetName().Name,
Attributes = new List<TagHelperAttributeDescriptor>
{
new TagHelperAttributeDescriptor
{
Name = "test-enum",
PropertyName = "testEnum",
TypeName = typeof(VariousParameterViewComponent).FullName + "." + nameof(VariousParameterViewComponent.TestEnum),
IsEnum = true
},
new TagHelperAttributeDescriptor
{
Name = "test-string",
PropertyName = "testString",
TypeName = typeof(string).FullName
},
new TagHelperAttributeDescriptor
{
Name = "baz",
PropertyName = "baz",
TypeName = typeof(int).FullName
}
},
RequiredAttributes = new List<TagHelperRequiredAttributeDescriptor>
{
new TagHelperRequiredAttributeDescriptor
{
Name = "test-enum"
},
new TagHelperRequiredAttributeDescriptor
{
Name = "test-string"
},
new TagHelperRequiredAttributeDescriptor
{
Name = "baz"
}
}
};
expectedDescriptor.PropertyBag.Add(ViewComponentTypes.ViewComponentNameKey, "VariousParameter");
// Act
var descriptor = factory.CreateDescriptor(viewComponent);
// Assert
Assert.Equal(expectedDescriptor, descriptor, CaseSensitiveTagHelperDescriptorComparer.Default);
}
[Fact]
public void CreateDescriptor_UnderstandsGenericParameters()
{
// Arrange
var testCompilation = TestCompilation.Create();
var viewComponent = testCompilation.GetTypeByMetadataName(typeof(GenericParameterViewComponent).FullName);
var factory = new ViewComponentTagHelperDescriptorFactory(testCompilation);
var expectedDescriptor = new TagHelperDescriptor
{
TagName = "vc:generic-parameter",
TypeName = "__Generated__GenericParameterViewComponentTagHelper",
AssemblyName = typeof(GenericParameterViewComponent).Assembly.GetName().Name,
Attributes = new List<TagHelperAttributeDescriptor>
{
new TagHelperAttributeDescriptor
{
Name = "foo",
PropertyName = "Foo",
TypeName = "System.Collections.Generic.List<System.String>"
},
new TagHelperAttributeDescriptor
{
Name = "bar",
PropertyName = "Bar",
TypeName = "System.Collections.Generic.Dictionary<System.String, System.Int32>"
},
new TagHelperAttributeDescriptor
{
Name = "bar-",
PropertyName = "Bar",
TypeName = typeof(int).FullName,
IsIndexer = true
}
},
RequiredAttributes = new List<TagHelperRequiredAttributeDescriptor>
{
new TagHelperRequiredAttributeDescriptor
{
Name = "foo"
}
}
};
expectedDescriptor.PropertyBag.Add(ViewComponentTypes.ViewComponentNameKey, "GenericParameter");
// Act
var descriptor = factory.CreateDescriptor(viewComponent);
// Assert
Assert.Equal(expectedDescriptor, descriptor, CaseSensitiveTagHelperDescriptorComparer.Default);
}
}
public class StringParameterViewComponent
{
public string Invoke(string foo, string bar) => null;
}
public class VariousParameterViewComponent
{
public string Invoke(TestEnum testEnum, string testString, int baz = 5) => null;
public enum TestEnum
{
A = 1,
B = 2,
C = 3
}
}
public class GenericParameterViewComponent
{
public string Invoke(List<string> Foo, Dictionary<string, int> Bar) => null;
}
}