Add `[HtmlAttributeName(..., DictionaryAttributePrefix="prefix")]` part III

- #89 remainder
- support adding attributes (that aren't otherwise bound) to a tag helper dictionary
- use two `TagHelperAttributeDescriptor`s for dictionary and indexer
 - most exising descriptor properties have two meanings depending on new `IsIndexer`
 - add `TagHelperAttributeDescriptor.IsNameMatch()`
- create no `TagHelperAttributeDescriptor`s if property name or `HtmlAttributeNameAttribute`
  is invalid
 - avoid corner case misfeatures where invalidity removes just one descriptor
- extend handling of invalid attribute names to include `[HtmlAttributeName]`
- handle prefix matches in `TagHelperBlockRewriter`
 - add parse error when resolved dictionary key is `string.Empty`
- generate code for indexer property assignments
 - add code generation for runtime error if using indexer when property is `null`
 - use new `GeneratedTagHelperContext.FormatInvalidIndexerAssignmentMethodName` for message
- code generation now handles attributes in source order; thus above errors occur only when
  expected if dictionary is also initialized in the Razor source
 - surprisingly generation order change did not break existing tests!

nits:
- improve `TagHelperDescriptorFactory_InvalidBoundAttributeName` wording
 - rename resource to `TagHelperDescriptorFactory_InvalidAttributeNameOrPrefixStart`
- correct order of arguments to `FormatTagHelperDescriptorFactory_InvalidBoundAttributeName`
- correct `TagHelperBlockRewriter_TagHelperAttributeListMustBeWellFormed` resource
- correct `TagHelperDescriptorFactoryTest` test names
 - remove a few unnecessary `ToArray()` calls
- update `TagHelperAttributeDescriptorComparer` comments
This commit is contained in:
Doug Bunting 2015-05-20 16:10:10 -07:00
parent 1879ac6427
commit 60c47c8874
22 changed files with 2562 additions and 461 deletions

View File

@ -186,6 +186,38 @@ namespace Microsoft.AspNet.Razor.Runtime
return GetString("TagHelperDescriptorFactory_Attribute");
}
/// <summary>
/// name
/// </summary>
internal static string TagHelperDescriptorFactory_Name
{
get { return GetString("TagHelperDescriptorFactory_Name"); }
}
/// <summary>
/// name
/// </summary>
internal static string FormatTagHelperDescriptorFactory_Name()
{
return GetString("TagHelperDescriptorFactory_Name");
}
/// <summary>
/// prefix
/// </summary>
internal static string TagHelperDescriptorFactory_Prefix
{
get { return GetString("TagHelperDescriptorFactory_Prefix"); }
}
/// <summary>
/// prefix
/// </summary>
internal static string FormatTagHelperDescriptorFactory_Prefix()
{
return GetString("TagHelperDescriptorFactory_Prefix");
}
/// <summary>
/// Tag
/// </summary>
@ -203,19 +235,67 @@ namespace Microsoft.AspNet.Razor.Runtime
}
/// <summary>
/// Invalid tag helper bound property '{0}.{1}'. Tag helpers cannot bind to HTML attributes beginning with '{2}'.
/// Invalid tag helper bound property '{0}.{1}'. Tag helpers cannot bind to HTML attributes with {2} '{3}' because {2} contains a '{4}' character.
/// </summary>
internal static string TagHelperDescriptorFactory_InvalidBoundAttributeName
internal static string TagHelperDescriptorFactory_InvalidAttributeNameOrPrefixCharacter
{
get { return GetString("TagHelperDescriptorFactory_InvalidBoundAttributeName"); }
get { return GetString("TagHelperDescriptorFactory_InvalidAttributeNameOrPrefixCharacter"); }
}
/// <summary>
/// Invalid tag helper bound property '{0}.{1}'. Tag helpers cannot bind to HTML attributes beginning with '{2}'.
/// Invalid tag helper bound property '{0}.{1}'. Tag helpers cannot bind to HTML attributes with {2} '{3}' because {2} contains a '{4}' character.
/// </summary>
internal static string FormatTagHelperDescriptorFactory_InvalidBoundAttributeName(object p0, object p1, object p2)
internal static string FormatTagHelperDescriptorFactory_InvalidAttributeNameOrPrefixCharacter(object p0, object p1, object p2, object p3, object p4)
{
return string.Format(CultureInfo.CurrentCulture, GetString("TagHelperDescriptorFactory_InvalidBoundAttributeName"), p0, p1, p2);
return string.Format(CultureInfo.CurrentCulture, GetString("TagHelperDescriptorFactory_InvalidAttributeNameOrPrefixCharacter"), p0, p1, p2, p3, p4);
}
/// <summary>
/// Invalid tag helper bound property '{0}.{1}'. Tag helpers cannot bind to HTML attributes with {2} '{3}' because {2} starts with '{4}'.
/// </summary>
internal static string TagHelperDescriptorFactory_InvalidAttributeNameOrPrefixStart
{
get { return GetString("TagHelperDescriptorFactory_InvalidAttributeNameOrPrefixStart"); }
}
/// <summary>
/// Invalid tag helper bound property '{0}.{1}'. Tag helpers cannot bind to HTML attributes with {2} '{3}' because {2} starts with '{4}'.
/// </summary>
internal static string FormatTagHelperDescriptorFactory_InvalidAttributeNameOrPrefixStart(object p0, object p1, object p2, object p3, object p4)
{
return string.Format(CultureInfo.CurrentCulture, GetString("TagHelperDescriptorFactory_InvalidAttributeNameOrPrefixStart"), p0, p1, p2, p3, p4);
}
/// <summary>
/// Invalid tag helper bound property '{0}.{1}'. Tag helpers cannot bind to HTML attributes with a whitespace {2}.
/// </summary>
internal static string TagHelperDescriptorFactory_InvalidAttributeNameOrPrefixWhitespace
{
get { return GetString("TagHelperDescriptorFactory_InvalidAttributeNameOrPrefixWhitespace"); }
}
/// <summary>
/// Invalid tag helper bound property '{0}.{1}'. Tag helpers cannot bind to HTML attributes with a whitespace {2}.
/// </summary>
internal static string FormatTagHelperDescriptorFactory_InvalidAttributeNameOrPrefixWhitespace(object p0, object p1, object p2)
{
return string.Format(CultureInfo.CurrentCulture, GetString("TagHelperDescriptorFactory_InvalidAttributeNameOrPrefixWhitespace"), p0, p1, p2);
}
/// <summary>
/// Invalid tag helper bound property '{0}.{1}'. '{2}.{3}' must be null unless property type implements '{4}'.
/// </summary>
internal static string TagHelperDescriptorFactory_InvalidAttributePrefix
{
get { return GetString("TagHelperDescriptorFactory_InvalidAttributePrefix"); }
}
/// <summary>
/// Invalid tag helper bound property '{0}.{1}'. '{2}.{3}' must be null unless property type implements '{4}'.
/// </summary>
internal static string FormatTagHelperDescriptorFactory_InvalidAttributePrefix(object p0, object p1, object p2, object p3, object p4)
{
return string.Format(CultureInfo.CurrentCulture, GetString("TagHelperDescriptorFactory_InvalidAttributePrefix"), p0, p1, p2, p3, p4);
}
/// <summary>

View File

@ -1,17 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
<!--
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
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>
@ -26,36 +26,36 @@
<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
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
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
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
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
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
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
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
@ -150,11 +150,26 @@
<data name="TagHelperDescriptorFactory_Attribute" xml:space="preserve">
<value>Attribute</value>
</data>
<data name="TagHelperDescriptorFactory_Name" xml:space="preserve">
<value>name</value>
</data>
<data name="TagHelperDescriptorFactory_Prefix" xml:space="preserve">
<value>prefix</value>
</data>
<data name="TagHelperDescriptorFactory_Tag" xml:space="preserve">
<value>Tag</value>
</data>
<data name="TagHelperDescriptorFactory_InvalidBoundAttributeName" xml:space="preserve">
<value>Invalid tag helper bound property '{0}.{1}'. Tag helpers cannot bind to HTML attributes beginning with '{2}'.</value>
<data name="TagHelperDescriptorFactory_InvalidAttributeNameOrPrefixCharacter" xml:space="preserve">
<value>Invalid tag helper bound property '{0}.{1}'. Tag helpers cannot bind to HTML attributes with {2} '{3}' because {2} contains a '{4}' character.</value>
</data>
<data name="TagHelperDescriptorFactory_InvalidAttributeNameOrPrefixStart" xml:space="preserve">
<value>Invalid tag helper bound property '{0}.{1}'. Tag helpers cannot bind to HTML attributes with {2} '{3}' because {2} starts with '{4}'.</value>
</data>
<data name="TagHelperDescriptorFactory_InvalidAttributeNameOrPrefixWhitespace" xml:space="preserve">
<value>Invalid tag helper bound property '{0}.{1}'. Tag helpers cannot bind to HTML attributes with a whitespace {2}.</value>
</data>
<data name="TagHelperDescriptorFactory_InvalidAttributePrefix" xml:space="preserve">
<value>Invalid tag helper bound property '{0}.{1}'. '{2}.{3}' must be null unless property type implements '{4}'.</value>
</data>
<data name="TagHelperAttributeList_CannotAddWithNullName" xml:space="preserve">
<value>Cannot add a '{0}' with a null '{1}'.</value>

View File

@ -11,6 +11,8 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
public sealed class HtmlAttributeNameAttribute : Attribute
{
private string _dictionaryAttributePrefix;
/// <summary>
/// Instantiates a new instance of the <see cref="HtmlAttributeNameAttribute"/> class.
/// </summary>
@ -29,5 +31,39 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
/// HTML attribute name of the associated property.
/// </summary>
public string Name { get; }
/// <summary>
/// Gets or sets the prefix used to match HTML attribute names. Matching attributes are added to the
/// associated property (an <see cref="System.Collections.Generic.IDictionary{TKey, TValue}"/>).
/// </summary>
/// <remarks>
/// If non-<c>null</c> associated property must be compatible with
/// <see cref="System.Collections.Generic.IDictionary{TKey, TValue}"/> where <c>TKey</c> is
/// <see cref="string"/>.
/// </remarks>
/// <value>
/// If associated property is compatible with
/// <see cref="System.Collections.Generic.IDictionary{TKey, TValue}"/>, default value is <c>Name + "-"</c>.
/// Otherwise default value is <c>null</c>.
/// </value>
public string DictionaryAttributePrefix
{
get
{
return _dictionaryAttributePrefix;
}
set
{
_dictionaryAttributePrefix = value;
DictionaryAttributePrefixSet = true;
}
}
/// <summary>
/// Gets an indication whether <see cref="DictionaryAttributePrefix"/> has been set. Used to distinguish an
/// uninitialized <see cref="DictionaryAttributePrefix"/> value from an explicit <c>null</c> setting.
/// </summary>
/// <value><c>true</c> if <see cref="DictionaryAttributePrefix"/> was set. <c>false</c> otherwise.</value>
public bool DictionaryAttributePrefixSet { get; private set; }
}
}

View File

