Add support for '*' at the end of required attributes.

- [TargetElement(Attributes ="prefix-*")] is now supported.
- Added '*' to the list of invalid non whitespace characters in TagHelperDescriptorFactory.
- Modified TagHelperDescriptorProvider to respect suffixed wildcards in TagHelperAttributeDescriptor.Attributes.
- Added tests to validate wildcard required attributes

#361
This commit is contained in:
N. Taylor Mullen 2015-05-20 17:42:29 -07:00
parent 60c47c8874
commit 0e61b49881
6 changed files with 222 additions and 62 deletions

View File

@ -31,7 +31,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
// https://github.com/aspnet/Razor/issues/165
public static ICollection<char> InvalidNonWhitespaceNameCharacters { get; } = new HashSet<char>(
new[] { '@', '!', '<', '/', '?', '[', '>', ']', '=', '"', '\'' });
new[] { '@', '!', '<', '/', '?', '[', '>', ']', '=', '"', '\'', '*' });
/// <summary>
/// Creates a <see cref="TagHelperDescriptor"/> from the given <paramref name="type"/>.
@ -169,6 +169,25 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
bool targetingAttributes,
ErrorSink errorSink)
{
if (!targetingAttributes &&
string.Equals(
name,
TagHelperDescriptorProvider.ElementCatchAllTarget,
StringComparison.OrdinalIgnoreCase))
{
// '*' as the entire name is OK in the TargetElement catch-all case.
return true;
}
else if (targetingAttributes &&
name.EndsWith(
TagHelperDescriptorProvider.RequiredAttributeWildcardSuffix,
StringComparison.OrdinalIgnoreCase))
{
// A single '*' at the end of a required attribute is valid; everywhere else is invalid. Strip it from
// the end so we can validate the rest of the name.
name = name.Substring(0, name.Length - 1);
}
var targetName = targetingAttributes ?
Resources.TagHelperDescriptorFactory_Attribute :
Resources.TagHelperDescriptorFactory_Tag;

View File

@ -12,16 +12,16 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
public sealed class TargetElementAttribute : Attribute
{
public const string CatchAllDescriptorTarget = TagHelperDescriptorProvider.CatchAllDescriptorTarget;
public const string ElementCatchAllTarget = TagHelperDescriptorProvider.ElementCatchAllTarget;
/// <summary>
/// Instantiates a new instance of the <see cref="TargetElementAttribute"/> class with <see cref="Tag"/>
/// set to <c>*</c>.
/// </summary>
/// <remarks>A <c>*</c> <see cref="Tag"/> value indicates an <see cref="ITagHelper"/>
/// <remarks>A <c>*</c> <see cref="Tag"/> value indicates an <see cref="ITagHelper"/>
/// that targets all HTML elements with the required <see cref="Attributes"/>.</remarks>
public TargetElementAttribute()
: this(CatchAllDescriptorTarget)
: this(ElementCatchAllTarget)
{
}
@ -31,7 +31,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
/// <param name="tag">
/// The HTML tag the <see cref="ITagHelper"/> targets.
/// </param>
/// <remarks>A <c>*</c> <paramref name="tag"/> value indicates an <see cref="ITagHelper"/>
/// <remarks>A <c>*</c> <paramref name="tag"/> value indicates an <see cref="ITagHelper"/>
/// that targets all HTML elements with the required <see cref="Attributes"/>.</remarks>
public TargetElementAttribute(string tag)
{
@ -41,14 +41,17 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
/// <summary>
/// The HTML tag the <see cref="ITagHelper"/> targets.
/// </summary>
/// <remarks>A <c>*</c> <see cref="Tag"/> value indicates an <see cref="ITagHelper"/>
/// <remarks>A <c>*</c> <see cref="Tag"/> value indicates an <see cref="ITagHelper"/>
/// that targets all HTML elements with the required <see cref="Attributes"/>.</remarks>
public string Tag { get; }
/// <summary>
/// A comma-separated <see cref="string"/> of attributes the HTML element must contain for the
/// A comma-separated <see cref="string"/> of attribute names the HTML element must contain for the
/// <see cref="ITagHelper"/> to run.
/// </summary>
/// <remarks>
/// <c>*</c> at the end of an attribute name acts as a prefix match.
/// </remarks>
public string Attributes { get; set; }
}
}

View File

@ -133,6 +133,9 @@ namespace Microsoft.AspNet.Razor.TagHelpers
/// <summary>
/// The list of required attribute names the tag helper expects to target an element.
/// </summary>
/// <remarks>
/// <c>*</c> at the end of an attribute name acts as a prefix match.
/// </remarks>
public IReadOnlyList<string> RequiredAttributes { get; private set; }
}
}

