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"); 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> /// <summary>
/// Tag /// Tag
/// </summary> /// </summary>
@ -203,19 +235,67 @@ namespace Microsoft.AspNet.Razor.Runtime
} }
/// <summary> /// <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> /// </summary>
internal static string TagHelperDescriptorFactory_InvalidBoundAttributeName internal static string TagHelperDescriptorFactory_InvalidAttributeNameOrPrefixCharacter
{ {
get { return GetString("TagHelperDescriptorFactory_InvalidBoundAttributeName"); } get { return GetString("TagHelperDescriptorFactory_InvalidAttributeNameOrPrefixCharacter"); }
} }
/// <summary> /// <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> /// </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> /// <summary>

View File

@ -150,11 +150,26 @@
<data name="TagHelperDescriptorFactory_Attribute" xml:space="preserve"> <data name="TagHelperDescriptorFactory_Attribute" xml:space="preserve">
<value>Attribute</value> <value>Attribute</value>
</data> </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"> <data name="TagHelperDescriptorFactory_Tag" xml:space="preserve">
<value>Tag</value> <value>Tag</value>
</data> </data>
<data name="TagHelperDescriptorFactory_InvalidBoundAttributeName" xml:space="preserve"> <data name="TagHelperDescriptorFactory_InvalidAttributeNameOrPrefixCharacter" xml:space="preserve">
<value>Invalid tag helper bound property '{0}.{1}'. Tag helpers cannot bind to HTML attributes beginning with '{2}'.</value> <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>
<data name="TagHelperAttributeList_CannotAddWithNullName" xml:space="preserve"> <data name="TagHelperAttributeList_CannotAddWithNullName" xml:space="preserve">
<value>Cannot add a '{0}' with a null '{1}'.</value> <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)] [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
public sealed class HtmlAttributeNameAttribute : Attribute public sealed class HtmlAttributeNameAttribute : Attribute
{ {
private string _dictionaryAttributePrefix;
/// <summary> /// <summary>
/// Instantiates a new instance of the <see cref="HtmlAttributeNameAttribute"/> class. /// Instantiates a new instance of the <see cref="HtmlAttributeNameAttribute"/> class.
/// </summary> /// </summary>
@ -29,5 +31,39 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
/// HTML attribute name of the associated property. /// HTML attribute name of the associated property.
/// </summary> /// </summary>
public string Name { get; } 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> /// </summary>
/// <param name="assemblyName">The assembly name that contains <paramref name="type"/>.</param> /// <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> /// <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( public static IEnumerable<TagHelperDescriptor> CreateDescriptors(
string assemblyName, string assemblyName,
[NotNull] Type type, [NotNull] Type type,
@ -209,48 +211,195 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
var accessibleProperties = type.GetRuntimeProperties().Where(IsAccessibleProperty); var accessibleProperties = type.GetRuntimeProperties().Where(IsAccessibleProperty);
var attributeDescriptors = new List<TagHelperAttributeDescriptor>(); 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) 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)) 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); attributeDescriptors.Add(descriptor);
if (indexerDescriptor != null)
{
indexerDescriptors.Add(indexerDescriptor);
}
} }
} }
attributeDescriptors.AddRange(indexerDescriptors);
return attributeDescriptors; return attributeDescriptors;
} }
private static bool ValidateTagHelperAttributeDescriptor( // Internal for testing.
internal static bool ValidateTagHelperAttributeDescriptor(
TagHelperAttributeDescriptor attributeDescriptor, TagHelperAttributeDescriptor attributeDescriptor,
Type parentType, Type parentType,
ErrorSink errorSink) 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 // 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. // 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( errorSink.OnError(
SourceLocation.Zero, SourceLocation.Zero,
Resources.FormatTagHelperDescriptorFactory_InvalidBoundAttributeName( Resources.FormatTagHelperDescriptorFactory_InvalidAttributeNameOrPrefixStart(
attributeDescriptor.PropertyName,
parentType.FullName, parentType.FullName,
propertyName,
nameOrPrefix,
attributeNameOrPrefix,
DataDashPrefix)); DataDashPrefix));
return false; 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 ? var attributeName = attributeNameAttribute != null ?
attributeNameAttribute.Name : attributeNameAttribute.Name :
ToHtmlCase(property.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) private static bool IsAccessibleProperty(PropertyInfo property)

View File