@ -38,7 +38,9 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
/// </summary>
/// <param name="assemblyName">The assembly name that contains <paramref name="type"/>.</param>
/// <param name="type">The type to create a <see cref="TagHelperDescriptor"/> from.</param>
/// <returns>A <see cref="TagHelperDescriptor"/> that describes the given <paramref name="type"/>.</returns>
/// <returns>
/// A collection of <see cref="TagHelperDescriptor"/>s that describe the given <paramref name="type"/>.
/// </returns>
public static IEnumerable<TagHelperDescriptor> CreateDescriptors(
string assemblyName,
[NotNull] Type type,
@ -209,48 +211,195 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
var accessibleProperties = type.GetRuntimeProperties().Where(IsAccessibleProperty);
var attributeDescriptors = new List<TagHelperAttributeDescriptor>();
// Keep indexer descriptors separate to avoid sorting the combined list later.
var indexerDescriptors = new List<TagHelperAttributeDescriptor>();
foreach (var property in accessibleProperties)
{
var descriptor = ToAttributeDescriptor(property);
var attributeNameAttribute = property.GetCustomAttribute<HtmlAttributeNameAttribute>(inherit: false);
var descriptor = ToAttributeDescriptor(property, attributeNameAttribute);
if (ValidateTagHelperAttributeDescriptor(descriptor, type, errorSink))
{
bool isInvalid;
var indexerDescriptor = ToIndexerAttributeDescriptor(
property,
attributeNameAttribute,
parentType: type,
errorSink: errorSink,
defaultPrefix: descriptor.Name + "-",
isInvalid: out isInvalid);
if (indexerDescriptor != null &&
!ValidateTagHelperAttributeDescriptor(indexerDescriptor, type, errorSink))
{
isInvalid = true;
}
if (isInvalid)
{
// HtmlAttributeNameAttribute was not valid. Ignore this property completely.
continue;
}
attributeDescriptors.Add(descriptor);
if (indexerDescriptor != null)
{
indexerDescriptors.Add(indexerDescriptor);
}
}
}
attributeDescriptors.AddRange(indexerDescriptors);
return attributeDescriptors;
}
private static bool ValidateTagHelperAttributeDescriptor(
// Internal for testing.
internal static bool ValidateTagHelperAttributeDescriptor(
TagHelperAttributeDescriptor attributeDescriptor,
Type parentType,
ErrorSink errorSink)
{
var nameOrPrefix = attributeDescriptor.IsIndexer ?
Resources.TagHelperDescriptorFactory_Prefix :
Resources.TagHelperDescriptorFactory_Name;
return ValidateTagHelperAttributeNameOrPrefix(
attributeDescriptor.Name,
parentType,
attributeDescriptor.PropertyName,
errorSink,
nameOrPrefix);
}
private static bool ValidateTagHelperAttributeNameOrPrefix(
string attributeNameOrPrefix,
Type parentType,
string propertyName,
ErrorSink errorSink,
string nameOrPrefix)
{
if (string.IsNullOrEmpty(attributeNameOrPrefix))
{
// HtmlAttributeNameAttribute validates Name is non-null and non-empty. Both are valid for
// DictionaryAttributePrefix. (Empty DictionaryAttributePrefix is a corner case which would bind every
// attribute of a target element. Likely not particularly useful but unclear what minimum length
// should be required and what scenarios a minimum length would break.)
return true;
}
if (string.IsNullOrWhiteSpace(attributeNameOrPrefix))
{
// Provide a single error if the entire name is whitespace, not an error per character.
errorSink.OnError(
SourceLocation.Zero,
Resources.FormatTagHelperDescriptorFactory_InvalidAttributeNameOrPrefixWhitespace(
parentType.FullName,
propertyName,
nameOrPrefix));
return false;
}
// data-* attributes are explicitly not implemented by user agents and are not intended for use on
// the server; therefore it's invalid for TagHelpers to bind to them.
if (attributeDescriptor.Name.StartsWith(DataDashPrefix, StringComparison.OrdinalIgnoreCase))
if (attributeNameOrPrefix.StartsWith(DataDashPrefix, StringComparison.OrdinalIgnoreCase))
{
errorSink.OnError(
SourceLocation.Zero,
Resources.FormatTagHelperDescriptorFactory_InvalidBoundAttributeName(
attributeDescriptor.PropertyName,
Resources.FormatTagHelperDescriptorFactory_InvalidAttributeNameOrPrefixStart(
parentType.FullName,
propertyName,
nameOrPrefix,
attributeNameOrPrefix,
DataDashPrefix));
return false;
}
return true;
var isValid = true;
foreach (var character in attributeNameOrPrefix)
{
if (char.IsWhiteSpace(character) || InvalidNonWhitespaceNameCharacters.Contains(character))
{
errorSink.OnError(
SourceLocation.Zero,
Resources.FormatTagHelperDescriptorFactory_InvalidAttributeNameOrPrefixCharacter(
parentType.FullName,
propertyName,
nameOrPrefix,
attributeNameOrPrefix,
character));
isValid = false;
}
}
return isValid;
}
private static TagHelperAttributeDescriptor ToAttributeDescriptor(PropertyInfo property)
private static TagHelperAttributeDescriptor ToAttributeDescriptor(
PropertyInfo property,
HtmlAttributeNameAttribute attributeNameAttribute)
{
var attributeNameAttribute = property.GetCustomAttribute<HtmlAttributeNameAttribute>(inherit: false);
var attributeName = attributeNameAttribute != null ?
attributeNameAttribute.Name :
ToHtmlCase(property.Name);
return new TagHelperAttributeDescriptor(attributeName, property.Name, property.PropertyType.FullName);
return new TagHelperAttributeDescriptor(
attributeName,
property.Name,
property.PropertyType.FullName,
isIndexer: false);
}
private static TagHelperAttributeDescriptor ToIndexerAttributeDescriptor(
PropertyInfo property,
HtmlAttributeNameAttribute attributeNameAttribute,
Type parentType,
ErrorSink errorSink,
string defaultPrefix,
out bool isInvalid)
{
isInvalid = false;
var dictionaryTypeArguments = ClosedGenericMatcher.ExtractGenericInterface(
property.PropertyType,
typeof(IDictionary<,>))
?.GenericTypeArguments;
if (dictionaryTypeArguments?[0] != typeof(string))
{
if (attributeNameAttribute?.DictionaryAttributePrefix != null)
{
// DictionaryAttributePrefix is not supported unless associated with an
// IDictionary<string, TValue> property.
isInvalid = true;
errorSink.OnError(
SourceLocation.Zero,
Resources.FormatTagHelperDescriptorFactory_InvalidAttributePrefix(
parentType.FullName,
property.Name,
nameof(HtmlAttributeNameAttribute),
nameof(HtmlAttributeNameAttribute.DictionaryAttributePrefix),
"IDictionary<string, TValue>"));
}
return null;
}
// Potential prefix case. Use default prefix (based on name)?
var useDefault = attributeNameAttribute == null || !attributeNameAttribute.DictionaryAttributePrefixSet;
var prefix = useDefault ? defaultPrefix : attributeNameAttribute.DictionaryAttributePrefix;
if (prefix == null)
{
// DictionaryAttributePrefix explicitly set to null. Ignore.
return null;
}
return new TagHelperAttributeDescriptor(
name: prefix,
propertyName: property.Name,
typeName: dictionaryTypeArguments[1].FullName,
isIndexer: true);
}
private static bool IsAccessibleProperty(PropertyInfo property)

View File

@ -4,6 +4,7 @@
"dependencies": {
"Microsoft.AspNet.Razor": "4.0.0-*",
"Microsoft.Framework.BufferEntryCollection.Sources": { "type": "build", "version": "1.0.0-*" },
"Microsoft.Framework.ClosedGenericMatcher.Sources": { "type": "build", "version": "1.0.0-*" },
"Microsoft.Framework.CopyOnWriteDictionary.Sources": { "type": "build", "version": "1.0.0-*" },
"Microsoft.Framework.NotNullAttribute.Sources": { "type": "build", "version": "1.0.0-*" }
},

View File

@ -69,10 +69,9 @@ namespace Microsoft.AspNet.Razor.Generator.Compiler.CSharp
// Determine what attributes exist in the element and divide them up.
var htmlAttributes = chunk.Attributes;
var attributeDescriptors = tagHelperDescriptors.SelectMany(descriptor => descriptor.Attributes);
var unboundHtmlAttributes = htmlAttributes.Where(
attribute => !attributeDescriptors.Any(
descriptor => string.Equals(attribute.Key, descriptor.Name, StringComparison.OrdinalIgnoreCase)));
var attributeDescriptors = tagHelperDescriptors.SelectMany(descriptor => descriptor.Attributes).ToArray();
var unboundHtmlAttributes = htmlAttributes.Where(attribute => !attributeDescriptors.Any(
attributeDescriptor => attributeDescriptor.IsNameMatch(attribute.Key)));
RenderUnboundHTMLAttributes(unboundHtmlAttributes);
@ -182,6 +181,7 @@ namespace Microsoft.AspNet.Razor.Generator.Compiler.CSharp
// Render all of the bound attribute values for the tag helper.
RenderBoundHTMLAttributes(
chunk.Attributes,
tagHelperDescriptor.TypeName,
tagHelperVariableName,
tagHelperDescriptor.Attributes,
htmlAttributeValues);
@ -190,81 +190,126 @@ namespace Microsoft.AspNet.Razor.Generator.Compiler.CSharp
private void RenderBoundHTMLAttributes(
IList<KeyValuePair<string, Chunk>> chunkAttributes,
string tagHelperTypeName,
string tagHelperVariableName,
IEnumerable<TagHelperAttributeDescriptor> attributeDescriptors,
Dictionary<string, string> htmlAttributeValues)
{
foreach (var attributeDescriptor in attributeDescriptors)
// Track dictionary properties we have confirmed are non-null.
var confirmedDictionaries = new HashSet<string>(StringComparer.Ordinal);
// First attribute wins, even if there are duplicates.
var distinctAttributes = chunkAttributes.Distinct(KeyValuePairKeyComparer.Default);
// Go through the HTML attributes in source order, assigning to properties or indexers as we go.
foreach (var attributeKeyValuePair in distinctAttributes)
{
var matchingAttributes = chunkAttributes.Where(
kvp => string.Equals(kvp.Key, attributeDescriptor.Name, StringComparison.OrdinalIgnoreCase));
if (matchingAttributes.Any())
var attributeName = attributeKeyValuePair.Key;
var attributeValueChunk = attributeKeyValuePair.Value;
if (attributeValueChunk == null)
{
// First attribute wins, even if there's duplicates.
var firstAttribute = matchingAttributes.First();
var attributeValueChunk = firstAttribute.Value;
// Minimized attributes are not valid for bound attributes. TagHelperBlockRewriter has already
// logged an error if it was a bound attribute; so we can skip.
continue;
}
// Minimized attributes are not valid for bound attributes. There will be an error for the bound
// attribute logged by TagHelperBlockRewriter already so we can skip.
if (attributeValueChunk == null)
// Find the matching TagHelperAttributeDescriptor.
var attributeDescriptor = attributeDescriptors.FirstOrDefault(
descriptor => descriptor.IsNameMatch(attributeName));
if (attributeDescriptor == null)
{
// Attribute is not bound to a property or indexer in this tag helper.
continue;
}
// We capture the tag helper's property value accessor so we can retrieve it later (if we need to).
var valueAccessor = string.Format(
CultureInfo.InvariantCulture,
"{0}.{1}",
tagHelperVariableName,
attributeDescriptor.PropertyName);
if (attributeDescriptor.IsIndexer)
{
// Need a different valueAccessor in this case. But first need to throw a reasonable Exception at
// runtime if the property is null. The check is not required at design time.
if (!_designTimeMode && confirmedDictionaries.Add(attributeDescriptor.PropertyName))
{
_writer
.Write("if (")
.Write(valueAccessor)
.WriteLine(" == null)");
using (_writer.BuildScope())
{
// System is in Host.NamespaceImports for all MVC scenarios. No need to generate FullName
// of InvalidOperationException type.
_writer
.Write("throw ")
.WriteStartNewObject(nameof(InvalidOperationException))
.WriteStartMethodInvocation(_tagHelperContext.FormatInvalidIndexerAssignmentMethodName)
.WriteStringLiteral(attributeName)
.WriteParameterSeparator()
.WriteStringLiteral(tagHelperTypeName)
.WriteParameterSeparator()
.WriteStringLiteral(attributeDescriptor.PropertyName)
.WriteEndMethodInvocation(endLine: false) // End of method call
.WriteEndMethodInvocation(endLine: true); // End of new expression / throw statement
}
}
var dictionaryKey = attributeName.Substring(attributeDescriptor.Name.Length);
valueAccessor = string.Format(
CultureInfo.InvariantCulture,
"{0}.{1}[\"{2}\"]",
tagHelperVariableName,
attributeDescriptor.PropertyName,
dictionaryKey);
}
// If we haven't recorded this attribute value before then we need to record its value.
var attributeValueRecorded = htmlAttributeValues.ContainsKey(attributeName);
if (!attributeValueRecorded)
{
// We only need to create attribute values once per HTML element (not once per tag helper).
// We're saving the value accessor so we can retrieve it later if there are more tag
// helpers that need the value.
htmlAttributeValues.Add(attributeName, valueAccessor);
// Bufferable attributes are attributes that can have Razor code inside of them. Such
// attributes have string values and may be calculated using a temporary TextWriter or other
// buffer.
var bufferableAttribute = attributeDescriptor.IsStringProperty;
RenderNewAttributeValueAssignment(
attributeDescriptor,
bufferableAttribute,
attributeValueChunk,
valueAccessor);
// Execution contexts are a runtime feature.
if (_designTimeMode)
{
continue;
}
// We capture the tag helpers property value accessor so we can retrieve it later (if we need to).
var valueAccessor = string.Format(
CultureInfo.InvariantCulture,
"{0}.{1}",
tagHelperVariableName,
attributeDescriptor.PropertyName);
// If we haven't recorded this attribute value before then we need to record its value.
var attributeValueRecorded = htmlAttributeValues.ContainsKey(attributeDescriptor.Name);
if (!attributeValueRecorded)
{
// We only need to create attribute values once per HTML element (not once per tag helper).
// We're saving the value accessor so we can retrieve it later if there are more tag
// helpers that need the value.
htmlAttributeValues.Add(attributeDescriptor.Name, valueAccessor);
// Bufferable attributes are attributes that can have Razor code inside of them. Such
// attributes have string values and may be calculated using a temporary TextWriter or other
// buffer.
var bufferableAttribute = attributeDescriptor.IsStringProperty;
RenderNewAttributeValueAssignment(
attributeDescriptor,
bufferableAttribute,
attributeValueChunk,
valueAccessor);
// Execution contexts are a runtime feature.
if (_designTimeMode)
{
continue;
}
// We need to inform the context of the attribute value.
var attributeName = firstAttribute.Key;
_writer
.WriteStartInstanceMethodInvocation(
ExecutionContextVariableName,
_tagHelperContext.ExecutionContextAddTagHelperAttributeMethodName)
.WriteStringLiteral(attributeName)
.WriteParameterSeparator()
.Write(valueAccessor)
.WriteEndMethodInvocation();
}
else
{
// The attribute value has already been recorded, lets retrieve it from the stored value
// accessors.
_writer
.WriteStartAssignment(valueAccessor)
.Write(htmlAttributeValues[attributeDescriptor.Name])
.WriteLine(";");
}
// We need to inform the context of the attribute value.
_writer
.WriteStartInstanceMethodInvocation(
ExecutionContextVariableName,
_tagHelperContext.ExecutionContextAddTagHelperAttributeMethodName)
.WriteStringLiteral(attributeName)
.WriteParameterSeparator()
.Write(valueAccessor)
.WriteEndMethodInvocation();
}
else
{
// The attribute value has already been recorded, lets retrieve it from the stored value
// accessors.
_writer
.WriteStartAssignment(valueAccessor)
.Write(htmlAttributeValues[attributeName])
.WriteLine(";");
}
}
}
@ -580,6 +625,26 @@ namespace Microsoft.AspNet.Razor.Generator.Compiler.CSharp
return true;
}
// An IEqualityComparer for string -> Chunk mappings which compares only the Key.
private class KeyValuePairKeyComparer : IEqualityComparer<KeyValuePair<string, Chunk>>
{
public static KeyValuePairKeyComparer Default = new KeyValuePairKeyComparer();
private KeyValuePairKeyComparer()
{
}
public bool Equals(KeyValuePair<string, Chunk> keyValuePairX, KeyValuePair<string, Chunk> keyValuePairY)
{
return string.Equals(keyValuePairX.Key, keyValuePairY.Key, StringComparison.OrdinalIgnoreCase);
}
public int GetHashCode(KeyValuePair<string, Chunk> keyValuePair)
{
return keyValuePair.Key.GetHashCode();
}
}
// A CSharpCodeVisitor which does not HTML encode values. Used when rendering bound string attribute values.
private class CSharpLiteralCodeVisitor : CSharpCodeVisitor
{

View File

@ -22,6 +22,7 @@ namespace Microsoft.AspNet.Razor.Generator
ExecutionContextAddMinimizedHtmlAttributeMethodName = "AddMinimizedHtmlAttribute";
ExecutionContextAddHtmlAttributeMethodName = "AddHtmlAttribute";
ExecutionContextOutputPropertyName = "Output";
FormatInvalidIndexerAssignmentMethodName = "FormatInvalidIndexerAssignment";
MarkAsHtmlEncodedMethodName = "Html.Raw";
StartTagHelperWritingScopeMethodName = "StartTagHelperWritingScope";
EndTagHelperWritingScopeMethodName = "EndTagHelperWritingScope";
@ -78,6 +79,21 @@ namespace Microsoft.AspNet.Razor.Generator
/// </summary>
public string ExecutionContextOutputPropertyName { get; set; }
/// <summary>
/// The name of the method used to format an error message about using an indexer when the tag helper property
/// is <c>null</c>.
/// </summary>
/// <remarks>
/// Method signature should be
/// <code>
/// public string FormatInvalidIndexerAssignment(
/// string attributeName, // Name of the HTML attribute associated with the indexer.
/// string tagHelperTypeName, // Full name of the tag helper type.
/// string propertyName) // Dictionary property in the tag helper.
/// </code>
/// </remarks>
public string FormatInvalidIndexerAssignmentMethodName { get; set; }
/// <summary>
/// The name of the method used to wrap a <see cref="string"/> value and mark it as HTML-encoded.
/// </summary>

View File

@ -1,7 +1,6 @@
// 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.Diagnostics;
using System.Linq;
@ -65,16 +64,18 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
// Only want to track the attribute if we succeeded in parsing its corresponding Block/Span.
if (result != null)
{
SourceLocation? errorLocation = null;
// Check if it's a bound attribute that is minimized or if it's a bound non-string attribute that
// is null or whitespace.
if ((result.IsBoundAttribute && result.AttributeValueNode == null) ||
(result.IsBoundNonStringAttribute &&
IsNullOrWhitespaceAttributeValue(result.AttributeValueNode)))
{
var errorLocation = GetAttributeNameStartLocation(child);
errorLocation = GetAttributeNameStartLocation(child);
errorSink.OnError(
errorLocation,
errorLocation.Value,
RazorResources.FormatRewriterError_EmptyTagHelperBoundAttribute(
result.AttributeName,
tagName,
@ -82,6 +83,23 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
result.AttributeName.Length);
}
// Check if the attribute was a prefix match for a tag helper dictionary property but the
// dictionary key would be the empty string.
if (result.IsMissingDictionaryKey)
{
if (!errorLocation.HasValue)
{
errorLocation = GetAttributeNameStartLocation(child);
}
errorSink.OnError(
errorLocation.Value,
RazorResources.FormatTagHelperBlockRewriter_IndexerAttributeNameMustIncludeKey(
result.AttributeName,
tagName),
result.AttributeName.Length);
}
attributes.Add(
new KeyValuePair<string, SyntaxTreeNode>(result.AttributeName, result.AttributeValueNode));
}
@ -224,24 +242,19 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
{
errorSink.OnError(
span.Start,
RazorResources.TagHelperBlockRewriter_TagHelperAttributeListMustBeWelformed,
RazorResources.TagHelperBlockRewriter_TagHelperAttributeListMustBeWellFormed,
span.Content.Length);
}
return null;
}
bool isBoundNonStringAttribute;
var result = new TryParseResult
{
IsBoundAttribute = IsBoundAttribute(name, descriptors, out isBoundNonStringAttribute),
AttributeName = name,
};
result.IsBoundNonStringAttribute = isBoundNonStringAttribute;
var result = CreateTryParseResult(name, descriptors);
// If we're not after an equal then we should treat the value as if it were a minimized attribute.
var attributeValueBuilder = afterEquals ? builder : null;
result.AttributeValueNode = CreateMarkupAttribute(name, attributeValueBuilder, isBoundNonStringAttribute);
result.AttributeValueNode =
CreateMarkupAttribute(name, attributeValueBuilder, result.IsBoundNonStringAttribute);
return result;
}
@ -286,13 +299,7 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
}
// Have a name now. Able to determine correct isBoundNonStringAttribute value.
bool isBoundNonStringAttribute;
var result = new TryParseResult
{
IsBoundAttribute = IsBoundAttribute(name, descriptors, out isBoundNonStringAttribute),
AttributeName = name,
};
result.IsBoundNonStringAttribute = isBoundNonStringAttribute;
var result = CreateTryParseResult(name, descriptors);
// Remove first child i.e. foo="
builder.Children.RemoveAt(0);
@ -324,13 +331,13 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
if (block.Children.Count() == 1)
{
var child = block.Children.First() as Span;
if (child != null)
{
// After pulling apart the block we just have a value span.
var spanBuilder = new SpanBuilder(child);
result.AttributeValueNode = CreateMarkupAttribute(name, spanBuilder, isBoundNonStringAttribute);
result.AttributeValueNode =
CreateMarkupAttribute(name, spanBuilder, result.IsBoundNonStringAttribute);
return result;
}
@ -485,17 +492,23 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
return firstBoundAttribute?.TypeName;
}
// Determines whether an attribute with the given name is bound to a non-string tag helper property.
private static bool IsBoundAttribute(
string name,
IEnumerable<TagHelperDescriptor> descriptors,
out bool isBoundNonStringAttribute)
// Create a TryParseResult for given name, filling in binding details.
private static TryParseResult CreateTryParseResult(string name, IEnumerable<TagHelperDescriptor> descriptors)
{
var firstBoundAttribute = FindFirstBoundAttribute(name, descriptors);
var isBoundAttribute = firstBoundAttribute != null;
isBoundNonStringAttribute = isBoundAttribute && !firstBoundAttribute.IsStringProperty;
var isBoundNonStringAttribute = isBoundAttribute && !firstBoundAttribute.IsStringProperty;
var isMissingDictionaryKey = isBoundAttribute &&
firstBoundAttribute.IsIndexer &&
name.Length == firstBoundAttribute.Name.Length;
return isBoundAttribute;
return new TryParseResult
{
AttributeName = name,
IsBoundAttribute = isBoundAttribute,
IsBoundNonStringAttribute = isBoundNonStringAttribute,
IsMissingDictionaryKey = isMissingDictionaryKey,
};
}
// Finds first TagHelperAttributeDescriptor matching given name.
@ -503,9 +516,13 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
string name,
IEnumerable<TagHelperDescriptor> descriptors)
{
return descriptors
// Non-indexers (exact HTML attribute name matches) have higher precedence than indexers (prefix matches).
// Attributes already sorted to ensure this precedence.
var firstBoundAttribute = descriptors
.SelectMany(descriptor => descriptor.Attributes)
.FirstOrDefault(attribute => string.Equals(attribute.Name, name, StringComparison.OrdinalIgnoreCase));
.FirstOrDefault(attributeDescriptor => attributeDescriptor.IsNameMatch(name));
return firstBoundAttribute;
}
private static bool IsQuote(HtmlSymbol htmlSymbol)
@ -523,6 +540,8 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
public bool IsBoundAttribute { get; set; }
public bool IsBoundNonStringAttribute { get; set; }
public bool IsMissingDictionaryKey { get; set; }
}
}
}

View File

@ -1367,19 +1367,35 @@ namespace Microsoft.AspNet.Razor
}
/// <summary>
/// TagHelper attributes must be welformed.
/// TagHelper attributes must be well-formed.
/// </summary>
internal static string TagHelperBlockRewriter_TagHelperAttributeListMustBeWelformed
internal static string TagHelperBlockRewriter_TagHelperAttributeListMustBeWellFormed
{
get { return GetString("TagHelperBlockRewriter_TagHelperAttributeListMustBeWelformed"); }
get { return GetString("TagHelperBlockRewriter_TagHelperAttributeListMustBeWellFormed"); }
}
/// <summary>
/// TagHelper attributes must be welformed.
/// TagHelper attributes must be well-formed.
/// </summary>
internal static string FormatTagHelperBlockRewriter_TagHelperAttributeListMustBeWelformed()
internal static string FormatTagHelperBlockRewriter_TagHelperAttributeListMustBeWellFormed()
{
return GetString("TagHelperBlockRewriter_TagHelperAttributeListMustBeWelformed");
return GetString("TagHelperBlockRewriter_TagHelperAttributeListMustBeWellFormed");
}
/// <summary>
/// The tag helper attribute '{0}' in element '{1}' is missing a key. The syntax is '&lt;{1} {0}{{ key }}="value"&gt;'.
/// </summary>
internal static string TagHelperBlockRewriter_IndexerAttributeNameMustIncludeKey
{
get { return GetString("TagHelperBlockRewriter_IndexerAttributeNameMustIncludeKey"); }
}
/// <summary>
/// The tag helper attribute '{0}' in element '{1}' is missing a key. The syntax is '&lt;{1} {0}{{ key }}="value"&gt;'.
/// </summary>
internal static string FormatTagHelperBlockRewriter_IndexerAttributeNameMustIncludeKey(object p0, object p1)
{
return string.Format(CultureInfo.CurrentCulture, GetString("TagHelperBlockRewriter_IndexerAttributeNameMustIncludeKey"), p0, p1);
}
/// <summary>

View File

@ -1,17 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
<!--
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
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>
@ -26,36 +26,36 @@
<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
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
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
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
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
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
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
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
@ -390,8 +390,11 @@ Instead, wrap the contents of the block in "{{}}":
<data name="TagHelpersParseTreeRewriter_MissingCloseAngle" xml:space="preserve">
<value>Missing close angle for tag helper '{0}'.</value>
</data>
<data name="TagHelperBlockRewriter_TagHelperAttributeListMustBeWelformed" xml:space="preserve">
<value>TagHelper attributes must be welformed.</value>
<data name="TagHelperBlockRewriter_TagHelperAttributeListMustBeWellFormed" xml:space="preserve">
<value>TagHelper attributes must be well-formed.</value>
</data>
<data name="TagHelperBlockRewriter_IndexerAttributeNameMustIncludeKey" xml:space="preserve">
<value>The tag helper attribute '{0}' in element '{1}' is missing a key. The syntax is '&lt;{1} {0}{{ key }}="value"&gt;'.</value>
</data>
<data name="TagHelpers_AttributeExpressionRequired" xml:space="preserve">
<value>Non-string tag helper attribute values must not be empty. Add an expression to this attribute value.</value>

View File

@ -18,6 +18,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers
name,
propertyInfo.Name,
propertyInfo.PropertyType.FullName,
isIndexer: false,
isStringProperty: propertyInfo.PropertyType == typeof(string))
{
}
@ -25,48 +26,77 @@ namespace Microsoft.AspNet.Razor.TagHelpers
/// <summary>
/// Instantiates a new instance of the <see cref="TagHelperAttributeDescriptor"/> class.
/// </summary>
/// <param name="name">The HTML attribute name.</param>
/// <param name="name">
/// The HTML attribute name or, if <paramref name="isIndexer"/> is <c>true</c>, the prefix for matching
/// attribute names.
/// </param>
/// <param name="propertyName">The name of the CLR property that corresponds to the HTML attribute.</param>
/// <param name="typeName">
/// The full name of the named (see <paramref name="propertyName"/>) property's <see cref="System.Type"/>.
/// The full name of the named (see <paramref name="propertyName"/>) property's <see cref="Type"/> or,
/// if <paramref name="isIndexer"/> is <c>true</c>, the full name of the indexer's value <see cref="Type"/>.
/// </param>
/// <param name="isIndexer">
/// If <c>true</c> this <see cref="TagHelperAttributeDescriptor"/> is used for dictionary indexer assignments.
/// Otherwise this <see cref="TagHelperAttributeDescriptor"/> is used for property assignment.
/// </param>
/// <remarks>
/// HTML attribute names are matched case-insensitively, regardless of <paramref name="isIndexer"/>.
/// </remarks>
public TagHelperAttributeDescriptor(
[NotNull] string name,
[NotNull] string propertyName,
[NotNull] string typeName)
[NotNull] string typeName,
bool isIndexer)
: this(
name,
propertyName,
typeName,
isIndexer,
isStringProperty: string.Equals(typeName, typeof(string).FullName, StringComparison.Ordinal))
{
}
// Internal for testing i.e. for confirming above constructor sets `IsStringProperty` as expected.
// Internal for testing i.e. for confirming above constructor sets IsStringProperty as expected.
internal TagHelperAttributeDescriptor(
[NotNull] string name,
[NotNull] string propertyName,
[NotNull] string typeName,
bool isIndexer,
bool isStringProperty)
{
Name = name;
PropertyName = propertyName;
TypeName = typeName;
IsIndexer = isIndexer;
IsStringProperty = isStringProperty;
}
/// <summary>
/// Gets an indication whether this property is of type <see cref="string"/>.
/// Gets an indication whether this <see cref="TagHelperAttributeDescriptor"/> is used for dictionary indexer
/// assignments.
/// </summary>
/// <value>
/// If <c>true</c> this <see cref="TagHelperAttributeDescriptor"/> should be associated with all HTML
/// attributes that have names starting with <see cref="Name"/>. Otherwise this
/// <see cref="TagHelperAttributeDescriptor"/> is used for property assignment and is only associated with an
/// HTML attribute that has the exact <see cref="Name"/>.
/// </value>
public bool IsIndexer { get; }
/// <summary>
/// Gets an indication whether this property is of type <see cref="string"/> or, if <see cref="IsIndexer"/> is
/// <c>true</c>, whether the indexer's value is of type <see cref="string"/>.
/// </summary>
/// <value>
/// If <c>true</c> the <see cref="TypeName"/> is for <see cref="string"/>. This causes the Razor parser
/// to allow empty values for attributes that have names matching <see cref="Name"/>. If <c>false</c>
/// empty values for such matching attributes lead to errors.
/// to allow empty values for HTML attributes matching this <see cref="TagHelperAttributeDescriptor"/>. If
/// <c>false</c> empty values for such matching attributes lead to errors.
/// </value>
public bool IsStringProperty { get; }
/// <summary>
/// The HTML attribute name.
/// The HTML attribute name or, if <see cref="IsIndexer"/> is <c>true</c>, the prefix for matching attribute
/// names.
/// </summary>
public string Name { get; }
@ -76,8 +106,30 @@ namespace Microsoft.AspNet.Razor.TagHelpers
public string PropertyName { get; }
/// <summary>
/// The full name of the named (see <see name="PropertyName"/>) property's <see cref="System.Type"/>.
/// The full name of the named (see <see name="PropertyName"/>) property's <see cref="Type"/> or, if
/// <see cref="IsIndexer"/> is <c>true</c>, the full name of the indexer's value <see cref="Type"/>.
/// </summary>
public string TypeName { get; }
/// <summary>
/// Determines whether HTML attribute <paramref name="name"/> matches this
/// <see cref="TagHelperAttributeDescriptor"/>.
/// </summary>
/// <param name="name">Name of the HTML attribute to check.</param>
/// <returns>
/// <c>true</c> if this <see cref="TagHelperAttributeDescriptor"/> matches <paramref name="name"/>.
/// <c>false</c> otherwise.
/// </returns>
public bool IsNameMatch(string name)
{
if (IsIndexer)
{
return name.StartsWith(Name, StringComparison.OrdinalIgnoreCase);
}
else
{
return string.Equals(name, Name, StringComparison.OrdinalIgnoreCase);
}
}
}
}

View File

@ -29,9 +29,11 @@ namespace Microsoft.AspNet.Razor.TagHelpers
/// <inheritdoc />
/// <remarks>
/// Determines equality based on <see cref="TagHelperAttributeDescriptor.Name"/>,
/// <see cref="TagHelperAttributeDescriptor.PropertyName"/>,
/// and <see cref="TagHelperAttributeDescriptor.TypeName"/>.
/// Determines equality based on <see cref="TagHelperAttributeDescriptor.IsIndexer"/>,
/// <see cref="TagHelperAttributeDescriptor.Name"/>, <see cref="TagHelperAttributeDescriptor.PropertyName"/>,
/// and <see cref="TagHelperAttributeDescriptor.TypeName"/>. Ignores
/// <see cref="TagHelperAttributeDescriptor.IsStringProperty"/> because it can be inferred directly from
/// <see cref="TagHelperAttributeDescriptor.TypeName"/>.
/// </remarks>
public virtual bool Equals(TagHelperAttributeDescriptor descriptorX, TagHelperAttributeDescriptor descriptorY)
{
@ -40,7 +42,11 @@ namespace Microsoft.AspNet.Razor.TagHelpers
return true;
}
// Check Name and TypeName though each property in a particular tag helper has at most two
// TagHelperAttributeDescriptors (one for the indexer and one not). May be comparing attributes between
// tag helpers and should be as specific as we can.
return descriptorX != null &&
descriptorX.IsIndexer == descriptorY.IsIndexer &&
string.Equals(descriptorX.Name, descriptorY.Name, StringComparison.OrdinalIgnoreCase) &&
string.Equals(descriptorX.PropertyName, descriptorY.PropertyName, StringComparison.Ordinal) &&
string.Equals(descriptorX.TypeName, descriptorY.TypeName, StringComparison.Ordinal);
@ -49,7 +55,10 @@ namespace Microsoft.AspNet.Razor.TagHelpers
/// <inheritdoc />
public virtual int GetHashCode([NotNull] TagHelperAttributeDescriptor descriptor)
{
// Rarely if ever hash TagHelperAttributeDescriptor. If we do, include the Name and TypeName since context
// information is not available in the hash.
return HashCodeCombiner.Start()
.Add(descriptor.IsIndexer)
.Add(descriptor.Name, StringComparer.OrdinalIgnoreCase)
.Add(descriptor.PropertyName, StringComparer.Ordinal)
.Add(descriptor.TypeName, StringComparer.Ordinal)

View File

@ -24,8 +24,8 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
return true;
}
// Normal comparer doesn't care about case, in tests we do. Also double-check IsStringProperty though
// it is inferred from TypeName.
// Base comparer does not care about Name case but in tests we do. Also double-check IsStringProperty
// though it is inferred from TypeName.
return base.Equals(descriptorX, descriptorY) &&
descriptorX.IsStringProperty == descriptorY.IsStringProperty &&
string.Equals(descriptorX.Name, descriptorY.Name, StringComparison.Ordinal);
@ -33,8 +33,8 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
public override int GetHashCode(TagHelperAttributeDescriptor descriptor)
{
// Rarely if ever hash TagHelperAttributeDescriptor. If we do, ignore IsStringProperty since it should
// not vary for a given TypeName i.e. will not change the bucket.
// Ignore IsStringProperty because it is directly inferred from TypeName and thus won't vary the hash
// bucket. Base comparer does not care about Name case in its hash code but in tests we do.
return HashCodeCombiner.Start()
.Add(base.GetHashCode(descriptor))
.Add(descriptor.Name, StringComparer.Ordinal)

View File

@ -34,7 +34,8 @@ namespace Microsoft.AspNet.Razor.Test.Generator
new TagHelperAttributeDescriptor(
"catchall-bound-string",
"BoundRequiredString",
typeof(string).FullName),
typeof(string).FullName,
isIndexer: false),
},
requiredAttributes: new[] { "catchall-unbound-required" }),
new TagHelperDescriptor(
@ -46,11 +47,13 @@ namespace Microsoft.AspNet.Razor.Test.Generator
new TagHelperAttributeDescriptor(
"input-bound-required-string",
"BoundRequiredString",
typeof(string).FullName),
typeof(string).FullName,
isIndexer: false),
new TagHelperAttributeDescriptor(
"input-bound-string",
"BoundString",
typeof(string).FullName)
typeof(string).FullName,
isIndexer: false)
},
requiredAttributes: new[] { "input-bound-required-string", "input-unbound-required" }),
};
@ -152,6 +155,80 @@ namespace Microsoft.AspNet.Razor.Test.Generator
}
}
private static IEnumerable<TagHelperDescriptor> PrefixedAttributeTagHelperDescriptors
{
get
{
return new[]
{
new TagHelperDescriptor(
tagName: "input",
typeName: "InputTagHelper1",
assemblyName: "SomeAssembly",
attributes: new[]
{
new TagHelperAttributeDescriptor(
name: "int-prefix-grabber",
propertyName: "IntProperty",
typeName: typeof(int).FullName,
isIndexer: false),
new TagHelperAttributeDescriptor(
name: "int-dictionary",
propertyName: "IntDictionaryProperty",
typeName: typeof(IDictionary<string, int>).FullName,
isIndexer: false),
new TagHelperAttributeDescriptor(
name: "string-dictionary",
propertyName: "StringDictionaryProperty",
typeName: "Namespace.DictionaryWithoutParameterlessConstructor<string, string>",
isIndexer: false),
new TagHelperAttributeDescriptor(
name: "string-prefix-grabber",
propertyName: "StringProperty",
typeName: typeof(string).FullName,
isIndexer: false),
new TagHelperAttributeDescriptor(
name: "int-prefix-",
propertyName: "IntDictionaryProperty",
typeName: typeof(int).FullName,
isIndexer: true),
new TagHelperAttributeDescriptor(
name: "string-prefix-",
propertyName: "StringDictionaryProperty",
typeName: typeof(string).FullName,
isIndexer: true),
}),
new TagHelperDescriptor(
tagName: "input",
typeName: "InputTagHelper2",
assemblyName: "SomeAssembly",
attributes: new[]
{
new TagHelperAttributeDescriptor(
name: "int-dictionary",
propertyName: "IntDictionaryProperty",
typeName: typeof(IDictionary<string, int>).FullName,
isIndexer: false),
new TagHelperAttributeDescriptor(
name: "string-dictionary",
propertyName: "StringDictionaryProperty",
typeName: "Namespace.DictionaryWithoutParameterlessConstructor<string, string>",
isIndexer: false),
new TagHelperAttributeDescriptor(
name: "int-prefix-",
propertyName: "IntDictionaryProperty",
typeName: typeof(int).FullName,
isIndexer: true),
new TagHelperAttributeDescriptor(
name: "string-prefix-",
propertyName: "StringDictionaryProperty",
typeName: typeof(string).FullName,
isIndexer: true),
}),
};
}
}
public static TheoryData TagHelperDescriptorFlowTestData
{
get
@ -498,6 +575,101 @@ namespace Microsoft.AspNet.Razor.Test.Generator
contentLength: 4)
}
},
{
"PrefixedAttributeTagHelpers",
"PrefixedAttributeTagHelpers.DesignTime",
PrefixedAttributeTagHelperDescriptors,
new List<LineMapping>
{
BuildLineMapping(
documentAbsoluteIndex: 14,
documentLineIndex: 0,
generatedAbsoluteIndex: 499,
generatedLineIndex: 15,
characterOffsetIndex: 14,
contentLength: 17),
BuildLineMapping(
documentAbsoluteIndex: 37,
documentLineIndex: 2,
generatedAbsoluteIndex: 996,
generatedLineIndex: 34,
characterOffsetIndex: 2,
contentLength: 242),
BuildLineMapping(
documentAbsoluteIndex: 370,
documentLineIndex: 15,
generatedAbsoluteIndex: 1430,
generatedLineIndex: 50,
characterOffsetIndex: 43,
contentLength: 13),
BuildLineMapping(
documentAbsoluteIndex: 404,
documentLineIndex: 15,
generatedAbsoluteIndex: 1601,
generatedLineIndex: 55,
characterOffsetIndex: 77,
contentLength: 16),
BuildLineMapping(
documentAbsoluteIndex: 468,
documentLineIndex: 16,
generatedAbsoluteIndex: 2077,
generatedLineIndex: 64,
characterOffsetIndex: 43,
contentLength: 13),
BuildLineMapping(
documentAbsoluteIndex: 502,
documentLineIndex: 16,
generatedAbsoluteIndex: 2248,
generatedLineIndex: 69,
characterOffsetIndex: 77,
contentLength: 2),
BuildLineMapping(
documentAbsoluteIndex: 526,
documentLineIndex: 16,
generatedAbsoluteIndex: 2432,
generatedLineIndex: 74,
characterOffsetIndex: 101,
contentLength: 2),
BuildLineMapping(
documentAbsoluteIndex: 590,
documentLineIndex: 18,
documentCharacterOffsetIndex: 31,
generatedAbsoluteIndex: 2994,
generatedLineIndex: 84,
generatedCharacterOffsetIndex: 32,
contentLength: 2),
BuildLineMapping(
documentAbsoluteIndex: 611,
documentLineIndex: 18,
generatedAbsoluteIndex: 3129,
generatedLineIndex: 89,
characterOffsetIndex: 52,
contentLength: 2),
BuildLineMapping(
documentAbsoluteIndex: 634,
documentLineIndex: 18,
generatedAbsoluteIndex: 3287,
generatedLineIndex: 94,
characterOffsetIndex: 75,
contentLength: 2),
BuildLineMapping(
documentAbsoluteIndex: 783,
documentLineIndex: 20,
documentCharacterOffsetIndex: 42,
generatedAbsoluteIndex: 3521,
generatedLineIndex: 101,
generatedCharacterOffsetIndex: 6,
contentLength: 8),
BuildLineMapping(
documentAbsoluteIndex: 826,
documentLineIndex: 21,
documentCharacterOffsetIndex: 29,
generatedAbsoluteIndex: 4552,
generatedLineIndex: 115,
generatedCharacterOffsetIndex: 51,
contentLength: 2),
}
},
};
}
}
@ -523,28 +695,36 @@ namespace Microsoft.AspNet.Razor.Test.Generator
{
// Test resource name, expected TagHelperDescriptors
// Note: The baseline resource name is equivalent to the test resource name.
return new TheoryData<string, IEnumerable<TagHelperDescriptor>>
return new TheoryData<string, string, IEnumerable<TagHelperDescriptor>>
{
{ "SingleTagHelper", DefaultPAndInputTagHelperDescriptors },
{ "BasicTagHelpers", DefaultPAndInputTagHelperDescriptors },
{ "BasicTagHelpers.RemoveTagHelper", DefaultPAndInputTagHelperDescriptors },
{ "BasicTagHelpers.Prefixed", PrefixedPAndInputTagHelperDescriptors },
{ "ComplexTagHelpers", DefaultPAndInputTagHelperDescriptors },
{ "DuplicateTargetTagHelper", DuplicateTargetTagHelperDescriptors },
{ "EmptyAttributeTagHelpers", DefaultPAndInputTagHelperDescriptors },
{ "EscapedTagHelpers", DefaultPAndInputTagHelperDescriptors },
{ "AttributeTargetingTagHelpers", AttributeTargetingTagHelperDescriptors },
{ "SingleTagHelper", null, DefaultPAndInputTagHelperDescriptors },
{ "BasicTagHelpers", null, DefaultPAndInputTagHelperDescriptors },
{ "BasicTagHelpers.RemoveTagHelper", null, DefaultPAndInputTagHelperDescriptors },
{ "BasicTagHelpers.Prefixed", null, PrefixedPAndInputTagHelperDescriptors },
{ "ComplexTagHelpers", null, DefaultPAndInputTagHelperDescriptors },
{ "DuplicateTargetTagHelper", null, DuplicateTargetTagHelperDescriptors },
{ "EmptyAttributeTagHelpers", null, DefaultPAndInputTagHelperDescriptors },
{ "EscapedTagHelpers", null, DefaultPAndInputTagHelperDescriptors },
{ "AttributeTargetingTagHelpers", null, AttributeTargetingTagHelperDescriptors },
{ "PrefixedAttributeTagHelpers", null, PrefixedAttributeTagHelperDescriptors },
{
"PrefixedAttributeTagHelpers",
"PrefixedAttributeTagHelpers.Reversed",
PrefixedAttributeTagHelperDescriptors.Reverse()
},
};
}
}
[Theory]
[MemberData(nameof(RuntimeTimeTagHelperTestData))]
public void TagHelpers_GenerateExpectedRuntimeOutput(string testName,
IEnumerable<TagHelperDescriptor> tagHelperDescriptors)
public void TagHelpers_GenerateExpectedRuntimeOutput(
string testName,
string baseLineName,
IEnumerable<TagHelperDescriptor> tagHelperDescriptors)
{
// Act & Assert
RunTagHelperTest(testName, tagHelperDescriptors: tagHelperDescriptors);
// Arrange & Act & Assert
RunTagHelperTest(testName, baseLineName, tagHelperDescriptors: tagHelperDescriptors);
}
[Fact]

View File

@ -181,6 +181,8 @@ namespace Microsoft.AspNet.Razor.TagHelpers
var noErrors = new RazorError[0];
var errorFormat = "Attribute '{0}' on tag helper element '{1}' requires a value. Tag helper bound " +
"attributes of type '{2}' cannot be empty or contain only whitespace.";
var emptyKeyFormat = "The tag helper attribute '{0}' in element '{1}' is missing a key. The " +
"syntax is '<{1} {0}{{ key }}=\"value\">'.";
var stringType = typeof(string).FullName;
var intType = typeof(int).FullName;
var expressionString = "@DateTime.Now + 1";
@ -269,6 +271,196 @@ namespace Microsoft.AspNet.Razor.TagHelpers
})),
new[] { new RazorError(string.Format(errorFormat, "bound-int", "p", intType), 3, 0, 3, 9) }
},
{
"<input int-dictionary/>",
new MarkupBlock(
new MarkupTagHelperBlock(
"input",
selfClosing: true,
attributes: new List<KeyValuePair<string, SyntaxTreeNode>>
{
new KeyValuePair<string, SyntaxTreeNode>("int-dictionary", null),
})),
new[]
{
new RazorError(
string.Format(errorFormat, "int-dictionary", "input", typeof(IDictionary<string, int>).FullName),
absoluteIndex: 7,
lineIndex: 0,
columnIndex: 7,
length: 14),
}
},
{
"<input string-dictionary />",
new MarkupBlock(
new MarkupTagHelperBlock(
"input",
selfClosing: true,
attributes: new List<KeyValuePair<string, SyntaxTreeNode>>
{
new KeyValuePair<string, SyntaxTreeNode>("string-dictionary", null),
})),
new[]
{
new RazorError(
string.Format(errorFormat, "string-dictionary", "input", typeof(IDictionary<string, string>).FullName),
absoluteIndex: 7,
lineIndex: 0,
columnIndex: 7,
length: 17),
}
},
{
"<input int-prefix- />",
new MarkupBlock(
new MarkupTagHelperBlock(
"input",
selfClosing: true,
attributes: new List<KeyValuePair<string, SyntaxTreeNode>>
{
new KeyValuePair<string, SyntaxTreeNode>("int-prefix-", null),
})),
new[]
{
new RazorError(
string.Format(errorFormat, "int-prefix-", "input", typeof(int).FullName),
absoluteIndex: 7,
lineIndex: 0,
columnIndex: 7,
length: 11),
new RazorError(
string.Format(emptyKeyFormat, "int-prefix-", "input"),
absoluteIndex: 7,
lineIndex: 0,
columnIndex: 7,
length: 11),
}
},
{
"<input string-prefix-/>",
new MarkupBlock(
new MarkupTagHelperBlock(
"input",
selfClosing: true,
attributes: new List<KeyValuePair<string, SyntaxTreeNode>>
{
new KeyValuePair<string, SyntaxTreeNode>("string-prefix-", null),
})),
new[]
{
new RazorError(
string.Format(errorFormat, "string-prefix-", "input", typeof(string).FullName),
absoluteIndex: 7,
lineIndex: 0,
columnIndex: 7,
length: 14),
new RazorError(
string.Format(emptyKeyFormat, "string-prefix-", "input"),
absoluteIndex: 7,
lineIndex: 0,
columnIndex: 7,
length: 14),
}
},
{
"<input int-prefix-value/>",
new MarkupBlock(
new MarkupTagHelperBlock(
"input",
selfClosing: true,
attributes: new List<KeyValuePair<string, SyntaxTreeNode>>
{
new KeyValuePair<string, SyntaxTreeNode>("int-prefix-value", null),
})),
new[]
{
new RazorError(
string.Format(errorFormat, "int-prefix-value", "input", typeof(int).FullName),
absoluteIndex: 7,
lineIndex: 0,
columnIndex: 7,
length: 16),
}
},
{
"<input string-prefix-value />",
new MarkupBlock(
new MarkupTagHelperBlock(
"input",
selfClosing: true,
attributes: new List<KeyValuePair<string, SyntaxTreeNode>>
{
new KeyValuePair<string, SyntaxTreeNode>("string-prefix-value", null),
})),
new[]
{
new RazorError(
string.Format(errorFormat, "string-prefix-value", "input", typeof(string).FullName),
absoluteIndex: 7,
lineIndex: 0,
columnIndex: 7,
length: 19),
}
},
{
"<input int-prefix-value='' />",
new MarkupBlock(
new MarkupTagHelperBlock(
"input",
selfClosing: true,
attributes: new List<KeyValuePair<string, SyntaxTreeNode>>
{
new KeyValuePair<string, SyntaxTreeNode>("int-prefix-value", new MarkupBlock()),
})),
new[]
{
new RazorError(
string.Format(errorFormat, "int-prefix-value", "input", typeof(int).FullName),
absoluteIndex: 7,
lineIndex: 0,
columnIndex: 7,
length: 16),
}
},
{
"<input string-prefix-value=''/>",
new MarkupBlock(
new MarkupTagHelperBlock(
"input",
selfClosing: true,
attributes: new List<KeyValuePair<string, SyntaxTreeNode>>
{
new KeyValuePair<string, SyntaxTreeNode>("string-prefix-value", new MarkupBlock()),
})),
new RazorError[0]
},
{
"<input int-prefix-value='3'/>",
new MarkupBlock(
new MarkupTagHelperBlock(
"input",
selfClosing: true,
attributes: new List<KeyValuePair<string, SyntaxTreeNode>>
{
new KeyValuePair<string, SyntaxTreeNode>("int-prefix-value", factory.CodeMarkup("3")),
})),
new RazorError[0]
},
{
"<input string-prefix-value='some string' />",
new MarkupBlock(
new MarkupTagHelperBlock(
"input",
selfClosing: true,
attributes: new List<KeyValuePair<string, SyntaxTreeNode>>
{
new KeyValuePair<string, SyntaxTreeNode>("string-prefix-value", new MarkupBlock(
factory.Markup("some"),
factory.Markup(" string"))),
})),
new RazorError[0]
},
{
"<input unbound-required bound-required-string />",
new MarkupBlock(
@ -878,7 +1070,8 @@ namespace Microsoft.AspNet.Razor.TagHelpers
new TagHelperAttributeDescriptor(
"bound-required-string",
"BoundRequiredString",
typeof(string).FullName)
typeof(string).FullName,
isIndexer: false)
},
requiredAttributes: new[] { "unbound-required" }),
new TagHelperDescriptor(
@ -890,7 +1083,8 @@ namespace Microsoft.AspNet.Razor.TagHelpers
new TagHelperAttributeDescriptor(
"bound-required-string",
"BoundRequiredString",
typeof(string).FullName)
typeof(string).FullName,
isIndexer: false)
},
requiredAttributes: new[] { "bound-required-string" }),
new TagHelperDescriptor(
@ -902,9 +1096,38 @@ namespace Microsoft.AspNet.Razor.TagHelpers
new TagHelperAttributeDescriptor(
"bound-required-int",
"BoundRequiredInt",
typeof(int).FullName)
typeof(int).FullName,
isIndexer: false)
},
requiredAttributes: new[] { "bound-required-int" }),
new TagHelperDescriptor(
tagName: "input",
typeName: "InputTagHelper3",
assemblyName: "SomeAssembly",
attributes: new[]
{
new TagHelperAttributeDescriptor(
"int-dictionary",
"DictionaryOfIntProperty",
typeof(IDictionary<string, int>).FullName,
isIndexer: false),
new TagHelperAttributeDescriptor(
"string-dictionary",
"DictionaryOfStringProperty",
typeof(IDictionary<string, string>).FullName,
isIndexer: false),
new TagHelperAttributeDescriptor(
"int-prefix-",
"DictionaryOfIntProperty",
typeof(int).FullName,
isIndexer: true),
new TagHelperAttributeDescriptor(
"string-prefix-",
"DictionaryOfStringProperty",
typeof(string).FullName,
isIndexer: true),
},
requiredAttributes: Enumerable.Empty<string>()),
new TagHelperDescriptor(
tagName: "p",
typeName: "PTagHelper",
@ -914,11 +1137,13 @@ namespace Microsoft.AspNet.Razor.TagHelpers
new TagHelperAttributeDescriptor(
"bound-string",
"BoundRequiredString",
typeof(string).FullName),
typeof(string).FullName,
isIndexer: false),
new TagHelperAttributeDescriptor(
"bound-int",
"BoundRequiredString",
typeof(int).FullName)
typeof(int).FullName,
isIndexer: false)
},
requiredAttributes: Enumerable.Empty<string>()),
};

View File

@ -52,11 +52,13 @@ namespace Microsoft.AspNet.Razor.TagHelpers
new TagHelperAttributeDescriptor(
name: "attribute one",
propertyName: "property name",
typeName: "property type name"),
typeName: "property type name",
isIndexer: false),
new TagHelperAttributeDescriptor(
name: "attribute two",
propertyName: "property name",
typeName: typeof(string).FullName),
typeName: typeof(string).FullName,
isIndexer: false),
},
requiredAttributes: Enumerable.Empty<string>());
var expectedSerializedDescriptor =
@ -66,11 +68,62 @@ namespace Microsoft.AspNet.Razor.TagHelpers
$"\"{ nameof(TagHelperDescriptor.TypeName) }\":\"type name\"," +
$"\"{ nameof(TagHelperDescriptor.AssemblyName) }\":\"assembly name\"," +
$"\"{ nameof(TagHelperDescriptor.Attributes) }\":[" +
$"{{\"{ nameof(TagHelperAttributeDescriptor.IsStringProperty) }\":false," +
$"{{\"{ nameof(TagHelperAttributeDescriptor.IsIndexer) }\":false," +
$"\"{ nameof(TagHelperAttributeDescriptor.IsStringProperty) }\":false," +
$"\"{ nameof(TagHelperAttributeDescriptor.Name) }\":\"attribute one\"," +
$"\"{ nameof(TagHelperAttributeDescriptor.PropertyName) }\":\"property name\"," +
$"\"{ nameof(TagHelperAttributeDescriptor.TypeName) }\":\"property type name\"}}," +
$"{{\"{ nameof(TagHelperAttributeDescriptor.IsStringProperty) }\":true," +
$"{{\"{ nameof(TagHelperAttributeDescriptor.IsIndexer) }\":false," +
$"\"{ nameof(TagHelperAttributeDescriptor.IsStringProperty) }\":true," +
$"\"{ nameof(TagHelperAttributeDescriptor.Name) }\":\"attribute two\"," +
$"\"{ nameof(TagHelperAttributeDescriptor.PropertyName) }\":\"property name\"," +
$"\"{ nameof(TagHelperAttributeDescriptor.TypeName) }\":\"{ typeof(string).FullName }\"}}]," +
$"\"{ nameof(TagHelperDescriptor.RequiredAttributes) }\":[]}}";
// Act
var serializedDescriptor = JsonConvert.SerializeObject(descriptor);
// Assert
Assert.Equal(expectedSerializedDescriptor, serializedDescriptor, StringComparer.Ordinal);
}
[Fact]
public void TagHelperDescriptor_WithIndexerAttributes_CanBeSerialized()
{
// Arrange
var descriptor = new TagHelperDescriptor(
prefix: "prefix:",
tagName: "tag name",
typeName: "type name",
assemblyName: "assembly name",
attributes: new[]
{
new TagHelperAttributeDescriptor(
name: "attribute one",
propertyName: "property name",
typeName: "property type name",
isIndexer: true),
new TagHelperAttributeDescriptor(
name: "attribute two",
propertyName: "property name",
typeName: typeof(string).FullName,
isIndexer: true),
},
requiredAttributes: Enumerable.Empty<string>());
var expectedSerializedDescriptor =
$"{{\"{ nameof(TagHelperDescriptor.Prefix) }\":\"prefix:\"," +
$"\"{ nameof(TagHelperDescriptor.TagName) }\":\"tag name\"," +
$"\"{ nameof(TagHelperDescriptor.FullTagName) }\":\"prefix:tag name\"," +
$"\"{ nameof(TagHelperDescriptor.TypeName) }\":\"type name\"," +
$"\"{ nameof(TagHelperDescriptor.AssemblyName) }\":\"assembly name\"," +
$"\"{ nameof(TagHelperDescriptor.Attributes) }\":[" +
$"{{\"{ nameof(TagHelperAttributeDescriptor.IsIndexer) }\":true," +
$"\"{ nameof(TagHelperAttributeDescriptor.IsStringProperty) }\":false," +
$"\"{ nameof(TagHelperAttributeDescriptor.Name) }\":\"attribute one\"," +
$"\"{ nameof(TagHelperAttributeDescriptor.PropertyName) }\":\"property name\"," +
$"\"{ nameof(TagHelperAttributeDescriptor.TypeName) }\":\"property type name\"}}," +
$"{{\"{ nameof(TagHelperAttributeDescriptor.IsIndexer) }\":true," +
$"\"{ nameof(TagHelperAttributeDescriptor.IsStringProperty) }\":true," +
$"\"{ nameof(TagHelperAttributeDescriptor.Name) }\":\"attribute two\"," +
$"\"{ nameof(TagHelperAttributeDescriptor.PropertyName) }\":\"property name\"," +
$"\"{ nameof(TagHelperAttributeDescriptor.TypeName) }\":\"{ typeof(string).FullName }\"}}]," +
@ -129,11 +182,13 @@ namespace Microsoft.AspNet.Razor.TagHelpers
$"\"{ nameof(TagHelperDescriptor.TypeName) }\":\"type name\"," +
$"\"{ nameof(TagHelperDescriptor.AssemblyName) }\":\"assembly name\"," +
$"\"{ nameof(TagHelperDescriptor.Attributes) }\":[" +
$"{{\"{ nameof(TagHelperAttributeDescriptor.IsStringProperty) }\":false," +
$"{{\"{ nameof(TagHelperAttributeDescriptor.IsIndexer) }\":false," +
$"\"{ nameof(TagHelperAttributeDescriptor.IsStringProperty) }\":false," +
$"\"{ nameof(TagHelperAttributeDescriptor.Name) }\":\"attribute one\"," +
$"\"{ nameof(TagHelperAttributeDescriptor.PropertyName) }\":\"property name\"," +
$"\"{ nameof(TagHelperAttributeDescriptor.TypeName) }\":\"property type name\"}}," +
$"{{\"{ nameof(TagHelperAttributeDescriptor.IsStringProperty) }\":true," +
$"{{\"{ nameof(TagHelperAttributeDescriptor.IsIndexer) }\":false," +
$"\"{ nameof(TagHelperAttributeDescriptor.IsStringProperty) }\":true," +
$"\"{ nameof(TagHelperAttributeDescriptor.Name) }\":\"attribute two\"," +
$"\"{ nameof(TagHelperAttributeDescriptor.PropertyName) }\":\"property name\"," +
$"\"{ nameof(TagHelperAttributeDescriptor.TypeName) }\":\"{ typeof(string).FullName }\"}}]," +
@ -148,11 +203,13 @@ namespace Microsoft.AspNet.Razor.TagHelpers
new TagHelperAttributeDescriptor(
name: "attribute one",
propertyName: "property name",
typeName: "property type name"),
typeName: "property type name",
isIndexer: false),
new TagHelperAttributeDescriptor(
name: "attribute two",
propertyName: "property name",
typeName: typeof(string).FullName),
typeName: typeof(string).FullName,
isIndexer: false),
},
requiredAttributes: Enumerable.Empty<string>());
@ -166,7 +223,9 @@ namespace Microsoft.AspNet.Razor.TagHelpers
Assert.Equal(expectedDescriptor.FullTagName, descriptor.FullTagName, StringComparer.Ordinal);
Assert.Equal(expectedDescriptor.TypeName, descriptor.TypeName, StringComparer.Ordinal);
Assert.Equal(expectedDescriptor.AssemblyName, descriptor.AssemblyName, StringComparer.Ordinal);
Assert.Equal(2, descriptor.Attributes.Count);
Assert.Equal(expectedDescriptor.Attributes[0].IsIndexer, descriptor.Attributes[0].IsIndexer);
Assert.Equal(expectedDescriptor.Attributes[0].IsStringProperty, descriptor.Attributes[0].IsStringProperty);
Assert.Equal(expectedDescriptor.Attributes[0].Name, descriptor.Attributes[0].Name, StringComparer.Ordinal);
Assert.Equal(
@ -177,6 +236,8 @@ namespace Microsoft.AspNet.Razor.TagHelpers
expectedDescriptor.Attributes[0].TypeName,
descriptor.Attributes[0].TypeName,
StringComparer.Ordinal);
Assert.Equal(expectedDescriptor.Attributes[1].IsIndexer, descriptor.Attributes[1].IsIndexer);
Assert.Equal(expectedDescriptor.Attributes[1].IsStringProperty, descriptor.Attributes[1].IsStringProperty);
Assert.Equal(expectedDescriptor.Attributes[1].Name, descriptor.Attributes[1].Name, StringComparer.Ordinal);
Assert.Equal(
@ -187,6 +248,88 @@ namespace Microsoft.AspNet.Razor.TagHelpers
expectedDescriptor.Attributes[1].TypeName,
descriptor.Attributes[1].TypeName,
StringComparer.Ordinal);
Assert.Empty(descriptor.RequiredAttributes);
}
[Fact]
public void TagHelperDescriptor_WithIndexerAttributes_CanBeDeserialized()
{
// Arrange
var serializedDescriptor =
$"{{\"{ nameof(TagHelperDescriptor.Prefix) }\":\"prefix:\"," +
$"\"{ nameof(TagHelperDescriptor.TagName) }\":\"tag name\"," +
$"\"{ nameof(TagHelperDescriptor.FullTagName) }\":\"prefix:tag name\"," +
$"\"{ nameof(TagHelperDescriptor.TypeName) }\":\"type name\"," +
$"\"{ nameof(TagHelperDescriptor.AssemblyName) }\":\"assembly name\"," +
$"\"{ nameof(TagHelperDescriptor.Attributes) }\":[" +
$"{{\"{ nameof(TagHelperAttributeDescriptor.IsIndexer) }\":true," +
$"\"{ nameof(TagHelperAttributeDescriptor.IsStringProperty) }\":false," +
$"\"{ nameof(TagHelperAttributeDescriptor.Name) }\":\"attribute one\"," +
$"\"{ nameof(TagHelperAttributeDescriptor.PropertyName) }\":\"property name\"," +
$"\"{ nameof(TagHelperAttributeDescriptor.TypeName) }\":\"property type name\"}}," +
$"{{\"{ nameof(TagHelperAttributeDescriptor.IsIndexer) }\":true," +
$"\"{ nameof(TagHelperAttributeDescriptor.IsStringProperty) }\":true," +
$"\"{ nameof(TagHelperAttributeDescriptor.Name) }\":\"attribute two\"," +
$"\"{ nameof(TagHelperAttributeDescriptor.PropertyName) }\":\"property name\"," +
$"\"{ nameof(TagHelperAttributeDescriptor.TypeName) }\":\"{ typeof(string).FullName }\"}}]," +
$"\"{ nameof(TagHelperDescriptor.RequiredAttributes) }\":[]}}";
var expectedDescriptor = new TagHelperDescriptor(
prefix: "prefix:",
tagName: "tag name",
typeName: "type name",
assemblyName: "assembly name",
attributes: new[]
{
new TagHelperAttributeDescriptor(
name: "attribute one",
propertyName: "property name",
typeName: "property type name",
isIndexer: true),
new TagHelperAttributeDescriptor(
name: "attribute two",
propertyName: "property name",
typeName: typeof(string).FullName,
isIndexer: true),
},
requiredAttributes: Enumerable.Empty<string>());
// Act
var descriptor = JsonConvert.DeserializeObject<TagHelperDescriptor>(serializedDescriptor);
// Assert
Assert.NotNull(descriptor);
Assert.Equal(expectedDescriptor.Prefix, descriptor.Prefix, StringComparer.Ordinal);
Assert.Equal(expectedDescriptor.TagName, descriptor.TagName, StringComparer.Ordinal);
Assert.Equal(expectedDescriptor.FullTagName, descriptor.FullTagName, StringComparer.Ordinal);
Assert.Equal(expectedDescriptor.TypeName, descriptor.TypeName, StringComparer.Ordinal);
Assert.Equal(expectedDescriptor.AssemblyName, descriptor.AssemblyName, StringComparer.Ordinal);
Assert.Equal(2, descriptor.Attributes.Count);
Assert.Equal(expectedDescriptor.Attributes[0].IsIndexer, descriptor.Attributes[0].IsIndexer);
Assert.Equal(expectedDescriptor.Attributes[0].IsStringProperty, descriptor.Attributes[0].IsStringProperty);
Assert.Equal(expectedDescriptor.Attributes[0].Name, descriptor.Attributes[0].Name, StringComparer.Ordinal);
Assert.Equal(
expectedDescriptor.Attributes[0].PropertyName,
descriptor.Attributes[0].PropertyName,
StringComparer.Ordinal);
Assert.Equal(
expectedDescriptor.Attributes[0].TypeName,
descriptor.Attributes[0].TypeName,
StringComparer.Ordinal);
Assert.Equal(expectedDescriptor.Attributes[1].IsIndexer, descriptor.Attributes[1].IsIndexer);
Assert.Equal(expectedDescriptor.Attributes[1].IsStringProperty, descriptor.Attributes[1].IsStringProperty);
Assert.Equal(expectedDescriptor.Attributes[1].Name, descriptor.Attributes[1].Name, StringComparer.Ordinal);
Assert.Equal(
expectedDescriptor.Attributes[1].PropertyName,
descriptor.Attributes[1].PropertyName,
StringComparer.Ordinal);
Assert.Equal(
expectedDescriptor.Attributes[1].TypeName,
descriptor.Attributes[1].TypeName,
StringComparer.Ordinal);
Assert.Empty(descriptor.RequiredAttributes);
}
}

View File

@ -921,7 +921,8 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers
new TagHelperAttributeDescriptor(
name: "bound",
propertyName: "Bound",
typeName: typeof(bool).FullName),
typeName: typeof(bool).FullName,
isIndexer: false),
},
requiredAttributes: Enumerable.Empty<string>())
};
@ -944,7 +945,8 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers
new TagHelperAttributeDescriptor(
name: "bound",
propertyName: "Bound",
typeName: typeof(bool).FullName),
typeName: typeof(bool).FullName,
isIndexer: false),
},
requiredAttributes: Enumerable.Empty<string>())
};
@ -1508,11 +1510,13 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers
new TagHelperAttributeDescriptor(
name: "bound",
propertyName: "Bound",
typeName: typeof(bool).FullName),
typeName: typeof(bool).FullName,
isIndexer: false),
new TagHelperAttributeDescriptor(
name: "name",
propertyName: "Name",
typeName: typeof(string).FullName)
typeName: typeof(string).FullName,
isIndexer: false)
})
};
var descriptorProvider = new TagHelperDescriptorProvider(descriptors);
@ -3396,7 +3400,7 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers
string.Format(CultureInfo.InvariantCulture, errorFormatUnclosed, "p"),
SourceLocation.Zero),
new RazorError(
"TagHelper attributes must be welformed.",
"TagHelper attributes must be well-formed.",
absoluteIndex: 12,
lineIndex: 0,
columnIndex: 12)
@ -3962,9 +3966,13 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers
new TagHelperDescriptor("person", "PersonTagHelper", "personAssembly",
attributes: new[]
{
new TagHelperAttributeDescriptor("age", "Age", typeof(int).FullName),
new TagHelperAttributeDescriptor("birthday", "BirthDay", typeof(DateTime).FullName),
new TagHelperAttributeDescriptor("name", "Name", typeof(string).FullName),
new TagHelperAttributeDescriptor("age", "Age", typeof(int).FullName, isIndexer: false),
new TagHelperAttributeDescriptor(
"birthday",
"BirthDay",
typeof(DateTime).FullName,
isIndexer: false),
new TagHelperAttributeDescriptor("name", "Name", typeof(string).FullName, isIndexer: false),
})
};
var providerContext = new TagHelperDescriptorProvider(descriptors);

View File

@ -0,0 +1,127 @@
namespace TestOutput
{
using Microsoft.AspNet.Razor.Runtime.TagHelpers;
using System;
using System.Threading.Tasks;
public class PrefixedAttributeTagHelpers
{
private static object @__o;
private void @__RazorDesignTimeHelpers__()
{
#pragma warning disable 219
string __tagHelperDirectiveSyntaxHelper = null;
__tagHelperDirectiveSyntaxHelper =
#line 1 "PrefixedAttributeTagHelpers.cshtml"
"something, nice"
#line default
#line hidden
;
#pragma warning restore 219
}
#line hidden
private InputTagHelper1 __InputTagHelper1 = null;
private InputTagHelper2 __InputTagHelper2 = null;
#line hidden
public PrefixedAttributeTagHelpers()
{
}
#pragma warning disable 1998
public override async Task ExecuteAsync()
{
#line 3 "PrefixedAttributeTagHelpers.cshtml"
var literate = "or illiterate";
var intDictionary = new Dictionary<string, int>
{
{ "three", 3 },
};
var stringDictionary = new SortedDictionary<string, string>
{
{ "name", "value" },
};
#line default
#line hidden
__InputTagHelper1 = CreateTagHelper<InputTagHelper1>();
#line 16 "PrefixedAttributeTagHelpers.cshtml"
__InputTagHelper1.IntDictionaryProperty = intDictionary;
#line default
#line hidden
#line 16 "PrefixedAttributeTagHelpers.cshtml"
__InputTagHelper1.StringDictionaryProperty = stringDictionary;
#line default
#line hidden
__InputTagHelper2 = CreateTagHelper<InputTagHelper2>();
__InputTagHelper2.IntDictionaryProperty = __InputTagHelper1.IntDictionaryProperty;
__InputTagHelper2.StringDictionaryProperty = __InputTagHelper1.StringDictionaryProperty;
__InputTagHelper1 = CreateTagHelper<InputTagHelper1>();
#line 17 "PrefixedAttributeTagHelpers.cshtml"
__InputTagHelper1.IntDictionaryProperty = intDictionary;
#line default
#line hidden
#line 17 "PrefixedAttributeTagHelpers.cshtml"
__InputTagHelper1.IntDictionaryProperty["garlic"] = 37;
#line default
#line hidden
#line 17 "PrefixedAttributeTagHelpers.cshtml"
__InputTagHelper1.IntProperty = 42;
#line default
#line hidden
__InputTagHelper2 = CreateTagHelper<InputTagHelper2>();
__InputTagHelper2.IntDictionaryProperty = __InputTagHelper1.IntDictionaryProperty;
__InputTagHelper2.IntDictionaryProperty["garlic"] = __InputTagHelper1.IntDictionaryProperty["garlic"];
__InputTagHelper2.IntDictionaryProperty["grabber"] = __InputTagHelper1.IntProperty;
__InputTagHelper1 = CreateTagHelper<InputTagHelper1>();
#line 19 "PrefixedAttributeTagHelpers.cshtml"
__InputTagHelper1.IntProperty = 42;
#line default
#line hidden
#line 19 "PrefixedAttributeTagHelpers.cshtml"
__InputTagHelper1.IntDictionaryProperty["salt"] = 37;
#line default
#line hidden
#line 19 "PrefixedAttributeTagHelpers.cshtml"
__InputTagHelper1.IntDictionaryProperty["pepper"] = 98;
#line default
#line hidden
__InputTagHelper1.StringProperty = "string";
__InputTagHelper1.StringDictionaryProperty["paprika"] = "another string";
#line 21 "PrefixedAttributeTagHelpers.cshtml"
__o = literate;
#line default
#line hidden
__InputTagHelper1.StringDictionaryProperty["cumin"] = string.Empty;
__InputTagHelper2 = CreateTagHelper<InputTagHelper2>();
__InputTagHelper2.IntDictionaryProperty["grabber"] = __InputTagHelper1.IntProperty;
__InputTagHelper2.IntDictionaryProperty["salt"] = __InputTagHelper1.IntDictionaryProperty["salt"];
__InputTagHelper2.IntDictionaryProperty["pepper"] = __InputTagHelper1.IntDictionaryProperty["pepper"];
__InputTagHelper2.StringDictionaryProperty["grabber"] = __InputTagHelper1.StringProperty;
__InputTagHelper2.StringDictionaryProperty["paprika"] = __InputTagHelper1.StringDictionaryProperty["paprika"];
__InputTagHelper2.StringDictionaryProperty["cumin"] = __InputTagHelper1.StringDictionaryProperty["cumin"];
__InputTagHelper1 = CreateTagHelper<InputTagHelper1>();
#line 22 "PrefixedAttributeTagHelpers.cshtml"
__InputTagHelper1.IntDictionaryProperty["value"] = 37;
#line default
#line hidden
__InputTagHelper1.StringDictionaryProperty["thyme"] = "string";
__InputTagHelper2 = CreateTagHelper<InputTagHelper2>();
__InputTagHelper2.IntDictionaryProperty["value"] = __InputTagHelper1.IntDictionaryProperty["value"];
__InputTagHelper2.StringDictionaryProperty["thyme"] = __InputTagHelper1.StringDictionaryProperty["thyme"];
}
#pragma warning restore 1998
}
}

View File

@ -0,0 +1,231 @@
#pragma checksum "PrefixedAttributeTagHelpers.cshtml" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "4e7fe9697b745af1a07d41f6a8532fdc288fa046"
namespace TestOutput
{
using Microsoft.AspNet.Razor.Runtime.TagHelpers;
using System;
using System.Threading.Tasks;
public class PrefixedAttributeTagHelpers
{
#line hidden
#pragma warning disable 0414
private TagHelperContent __tagHelperStringValueBuffer = null;
#pragma warning restore 0414
private TagHelperExecutionContext __tagHelperExecutionContext = null;
private TagHelperRunner __tagHelperRunner = null;
private TagHelperScopeManager __tagHelperScopeManager = new TagHelperScopeManager();
private InputTagHelper2 __InputTagHelper2 = null;
private InputTagHelper1 __InputTagHelper1 = null;
#line hidden
public PrefixedAttributeTagHelpers()
{
}
#pragma warning disable 1998
public override async Task ExecuteAsync()
{
__tagHelperRunner = __tagHelperRunner ?? new TagHelperRunner();
Instrumentation.BeginContext(33, 2, true);
WriteLiteral("\r\n");
Instrumentation.EndContext();
#line 3 "PrefixedAttributeTagHelpers.cshtml"
var literate = "or illiterate";
var intDictionary = new Dictionary<string, int>
{
{ "three", 3 },
};
var stringDictionary = new SortedDictionary<string, string>
{
{ "name", "value" },
};
#line default
#line hidden
Instrumentation.BeginContext(280, 51, true);
WriteLiteral("\r\n\r\n<div class=\"randomNonTagHelperAttribute\">\r\n ");
Instrumentation.EndContext();
__tagHelperExecutionContext = __tagHelperScopeManager.Begin("input", true, "test", async() => {
}
, StartTagHelperWritingScope, EndTagHelperWritingScope);
__InputTagHelper2 = CreateTagHelper<InputTagHelper2>();
__tagHelperExecutionContext.Add(__InputTagHelper2);
#line 16 "PrefixedAttributeTagHelpers.cshtml"
__InputTagHelper2.IntDictionaryProperty = intDictionary;
#line default
#line hidden
__tagHelperExecutionContext.AddTagHelperAttribute("int-dictionary", __InputTagHelper2.IntDictionaryProperty);
#line 16 "PrefixedAttributeTagHelpers.cshtml"
__InputTagHelper2.StringDictionaryProperty = stringDictionary;
#line default
#line hidden
__tagHelperExecutionContext.AddTagHelperAttribute("string-dictionary", __InputTagHelper2.StringDictionaryProperty);
__InputTagHelper1 = CreateTagHelper<InputTagHelper1>();
__tagHelperExecutionContext.Add(__InputTagHelper1);
__InputTagHelper1.IntDictionaryProperty = __InputTagHelper2.IntDictionaryProperty;
__InputTagHelper1.StringDictionaryProperty = __InputTagHelper2.StringDictionaryProperty;
__tagHelperExecutionContext.AddHtmlAttribute("type", Html.Raw("checkbox"));
__tagHelperExecutionContext.Output = await __tagHelperRunner.RunAsync(__tagHelperExecutionContext);
await WriteTagHelperAsync(__tagHelperExecutionContext);
__tagHelperExecutionContext = __tagHelperScopeManager.End();
Instrumentation.BeginContext(423, 6, true);
WriteLiteral("\r\n ");
Instrumentation.EndContext();
__tagHelperExecutionContext = __tagHelperScopeManager.Begin("input", true, "test", async() => {
}
, StartTagHelperWritingScope, EndTagHelperWritingScope);
__InputTagHelper2 = CreateTagHelper<InputTagHelper2>();
__tagHelperExecutionContext.Add(__InputTagHelper2);
#line 17 "PrefixedAttributeTagHelpers.cshtml"
__InputTagHelper2.IntDictionaryProperty = intDictionary;
#line default
#line hidden
__tagHelperExecutionContext.AddTagHelperAttribute("int-dictionary", __InputTagHelper2.IntDictionaryProperty);
if (__InputTagHelper2.IntDictionaryProperty == null)
{
throw new InvalidOperationException(FormatInvalidIndexerAssignment("int-prefix-garlic", "InputTagHelper2", "IntDictionaryProperty"));
}
#line 17 "PrefixedAttributeTagHelpers.cshtml"
__InputTagHelper2.IntDictionaryProperty["garlic"] = 37;
#line default
#line hidden
__tagHelperExecutionContext.AddTagHelperAttribute("int-prefix-garlic", __InputTagHelper2.IntDictionaryProperty["garlic"]);
#line 17 "PrefixedAttributeTagHelpers.cshtml"
__InputTagHelper2.IntDictionaryProperty["grabber"] = 42;
#line default
#line hidden
__tagHelperExecutionContext.AddTagHelperAttribute("int-prefix-grabber", __InputTagHelper2.IntDictionaryProperty["grabber"]);
__InputTagHelper1 = CreateTagHelper<InputTagHelper1>();
__tagHelperExecutionContext.Add(__InputTagHelper1);
__InputTagHelper1.IntDictionaryProperty = __InputTagHelper2.IntDictionaryProperty;
if (__InputTagHelper1.IntDictionaryProperty == null)
{
throw new InvalidOperationException(FormatInvalidIndexerAssignment("int-prefix-garlic", "InputTagHelper1", "IntDictionaryProperty"));
}
__InputTagHelper1.IntDictionaryProperty["garlic"] = __InputTagHelper2.IntDictionaryProperty["garlic"];
__InputTagHelper1.IntProperty = __InputTagHelper2.IntDictionaryProperty["grabber"];
__tagHelperExecutionContext.AddHtmlAttribute("type", Html.Raw("password"));
__tagHelperExecutionContext.Output = await __tagHelperRunner.RunAsync(__tagHelperExecutionContext);
await WriteTagHelperAsync(__tagHelperExecutionContext);
__tagHelperExecutionContext = __tagHelperScopeManager.End();
Instrumentation.BeginContext(532, 6, true);
WriteLiteral("\r\n ");
Instrumentation.EndContext();
__tagHelperExecutionContext = __tagHelperScopeManager.Begin("input", true, "test", async() => {
}
, StartTagHelperWritingScope, EndTagHelperWritingScope);
__InputTagHelper2 = CreateTagHelper<InputTagHelper2>();
__tagHelperExecutionContext.Add(__InputTagHelper2);
if (__InputTagHelper2.IntDictionaryProperty == null)
{
throw new InvalidOperationException(FormatInvalidIndexerAssignment("int-prefix-grabber", "InputTagHelper2", "IntDictionaryProperty"));
}
#line 19 "PrefixedAttributeTagHelpers.cshtml"
__InputTagHelper2.IntDictionaryProperty["grabber"] = 42;
#line default
#line hidden
__tagHelperExecutionContext.AddTagHelperAttribute("int-prefix-grabber", __InputTagHelper2.IntDictionaryProperty["grabber"]);
#line 19 "PrefixedAttributeTagHelpers.cshtml"
__InputTagHelper2.IntDictionaryProperty["salt"] = 37;
#line default
#line hidden
__tagHelperExecutionContext.AddTagHelperAttribute("int-prefix-salt", __InputTagHelper2.IntDictionaryProperty["salt"]);
#line 19 "PrefixedAttributeTagHelpers.cshtml"
__InputTagHelper2.IntDictionaryProperty["pepper"] = 98;
#line default
#line hidden
__tagHelperExecutionContext.AddTagHelperAttribute("int-prefix-pepper", __InputTagHelper2.IntDictionaryProperty["pepper"]);
if (__InputTagHelper2.StringDictionaryProperty == null)
{
throw new InvalidOperationException(FormatInvalidIndexerAssignment("string-prefix-grabber", "InputTagHelper2", "StringDictionaryProperty"));
}
__InputTagHelper2.StringDictionaryProperty["grabber"] = "string";
__tagHelperExecutionContext.AddTagHelperAttribute("string-prefix-grabber", __InputTagHelper2.StringDictionaryProperty["grabber"]);
__InputTagHelper2.StringDictionaryProperty["paprika"] = "another string";
__tagHelperExecutionContext.AddTagHelperAttribute("string-prefix-paprika", __InputTagHelper2.StringDictionaryProperty["paprika"]);
StartTagHelperWritingScope();
WriteLiteral("literate ");
#line 21 "PrefixedAttributeTagHelpers.cshtml"
WriteLiteral(literate);
#line default
#line hidden
WriteLiteral("?");
__tagHelperStringValueBuffer = EndTagHelperWritingScope();
__InputTagHelper2.StringDictionaryProperty["cumin"] = __tagHelperStringValueBuffer.ToString();
__tagHelperExecutionContext.AddTagHelperAttribute("string-prefix-cumin", __InputTagHelper2.StringDictionaryProperty["cumin"]);
__InputTagHelper1 = CreateTagHelper<InputTagHelper1>();
__tagHelperExecutionContext.Add(__InputTagHelper1);
__InputTagHelper1.IntProperty = __InputTagHelper2.IntDictionaryProperty["grabber"];
if (__InputTagHelper1.IntDictionaryProperty == null)
{
throw new InvalidOperationException(FormatInvalidIndexerAssignment("int-prefix-salt", "InputTagHelper1", "IntDictionaryProperty"));
}
__InputTagHelper1.IntDictionaryProperty["salt"] = __InputTagHelper2.IntDictionaryProperty["salt"];
__InputTagHelper1.IntDictionaryProperty["pepper"] = __InputTagHelper2.IntDictionaryProperty["pepper"];
__InputTagHelper1.StringProperty = __InputTagHelper2.StringDictionaryProperty["grabber"];
if (__InputTagHelper1.StringDictionaryProperty == null)
{
throw new InvalidOperationException(FormatInvalidIndexerAssignment("string-prefix-paprika", "InputTagHelper1", "StringDictionaryProperty"));
}
__InputTagHelper1.StringDictionaryProperty["paprika"] = __InputTagHelper2.StringDictionaryProperty["paprika"];
__InputTagHelper1.StringDictionaryProperty["cumin"] = __InputTagHelper2.StringDictionaryProperty["cumin"];
__tagHelperExecutionContext.AddHtmlAttribute("type", Html.Raw("radio"));
__tagHelperExecutionContext.Output = await __tagHelperRunner.RunAsync(__tagHelperExecutionContext);
await WriteTagHelperAsync(__tagHelperExecutionContext);
__tagHelperExecutionContext = __tagHelperScopeManager.End();
Instrumentation.BeginContext(795, 6, true);
WriteLiteral("\r\n ");
Instrumentation.EndContext();
__tagHelperExecutionContext = __tagHelperScopeManager.Begin("input", true, "test", async() => {
}
, StartTagHelperWritingScope, EndTagHelperWritingScope);
__InputTagHelper2 = CreateTagHelper<InputTagHelper2>();
__tagHelperExecutionContext.Add(__InputTagHelper2);
if (__InputTagHelper2.IntDictionaryProperty == null)
{
throw new InvalidOperationException(FormatInvalidIndexerAssignment("int-prefix-value", "InputTagHelper2", "IntDictionaryProperty"));
}
#line 22 "PrefixedAttributeTagHelpers.cshtml"
__InputTagHelper2.IntDictionaryProperty["value"] = 37;
#line default
#line hidden
__tagHelperExecutionContext.AddTagHelperAttribute("int-prefix-value", __InputTagHelper2.IntDictionaryProperty["value"]);
if (__InputTagHelper2.StringDictionaryProperty == null)
{
throw new InvalidOperationException(FormatInvalidIndexerAssignment("string-prefix-thyme", "InputTagHelper2", "StringDictionaryProperty"));
}
__InputTagHelper2.StringDictionaryProperty["thyme"] = "string";
__tagHelperExecutionContext.AddTagHelperAttribute("string-prefix-thyme", __InputTagHelper2.StringDictionaryProperty["thyme"]);
__InputTagHelper1 = CreateTagHelper<InputTagHelper1>();
__tagHelperExecutionContext.Add(__InputTagHelper1);
if (__InputTagHelper1.IntDictionaryProperty == null)
{
throw new InvalidOperationException(FormatInvalidIndexerAssignment("int-prefix-value", "InputTagHelper1", "IntDictionaryProperty"));
}
__InputTagHelper1.IntDictionaryProperty["value"] = __InputTagHelper2.IntDictionaryProperty["value"];
if (__InputTagHelper1.StringDictionaryProperty == null)
{
throw new InvalidOperationException(FormatInvalidIndexerAssignment("string-prefix-thyme", "InputTagHelper1", "StringDictionaryProperty"));
}
__InputTagHelper1.StringDictionaryProperty["thyme"] = __InputTagHelper2.StringDictionaryProperty["thyme"];
__tagHelperExecutionContext.Output = await __tagHelperRunner.RunAsync(__tagHelperExecutionContext);
await WriteTagHelperAsync(__tagHelperExecutionContext);
__tagHelperExecutionContext = __tagHelperScopeManager.End();
Instrumentation.BeginContext(861, 8, true);
WriteLiteral("\r\n</div>");
Instrumentation.EndContext();
}
#pragma warning restore 1998
}
}