View File

@ -12,7 +12,9 @@ namespace Microsoft.AspNet.Razor.TagHelpers
/// </summary>
public class TagHelperDescriptorProvider
{
public const string CatchAllDescriptorTarget = "*";
public const string ElementCatchAllTarget = "*";
public static readonly string RequiredAttributeWildcardSuffix = "*";
private IDictionary<string, HashSet<TagHelperDescriptor>> _registrations;
private string _tagHelperPrefix;
@ -55,7 +57,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers
IEnumerable<TagHelperDescriptor> descriptors;
// Ensure there's a HashSet to use.
if (!_registrations.TryGetValue(CatchAllDescriptorTarget, out catchAllDescriptors))
if (!_registrations.TryGetValue(ElementCatchAllTarget, out catchAllDescriptors))
{
descriptors = new HashSet<TagHelperDescriptor>(TagHelperDescriptorComparer.Default);
}
@ -66,9 +68,9 @@ namespace Microsoft.AspNet.Razor.TagHelpers
// If the requested tag name is the catch-all target, we shouldn't do the work of concatenating extra
// descriptors.
if (!tagName.Equals(CatchAllDescriptorTarget, StringComparison.OrdinalIgnoreCase))
if (!tagName.Equals(ElementCatchAllTarget, StringComparison.OrdinalIgnoreCase))
{
// If we have a tag name associated with the requested name, we need to combine matchingDescriptors
// If we have a tag name associated with the requested name, we need to combine matchingDescriptors
// with all the catch-all descriptors.
HashSet<TagHelperDescriptor> matchingDescriptors;
if (_registrations.TryGetValue(tagName, out matchingDescriptors))
@ -91,7 +93,23 @@ namespace Microsoft.AspNet.Razor.TagHelpers
{
foreach (var requiredAttribute in descriptor.RequiredAttributes)
{
if (!attributeNames.Contains(requiredAttribute, StringComparer.OrdinalIgnoreCase))
// '*' at the end of a required attribute indicates: apply to attributes prefixed with the
// required attribute value.
if (requiredAttribute.EndsWith(
RequiredAttributeWildcardSuffix,
StringComparison.OrdinalIgnoreCase))
{
var prefix = requiredAttribute.Substring(0, requiredAttribute.Length - 1);
if (!attributeNames.Any(
attributeName =>
attributeName.StartsWith(prefix, StringComparison.OrdinalIgnoreCase) &&
!string.Equals(attributeName, prefix, StringComparison.OrdinalIgnoreCase)))
{
return false;
}
}
else if (!attributeNames.Contains(requiredAttribute, StringComparer.OrdinalIgnoreCase))
{
return false;
}
@ -111,8 +129,8 @@ namespace Microsoft.AspNet.Razor.TagHelpers
}
var registrationKey =
string.Equals(descriptor.TagName, CatchAllDescriptorTarget, StringComparison.Ordinal) ?
CatchAllDescriptorTarget :
string.Equals(descriptor.TagName, ElementCatchAllTarget, StringComparison.Ordinal) ?
ElementCatchAllTarget :
descriptor.FullTagName;
// Ensure there's a HashSet to add the descriptor to.

View File

@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using Microsoft.AspNet.Razor.TagHelpers;
@ -29,7 +30,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
new[]
{
new TagHelperDescriptor(
TagHelperDescriptorProvider.CatchAllDescriptorTarget,
TagHelperDescriptorProvider.ElementCatchAllTarget,
typeof(AttributeTargetingTagHelper).FullName,
AssemblyName,
attributes,
@ -41,7 +42,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
new[]
{
new TagHelperDescriptor(
TagHelperDescriptorProvider.CatchAllDescriptorTarget,
TagHelperDescriptorProvider.ElementCatchAllTarget,
typeof(MultiAttributeTargetingTagHelper).FullName,
AssemblyName,
attributes,
@ -53,13 +54,13 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
new[]
{
new TagHelperDescriptor(
TagHelperDescriptorProvider.CatchAllDescriptorTarget,
TagHelperDescriptorProvider.ElementCatchAllTarget,
typeof(MultiAttributeAttributeTargetingTagHelper).FullName,
AssemblyName,
attributes,
requiredAttributes: new[] { "custom" }),
new TagHelperDescriptor(
TagHelperDescriptorProvider.CatchAllDescriptorTarget,
TagHelperDescriptorProvider.ElementCatchAllTarget,
typeof(MultiAttributeAttributeTargetingTagHelper).FullName,
AssemblyName,
attributes,
@ -71,7 +72,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
new[]
{
new TagHelperDescriptor(
TagHelperDescriptorProvider.CatchAllDescriptorTarget,
TagHelperDescriptorProvider.ElementCatchAllTarget,
typeof(InheritedAttributeTargetingTagHelper).FullName,
AssemblyName,
attributes,
@ -168,6 +169,30 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
requiredAttributes: new[] { "class", "style" }),
}
},
{
typeof(AttributeWildcardTargetingTagHelper),
new[]
{
new TagHelperDescriptor(
TagHelperDescriptorProvider.ElementCatchAllTarget,
typeof(AttributeWildcardTargetingTagHelper).FullName,
AssemblyName,
attributes,
requiredAttributes: new[] { "class*" })
}
},
{
typeof(MultiAttributeWildcardTargetingTagHelper),
new[]
{
new TagHelperDescriptor(
TagHelperDescriptorProvider.ElementCatchAllTarget,
typeof(MultiAttributeWildcardTargetingTagHelper).FullName,
AssemblyName,
attributes,
requiredAttributes: new[] { "class*", "style*" })
}
},
};
}
}
@ -1381,51 +1406,65 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
onNameError("'he'lo'", "'"),
}
},
{ "hello*", new[] { onNameError("hello*", "*") } },
{ "*hello", new[] { onNameError("*hello", "*") } },
{ "he*lo", new[] { onNameError("he*lo", "*") } },
{
"*he*lo*",
new[]
{
onNameError("*he*lo*", "*"),
onNameError("*he*lo*", "*"),
onNameError("*he*lo*", "*"),
}
},
{ Environment.NewLine, new[] { whitespaceErrorString } },
{ "\t", new[] { whitespaceErrorString } },
{ " \t ", new[] { whitespaceErrorString } },
{ " ", new[] { whitespaceErrorString } },
{ Environment.NewLine + " ", new[] { whitespaceErrorString } },
{
"! \t\r\n@/<>?[]=\"'",
"! \t\r\n@/<>?[]=\"'*",
new[]
{
onNameError("! \t\r\n@/<>?[]=\"'", "!"),
onNameError("! \t\r\n@/<>?[]=\"'", " "),
onNameError("! \t\r\n@/<>?[]=\"'", "\t"),
onNameError("! \t\r\n@/<>?[]=\"'", "\r"),
onNameError("! \t\r\n@/<>?[]=\"'", "\n"),
onNameError("! \t\r\n@/<>?[]=\"'", "@"),
onNameError("! \t\r\n@/<>?[]=\"'", "/"),
onNameError("! \t\r\n@/<>?[]=\"'", "<"),
onNameError("! \t\r\n@/<>?[]=\"'", ">"),
onNameError("! \t\r\n@/<>?[]=\"'", "?"),
onNameError("! \t\r\n@/<>?[]=\"'", "["),
onNameError("! \t\r\n@/<>?[]=\"'", "]"),
onNameError("! \t\r\n@/<>?[]=\"'", "="),
onNameError("! \t\r\n@/<>?[]=\"'", "\""),
onNameError("! \t\r\n@/<>?[]=\"'", "'"),
onNameError("! \t\r\n@/<>?[]=\"'*", "!"),
onNameError("! \t\r\n@/<>?[]=\"'*", " "),
onNameError("! \t\r\n@/<>?[]=\"'*", "\t"),
onNameError("! \t\r\n@/<>?[]=\"'*", "\r"),
onNameError("! \t\r\n@/<>?[]=\"'*", "\n"),
onNameError("! \t\r\n@/<>?[]=\"'*", "@"),
onNameError("! \t\r\n@/<>?[]=\"'*", "/"),
onNameError("! \t\r\n@/<>?[]=\"'*", "<"),
onNameError("! \t\r\n@/<>?[]=\"'*", ">"),
onNameError("! \t\r\n@/<>?[]=\"'*", "?"),
onNameError("! \t\r\n@/<>?[]=\"'*", "["),
onNameError("! \t\r\n@/<>?[]=\"'*", "]"),
onNameError("! \t\r\n@/<>?[]=\"'*", "="),
onNameError("! \t\r\n@/<>?[]=\"'*", "\""),
onNameError("! \t\r\n@/<>?[]=\"'*", "'"),
onNameError("! \t\r\n@/<>?[]=\"'*", "*"),
}
},
{
"! \tv\ra\nl@i/d<>?[]=\"'",
"! \tv\ra\nl@i/d<>?[]=\"'*",
new[]
{
onNameError("! \tv\ra\nl@i/d<>?[]=\"'", "!"),
onNameError("! \tv\ra\nl@i/d<>?[]=\"'", " "),
onNameError("! \tv\ra\nl@i/d<>?[]=\"'", "\t"),
onNameError("! \tv\ra\nl@i/d<>?[]=\"'", "\r"),
onNameError("! \tv\ra\nl@i/d<>?[]=\"'", "\n"),
onNameError("! \tv\ra\nl@i/d<>?[]=\"'", "@"),
onNameError("! \tv\ra\nl@i/d<>?[]=\"'", "/"),
onNameError("! \tv\ra\nl@i/d<>?[]=\"'", "<"),
onNameError("! \tv\ra\nl@i/d<>?[]=\"'", ">"),
onNameError("! \tv\ra\nl@i/d<>?[]=\"'", "?"),
onNameError("! \tv\ra\nl@i/d<>?[]=\"'", "["),
onNameError("! \tv\ra\nl@i/d<>?[]=\"'", "]"),
onNameError("! \tv\ra\nl@i/d<>?[]=\"'", "="),
onNameError("! \tv\ra\nl@i/d<>?[]=\"'", "\""),
onNameError("! \tv\ra\nl@i/d<>?[]=\"'", "'"),
onNameError("! \tv\ra\nl@i/d<>?[]=\"'*", "!"),
onNameError("! \tv\ra\nl@i/d<>?[]=\"'*", " "),
onNameError("! \tv\ra\nl@i/d<>?[]=\"'*", "\t"),
onNameError("! \tv\ra\nl@i/d<>?[]=\"'*", "\r"),
onNameError("! \tv\ra\nl@i/d<>?[]=\"'*", "\n"),
onNameError("! \tv\ra\nl@i/d<>?[]=\"'*", "@"),
onNameError("! \tv\ra\nl@i/d<>?[]=\"'*", "/"),
onNameError("! \tv\ra\nl@i/d<>?[]=\"'*", "<"),
onNameError("! \tv\ra\nl@i/d<>?[]=\"'*", ">"),
onNameError("! \tv\ra\nl@i/d<>?[]=\"'*", "?"),
onNameError("! \tv\ra\nl@i/d<>?[]=\"'*", "["),
onNameError("! \tv\ra\nl@i/d<>?[]=\"'*", "]"),
onNameError("! \tv\ra\nl@i/d<>?[]=\"'*", "="),
onNameError("! \tv\ra\nl@i/d<>?[]=\"'*", "\""),
onNameError("! \tv\ra\nl@i/d<>?[]=\"'*", "'"),
onNameError("! \tv\ra\nl@i/d<>?[]=\"'*", "*"),
}
},
};
@ -1441,6 +1480,16 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
return data;
}
[TargetElement(Attributes = "class*")]
private class AttributeWildcardTargetingTagHelper : TagHelper
{
}
[TargetElement(Attributes = "class*,style*")]
private class MultiAttributeWildcardTargetingTagHelper : TagHelper
{
}
[TargetElement(Attributes = "class")]
private class AttributeTargetingTagHelper : TagHelper
{

View File

@ -25,20 +25,34 @@ namespace Microsoft.AspNet.Razor.TagHelpers
assemblyName: "SomeAssembly",
attributes: Enumerable.Empty<TagHelperAttributeDescriptor>(),
requiredAttributes: new[] { "class", "style" });
var inputWildcardPrefixDescriptor = new TagHelperDescriptor(
tagName: "input",
typeName: "InputWildCardAttribute",
assemblyName: "SomeAssembly",
attributes: Enumerable.Empty<TagHelperAttributeDescriptor>(),
requiredAttributes: new[] { "nodashprefix*" });
var catchAllDescriptor = new TagHelperDescriptor(
tagName: TagHelperDescriptorProvider.CatchAllDescriptorTarget,
tagName: TagHelperDescriptorProvider.ElementCatchAllTarget,
typeName: "CatchAllTagHelper",
assemblyName: "SomeAssembly",
attributes: Enumerable.Empty<TagHelperAttributeDescriptor>(),
requiredAttributes: new[] { "class" });
var catchAllDescriptor2 = new TagHelperDescriptor(
tagName: TagHelperDescriptorProvider.CatchAllDescriptorTarget,
tagName: TagHelperDescriptorProvider.ElementCatchAllTarget,
typeName: "CatchAllTagHelper",
assemblyName: "SomeAssembly",
attributes: Enumerable.Empty<TagHelperAttributeDescriptor>(),
requiredAttributes: new[] { "custom", "class" });
var catchAllWildcardPrefixDescriptor = new TagHelperDescriptor(
tagName: TagHelperDescriptorProvider.ElementCatchAllTarget,
typeName: "CatchAllWildCardAttribute",
assemblyName: "SomeAssembly",
attributes: Enumerable.Empty<TagHelperAttributeDescriptor>(),
requiredAttributes: new[] { "prefix-*" });
var defaultAvailableDescriptors =
new[] { divDescriptor, inputDescriptor, catchAllDescriptor, catchAllDescriptor2 };
var defaultWildcardDescriptors =
new[] { inputWildcardPrefixDescriptor, catchAllWildcardPrefixDescriptor };
return new TheoryData<
string, // tagName
@ -73,29 +87,83 @@ namespace Microsoft.AspNet.Razor.TagHelpers
new[] { inputDescriptor, catchAllDescriptor }
},
{
TagHelperDescriptorProvider.CatchAllDescriptorTarget,
TagHelperDescriptorProvider.ElementCatchAllTarget,
new[] { "custom" },
defaultAvailableDescriptors,
Enumerable.Empty<TagHelperDescriptor>()
},
{
TagHelperDescriptorProvider.CatchAllDescriptorTarget,
TagHelperDescriptorProvider.ElementCatchAllTarget,
new[] { "class" },
defaultAvailableDescriptors,
new[] { catchAllDescriptor }
},
{
TagHelperDescriptorProvider.CatchAllDescriptorTarget,
TagHelperDescriptorProvider.ElementCatchAllTarget,
new[] { "class", "style" },
defaultAvailableDescriptors,
new[] { catchAllDescriptor }
},
{
TagHelperDescriptorProvider.CatchAllDescriptorTarget,
TagHelperDescriptorProvider.ElementCatchAllTarget,
new[] { "class", "custom" },
defaultAvailableDescriptors,
new[] { catchAllDescriptor, catchAllDescriptor2 }
},
{
"input",
new[] { "nodashprefixA" },
defaultWildcardDescriptors,
new[] { inputWildcardPrefixDescriptor }
},
{
"input",
new[] { "nodashprefix-ABC-DEF", "random" },
defaultWildcardDescriptors,
new[] { inputWildcardPrefixDescriptor }
},
{
"input",
new[] { "prefixABCnodashprefix" },
defaultWildcardDescriptors,
Enumerable.Empty<TagHelperDescriptor>()
},
{
"input",
new[] { "prefix-" },
defaultWildcardDescriptors,
Enumerable.Empty<TagHelperDescriptor>()
},
{
"input",
new[] { "nodashprefix" },
defaultWildcardDescriptors,
Enumerable.Empty<TagHelperDescriptor>()
},
{
"input",
new[] { "prefix-A" },
defaultWildcardDescriptors,
new[] { catchAllWildcardPrefixDescriptor }
},
{
"input",
new[] { "prefix-ABC-DEF", "random" },
defaultWildcardDescriptors,
new[] { catchAllWildcardPrefixDescriptor }
},
{
"input",
new[] { "prefix-abc", "nodashprefix-def" },
defaultWildcardDescriptors,
new[] { inputWildcardPrefixDescriptor, catchAllWildcardPrefixDescriptor }
},
{
"input",
new[] { "class", "prefix-abc", "onclick", "nodashprefix-def", "style" },
defaultWildcardDescriptors,
new[] { inputWildcardPrefixDescriptor, catchAllWildcardPrefixDescriptor }
},
};
}
}
@ -124,7 +192,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers
// Arrange
var catchAllDescriptor = CreatePrefixedDescriptor(
"th",
TagHelperDescriptorProvider.CatchAllDescriptorTarget,
TagHelperDescriptorProvider.ElementCatchAllTarget,
"foo1");
var descriptors = new[] { catchAllDescriptor };
var provider = new TagHelperDescriptorProvider(descriptors);
@ -159,7 +227,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers
public void GetDescriptors_ReturnsCatchAllDescriptorsForPrefixedTags()
{
// Arrange
var catchAllDescriptor = CreatePrefixedDescriptor("th:", TagHelperDescriptorProvider.CatchAllDescriptorTarget, "foo1");
var catchAllDescriptor = CreatePrefixedDescriptor("th:", TagHelperDescriptorProvider.ElementCatchAllTarget, "foo1");
var descriptors = new[] { catchAllDescriptor };
var provider = new TagHelperDescriptorProvider(descriptors);
@ -230,14 +298,14 @@ namespace Microsoft.AspNet.Razor.TagHelpers
var divDescriptor = new TagHelperDescriptor("div", "foo1", "SomeAssembly");
var spanDescriptor = new TagHelperDescriptor("span", "foo2", "SomeAssembly");
var catchAllDescriptor = new TagHelperDescriptor(
TagHelperDescriptorProvider.CatchAllDescriptorTarget,
TagHelperDescriptorProvider.ElementCatchAllTarget,
"foo3",
"SomeAssembly");
var descriptors = new TagHelperDescriptor[] { divDescriptor, spanDescriptor, catchAllDescriptor };
var provider = new TagHelperDescriptorProvider(descriptors);
// Act
var retrievedDescriptors = provider.GetDescriptors(TagHelperDescriptorProvider.CatchAllDescriptorTarget, attributeNames: Enumerable.Empty<string>());
var retrievedDescriptors = provider.GetDescriptors(TagHelperDescriptorProvider.ElementCatchAllTarget, attributeNames: Enumerable.Empty<string>());
// Assert
var descriptor = Assert.Single(retrievedDescriptors);
@ -251,7 +319,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers
var divDescriptor = new TagHelperDescriptor("div", "foo1", "SomeAssembly");
var spanDescriptor = new TagHelperDescriptor("span", "foo2", "SomeAssembly");
var catchAllDescriptor = new TagHelperDescriptor(
TagHelperDescriptorProvider.CatchAllDescriptorTarget,
TagHelperDescriptorProvider.ElementCatchAllTarget,
"foo3",
"SomeAssembly");
var descriptors = new TagHelperDescriptor[] { divDescriptor, spanDescriptor, catchAllDescriptor };