@ -4,6 +4,7 @@
"dependencies": { "dependencies": {
"Microsoft.AspNet.Razor": "4.0.0-*", "Microsoft.AspNet.Razor": "4.0.0-*",
"Microsoft.Framework.BufferEntryCollection.Sources": { "type": "build", "version": "1.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.CopyOnWriteDictionary.Sources": { "type": "build", "version": "1.0.0-*" },
"Microsoft.Framework.NotNullAttribute.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. // Determine what attributes exist in the element and divide them up.
var htmlAttributes = chunk.Attributes; var htmlAttributes = chunk.Attributes;
var attributeDescriptors = tagHelperDescriptors.SelectMany(descriptor => descriptor.Attributes); var attributeDescriptors = tagHelperDescriptors.SelectMany(descriptor => descriptor.Attributes).ToArray();
var unboundHtmlAttributes = htmlAttributes.Where( var unboundHtmlAttributes = htmlAttributes.Where(attribute => !attributeDescriptors.Any(
attribute => !attributeDescriptors.Any( attributeDescriptor => attributeDescriptor.IsNameMatch(attribute.Key)));
descriptor => string.Equals(attribute.Key, descriptor.Name, StringComparison.OrdinalIgnoreCase)));
RenderUnboundHTMLAttributes(unboundHtmlAttributes); RenderUnboundHTMLAttributes(unboundHtmlAttributes);
@ -182,6 +181,7 @@ namespace Microsoft.AspNet.Razor.Generator.Compiler.CSharp
// Render all of the bound attribute values for the tag helper. // Render all of the bound attribute values for the tag helper.
RenderBoundHTMLAttributes( RenderBoundHTMLAttributes(
chunk.Attributes, chunk.Attributes,
tagHelperDescriptor.TypeName,
tagHelperVariableName, tagHelperVariableName,
tagHelperDescriptor.Attributes, tagHelperDescriptor.Attributes,
htmlAttributeValues); htmlAttributeValues);
@ -190,81 +190,126 @@ namespace Microsoft.AspNet.Razor.Generator.Compiler.CSharp
private void RenderBoundHTMLAttributes( private void RenderBoundHTMLAttributes(
IList<KeyValuePair<string, Chunk>> chunkAttributes, IList<KeyValuePair<string, Chunk>> chunkAttributes,
string tagHelperTypeName,
string tagHelperVariableName, string tagHelperVariableName,
IEnumerable<TagHelperAttributeDescriptor> attributeDescriptors, IEnumerable<TagHelperAttributeDescriptor> attributeDescriptors,
Dictionary<string, string> htmlAttributeValues) 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( var attributeName = attributeKeyValuePair.Key;
kvp => string.Equals(kvp.Key, attributeDescriptor.Name, StringComparison.OrdinalIgnoreCase)); var attributeValueChunk = attributeKeyValuePair.Value;
if (attributeValueChunk == null)
if (matchingAttributes.Any())
{ {
// First attribute wins, even if there's duplicates. // Minimized attributes are not valid for bound attributes. TagHelperBlockRewriter has already
var firstAttribute = matchingAttributes.First(); // logged an error if it was a bound attribute; so we can skip.
var attributeValueChunk = firstAttribute.Value; continue;
}
// Minimized attributes are not valid for bound attributes. There will be an error for the bound // Find the matching TagHelperAttributeDescriptor.
// attribute logged by TagHelperBlockRewriter already so we can skip. var attributeDescriptor = attributeDescriptors.FirstOrDefault(
if (attributeValueChunk == null) 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; continue;
} }
// We capture the tag helpers property value accessor so we can retrieve it later (if we need to). // We need to inform the context of the attribute value.
var valueAccessor = string.Format( _writer
CultureInfo.InvariantCulture, .WriteStartInstanceMethodInvocation(
"{0}.{1}", ExecutionContextVariableName,
tagHelperVariableName, _tagHelperContext.ExecutionContextAddTagHelperAttributeMethodName)
attributeDescriptor.PropertyName); .WriteStringLiteral(attributeName)
.WriteParameterSeparator()
// If we haven't recorded this attribute value before then we need to record its value. .Write(valueAccessor)
var attributeValueRecorded = htmlAttributeValues.ContainsKey(attributeDescriptor.Name); .WriteEndMethodInvocation();
if (!attributeValueRecorded) }
{ else
// 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 // The attribute value has already been recorded, lets retrieve it from the stored value
// helpers that need the value. // accessors.
htmlAttributeValues.Add(attributeDescriptor.Name, valueAccessor); _writer
.WriteStartAssignment(valueAccessor)
// Bufferable attributes are attributes that can have Razor code inside of them. Such .Write(htmlAttributeValues[attributeName])
// attributes have string values and may be calculated using a temporary TextWriter or other .WriteLine(";");
// 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(";");
}
} }
} }
} }
@ -580,6 +625,26 @@ namespace Microsoft.AspNet.Razor.Generator.Compiler.CSharp
return true; 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. // A CSharpCodeVisitor which does not HTML encode values. Used when rendering bound string attribute values.
private class CSharpLiteralCodeVisitor : CSharpCodeVisitor private class CSharpLiteralCodeVisitor : CSharpCodeVisitor
{ {

View File

@ -22,6 +22,7 @@ namespace Microsoft.AspNet.Razor.Generator
ExecutionContextAddMinimizedHtmlAttributeMethodName = "AddMinimizedHtmlAttribute"; ExecutionContextAddMinimizedHtmlAttributeMethodName = "AddMinimizedHtmlAttribute";
ExecutionContextAddHtmlAttributeMethodName = "AddHtmlAttribute"; ExecutionContextAddHtmlAttributeMethodName = "AddHtmlAttribute";
ExecutionContextOutputPropertyName = "Output"; ExecutionContextOutputPropertyName = "Output";
FormatInvalidIndexerAssignmentMethodName = "FormatInvalidIndexerAssignment";
MarkAsHtmlEncodedMethodName = "Html.Raw"; MarkAsHtmlEncodedMethodName = "Html.Raw";
StartTagHelperWritingScopeMethodName = "StartTagHelperWritingScope"; StartTagHelperWritingScopeMethodName = "StartTagHelperWritingScope";
EndTagHelperWritingScopeMethodName = "EndTagHelperWritingScope"; EndTagHelperWritingScopeMethodName = "EndTagHelperWritingScope";
@ -78,6 +79,21 @@ namespace Microsoft.AspNet.Razor.Generator
/// </summary> /// </summary>
public string ExecutionContextOutputPropertyName { get; set; } 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> /// <summary>
/// The name of the method used to wrap a <see cref="string"/> value and mark it as HTML-encoded. /// The name of the method used to wrap a <see cref="string"/> value and mark it as HTML-encoded.
/// </summary> /// </summary>

View File

@ -1,7 +1,6 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; 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. // Only want to track the attribute if we succeeded in parsing its corresponding Block/Span.
if (result != null) 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 // Check if it's a bound attribute that is minimized or if it's a bound non-string attribute that
// is null or whitespace. // is null or whitespace.
if ((result.IsBoundAttribute && result.AttributeValueNode == null) || if ((result.IsBoundAttribute && result.AttributeValueNode == null) ||
(result.IsBoundNonStringAttribute && (result.IsBoundNonStringAttribute &&
IsNullOrWhitespaceAttributeValue(result.AttributeValueNode))) IsNullOrWhitespaceAttributeValue(result.AttributeValueNode)))
{ {
var errorLocation = GetAttributeNameStartLocation(child); errorLocation = GetAttributeNameStartLocation(child);
errorSink.OnError( errorSink.OnError(
errorLocation, errorLocation.Value,
RazorResources.FormatRewriterError_EmptyTagHelperBoundAttribute( RazorResources.FormatRewriterError_EmptyTagHelperBoundAttribute(
result.AttributeName, result.AttributeName,
tagName, tagName,
@ -82,6 +83,23 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
result.AttributeName.Length); 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( attributes.Add(
new KeyValuePair<string, SyntaxTreeNode>(result.AttributeName, result.AttributeValueNode)); new KeyValuePair<string, SyntaxTreeNode>(result.AttributeName, result.AttributeValueNode));
} }
@ -224,24 +242,19 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
{ {
errorSink.OnError( errorSink.OnError(
span.Start, span.Start,
RazorResources.TagHelperBlockRewriter_TagHelperAttributeListMustBeWelformed, RazorResources.TagHelperBlockRewriter_TagHelperAttributeListMustBeWellFormed,
span.Content.Length); span.Content.Length);
} }
return null; return null;
} }
bool isBoundNonStringAttribute; var result = CreateTryParseResult(name, descriptors);
var result = new TryParseResult
{
IsBoundAttribute = IsBoundAttribute(name, descriptors, out isBoundNonStringAttribute),
AttributeName = name,
};
result.IsBoundNonStringAttribute = isBoundNonStringAttribute;
// If we're not after an equal then we should treat the value as if it were a minimized attribute. // 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; var attributeValueBuilder = afterEquals ? builder : null;
result.AttributeValueNode = CreateMarkupAttribute(name, attributeValueBuilder, isBoundNonStringAttribute); result.AttributeValueNode =
CreateMarkupAttribute(name, attributeValueBuilder, result.IsBoundNonStringAttribute);
return result; return result;
} }
@ -286,13 +299,7 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
} }
// Have a name now. Able to determine correct isBoundNonStringAttribute value. // Have a name now. Able to determine correct isBoundNonStringAttribute value.
bool isBoundNonStringAttribute; var result = CreateTryParseResult(name, descriptors);
var result = new TryParseResult
{
IsBoundAttribute = IsBoundAttribute(name, descriptors, out isBoundNonStringAttribute),
AttributeName = name,
};
result.IsBoundNonStringAttribute = isBoundNonStringAttribute;
// Remove first child i.e. foo=" // Remove first child i.e. foo="
builder.Children.RemoveAt(0); builder.Children.RemoveAt(0);
@ -324,13 +331,13 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
if (block.Children.Count() == 1) if (block.Children.Count() == 1)
{ {
var child = block.Children.First() as Span; var child = block.Children.First() as Span;
if (child != null) if (child != null)
{ {
// After pulling apart the block we just have a value span. // After pulling apart the block we just have a value span.
var spanBuilder = new SpanBuilder(child); var spanBuilder = new SpanBuilder(child);
result.AttributeValueNode = CreateMarkupAttribute(name, spanBuilder, isBoundNonStringAttribute); result.AttributeValueNode =
CreateMarkupAttribute(name, spanBuilder, result.IsBoundNonStringAttribute);
return result; return result;
} }
@ -485,17 +492,23 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
return firstBoundAttribute?.TypeName; return firstBoundAttribute?.TypeName;
} }
// Determines whether an attribute with the given name is bound to a non-string tag helper property. // Create a TryParseResult for given name, filling in binding details.
private static bool IsBoundAttribute( private static TryParseResult CreateTryParseResult(string name, IEnumerable<TagHelperDescriptor> descriptors)
string name,
IEnumerable<TagHelperDescriptor> descriptors,
out bool isBoundNonStringAttribute)
{ {
var firstBoundAttribute = FindFirstBoundAttribute(name, descriptors); var firstBoundAttribute = FindFirstBoundAttribute(name, descriptors);
var isBoundAttribute = firstBoundAttribute != null; 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. // Finds first TagHelperAttributeDescriptor matching given name.
@ -503,9 +516,13 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
string name, string name,
IEnumerable<TagHelperDescriptor> descriptors) 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) .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) private static bool IsQuote(HtmlSymbol htmlSymbol)
@ -523,6 +540,8 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
public bool IsBoundAttribute { get; set; } public bool IsBoundAttribute { get; set; }
public bool IsBoundNonStringAttribute { get; set; } public bool IsBoundNonStringAttribute { get; set; }
public bool IsMissingDictionaryKey { get; set; }
} }
} }
} }

View File

@ -1367,19 +1367,35 @@ namespace Microsoft.AspNet.Razor
} }
/// <summary> /// <summary>
/// TagHelper attributes must be welformed. /// TagHelper attributes must be well-formed.
/// </summary> /// </summary>
internal static string TagHelperBlockRewriter_TagHelperAttributeListMustBeWelformed internal static string TagHelperBlockRewriter_TagHelperAttributeListMustBeWellFormed
{ {
get { return GetString("TagHelperBlockRewriter_TagHelperAttributeListMustBeWelformed"); } get { return GetString("TagHelperBlockRewriter_TagHelperAttributeListMustBeWellFormed"); }
} }
/// <summary> /// <summary>
/// TagHelper attributes must be welformed. /// TagHelper attributes must be well-formed.
/// </summary> /// </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> /// <summary>

View File

@ -390,8 +390,11 @@ Instead, wrap the contents of the block in "{{}}":
<data name="TagHelpersParseTreeRewriter_MissingCloseAngle" xml:space="preserve"> <data name="TagHelpersParseTreeRewriter_MissingCloseAngle" xml:space="preserve">
<value>Missing close angle for tag helper '{0}'.</value> <value>Missing close angle for tag helper '{0}'.</value>
</data> </data>
<data name="TagHelperBlockRewriter_TagHelperAttributeListMustBeWelformed" xml:space="preserve"> <data name="TagHelperBlockRewriter_TagHelperAttributeListMustBeWellFormed" xml:space="preserve">
<value>TagHelper attributes must be welformed.</value> <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>
<data name="TagHelpers_AttributeExpressionRequired" xml:space="preserve"> <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> <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, name,
propertyInfo.Name, propertyInfo.Name,
propertyInfo.PropertyType.FullName, propertyInfo.PropertyType.FullName,
isIndexer: false,
isStringProperty: propertyInfo.PropertyType == typeof(string)) isStringProperty: propertyInfo.PropertyType == typeof(string))
{ {
} }
@ -25,48 +26,77 @@ namespace Microsoft.AspNet.Razor.TagHelpers
/// <summary> /// <summary>
/// Instantiates a new instance of the <see cref="TagHelperAttributeDescriptor"/> class. /// Instantiates a new instance of the <see cref="TagHelperAttributeDescriptor"/> class.
/// </summary> /// </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="propertyName">The name of the CLR property that corresponds to the HTML attribute.</param>
/// <param name="typeName"> /// <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>
/// <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( public TagHelperAttributeDescriptor(
[NotNull] string name, [NotNull] string name,
[NotNull] string propertyName, [NotNull] string propertyName,
[NotNull] string typeName) [NotNull] string typeName,
bool isIndexer)
: this( : this(
name, name,
propertyName, propertyName,
typeName, typeName,
isIndexer,
isStringProperty: string.Equals(typeName, typeof(string).FullName, StringComparison.Ordinal)) 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( internal TagHelperAttributeDescriptor(
[NotNull] string name, [NotNull] string name,
[NotNull] string propertyName, [NotNull] string propertyName,
[NotNull] string typeName, [NotNull] string typeName,
bool isIndexer,
bool isStringProperty) bool isStringProperty)
{ {
Name = name; Name = name;
PropertyName = propertyName; PropertyName = propertyName;
TypeName = typeName; TypeName = typeName;
IsIndexer = isIndexer;
IsStringProperty = isStringProperty; IsStringProperty = isStringProperty;
} }
/// <summary> /// <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> /// </summary>
/// <value> /// <value>
/// If <c>true</c> the <see cref="TypeName"/> is for <see cref="string"/>. This causes the Razor parser /// 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> /// to allow empty values for HTML attributes matching this <see cref="TagHelperAttributeDescriptor"/>. If
/// empty values for such matching attributes lead to errors. /// <c>false</c> empty values for such matching attributes lead to errors.
/// </value> /// </value>
public bool IsStringProperty { get; } public bool IsStringProperty { get; }
/// <summary> /// <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> /// </summary>
public string Name { get; } public string Name { get; }
@ -76,8 +106,30 @@ namespace Microsoft.AspNet.Razor.TagHelpers
public string PropertyName { get; } public string PropertyName { get; }
/// <summary> /// <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> /// </summary>
public string TypeName { get; } 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 /> /// <inheritdoc />
/// <remarks> /// <remarks>
/// Determines equality based on <see cref="TagHelperAttributeDescriptor.Name"/>, /// Determines equality based on <see cref="TagHelperAttributeDescriptor.IsIndexer"/>,
/// <see cref="TagHelperAttributeDescriptor.PropertyName"/>, /// <see cref="TagHelperAttributeDescriptor.Name"/>, <see cref="TagHelperAttributeDescriptor.PropertyName"/>,
/// and <see cref="TagHelperAttributeDescriptor.TypeName"/>. /// and <see cref="TagHelperAttributeDescriptor.TypeName"/>. Ignores
/// <see cref="TagHelperAttributeDescriptor.IsStringProperty"/> because it can be inferred directly from
/// <see cref="TagHelperAttributeDescriptor.TypeName"/>.
/// </remarks> /// </remarks>
public virtual bool Equals(TagHelperAttributeDescriptor descriptorX, TagHelperAttributeDescriptor descriptorY) public virtual bool Equals(TagHelperAttributeDescriptor descriptorX, TagHelperAttributeDescriptor descriptorY)
{ {
@ -40,7 +42,11 @@ namespace Microsoft.AspNet.Razor.TagHelpers
return true; 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 && return descriptorX != null &&
descriptorX.IsIndexer == descriptorY.IsIndexer &&
string.Equals(descriptorX.Name, descriptorY.Name, StringComparison.OrdinalIgnoreCase) && string.Equals(descriptorX.Name, descriptorY.Name, StringComparison.OrdinalIgnoreCase) &&
string.Equals(descriptorX.PropertyName, descriptorY.PropertyName, StringComparison.Ordinal) && string.Equals(descriptorX.PropertyName, descriptorY.PropertyName, StringComparison.Ordinal) &&
string.Equals(descriptorX.TypeName, descriptorY.TypeName, StringComparison.Ordinal); string.Equals(descriptorX.TypeName, descriptorY.TypeName, StringComparison.Ordinal);
@ -49,7 +55,10 @@ namespace Microsoft.AspNet.Razor.TagHelpers
/// <inheritdoc /> /// <inheritdoc />
public virtual int GetHashCode([NotNull] TagHelperAttributeDescriptor descriptor) 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() return HashCodeCombiner.Start()
.Add(descriptor.IsIndexer)
.Add(descriptor.Name, StringComparer.OrdinalIgnoreCase) .Add(descriptor.Name, StringComparer.OrdinalIgnoreCase)
.Add(descriptor.PropertyName, StringComparer.Ordinal) .Add(descriptor.PropertyName, StringComparer.Ordinal)
.Add(descriptor.TypeName, StringComparer.Ordinal) .Add(descriptor.TypeName, StringComparer.Ordinal)

View File

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

View File

@ -34,7 +34,8 @@ namespace Microsoft.AspNet.Razor.Test.Generator
new TagHelperAttributeDescriptor( new TagHelperAttributeDescriptor(
"catchall-bound-string", "catchall-bound-string",
"BoundRequiredString", "BoundRequiredString",
typeof(string).FullName), typeof(string).FullName,
isIndexer: false),
}, },
requiredAttributes: new[] { "catchall-unbound-required" }), requiredAttributes: new[] { "catchall-unbound-required" }),
new TagHelperDescriptor( new TagHelperDescriptor(
@ -46,11 +47,13 @@ namespace Microsoft.AspNet.Razor.Test.Generator
new TagHelperAttributeDescriptor( new TagHelperAttributeDescriptor(
"input-bound-required-string", "input-bound-required-string",
"BoundRequiredString", "BoundRequiredString",
typeof(string).FullName), typeof(string).FullName,
isIndexer: false),
new TagHelperAttributeDescriptor( new TagHelperAttributeDescriptor(
"input-bound-string", "input-bound-string",
"BoundString", "BoundString",
typeof(string).FullName) typeof(string).FullName,
isIndexer: false)
}, },
requiredAttributes: new[] { "input-bound-required-string", "input-unbound-required" }), 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 public static TheoryData TagHelperDescriptorFlowTestData
{ {
get get
@ -498,6 +575,101 @@ namespace Microsoft.AspNet.Razor.Test.Generator
contentLength: 4) 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 // Test resource name, expected TagHelperDescriptors
// Note: The baseline resource name is equivalent to the test resource name. // 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 }, { "SingleTagHelper", null, DefaultPAndInputTagHelperDescriptors },
{ "BasicTagHelpers", DefaultPAndInputTagHelperDescriptors }, { "BasicTagHelpers", null, DefaultPAndInputTagHelperDescriptors },
{ "BasicTagHelpers.RemoveTagHelper", DefaultPAndInputTagHelperDescriptors }, { "BasicTagHelpers.RemoveTagHelper", null, DefaultPAndInputTagHelperDescriptors },
{ "BasicTagHelpers.Prefixed", PrefixedPAndInputTagHelperDescriptors }, { "BasicTagHelpers.Prefixed", null, PrefixedPAndInputTagHelperDescriptors },
{ "ComplexTagHelpers", DefaultPAndInputTagHelperDescriptors }, { "ComplexTagHelpers", null, DefaultPAndInputTagHelperDescriptors },
{ "DuplicateTargetTagHelper", DuplicateTargetTagHelperDescriptors }, { "DuplicateTargetTagHelper", null, DuplicateTargetTagHelperDescriptors },
{ "EmptyAttributeTagHelpers", DefaultPAndInputTagHelperDescriptors }, { "EmptyAttributeTagHelpers", null, DefaultPAndInputTagHelperDescriptors },
{ "EscapedTagHelpers", DefaultPAndInputTagHelperDescriptors }, { "EscapedTagHelpers", null, DefaultPAndInputTagHelperDescriptors },
{ "AttributeTargetingTagHelpers", AttributeTargetingTagHelperDescriptors }, { "AttributeTargetingTagHelpers", null, AttributeTargetingTagHelperDescriptors },
{ "PrefixedAttributeTagHelpers", null, PrefixedAttributeTagHelperDescriptors },
{
"PrefixedAttributeTagHelpers",
"PrefixedAttributeTagHelpers.Reversed",
PrefixedAttributeTagHelperDescriptors.Reverse()
},
}; };
} }
} }
[Theory] [Theory]
[MemberData(nameof(RuntimeTimeTagHelperTestData))] [MemberData(nameof(RuntimeTimeTagHelperTestData))]
public void TagHelpers_GenerateExpectedRuntimeOutput(string testName, public void TagHelpers_GenerateExpectedRuntimeOutput(
IEnumerable<TagHelperDescriptor> tagHelperDescriptors) string testName,
string baseLineName,
IEnumerable<TagHelperDescriptor> tagHelperDescriptors)
{ {
// Act & Assert // Arrange & Act & Assert
RunTagHelperTest(testName, tagHelperDescriptors: tagHelperDescriptors); RunTagHelperTest(testName, baseLineName, tagHelperDescriptors: tagHelperDescriptors);
} }
[Fact] [Fact]

View File

@ -181,6 +181,8 @@ namespace Microsoft.AspNet.Razor.TagHelpers
var noErrors = new RazorError[0]; var noErrors = new RazorError[0];
var errorFormat = "Attribute '{0}' on tag helper element '{1}' requires a value. Tag helper bound " + 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."; "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 stringType = typeof(string).FullName;
var intType = typeof(int).FullName; var intType = typeof(int).FullName;
var expressionString = "@DateTime.Now + 1"; 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) } 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 />", "<input unbound-required bound-required-string />",
new MarkupBlock( new MarkupBlock(
@ -878,7 +1070,8 @@ namespace Microsoft.AspNet.Razor.TagHelpers
new TagHelperAttributeDescriptor( new TagHelperAttributeDescriptor(
"bound-required-string", "bound-required-string",
"BoundRequiredString", "BoundRequiredString",
typeof(string).FullName) typeof(string).FullName,
isIndexer: false)
}, },
requiredAttributes: new[] { "unbound-required" }), requiredAttributes: new[] { "unbound-required" }),
new TagHelperDescriptor( new TagHelperDescriptor(
@ -890,7 +1083,8 @@ namespace Microsoft.AspNet.Razor.TagHelpers
new TagHelperAttributeDescriptor( new TagHelperAttributeDescriptor(
"bound-required-string", "bound-required-string",
"BoundRequiredString", "BoundRequiredString",
typeof(string).FullName) typeof(string).FullName,
isIndexer: false)
}, },
requiredAttributes: new[] { "bound-required-string" }), requiredAttributes: new[] { "bound-required-string" }),
new TagHelperDescriptor( new TagHelperDescriptor(
@ -902,9 +1096,38 @@ namespace Microsoft.AspNet.Razor.TagHelpers
new TagHelperAttributeDescriptor( new TagHelperAttributeDescriptor(
"bound-required-int", "bound-required-int",
"BoundRequiredInt", "BoundRequiredInt",
typeof(int).FullName) typeof(int).FullName,
isIndexer: false)
}, },
requiredAttributes: new[] { "bound-required-int" }), 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( new TagHelperDescriptor(
tagName: "p", tagName: "p",
typeName: "PTagHelper", typeName: "PTagHelper",
@ -914,11 +1137,13 @@ namespace Microsoft.AspNet.Razor.TagHelpers
new TagHelperAttributeDescriptor( new TagHelperAttributeDescriptor(
"bound-string", "bound-string",
"BoundRequiredString", "BoundRequiredString",
typeof(string).FullName), typeof(string).FullName,
isIndexer: false),
new TagHelperAttributeDescriptor( new TagHelperAttributeDescriptor(
"bound-int", "bound-int",
"BoundRequiredString", "BoundRequiredString",
typeof(int).FullName) typeof(int).FullName,
isIndexer: false)
}, },
requiredAttributes: Enumerable.Empty<string>()), requiredAttributes: Enumerable.Empty<string>()),
}; };

View File

@ -52,11 +52,13 @@ namespace Microsoft.AspNet.Razor.TagHelpers
new TagHelperAttributeDescriptor( new TagHelperAttributeDescriptor(
name: "attribute one", name: "attribute one",
propertyName: "property name", propertyName: "property name",
typeName: "property type name"), typeName: "property type name",
isIndexer: false),
new TagHelperAttributeDescriptor( new TagHelperAttributeDescriptor(
name: "attribute two", name: "attribute two",
propertyName: "property name", propertyName: "property name",
typeName: typeof(string).FullName), typeName: typeof(string).FullName,
isIndexer: false),
}, },
requiredAttributes: Enumerable.Empty<string>()); requiredAttributes: Enumerable.Empty<string>());
var expectedSerializedDescriptor = var expectedSerializedDescriptor =
@ -66,11 +68,62 @@ namespace Microsoft.AspNet.Razor.TagHelpers
$"\"{ nameof(TagHelperDescriptor.TypeName) }\":\"type name\"," + $"\"{ nameof(TagHelperDescriptor.TypeName) }\":\"type name\"," +
$"\"{ nameof(TagHelperDescriptor.AssemblyName) }\":\"assembly name\"," + $"\"{ nameof(TagHelperDescriptor.AssemblyName) }\":\"assembly name\"," +
$"\"{ nameof(TagHelperDescriptor.Attributes) }\":[" + $"\"{ nameof(TagHelperDescriptor.Attributes) }\":[" +
$"{{\"{ nameof(TagHelperAttributeDescriptor.IsStringProperty) }\":false," + $"{{\"{ nameof(TagHelperAttributeDescriptor.IsIndexer) }\":false," +
$"\"{ nameof(TagHelperAttributeDescriptor.IsStringProperty) }\":false," +
$"\"{ nameof(TagHelperAttributeDescriptor.Name) }\":\"attribute one\"," + $"\"{ nameof(TagHelperAttributeDescriptor.Name) }\":\"attribute one\"," +
$"\"{ nameof(TagHelperAttributeDescriptor.PropertyName) }\":\"property name\"," + $"\"{ nameof(TagHelperAttributeDescriptor.PropertyName) }\":\"property name\"," +
$"\"{ nameof(TagHelperAttributeDescriptor.TypeName) }\":\"property type 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.Name) }\":\"attribute two\"," +
$"\"{ nameof(TagHelperAttributeDescriptor.PropertyName) }\":\"property name\"," + $"\"{ nameof(TagHelperAttributeDescriptor.PropertyName) }\":\"property name\"," +
$"\"{ nameof(TagHelperAttributeDescriptor.TypeName) }\":\"{ typeof(string).FullName }\"}}]," + $"\"{ nameof(TagHelperAttributeDescriptor.TypeName) }\":\"{ typeof(string).FullName }\"}}]," +
@ -129,11 +182,13 @@ namespace Microsoft.AspNet.Razor.TagHelpers
$"\"{ nameof(TagHelperDescriptor.TypeName) }\":\"type name\"," + $"\"{ nameof(TagHelperDescriptor.TypeName) }\":\"type name\"," +
$"\"{ nameof(TagHelperDescriptor.AssemblyName) }\":\"assembly name\"," + $"\"{ nameof(TagHelperDescriptor.AssemblyName) }\":\"assembly name\"," +
$"\"{ nameof(TagHelperDescriptor.Attributes) }\":[" + $"\"{ nameof(TagHelperDescriptor.Attributes) }\":[" +
$"{{\"{ nameof(TagHelperAttributeDescriptor.IsStringProperty) }\":false," + $"{{\"{ nameof(TagHelperAttributeDescriptor.IsIndexer) }\":false," +
$"\"{ nameof(TagHelperAttributeDescriptor.IsStringProperty) }\":false," +
$"\"{ nameof(TagHelperAttributeDescriptor.Name) }\":\"attribute one\"," + $"\"{ nameof(TagHelperAttributeDescriptor.Name) }\":\"attribute one\"," +
$"\"{ nameof(TagHelperAttributeDescriptor.PropertyName) }\":\"property name\"," + $"\"{ nameof(TagHelperAttributeDescriptor.PropertyName) }\":\"property name\"," +
$"\"{ nameof(TagHelperAttributeDescriptor.TypeName) }\":\"property type 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.Name) }\":\"attribute two\"," +
$"\"{ nameof(TagHelperAttributeDescriptor.PropertyName) }\":\"property name\"," + $"\"{ nameof(TagHelperAttributeDescriptor.PropertyName) }\":\"property name\"," +
$"\"{ nameof(TagHelperAttributeDescriptor.TypeName) }\":\"{ typeof(string).FullName }\"}}]," + $"\"{ nameof(TagHelperAttributeDescriptor.TypeName) }\":\"{ typeof(string).FullName }\"}}]," +
@ -148,11 +203,13 @@ namespace Microsoft.AspNet.Razor.TagHelpers
new TagHelperAttributeDescriptor( new TagHelperAttributeDescriptor(
name: "attribute one", name: "attribute one",
propertyName: "property name", propertyName: "property name",
typeName: "property type name"), typeName: "property type name",
isIndexer: false),
new TagHelperAttributeDescriptor( new TagHelperAttributeDescriptor(
name: "attribute two", name: "attribute two",
propertyName: "property name", propertyName: "property name",
typeName: typeof(string).FullName), typeName: typeof(string).FullName,
isIndexer: false),
}, },
requiredAttributes: Enumerable.Empty<string>()); requiredAttributes: Enumerable.Empty<string>());
@ -166,7 +223,9 @@ namespace Microsoft.AspNet.Razor.TagHelpers
Assert.Equal(expectedDescriptor.FullTagName, descriptor.FullTagName, StringComparer.Ordinal); Assert.Equal(expectedDescriptor.FullTagName, descriptor.FullTagName, StringComparer.Ordinal);
Assert.Equal(expectedDescriptor.TypeName, descriptor.TypeName, StringComparer.Ordinal); Assert.Equal(expectedDescriptor.TypeName, descriptor.TypeName, StringComparer.Ordinal);
Assert.Equal(expectedDescriptor.AssemblyName, descriptor.AssemblyName, StringComparer.Ordinal); Assert.Equal(expectedDescriptor.AssemblyName, descriptor.AssemblyName, StringComparer.Ordinal);
Assert.Equal(2, descriptor.Attributes.Count); 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].IsStringProperty, descriptor.Attributes[0].IsStringProperty);
Assert.Equal(expectedDescriptor.Attributes[0].Name, descriptor.Attributes[0].Name, StringComparer.Ordinal); Assert.Equal(expectedDescriptor.Attributes[0].Name, descriptor.Attributes[0].Name, StringComparer.Ordinal);
Assert.Equal( Assert.Equal(
@ -177,6 +236,8 @@ namespace Microsoft.AspNet.Razor.TagHelpers
expectedDescriptor.Attributes[0].TypeName, expectedDescriptor.Attributes[0].TypeName,
descriptor.Attributes[0].TypeName, descriptor.Attributes[0].TypeName,
StringComparer.Ordinal); 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].IsStringProperty, descriptor.Attributes[1].IsStringProperty);
Assert.Equal(expectedDescriptor.Attributes[1].Name, descriptor.Attributes[1].Name, StringComparer.Ordinal); Assert.Equal(expectedDescriptor.Attributes[1].Name, descriptor.Attributes[1].Name, StringComparer.Ordinal);
Assert.Equal( Assert.Equal(
@ -187,6 +248,88 @@ namespace Microsoft.AspNet.Razor.TagHelpers
expectedDescriptor.Attributes[1].TypeName, expectedDescriptor.Attributes[1].TypeName,
descriptor.Attributes[1].TypeName, descriptor.Attributes[1].TypeName,
StringComparer.Ordinal); 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); Assert.Empty(descriptor.RequiredAttributes);
} }
} }

View File

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