View File

@ -0,0 +1,231 @@
#pragma checksum "PrefixedAttributeTagHelpers.cshtml" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "4e7fe9697b745af1a07d41f6a8532fdc288fa046"
namespace TestOutput
{
using Microsoft.AspNet.Razor.Runtime.TagHelpers;
using System;
using System.Threading.Tasks;
public class PrefixedAttributeTagHelpers
{
#line hidden
#pragma warning disable 0414
private TagHelperContent __tagHelperStringValueBuffer = null;
#pragma warning restore 0414
private TagHelperExecutionContext __tagHelperExecutionContext = null;
private TagHelperRunner __tagHelperRunner = null;
private TagHelperScopeManager __tagHelperScopeManager = new TagHelperScopeManager();
private InputTagHelper1 __InputTagHelper1 = null;
private InputTagHelper2 __InputTagHelper2 = null;
#line hidden
public PrefixedAttributeTagHelpers()
{
}
#pragma warning disable 1998
public override async Task ExecuteAsync()
{
__tagHelperRunner = __tagHelperRunner ?? new TagHelperRunner();
Instrumentation.BeginContext(33, 2, true);
WriteLiteral("\r\n");
Instrumentation.EndContext();
#line 3 "PrefixedAttributeTagHelpers.cshtml"
var literate = "or illiterate";
var intDictionary = new Dictionary<string, int>
{
{ "three", 3 },
};
var stringDictionary = new SortedDictionary<string, string>
{
{ "name", "value" },
};
#line default
#line hidden
Instrumentation.BeginContext(280, 51, true);
WriteLiteral("\r\n\r\n<div class=\"randomNonTagHelperAttribute\">\r\n ");
Instrumentation.EndContext();
__tagHelperExecutionContext = __tagHelperScopeManager.Begin("input", true, "test", async() => {
}
, StartTagHelperWritingScope, EndTagHelperWritingScope);
__InputTagHelper1 = CreateTagHelper<InputTagHelper1>();
__tagHelperExecutionContext.Add(__InputTagHelper1);
#line 16 "PrefixedAttributeTagHelpers.cshtml"
__InputTagHelper1.IntDictionaryProperty = intDictionary;
#line default
#line hidden
__tagHelperExecutionContext.AddTagHelperAttribute("int-dictionary", __InputTagHelper1.IntDictionaryProperty);
#line 16 "PrefixedAttributeTagHelpers.cshtml"
__InputTagHelper1.StringDictionaryProperty = stringDictionary;
#line default
#line hidden
__tagHelperExecutionContext.AddTagHelperAttribute("string-dictionary", __InputTagHelper1.StringDictionaryProperty);
__InputTagHelper2 = CreateTagHelper<InputTagHelper2>();
__tagHelperExecutionContext.Add(__InputTagHelper2);
__InputTagHelper2.IntDictionaryProperty = __InputTagHelper1.IntDictionaryProperty;
__InputTagHelper2.StringDictionaryProperty = __InputTagHelper1.StringDictionaryProperty;
__tagHelperExecutionContext.AddHtmlAttribute("type", Html.Raw("checkbox"));
__tagHelperExecutionContext.Output = await __tagHelperRunner.RunAsync(__tagHelperExecutionContext);
await WriteTagHelperAsync(__tagHelperExecutionContext);
__tagHelperExecutionContext = __tagHelperScopeManager.End();
Instrumentation.BeginContext(423, 6, true);
WriteLiteral("\r\n ");
Instrumentation.EndContext();
__tagHelperExecutionContext = __tagHelperScopeManager.Begin("input", true, "test", async() => {
}
, StartTagHelperWritingScope, EndTagHelperWritingScope);
__InputTagHelper1 = CreateTagHelper<InputTagHelper1>();
__tagHelperExecutionContext.Add(__InputTagHelper1);
#line 17 "PrefixedAttributeTagHelpers.cshtml"
__InputTagHelper1.IntDictionaryProperty = intDictionary;
#line default
#line hidden
__tagHelperExecutionContext.AddTagHelperAttribute("int-dictionary", __InputTagHelper1.IntDictionaryProperty);
if (__InputTagHelper1.IntDictionaryProperty == null)
{
throw new InvalidOperationException(FormatInvalidIndexerAssignment("int-prefix-garlic", "InputTagHelper1", "IntDictionaryProperty"));
}
#line 17 "PrefixedAttributeTagHelpers.cshtml"
__InputTagHelper1.IntDictionaryProperty["garlic"] = 37;
#line default
#line hidden
__tagHelperExecutionContext.AddTagHelperAttribute("int-prefix-garlic", __InputTagHelper1.IntDictionaryProperty["garlic"]);
#line 17 "PrefixedAttributeTagHelpers.cshtml"
__InputTagHelper1.IntProperty = 42;
#line default
#line hidden
__tagHelperExecutionContext.AddTagHelperAttribute("int-prefix-grabber", __InputTagHelper1.IntProperty);
__InputTagHelper2 = CreateTagHelper<InputTagHelper2>();
__tagHelperExecutionContext.Add(__InputTagHelper2);
__InputTagHelper2.IntDictionaryProperty = __InputTagHelper1.IntDictionaryProperty;
if (__InputTagHelper2.IntDictionaryProperty == null)
{
throw new InvalidOperationException(FormatInvalidIndexerAssignment("int-prefix-garlic", "InputTagHelper2", "IntDictionaryProperty"));
}
__InputTagHelper2.IntDictionaryProperty["garlic"] = __InputTagHelper1.IntDictionaryProperty["garlic"];
__InputTagHelper2.IntDictionaryProperty["grabber"] = __InputTagHelper1.IntProperty;
__tagHelperExecutionContext.AddHtmlAttribute("type", Html.Raw("password"));
__tagHelperExecutionContext.Output = await __tagHelperRunner.RunAsync(__tagHelperExecutionContext);
await WriteTagHelperAsync(__tagHelperExecutionContext);
__tagHelperExecutionContext = __tagHelperScopeManager.End();
Instrumentation.BeginContext(532, 6, true);
WriteLiteral("\r\n ");
Instrumentation.EndContext();
__tagHelperExecutionContext = __tagHelperScopeManager.Begin("input", true, "test", async() => {
}
, StartTagHelperWritingScope, EndTagHelperWritingScope);
__InputTagHelper1 = CreateTagHelper<InputTagHelper1>();
__tagHelperExecutionContext.Add(__InputTagHelper1);
#line 19 "PrefixedAttributeTagHelpers.cshtml"
__InputTagHelper1.IntProperty = 42;
#line default
#line hidden
__tagHelperExecutionContext.AddTagHelperAttribute("int-prefix-grabber", __InputTagHelper1.IntProperty);
if (__InputTagHelper1.IntDictionaryProperty == null)
{
throw new InvalidOperationException(FormatInvalidIndexerAssignment("int-prefix-salt", "InputTagHelper1", "IntDictionaryProperty"));
}
#line 19 "PrefixedAttributeTagHelpers.cshtml"
__InputTagHelper1.IntDictionaryProperty["salt"] = 37;
#line default
#line hidden
__tagHelperExecutionContext.AddTagHelperAttribute("int-prefix-salt", __InputTagHelper1.IntDictionaryProperty["salt"]);
#line 19 "PrefixedAttributeTagHelpers.cshtml"
__InputTagHelper1.IntDictionaryProperty["pepper"] = 98;
#line default
#line hidden
__tagHelperExecutionContext.AddTagHelperAttribute("int-prefix-pepper", __InputTagHelper1.IntDictionaryProperty["pepper"]);
__InputTagHelper1.StringProperty = "string";
__tagHelperExecutionContext.AddTagHelperAttribute("string-prefix-grabber", __InputTagHelper1.StringProperty);
if (__InputTagHelper1.StringDictionaryProperty == null)
{
throw new InvalidOperationException(FormatInvalidIndexerAssignment("string-prefix-paprika", "InputTagHelper1", "StringDictionaryProperty"));
}
__InputTagHelper1.StringDictionaryProperty["paprika"] = "another string";
__tagHelperExecutionContext.AddTagHelperAttribute("string-prefix-paprika", __InputTagHelper1.StringDictionaryProperty["paprika"]);
StartTagHelperWritingScope();
WriteLiteral("literate ");
#line 21 "PrefixedAttributeTagHelpers.cshtml"
WriteLiteral(literate);
#line default
#line hidden
WriteLiteral("?");
__tagHelperStringValueBuffer = EndTagHelperWritingScope();
__InputTagHelper1.StringDictionaryProperty["cumin"] = __tagHelperStringValueBuffer.ToString();
__tagHelperExecutionContext.AddTagHelperAttribute("string-prefix-cumin", __InputTagHelper1.StringDictionaryProperty["cumin"]);
__InputTagHelper2 = CreateTagHelper<InputTagHelper2>();
__tagHelperExecutionContext.Add(__InputTagHelper2);
if (__InputTagHelper2.IntDictionaryProperty == null)
{
throw new InvalidOperationException(FormatInvalidIndexerAssignment("int-prefix-grabber", "InputTagHelper2", "IntDictionaryProperty"));
}
__InputTagHelper2.IntDictionaryProperty["grabber"] = __InputTagHelper1.IntProperty;
__InputTagHelper2.IntDictionaryProperty["salt"] = __InputTagHelper1.IntDictionaryProperty["salt"];
__InputTagHelper2.IntDictionaryProperty["pepper"] = __InputTagHelper1.IntDictionaryProperty["pepper"];
if (__InputTagHelper2.StringDictionaryProperty == null)
{
throw new InvalidOperationException(FormatInvalidIndexerAssignment("string-prefix-grabber", "InputTagHelper2", "StringDictionaryProperty"));
}
__InputTagHelper2.StringDictionaryProperty["grabber"] = __InputTagHelper1.StringProperty;
__InputTagHelper2.StringDictionaryProperty["paprika"] = __InputTagHelper1.StringDictionaryProperty["paprika"];
__InputTagHelper2.StringDictionaryProperty["cumin"] = __InputTagHelper1.StringDictionaryProperty["cumin"];
__tagHelperExecutionContext.AddHtmlAttribute("type", Html.Raw("radio"));
__tagHelperExecutionContext.Output = await __tagHelperRunner.RunAsync(__tagHelperExecutionContext);
await WriteTagHelperAsync(__tagHelperExecutionContext);
__tagHelperExecutionContext = __tagHelperScopeManager.End();
Instrumentation.BeginContext(795, 6, true);
WriteLiteral("\r\n ");
Instrumentation.EndContext();
__tagHelperExecutionContext = __tagHelperScopeManager.Begin("input", true, "test", async() => {
}
, StartTagHelperWritingScope, EndTagHelperWritingScope);
__InputTagHelper1 = CreateTagHelper<InputTagHelper1>();
__tagHelperExecutionContext.Add(__InputTagHelper1);
if (__InputTagHelper1.IntDictionaryProperty == null)
{
throw new InvalidOperationException(FormatInvalidIndexerAssignment("int-prefix-value", "InputTagHelper1", "IntDictionaryProperty"));
}
#line 22 "PrefixedAttributeTagHelpers.cshtml"
__InputTagHelper1.IntDictionaryProperty["value"] = 37;
#line default
#line hidden
__tagHelperExecutionContext.AddTagHelperAttribute("int-prefix-value", __InputTagHelper1.IntDictionaryProperty["value"]);
if (__InputTagHelper1.StringDictionaryProperty == null)
{
throw new InvalidOperationException(FormatInvalidIndexerAssignment("string-prefix-thyme", "InputTagHelper1", "StringDictionaryProperty"));
}
__InputTagHelper1.StringDictionaryProperty["thyme"] = "string";
__tagHelperExecutionContext.AddTagHelperAttribute("string-prefix-thyme", __InputTagHelper1.StringDictionaryProperty["thyme"]);
__InputTagHelper2 = CreateTagHelper<InputTagHelper2>();
__tagHelperExecutionContext.Add(__InputTagHelper2);
if (__InputTagHelper2.IntDictionaryProperty == null)
{
throw new InvalidOperationException(FormatInvalidIndexerAssignment("int-prefix-value", "InputTagHelper2", "IntDictionaryProperty"));
}
__InputTagHelper2.IntDictionaryProperty["value"] = __InputTagHelper1.IntDictionaryProperty["value"];
if (__InputTagHelper2.StringDictionaryProperty == null)
{
throw new InvalidOperationException(FormatInvalidIndexerAssignment("string-prefix-thyme", "InputTagHelper2", "StringDictionaryProperty"));
}
__InputTagHelper2.StringDictionaryProperty["thyme"] = __InputTagHelper1.StringDictionaryProperty["thyme"];
__tagHelperExecutionContext.Output = await __tagHelperRunner.RunAsync(__tagHelperExecutionContext);
await WriteTagHelperAsync(__tagHelperExecutionContext);
__tagHelperExecutionContext = __tagHelperScopeManager.End();
Instrumentation.BeginContext(861, 8, true);
WriteLiteral("\r\n</div>");
Instrumentation.EndContext();
}
#pragma warning restore 1998
}
}

View File

@ -0,0 +1,23 @@
@addTagHelper "something, nice"
@{
var literate = "or illiterate";
var intDictionary = new Dictionary<string, int>
{
{ "three", 3 },
};
var stringDictionary = new SortedDictionary<string, string>
{
{ "name", "value" },
};
}
<div class="randomNonTagHelperAttribute">
<input type="checkbox" int-dictionary="intDictionary" string-dictionary="stringDictionary"/>
<input type="password" int-dictionary="intDictionary" int-prefix-garlic="37" int-prefix-grabber="42" />
<input type="radio"
int-prefix-grabber="42" int-prefix-salt="37" int-prefix-pepper="98" int-prefix-salt="8"
string-prefix-grabber="string" string-prefix-paprika="another string"
string-prefix-cumin="literate @literate?"/>
<input int-prefix-value="37" string-prefix-thyme="string" />
</div>