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:
parent
a84f35022e
commit
6e647854fa
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
110
src/Microsoft.CodeAnalysis.Razor.Workspaces/Properties/ViewComponentResources.Designer.cs
generated
Normal file
110
src/Microsoft.CodeAnalysis.Razor.Workspaces/Properties/ViewComponentResources.Designer.cs
generated
Normal 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}&lt;T&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}&lt;T&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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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}&lt;T&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>
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue