diff --git a/src/Microsoft.AspNet.Razor.Runtime/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Razor.Runtime/Properties/Resources.Designer.cs index abc7c4710b..6416da158b 100644 --- a/src/Microsoft.AspNet.Razor.Runtime/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNet.Razor.Runtime/Properties/Resources.Designer.cs @@ -186,6 +186,38 @@ namespace Microsoft.AspNet.Razor.Runtime return GetString("TagHelperDescriptorFactory_Attribute"); } + /// + /// name + /// + internal static string TagHelperDescriptorFactory_Name + { + get { return GetString("TagHelperDescriptorFactory_Name"); } + } + + /// + /// name + /// + internal static string FormatTagHelperDescriptorFactory_Name() + { + return GetString("TagHelperDescriptorFactory_Name"); + } + + /// + /// prefix + /// + internal static string TagHelperDescriptorFactory_Prefix + { + get { return GetString("TagHelperDescriptorFactory_Prefix"); } + } + + /// + /// prefix + /// + internal static string FormatTagHelperDescriptorFactory_Prefix() + { + return GetString("TagHelperDescriptorFactory_Prefix"); + } + /// /// Tag /// @@ -203,19 +235,67 @@ namespace Microsoft.AspNet.Razor.Runtime } /// - /// 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. /// - internal static string TagHelperDescriptorFactory_InvalidBoundAttributeName + internal static string TagHelperDescriptorFactory_InvalidAttributeNameOrPrefixCharacter { - get { return GetString("TagHelperDescriptorFactory_InvalidBoundAttributeName"); } + get { return GetString("TagHelperDescriptorFactory_InvalidAttributeNameOrPrefixCharacter"); } } /// - /// 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. /// - 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); + } + + /// + /// Invalid tag helper bound property '{0}.{1}'. Tag helpers cannot bind to HTML attributes with {2} '{3}' because {2} starts with '{4}'. + /// + internal static string TagHelperDescriptorFactory_InvalidAttributeNameOrPrefixStart + { + get { return GetString("TagHelperDescriptorFactory_InvalidAttributeNameOrPrefixStart"); } + } + + /// + /// Invalid tag helper bound property '{0}.{1}'. Tag helpers cannot bind to HTML attributes with {2} '{3}' because {2} starts with '{4}'. + /// + 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); + } + + /// + /// Invalid tag helper bound property '{0}.{1}'. Tag helpers cannot bind to HTML attributes with a whitespace {2}. + /// + internal static string TagHelperDescriptorFactory_InvalidAttributeNameOrPrefixWhitespace + { + get { return GetString("TagHelperDescriptorFactory_InvalidAttributeNameOrPrefixWhitespace"); } + } + + /// + /// Invalid tag helper bound property '{0}.{1}'. Tag helpers cannot bind to HTML attributes with a whitespace {2}. + /// + internal static string FormatTagHelperDescriptorFactory_InvalidAttributeNameOrPrefixWhitespace(object p0, object p1, object p2) + { + return string.Format(CultureInfo.CurrentCulture, GetString("TagHelperDescriptorFactory_InvalidAttributeNameOrPrefixWhitespace"), p0, p1, p2); + } + + /// + /// Invalid tag helper bound property '{0}.{1}'. '{2}.{3}' must be null unless property type implements '{4}'. + /// + internal static string TagHelperDescriptorFactory_InvalidAttributePrefix + { + get { return GetString("TagHelperDescriptorFactory_InvalidAttributePrefix"); } + } + + /// + /// Invalid tag helper bound property '{0}.{1}'. '{2}.{3}' must be null unless property type implements '{4}'. + /// + 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); } /// diff --git a/src/Microsoft.AspNet.Razor.Runtime/Resources.resx b/src/Microsoft.AspNet.Razor.Runtime/Resources.resx index af51cbfab4..3d845ce34e 100644 --- a/src/Microsoft.AspNet.Razor.Runtime/Resources.resx +++ b/src/Microsoft.AspNet.Razor.Runtime/Resources.resx @@ -1,17 +1,17 @@ - @@ -150,11 +150,26 @@ Attribute + + name + + + prefix + Tag - - 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. + + + Invalid tag helper bound property '{0}.{1}'. Tag helpers cannot bind to HTML attributes with {2} '{3}' because {2} starts with '{4}'. + + + Invalid tag helper bound property '{0}.{1}'. Tag helpers cannot bind to HTML attributes with a whitespace {2}. + + + Invalid tag helper bound property '{0}.{1}'. '{2}.{3}' must be null unless property type implements '{4}'. Cannot add a '{0}' with a null '{1}'. diff --git a/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/HtmlAttributeNameAttribute.cs b/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/HtmlAttributeNameAttribute.cs index 0bb06da866..f22fb4b40f 100644 --- a/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/HtmlAttributeNameAttribute.cs +++ b/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/HtmlAttributeNameAttribute.cs @@ -11,6 +11,8 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)] public sealed class HtmlAttributeNameAttribute : Attribute { + private string _dictionaryAttributePrefix; + /// /// Instantiates a new instance of the class. /// @@ -29,5 +31,39 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers /// HTML attribute name of the associated property. /// public string Name { get; } + + /// + /// Gets or sets the prefix used to match HTML attribute names. Matching attributes are added to the + /// associated property (an ). + /// + /// + /// If non-null associated property must be compatible with + /// where TKey is + /// . + /// + /// + /// If associated property is compatible with + /// , default value is Name + "-". + /// Otherwise default value is null. + /// + public string DictionaryAttributePrefix + { + get + { + return _dictionaryAttributePrefix; + } + set + { + _dictionaryAttributePrefix = value; + DictionaryAttributePrefixSet = true; + } + } + + /// + /// Gets an indication whether has been set. Used to distinguish an + /// uninitialized value from an explicit null setting. + /// + /// true if was set. false otherwise. + public bool DictionaryAttributePrefixSet { get; private set; } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperDescriptorFactory.cs b/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperDescriptorFactory.cs index a59736a367..7c6fe4192c 100644 --- a/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperDescriptorFactory.cs +++ b/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperDescriptorFactory.cs @@ -38,7 +38,9 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers /// /// The assembly name that contains . /// The type to create a from. - /// A that describes the given . + /// + /// A collection of s that describe the given . + /// public static IEnumerable CreateDescriptors( string assemblyName, [NotNull] Type type, @@ -209,48 +211,195 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers var accessibleProperties = type.GetRuntimeProperties().Where(IsAccessibleProperty); var attributeDescriptors = new List(); + // Keep indexer descriptors separate to avoid sorting the combined list later. + var indexerDescriptors = new List(); + foreach (var property in accessibleProperties) { - var descriptor = ToAttributeDescriptor(property); + var attributeNameAttribute = property.GetCustomAttribute(inherit: false); + var descriptor = ToAttributeDescriptor(property, attributeNameAttribute); if (ValidateTagHelperAttributeDescriptor(descriptor, type, errorSink)) { + bool isInvalid; + var indexerDescriptor = ToIndexerAttributeDescriptor( + property, + attributeNameAttribute, + parentType: type, + errorSink: errorSink, + defaultPrefix: descriptor.Name + "-", + isInvalid: out isInvalid); + + if (indexerDescriptor != null && + !ValidateTagHelperAttributeDescriptor(indexerDescriptor, type, errorSink)) + { + isInvalid = true; + } + + if (isInvalid) + { + // HtmlAttributeNameAttribute was not valid. Ignore this property completely. + continue; + } + attributeDescriptors.Add(descriptor); + if (indexerDescriptor != null) + { + indexerDescriptors.Add(indexerDescriptor); + } } } + attributeDescriptors.AddRange(indexerDescriptors); + return attributeDescriptors; } - private static bool ValidateTagHelperAttributeDescriptor( + // Internal for testing. + internal static bool ValidateTagHelperAttributeDescriptor( TagHelperAttributeDescriptor attributeDescriptor, Type parentType, ErrorSink errorSink) { + var nameOrPrefix = attributeDescriptor.IsIndexer ? + Resources.TagHelperDescriptorFactory_Prefix : + Resources.TagHelperDescriptorFactory_Name; + return ValidateTagHelperAttributeNameOrPrefix( + attributeDescriptor.Name, + parentType, + attributeDescriptor.PropertyName, + errorSink, + nameOrPrefix); + } + + private static bool ValidateTagHelperAttributeNameOrPrefix( + string attributeNameOrPrefix, + Type parentType, + string propertyName, + ErrorSink errorSink, + string nameOrPrefix) + { + if (string.IsNullOrEmpty(attributeNameOrPrefix)) + { + // HtmlAttributeNameAttribute validates Name is non-null and non-empty. Both are valid for + // DictionaryAttributePrefix. (Empty DictionaryAttributePrefix is a corner case which would bind every + // attribute of a target element. Likely not particularly useful but unclear what minimum length + // should be required and what scenarios a minimum length would break.) + return true; + } + + if (string.IsNullOrWhiteSpace(attributeNameOrPrefix)) + { + // Provide a single error if the entire name is whitespace, not an error per character. + errorSink.OnError( + SourceLocation.Zero, + Resources.FormatTagHelperDescriptorFactory_InvalidAttributeNameOrPrefixWhitespace( + parentType.FullName, + propertyName, + nameOrPrefix)); + + return false; + } + // data-* attributes are explicitly not implemented by user agents and are not intended for use on // the server; therefore it's invalid for TagHelpers to bind to them. - if (attributeDescriptor.Name.StartsWith(DataDashPrefix, StringComparison.OrdinalIgnoreCase)) + if (attributeNameOrPrefix.StartsWith(DataDashPrefix, StringComparison.OrdinalIgnoreCase)) { errorSink.OnError( SourceLocation.Zero, - Resources.FormatTagHelperDescriptorFactory_InvalidBoundAttributeName( - attributeDescriptor.PropertyName, + Resources.FormatTagHelperDescriptorFactory_InvalidAttributeNameOrPrefixStart( parentType.FullName, + propertyName, + nameOrPrefix, + attributeNameOrPrefix, DataDashPrefix)); return false; } - return true; + var isValid = true; + foreach (var character in attributeNameOrPrefix) + { + if (char.IsWhiteSpace(character) || InvalidNonWhitespaceNameCharacters.Contains(character)) + { + errorSink.OnError( + SourceLocation.Zero, + Resources.FormatTagHelperDescriptorFactory_InvalidAttributeNameOrPrefixCharacter( + parentType.FullName, + propertyName, + nameOrPrefix, + attributeNameOrPrefix, + character)); + + isValid = false; + } + } + + return isValid; } - private static TagHelperAttributeDescriptor ToAttributeDescriptor(PropertyInfo property) + private static TagHelperAttributeDescriptor ToAttributeDescriptor( + PropertyInfo property, + HtmlAttributeNameAttribute attributeNameAttribute) { - var attributeNameAttribute = property.GetCustomAttribute(inherit: false); var attributeName = attributeNameAttribute != null ? attributeNameAttribute.Name : ToHtmlCase(property.Name); - return new TagHelperAttributeDescriptor(attributeName, property.Name, property.PropertyType.FullName); + return new TagHelperAttributeDescriptor( + attributeName, + property.Name, + property.PropertyType.FullName, + isIndexer: false); + } + + private static TagHelperAttributeDescriptor ToIndexerAttributeDescriptor( + PropertyInfo property, + HtmlAttributeNameAttribute attributeNameAttribute, + Type parentType, + ErrorSink errorSink, + string defaultPrefix, + out bool isInvalid) + { + isInvalid = false; + var dictionaryTypeArguments = ClosedGenericMatcher.ExtractGenericInterface( + property.PropertyType, + typeof(IDictionary<,>)) + ?.GenericTypeArguments; + if (dictionaryTypeArguments?[0] != typeof(string)) + { + if (attributeNameAttribute?.DictionaryAttributePrefix != null) + { + // DictionaryAttributePrefix is not supported unless associated with an + // IDictionary property. + isInvalid = true; + errorSink.OnError( + SourceLocation.Zero, + Resources.FormatTagHelperDescriptorFactory_InvalidAttributePrefix( + parentType.FullName, + property.Name, + nameof(HtmlAttributeNameAttribute), + nameof(HtmlAttributeNameAttribute.DictionaryAttributePrefix), + "IDictionary")); + } + + 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) diff --git a/src/Microsoft.AspNet.Razor.Runtime/project.json b/src/Microsoft.AspNet.Razor.Runtime/project.json index 7ab85eb86b..e37620525c 100644 --- a/src/Microsoft.AspNet.Razor.Runtime/project.json +++ b/src/Microsoft.AspNet.Razor.Runtime/project.json @@ -4,6 +4,7 @@ "dependencies": { "Microsoft.AspNet.Razor": "4.0.0-*", "Microsoft.Framework.BufferEntryCollection.Sources": { "type": "build", "version": "1.0.0-*" }, + "Microsoft.Framework.ClosedGenericMatcher.Sources": { "type": "build", "version": "1.0.0-*" }, "Microsoft.Framework.CopyOnWriteDictionary.Sources": { "type": "build", "version": "1.0.0-*" }, "Microsoft.Framework.NotNullAttribute.Sources": { "type": "build", "version": "1.0.0-*" } }, diff --git a/src/Microsoft.AspNet.Razor/Generator/Compiler/CodeBuilder/CSharp/CSharpTagHelperCodeRenderer.cs b/src/Microsoft.AspNet.Razor/Generator/Compiler/CodeBuilder/CSharp/CSharpTagHelperCodeRenderer.cs index 693ecf6288..de072cc497 100644 --- a/src/Microsoft.AspNet.Razor/Generator/Compiler/CodeBuilder/CSharp/CSharpTagHelperCodeRenderer.cs +++ b/src/Microsoft.AspNet.Razor/Generator/Compiler/CodeBuilder/CSharp/CSharpTagHelperCodeRenderer.cs @@ -69,10 +69,9 @@ namespace Microsoft.AspNet.Razor.Generator.Compiler.CSharp // Determine what attributes exist in the element and divide them up. var htmlAttributes = chunk.Attributes; - var attributeDescriptors = tagHelperDescriptors.SelectMany(descriptor => descriptor.Attributes); - var unboundHtmlAttributes = htmlAttributes.Where( - attribute => !attributeDescriptors.Any( - descriptor => string.Equals(attribute.Key, descriptor.Name, StringComparison.OrdinalIgnoreCase))); + var attributeDescriptors = tagHelperDescriptors.SelectMany(descriptor => descriptor.Attributes).ToArray(); + var unboundHtmlAttributes = htmlAttributes.Where(attribute => !attributeDescriptors.Any( + attributeDescriptor => attributeDescriptor.IsNameMatch(attribute.Key))); RenderUnboundHTMLAttributes(unboundHtmlAttributes); @@ -182,6 +181,7 @@ namespace Microsoft.AspNet.Razor.Generator.Compiler.CSharp // Render all of the bound attribute values for the tag helper. RenderBoundHTMLAttributes( chunk.Attributes, + tagHelperDescriptor.TypeName, tagHelperVariableName, tagHelperDescriptor.Attributes, htmlAttributeValues); @@ -190,81 +190,126 @@ namespace Microsoft.AspNet.Razor.Generator.Compiler.CSharp private void RenderBoundHTMLAttributes( IList> chunkAttributes, + string tagHelperTypeName, string tagHelperVariableName, IEnumerable attributeDescriptors, Dictionary htmlAttributeValues) { - foreach (var attributeDescriptor in attributeDescriptors) + // Track dictionary properties we have confirmed are non-null. + var confirmedDictionaries = new HashSet(StringComparer.Ordinal); + + // First attribute wins, even if there are duplicates. + var distinctAttributes = chunkAttributes.Distinct(KeyValuePairKeyComparer.Default); + + // Go through the HTML attributes in source order, assigning to properties or indexers as we go. + foreach (var attributeKeyValuePair in distinctAttributes) { - var matchingAttributes = chunkAttributes.Where( - kvp => string.Equals(kvp.Key, attributeDescriptor.Name, StringComparison.OrdinalIgnoreCase)); - - if (matchingAttributes.Any()) + var attributeName = attributeKeyValuePair.Key; + var attributeValueChunk = attributeKeyValuePair.Value; + if (attributeValueChunk == null) { - // First attribute wins, even if there's duplicates. - var firstAttribute = matchingAttributes.First(); - var attributeValueChunk = firstAttribute.Value; + // Minimized attributes are not valid for bound attributes. TagHelperBlockRewriter has already + // logged an error if it was a bound attribute; so we can skip. + continue; + } - // Minimized attributes are not valid for bound attributes. There will be an error for the bound - // attribute logged by TagHelperBlockRewriter already so we can skip. - if (attributeValueChunk == null) + // Find the matching TagHelperAttributeDescriptor. + var attributeDescriptor = attributeDescriptors.FirstOrDefault( + descriptor => descriptor.IsNameMatch(attributeName)); + if (attributeDescriptor == null) + { + // Attribute is not bound to a property or indexer in this tag helper. + continue; + } + + // We capture the tag helper's property value accessor so we can retrieve it later (if we need to). + var valueAccessor = string.Format( + CultureInfo.InvariantCulture, + "{0}.{1}", + tagHelperVariableName, + attributeDescriptor.PropertyName); + + if (attributeDescriptor.IsIndexer) + { + // Need a different valueAccessor in this case. But first need to throw a reasonable Exception at + // runtime if the property is null. The check is not required at design time. + if (!_designTimeMode && confirmedDictionaries.Add(attributeDescriptor.PropertyName)) + { + _writer + .Write("if (") + .Write(valueAccessor) + .WriteLine(" == null)"); + using (_writer.BuildScope()) + { + // System is in Host.NamespaceImports for all MVC scenarios. No need to generate FullName + // of InvalidOperationException type. + _writer + .Write("throw ") + .WriteStartNewObject(nameof(InvalidOperationException)) + .WriteStartMethodInvocation(_tagHelperContext.FormatInvalidIndexerAssignmentMethodName) + .WriteStringLiteral(attributeName) + .WriteParameterSeparator() + .WriteStringLiteral(tagHelperTypeName) + .WriteParameterSeparator() + .WriteStringLiteral(attributeDescriptor.PropertyName) + .WriteEndMethodInvocation(endLine: false) // End of method call + .WriteEndMethodInvocation(endLine: true); // End of new expression / throw statement + } + } + + var dictionaryKey = attributeName.Substring(attributeDescriptor.Name.Length); + valueAccessor = string.Format( + CultureInfo.InvariantCulture, + "{0}.{1}[\"{2}\"]", + tagHelperVariableName, + attributeDescriptor.PropertyName, + dictionaryKey); + } + + // If we haven't recorded this attribute value before then we need to record its value. + var attributeValueRecorded = htmlAttributeValues.ContainsKey(attributeName); + if (!attributeValueRecorded) + { + // We only need to create attribute values once per HTML element (not once per tag helper). + // We're saving the value accessor so we can retrieve it later if there are more tag + // helpers that need the value. + htmlAttributeValues.Add(attributeName, valueAccessor); + + // Bufferable attributes are attributes that can have Razor code inside of them. Such + // attributes have string values and may be calculated using a temporary TextWriter or other + // buffer. + var bufferableAttribute = attributeDescriptor.IsStringProperty; + + RenderNewAttributeValueAssignment( + attributeDescriptor, + bufferableAttribute, + attributeValueChunk, + valueAccessor); + + // Execution contexts are a runtime feature. + if (_designTimeMode) { continue; } - // We capture the tag helpers property value accessor so we can retrieve it later (if we need to). - var valueAccessor = string.Format( - CultureInfo.InvariantCulture, - "{0}.{1}", - tagHelperVariableName, - attributeDescriptor.PropertyName); - - // If we haven't recorded this attribute value before then we need to record its value. - var attributeValueRecorded = htmlAttributeValues.ContainsKey(attributeDescriptor.Name); - if (!attributeValueRecorded) - { - // We only need to create attribute values once per HTML element (not once per tag helper). - // We're saving the value accessor so we can retrieve it later if there are more tag - // helpers that need the value. - htmlAttributeValues.Add(attributeDescriptor.Name, valueAccessor); - - // Bufferable attributes are attributes that can have Razor code inside of them. Such - // attributes have string values and may be calculated using a temporary TextWriter or other - // buffer. - var bufferableAttribute = attributeDescriptor.IsStringProperty; - - RenderNewAttributeValueAssignment( - attributeDescriptor, - bufferableAttribute, - attributeValueChunk, - valueAccessor); - - // Execution contexts are a runtime feature. - if (_designTimeMode) - { - continue; - } - - // We need to inform the context of the attribute value. - var attributeName = firstAttribute.Key; - _writer - .WriteStartInstanceMethodInvocation( - ExecutionContextVariableName, - _tagHelperContext.ExecutionContextAddTagHelperAttributeMethodName) - .WriteStringLiteral(attributeName) - .WriteParameterSeparator() - .Write(valueAccessor) - .WriteEndMethodInvocation(); - } - else - { - // The attribute value has already been recorded, lets retrieve it from the stored value - // accessors. - _writer - .WriteStartAssignment(valueAccessor) - .Write(htmlAttributeValues[attributeDescriptor.Name]) - .WriteLine(";"); - } + // We need to inform the context of the attribute value. + _writer + .WriteStartInstanceMethodInvocation( + ExecutionContextVariableName, + _tagHelperContext.ExecutionContextAddTagHelperAttributeMethodName) + .WriteStringLiteral(attributeName) + .WriteParameterSeparator() + .Write(valueAccessor) + .WriteEndMethodInvocation(); + } + else + { + // The attribute value has already been recorded, lets retrieve it from the stored value + // accessors. + _writer + .WriteStartAssignment(valueAccessor) + .Write(htmlAttributeValues[attributeName]) + .WriteLine(";"); } } } @@ -580,6 +625,26 @@ namespace Microsoft.AspNet.Razor.Generator.Compiler.CSharp return true; } + // An IEqualityComparer for string -> Chunk mappings which compares only the Key. + private class KeyValuePairKeyComparer : IEqualityComparer> + { + public static KeyValuePairKeyComparer Default = new KeyValuePairKeyComparer(); + + private KeyValuePairKeyComparer() + { + } + + public bool Equals(KeyValuePair keyValuePairX, KeyValuePair keyValuePairY) + { + return string.Equals(keyValuePairX.Key, keyValuePairY.Key, StringComparison.OrdinalIgnoreCase); + } + + public int GetHashCode(KeyValuePair keyValuePair) + { + return keyValuePair.Key.GetHashCode(); + } + } + // A CSharpCodeVisitor which does not HTML encode values. Used when rendering bound string attribute values. private class CSharpLiteralCodeVisitor : CSharpCodeVisitor { diff --git a/src/Microsoft.AspNet.Razor/Generator/GeneratedTagHelperContext.cs b/src/Microsoft.AspNet.Razor/Generator/GeneratedTagHelperContext.cs index 57eb270b56..060d3fd2f9 100644 --- a/src/Microsoft.AspNet.Razor/Generator/GeneratedTagHelperContext.cs +++ b/src/Microsoft.AspNet.Razor/Generator/GeneratedTagHelperContext.cs @@ -22,6 +22,7 @@ namespace Microsoft.AspNet.Razor.Generator ExecutionContextAddMinimizedHtmlAttributeMethodName = "AddMinimizedHtmlAttribute"; ExecutionContextAddHtmlAttributeMethodName = "AddHtmlAttribute"; ExecutionContextOutputPropertyName = "Output"; + FormatInvalidIndexerAssignmentMethodName = "FormatInvalidIndexerAssignment"; MarkAsHtmlEncodedMethodName = "Html.Raw"; StartTagHelperWritingScopeMethodName = "StartTagHelperWritingScope"; EndTagHelperWritingScopeMethodName = "EndTagHelperWritingScope"; @@ -78,6 +79,21 @@ namespace Microsoft.AspNet.Razor.Generator /// public string ExecutionContextOutputPropertyName { get; set; } + /// + /// The name of the method used to format an error message about using an indexer when the tag helper property + /// is null. + /// + /// + /// Method signature should be + /// + /// 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. + /// + /// + public string FormatInvalidIndexerAssignmentMethodName { get; set; } + /// /// The name of the method used to wrap a value and mark it as HTML-encoded. /// diff --git a/src/Microsoft.AspNet.Razor/Parser/TagHelpers/TagHelperBlockRewriter.cs b/src/Microsoft.AspNet.Razor/Parser/TagHelpers/TagHelperBlockRewriter.cs index 6d0f98ec85..7ba4c97e06 100644 --- a/src/Microsoft.AspNet.Razor/Parser/TagHelpers/TagHelperBlockRewriter.cs +++ b/src/Microsoft.AspNet.Razor/Parser/TagHelpers/TagHelperBlockRewriter.cs @@ -1,7 +1,6 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; @@ -65,16 +64,18 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal // Only want to track the attribute if we succeeded in parsing its corresponding Block/Span. if (result != null) { + SourceLocation? errorLocation = null; + // Check if it's a bound attribute that is minimized or if it's a bound non-string attribute that // is null or whitespace. if ((result.IsBoundAttribute && result.AttributeValueNode == null) || (result.IsBoundNonStringAttribute && IsNullOrWhitespaceAttributeValue(result.AttributeValueNode))) { - var errorLocation = GetAttributeNameStartLocation(child); + errorLocation = GetAttributeNameStartLocation(child); errorSink.OnError( - errorLocation, + errorLocation.Value, RazorResources.FormatRewriterError_EmptyTagHelperBoundAttribute( result.AttributeName, tagName, @@ -82,6 +83,23 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal result.AttributeName.Length); } + // Check if the attribute was a prefix match for a tag helper dictionary property but the + // dictionary key would be the empty string. + if (result.IsMissingDictionaryKey) + { + if (!errorLocation.HasValue) + { + errorLocation = GetAttributeNameStartLocation(child); + } + + errorSink.OnError( + errorLocation.Value, + RazorResources.FormatTagHelperBlockRewriter_IndexerAttributeNameMustIncludeKey( + result.AttributeName, + tagName), + result.AttributeName.Length); + } + attributes.Add( new KeyValuePair(result.AttributeName, result.AttributeValueNode)); } @@ -224,24 +242,19 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal { errorSink.OnError( span.Start, - RazorResources.TagHelperBlockRewriter_TagHelperAttributeListMustBeWelformed, + RazorResources.TagHelperBlockRewriter_TagHelperAttributeListMustBeWellFormed, span.Content.Length); } return null; } - bool isBoundNonStringAttribute; - var result = new TryParseResult - { - IsBoundAttribute = IsBoundAttribute(name, descriptors, out isBoundNonStringAttribute), - AttributeName = name, - }; - result.IsBoundNonStringAttribute = isBoundNonStringAttribute; + var result = CreateTryParseResult(name, descriptors); // If we're not after an equal then we should treat the value as if it were a minimized attribute. var attributeValueBuilder = afterEquals ? builder : null; - result.AttributeValueNode = CreateMarkupAttribute(name, attributeValueBuilder, isBoundNonStringAttribute); + result.AttributeValueNode = + CreateMarkupAttribute(name, attributeValueBuilder, result.IsBoundNonStringAttribute); return result; } @@ -286,13 +299,7 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal } // Have a name now. Able to determine correct isBoundNonStringAttribute value. - bool isBoundNonStringAttribute; - var result = new TryParseResult - { - IsBoundAttribute = IsBoundAttribute(name, descriptors, out isBoundNonStringAttribute), - AttributeName = name, - }; - result.IsBoundNonStringAttribute = isBoundNonStringAttribute; + var result = CreateTryParseResult(name, descriptors); // Remove first child i.e. foo=" builder.Children.RemoveAt(0); @@ -324,13 +331,13 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal if (block.Children.Count() == 1) { var child = block.Children.First() as Span; - if (child != null) { // After pulling apart the block we just have a value span. var spanBuilder = new SpanBuilder(child); - result.AttributeValueNode = CreateMarkupAttribute(name, spanBuilder, isBoundNonStringAttribute); + result.AttributeValueNode = + CreateMarkupAttribute(name, spanBuilder, result.IsBoundNonStringAttribute); return result; } @@ -485,17 +492,23 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal return firstBoundAttribute?.TypeName; } - // Determines whether an attribute with the given name is bound to a non-string tag helper property. - private static bool IsBoundAttribute( - string name, - IEnumerable descriptors, - out bool isBoundNonStringAttribute) + // Create a TryParseResult for given name, filling in binding details. + private static TryParseResult CreateTryParseResult(string name, IEnumerable descriptors) { var firstBoundAttribute = FindFirstBoundAttribute(name, descriptors); var isBoundAttribute = firstBoundAttribute != null; - isBoundNonStringAttribute = isBoundAttribute && !firstBoundAttribute.IsStringProperty; + var isBoundNonStringAttribute = isBoundAttribute && !firstBoundAttribute.IsStringProperty; + var isMissingDictionaryKey = isBoundAttribute && + firstBoundAttribute.IsIndexer && + name.Length == firstBoundAttribute.Name.Length; - return isBoundAttribute; + return new TryParseResult + { + AttributeName = name, + IsBoundAttribute = isBoundAttribute, + IsBoundNonStringAttribute = isBoundNonStringAttribute, + IsMissingDictionaryKey = isMissingDictionaryKey, + }; } // Finds first TagHelperAttributeDescriptor matching given name. @@ -503,9 +516,13 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal string name, IEnumerable descriptors) { - return descriptors + // Non-indexers (exact HTML attribute name matches) have higher precedence than indexers (prefix matches). + // Attributes already sorted to ensure this precedence. + var firstBoundAttribute = descriptors .SelectMany(descriptor => descriptor.Attributes) - .FirstOrDefault(attribute => string.Equals(attribute.Name, name, StringComparison.OrdinalIgnoreCase)); + .FirstOrDefault(attributeDescriptor => attributeDescriptor.IsNameMatch(name)); + + return firstBoundAttribute; } private static bool IsQuote(HtmlSymbol htmlSymbol) @@ -523,6 +540,8 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal public bool IsBoundAttribute { get; set; } public bool IsBoundNonStringAttribute { get; set; } + + public bool IsMissingDictionaryKey { get; set; } } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor/Properties/RazorResources.Designer.cs b/src/Microsoft.AspNet.Razor/Properties/RazorResources.Designer.cs index 59baba43be..2dda1a87a6 100644 --- a/src/Microsoft.AspNet.Razor/Properties/RazorResources.Designer.cs +++ b/src/Microsoft.AspNet.Razor/Properties/RazorResources.Designer.cs @@ -1367,19 +1367,35 @@ namespace Microsoft.AspNet.Razor } /// - /// TagHelper attributes must be welformed. + /// TagHelper attributes must be well-formed. /// - internal static string TagHelperBlockRewriter_TagHelperAttributeListMustBeWelformed + internal static string TagHelperBlockRewriter_TagHelperAttributeListMustBeWellFormed { - get { return GetString("TagHelperBlockRewriter_TagHelperAttributeListMustBeWelformed"); } + get { return GetString("TagHelperBlockRewriter_TagHelperAttributeListMustBeWellFormed"); } } /// - /// TagHelper attributes must be welformed. + /// TagHelper attributes must be well-formed. /// - internal static string FormatTagHelperBlockRewriter_TagHelperAttributeListMustBeWelformed() + internal static string FormatTagHelperBlockRewriter_TagHelperAttributeListMustBeWellFormed() { - return GetString("TagHelperBlockRewriter_TagHelperAttributeListMustBeWelformed"); + return GetString("TagHelperBlockRewriter_TagHelperAttributeListMustBeWellFormed"); + } + + /// + /// The tag helper attribute '{0}' in element '{1}' is missing a key. The syntax is '<{1} {0}{{ key }}="value">'. + /// + internal static string TagHelperBlockRewriter_IndexerAttributeNameMustIncludeKey + { + get { return GetString("TagHelperBlockRewriter_IndexerAttributeNameMustIncludeKey"); } + } + + /// + /// The tag helper attribute '{0}' in element '{1}' is missing a key. The syntax is '<{1} {0}{{ key }}="value">'. + /// + internal static string FormatTagHelperBlockRewriter_IndexerAttributeNameMustIncludeKey(object p0, object p1) + { + return string.Format(CultureInfo.CurrentCulture, GetString("TagHelperBlockRewriter_IndexerAttributeNameMustIncludeKey"), p0, p1); } /// diff --git a/src/Microsoft.AspNet.Razor/RazorResources.resx b/src/Microsoft.AspNet.Razor/RazorResources.resx index a94a7393dc..2286330599 100644 --- a/src/Microsoft.AspNet.Razor/RazorResources.resx +++ b/src/Microsoft.AspNet.Razor/RazorResources.resx @@ -1,17 +1,17 @@ - @@ -390,8 +390,11 @@ Instead, wrap the contents of the block in "{{}}": Missing close angle for tag helper '{0}'. - - TagHelper attributes must be welformed. + + TagHelper attributes must be well-formed. + + + The tag helper attribute '{0}' in element '{1}' is missing a key. The syntax is '<{1} {0}{{ key }}="value">'. Non-string tag helper attribute values must not be empty. Add an expression to this attribute value. diff --git a/src/Microsoft.AspNet.Razor/TagHelpers/TagHelperAttributeDescriptor.cs b/src/Microsoft.AspNet.Razor/TagHelpers/TagHelperAttributeDescriptor.cs index 36ee0854c3..a4c370ae7b 100644 --- a/src/Microsoft.AspNet.Razor/TagHelpers/TagHelperAttributeDescriptor.cs +++ b/src/Microsoft.AspNet.Razor/TagHelpers/TagHelperAttributeDescriptor.cs @@ -18,6 +18,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers name, propertyInfo.Name, propertyInfo.PropertyType.FullName, + isIndexer: false, isStringProperty: propertyInfo.PropertyType == typeof(string)) { } @@ -25,48 +26,77 @@ namespace Microsoft.AspNet.Razor.TagHelpers /// /// Instantiates a new instance of the class. /// - /// The HTML attribute name. + /// + /// The HTML attribute name or, if is true, the prefix for matching + /// attribute names. + /// /// The name of the CLR property that corresponds to the HTML attribute. /// - /// The full name of the named (see ) property's . + /// The full name of the named (see ) property's or, + /// if is true, the full name of the indexer's value . /// + /// + /// If true this is used for dictionary indexer assignments. + /// Otherwise this is used for property assignment. + /// + /// + /// HTML attribute names are matched case-insensitively, regardless of . + /// public TagHelperAttributeDescriptor( [NotNull] string name, [NotNull] string propertyName, - [NotNull] string typeName) + [NotNull] string typeName, + bool isIndexer) : this( name, propertyName, typeName, + isIndexer, isStringProperty: string.Equals(typeName, typeof(string).FullName, StringComparison.Ordinal)) { } - // Internal for testing i.e. for confirming above constructor sets `IsStringProperty` as expected. + // Internal for testing i.e. for confirming above constructor sets IsStringProperty as expected. internal TagHelperAttributeDescriptor( [NotNull] string name, [NotNull] string propertyName, [NotNull] string typeName, + bool isIndexer, bool isStringProperty) { Name = name; PropertyName = propertyName; TypeName = typeName; + IsIndexer = isIndexer; IsStringProperty = isStringProperty; } /// - /// Gets an indication whether this property is of type . + /// Gets an indication whether this is used for dictionary indexer + /// assignments. + /// + /// + /// If true this should be associated with all HTML + /// attributes that have names starting with . Otherwise this + /// is used for property assignment and is only associated with an + /// HTML attribute that has the exact . + /// + public bool IsIndexer { get; } + + /// + /// Gets an indication whether this property is of type or, if is + /// true, whether the indexer's value is of type . /// /// /// If true the is for . This causes the Razor parser - /// to allow empty values for attributes that have names matching . If false - /// empty values for such matching attributes lead to errors. + /// to allow empty values for HTML attributes matching this . If + /// false empty values for such matching attributes lead to errors. /// public bool IsStringProperty { get; } /// - /// The HTML attribute name. + /// The HTML attribute name or, if is true, the prefix for matching attribute + /// names. /// public string Name { get; } @@ -76,8 +106,30 @@ namespace Microsoft.AspNet.Razor.TagHelpers public string PropertyName { get; } /// - /// The full name of the named (see ) property's . + /// The full name of the named (see ) property's or, if + /// is true, the full name of the indexer's value . /// public string TypeName { get; } + + /// + /// Determines whether HTML attribute matches this + /// . + /// + /// Name of the HTML attribute to check. + /// + /// true if this matches . + /// false otherwise. + /// + public bool IsNameMatch(string name) + { + if (IsIndexer) + { + return name.StartsWith(Name, StringComparison.OrdinalIgnoreCase); + } + else + { + return string.Equals(name, Name, StringComparison.OrdinalIgnoreCase); + } + } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor/TagHelpers/TagHelperAttributeDescriptorComparer.cs b/src/Microsoft.AspNet.Razor/TagHelpers/TagHelperAttributeDescriptorComparer.cs index fa2b082d2d..c118af1406 100644 --- a/src/Microsoft.AspNet.Razor/TagHelpers/TagHelperAttributeDescriptorComparer.cs +++ b/src/Microsoft.AspNet.Razor/TagHelpers/TagHelperAttributeDescriptorComparer.cs @@ -29,9 +29,11 @@ namespace Microsoft.AspNet.Razor.TagHelpers /// /// - /// Determines equality based on , - /// , - /// and . + /// Determines equality based on , + /// , , + /// and . Ignores + /// because it can be inferred directly from + /// . /// public virtual bool Equals(TagHelperAttributeDescriptor descriptorX, TagHelperAttributeDescriptor descriptorY) { @@ -40,7 +42,11 @@ namespace Microsoft.AspNet.Razor.TagHelpers return true; } + // Check Name and TypeName though each property in a particular tag helper has at most two + // TagHelperAttributeDescriptors (one for the indexer and one not). May be comparing attributes between + // tag helpers and should be as specific as we can. return descriptorX != null && + descriptorX.IsIndexer == descriptorY.IsIndexer && string.Equals(descriptorX.Name, descriptorY.Name, StringComparison.OrdinalIgnoreCase) && string.Equals(descriptorX.PropertyName, descriptorY.PropertyName, StringComparison.Ordinal) && string.Equals(descriptorX.TypeName, descriptorY.TypeName, StringComparison.Ordinal); @@ -49,7 +55,10 @@ namespace Microsoft.AspNet.Razor.TagHelpers /// public virtual int GetHashCode([NotNull] TagHelperAttributeDescriptor descriptor) { + // Rarely if ever hash TagHelperAttributeDescriptor. If we do, include the Name and TypeName since context + // information is not available in the hash. return HashCodeCombiner.Start() + .Add(descriptor.IsIndexer) .Add(descriptor.Name, StringComparer.OrdinalIgnoreCase) .Add(descriptor.PropertyName, StringComparer.Ordinal) .Add(descriptor.TypeName, StringComparer.Ordinal) diff --git a/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/CaseSensitiveTagHelperAttributeDescriptorComparer.cs b/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/CaseSensitiveTagHelperAttributeDescriptorComparer.cs index 9fa50dbc45..b31394fbef 100644 --- a/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/CaseSensitiveTagHelperAttributeDescriptorComparer.cs +++ b/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/CaseSensitiveTagHelperAttributeDescriptorComparer.cs @@ -24,8 +24,8 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers return true; } - // Normal comparer doesn't care about case, in tests we do. Also double-check IsStringProperty though - // it is inferred from TypeName. + // Base comparer does not care about Name case but in tests we do. Also double-check IsStringProperty + // though it is inferred from TypeName. return base.Equals(descriptorX, descriptorY) && descriptorX.IsStringProperty == descriptorY.IsStringProperty && string.Equals(descriptorX.Name, descriptorY.Name, StringComparison.Ordinal); @@ -33,8 +33,8 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers public override int GetHashCode(TagHelperAttributeDescriptor descriptor) { - // Rarely if ever hash TagHelperAttributeDescriptor. If we do, ignore IsStringProperty since it should - // not vary for a given TypeName i.e. will not change the bucket. + // Ignore IsStringProperty because it is directly inferred from TypeName and thus won't vary the hash + // bucket. Base comparer does not care about Name case in its hash code but in tests we do. return HashCodeCombiner.Start() .Add(base.GetHashCode(descriptor)) .Add(descriptor.Name, StringComparer.Ordinal) diff --git a/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/TagHelperDescriptorFactoryTest.cs b/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/TagHelperDescriptorFactoryTest.cs index b103a7617f..06520824a7 100644 --- a/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/TagHelperDescriptorFactoryTest.cs +++ b/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/TagHelperDescriptorFactoryTest.cs @@ -221,7 +221,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers [Theory] [MemberData(nameof(HtmlCaseData))] - public void CreateDescriptor_HtmlCasesTagNameAndAttributeName( + public void CreateDescriptors_HtmlCasesTagNameAndAttributeName( Type tagHelperType, string expectedTagName, string expectedAttributeName) @@ -244,7 +244,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers } [Fact] - public void CreateDescriptor_OverridesAttributeNameFromAttribute() + public void CreateDescriptors_OverridesAttributeNameFromAttribute() { // Arrange var errorSink = new ErrorSink(); @@ -273,11 +273,11 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers // Assert Assert.Empty(errorSink.Errors); - Assert.Equal(expectedDescriptors, descriptors.ToArray(), CaseSensitiveTagHelperDescriptorComparer.Default); + Assert.Equal(expectedDescriptors, descriptors, CaseSensitiveTagHelperDescriptorComparer.Default); } [Fact] - public void CreateDescriptor_DoesNotInheritOverridenAttributeName() + public void CreateDescriptors_DoesNotInheritOverridenAttributeName() { // Arrange var errorSink = new ErrorSink(); @@ -306,11 +306,11 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers // Assert Assert.Empty(errorSink.Errors); - Assert.Equal(expectedDescriptors, descriptors.ToArray(), CaseSensitiveTagHelperDescriptorComparer.Default); + Assert.Equal(expectedDescriptors, descriptors, CaseSensitiveTagHelperDescriptorComparer.Default); } [Fact] - public void CreateDescriptor_AllowsOverridenAttributeNameOnUnimplementedVirtual() + public void CreateDescriptors_AllowsOverridenAttributeNameOnUnimplementedVirtual() { // Arrange var errorSink = new ErrorSink(); @@ -343,7 +343,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers } [Fact] - public void CreateDescriptor_BuildsDescriptorsFromSimpleTypes() + public void CreateDescriptors_BuildsDescriptorsFromSimpleTypes() { // Arrange var errorSink = new ErrorSink(); @@ -364,7 +364,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers } [Fact] - public void CreateDescriptor_BuildsDescriptorsWithInheritedProperties() + public void CreateDescriptors_BuildsDescriptorsWithInheritedProperties() { // Arrange var errorSink = new ErrorSink(); @@ -380,6 +380,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers "int-attribute", nameof(InheritedSingleAttributeTagHelper.IntAttribute), typeof(int).FullName, + isIndexer: false, isStringProperty: false) }); @@ -396,7 +397,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers } [Fact] - public void CreateDescriptor_BuildsDescriptorsWithConventionNames() + public void CreateDescriptors_BuildsDescriptorsWithConventionNames() { // Arrange var errorSink = new ErrorSink(); @@ -423,7 +424,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers } [Fact] - public void CreateDescriptor_OnlyAcceptsPropertiesWithGetAndSet() + public void CreateDescriptors_OnlyAcceptsPropertiesWithGetAndSet() { // Arrange var errorSink = new ErrorSink(); @@ -451,7 +452,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers } [Fact] - public void CreateDescriptor_OnlyAcceptsPropertiesWithPublicGetAndSet() + public void CreateDescriptors_OnlyAcceptsPropertiesWithPublicGetAndSet() { // Arrange var errorSink = new ErrorSink(); @@ -479,7 +480,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers } [Fact] - public void CreateDescriptor_DoesNotIncludePropertiesWithNotBound() + public void CreateDescriptors_DoesNotIncludePropertiesWithNotBound() { // Arrange var errorSink = new ErrorSink(); @@ -495,6 +496,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers "bound-property", nameof(NotBoundAttributeTagHelper.BoundProperty), typeof(object).FullName, + isIndexer: false, isStringProperty: false) }); @@ -511,7 +513,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers } [Fact(Skip = "#364")] - public void CreateDescriptor_AddsErrorForTagHelperWithDuplicateAttributeNames() + public void CreateDescriptors_AddsErrorForTagHelperWithDuplicateAttributeNames() { // Arrange var errorSink = new ErrorSink(); @@ -528,7 +530,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers } [Fact] - public void CreateDescriptor_ResolvesMultipleTagHelperDescriptorsFromSingleType() + public void CreateDescriptors_ResolvesMultipleTagHelperDescriptorsFromSingleType() { // Arrange var errorSink = new ErrorSink(); @@ -546,6 +548,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers "valid-attribute", nameof(MultiTagTagHelper.ValidAttribute), typeof(string).FullName, + isIndexer: false, isStringProperty: true) }), new TagHelperDescriptor( @@ -558,6 +561,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers "valid-attribute", nameof(MultiTagTagHelper.ValidAttribute), typeof(string).FullName, + isIndexer: false, isStringProperty: true) }) }; @@ -579,7 +583,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers } [Fact] - public void CreateDescriptor_DoesntResolveInheritedTagNames() + public void CreateDescriptors_DoesNotResolveInheritedTagNames() { // Arrange var errorSink = new ErrorSink(); @@ -606,7 +610,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers } [Fact] - public void CreateDescriptor_IgnoresDuplicateTagNamesFromAttribute() + public void CreateDescriptors_IgnoresDuplicateTagNamesFromAttribute() { // Arrange var errorSink = new ErrorSink(); @@ -639,7 +643,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers } [Fact] - public void CreateDescriptor_OverridesTagNameFromAttribute() + public void CreateDescriptors_OverridesTagNameFromAttribute() { // Arrange var errorSink = new ErrorSink(); @@ -658,215 +662,23 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers // Assert Assert.Empty(errorSink.Errors); - Assert.Equal(expectedDescriptors, descriptors.ToArray(), CaseSensitiveTagHelperDescriptorComparer.Default); + Assert.Equal(expectedDescriptors, descriptors, CaseSensitiveTagHelperDescriptorComparer.Default); } - public static TheoryData InvalidNameData + // name, expectedErrorMessages + public static TheoryData InvalidNameData { get { - var invalidNameError = - "Tag helpers cannot target {0} name '{1}' because it contains a '{2}' character."; - var nullOrWhitespaceNameError = - "{0} name cannot be null or whitespace."; - Func onNameError = (invalidText, invalidCharacter) => - string.Format(invalidNameError, "tag", invalidText, invalidCharacter); + Func onNameError = + (invalidText, invalidCharacter) => $"Tag helpers cannot target tag name '{ invalidText }' " + + $"because it contains a '{ invalidCharacter }' character."; + var whitespaceErrorString = "Tag name cannot be null or whitespace."; - // name, expectedErrorMessages - return new TheoryData - { - { "!", new[] { onNameError("!", "!") } }, - { "hello!", new[] { onNameError("hello!", "!") } }, - { "!hello", new[] { onNameError("!hello", "!") } }, - { "he!lo", new[] { onNameError("he!lo", "!") } }, - { - "!he!lo!", - new[] - { - onNameError("!he!lo!", "!"), - onNameError("!he!lo!", "!"), - onNameError("!he!lo!", "!") - } - }, - { "@", new[] { onNameError("@", "@") } }, - { "hello@", new[] { onNameError("hello@", "@") } }, - { "@hello", new[] { onNameError("@hello", "@") } }, - { "he@lo", new[] { onNameError("he@lo", "@") } }, - { - "@he@lo@", - new[] - { - onNameError("@he@lo@", "@"), - onNameError("@he@lo@", "@"), - onNameError("@he@lo@", "@") - } - }, - { "/", new[] { onNameError("/", "/") } }, - { "hello/", new[] { onNameError("hello/", "/") } }, - { "/hello", new[] { onNameError("/hello", "/") } }, - { "he/lo", new[] { onNameError("he/lo", "/") } }, - { - "/he/lo/", - new[] - { - onNameError("/he/lo/", "/"), - onNameError("/he/lo/", "/"), - onNameError("/he/lo/", "/") - } - }, - { "<", new[] { onNameError("<", "<") } }, - { "hello<", new[] { onNameError("hello<", "<") } }, - { "", new[] { onNameError(">", ">") } }, - { "hello>", new[] { onNameError("hello>", ">") } }, - { ">hello", new[] { onNameError(">hello", ">") } }, - { "he>lo", new[] { onNameError("he>lo", ">") } }, - { - ">he>lo>", - new[] - { - onNameError(">he>lo>", ">"), - onNameError(">he>lo>", ">"), - onNameError(">he>lo>", ">") - } - }, - { "]", new[] { onNameError("]", "]") } }, - { "hello]", new[] { onNameError("hello]", "]") } }, - { "]hello", new[] { onNameError("]hello", "]") } }, - { "he]lo", new[] { onNameError("he]lo", "]") } }, - { - "]he]lo]", - new[] - { - onNameError("]he]lo]", "]"), - onNameError("]he]lo]", "]"), - onNameError("]he]lo]", "]") - } - }, - { "=", new[] { onNameError("=", "=") } }, - { "hello=", new[] { onNameError("hello=", "=") } }, - { "=hello", new[] { onNameError("=hello", "=") } }, - { "he=lo", new[] { onNameError("he=lo", "=") } }, - { - "=he=lo=", - new[] - { - onNameError("=he=lo=", "="), - onNameError("=he=lo=", "="), - onNameError("=he=lo=", "=") - } - }, - { "\"", new[] { onNameError("\"", "\"") } }, - { "hello\"", new[] { onNameError("hello\"", "\"") } }, - { "\"hello", new[] { onNameError("\"hello", "\"") } }, - { "he\"lo", new[] { onNameError("he\"lo", "\"") } }, - { - "\"he\"lo\"", - new[] - { - onNameError("\"he\"lo\"", "\""), - onNameError("\"he\"lo\"", "\""), - onNameError("\"he\"lo\"", "\"") - } - }, - { "'", new[] { onNameError("'", "'") } }, - { "hello'", new[] { onNameError("hello'", "'") } }, - { "'hello", new[] { onNameError("'hello", "'") } }, - { "he'lo", new[] { onNameError("he'lo", "'") } }, - { - "'he'lo'", - new[] - { - onNameError("'he'lo'", "'"), - onNameError("'he'lo'", "'"), - onNameError("'he'lo'", "'") - } - }, - { string.Empty, new[] { string.Format(nullOrWhitespaceNameError, "Tag") } }, - { Environment.NewLine, new[] { string.Format(nullOrWhitespaceNameError, "Tag") } }, - { "\t", new[] { string.Format(nullOrWhitespaceNameError, "Tag") } }, - { " \t ", new[] { string.Format(nullOrWhitespaceNameError, "Tag") } }, - { " ", new[] { string.Format(nullOrWhitespaceNameError, "Tag") } }, - { Environment.NewLine + " ", new[] { string.Format(nullOrWhitespaceNameError, "Tag") } }, - { - "! \t\r\n@/<>?[]=\"'", - new[] - { - onNameError("! \t\r\n@/<>?[]=\"'", "!"), - onNameError("! \t\r\n@/<>?[]=\"'", " "), - onNameError("! \t\r\n@/<>?[]=\"'", "\t"), - onNameError("! \t\r\n@/<>?[]=\"'", "\r"), - onNameError("! \t\r\n@/<>?[]=\"'", "\n"), - onNameError("! \t\r\n@/<>?[]=\"'", "@"), - onNameError("! \t\r\n@/<>?[]=\"'", "/"), - onNameError("! \t\r\n@/<>?[]=\"'", "<"), - onNameError("! \t\r\n@/<>?[]=\"'", ">"), - onNameError("! \t\r\n@/<>?[]=\"'", "?"), - onNameError("! \t\r\n@/<>?[]=\"'", "["), - onNameError("! \t\r\n@/<>?[]=\"'", "]"), - onNameError("! \t\r\n@/<>?[]=\"'", "="), - onNameError("! \t\r\n@/<>?[]=\"'", "\""), - onNameError("! \t\r\n@/<>?[]=\"'", "'"), - } - }, - { - "! \tv\ra\nl@i/d<>?[]=\"'", - new[] - { - onNameError("! \tv\ra\nl@i/d<>?[]=\"'", "!"), - onNameError("! \tv\ra\nl@i/d<>?[]=\"'", " "), - onNameError("! \tv\ra\nl@i/d<>?[]=\"'", "\t"), - onNameError("! \tv\ra\nl@i/d<>?[]=\"'", "\r"), - onNameError("! \tv\ra\nl@i/d<>?[]=\"'", "\n"), - onNameError("! \tv\ra\nl@i/d<>?[]=\"'", "@"), - onNameError("! \tv\ra\nl@i/d<>?[]=\"'", "/"), - onNameError("! \tv\ra\nl@i/d<>?[]=\"'", "<"), - onNameError("! \tv\ra\nl@i/d<>?[]=\"'", ">"), - onNameError("! \tv\ra\nl@i/d<>?[]=\"'", "?"), - onNameError("! \tv\ra\nl@i/d<>?[]=\"'", "["), - onNameError("! \tv\ra\nl@i/d<>?[]=\"'", "]"), - onNameError("! \tv\ra\nl@i/d<>?[]=\"'", "="), - onNameError("! \tv\ra\nl@i/d<>?[]=\"'", "\""), - onNameError("! \tv\ra\nl@i/d<>?[]=\"'", "'"), - } - }, - }; + var data = GetInvalidNameOrPrefixData(onNameError, whitespaceErrorString, onDataError: null); + data.Add(string.Empty, new[] { whitespaceErrorString }); + + return data; } } @@ -884,10 +696,12 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers // Assert var errors = errorSink.Errors.ToArray(); - for (var i = 0; i < errors.Length; i++) + Assert.Equal(expectedErrorMessages.Length, errors.Length); + for (var i = 0; i < expectedErrorMessages.Length; i++) { - Assert.Equal(expectedErrorMessages[i], errors[i].Message); + Assert.Equal(1, errors[i].Length); Assert.Equal(SourceLocation.Zero, errors[i].Location); + Assert.Equal(expectedErrorMessages[i], errors[i].Message, StringComparer.Ordinal); } } @@ -943,7 +757,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers get { var errorFormat = "Invalid tag helper bound property '{0}.{1}'. Tag helpers cannot bind to HTML " + - "attributes beginning with 'data-'."; + "attributes with name '{2}' because name starts with 'data-'."; // type, expectedAttributeDescriptors, expectedErrors return new TheoryData, string[]> @@ -955,8 +769,9 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers { string.Format( errorFormat, + typeof(InvalidBoundAttribute).FullName, nameof(InvalidBoundAttribute.DataSomething), - typeof(InvalidBoundAttribute).FullName) + "data-something") } }, { @@ -972,8 +787,9 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers { string.Format( errorFormat, + typeof(InvalidBoundAttributeWithValid).FullName, nameof(InvalidBoundAttributeWithValid.DataSomething), - typeof(InvalidBoundAttributeWithValid).FullName) + "data-something") } }, { @@ -994,8 +810,9 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers { string.Format( errorFormat, + typeof(OverriddenValidBoundAttributeWithInvalid).FullName, nameof(OverriddenValidBoundAttributeWithInvalid.ValidSomething), - typeof(OverriddenValidBoundAttributeWithInvalid).FullName) + "data-something") } }, { @@ -1005,8 +822,9 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers { string.Format( errorFormat, + typeof(OverriddenValidBoundAttributeWithInvalidUpperCase).FullName, nameof(OverriddenValidBoundAttributeWithInvalidUpperCase.ValidSomething), - typeof(OverriddenValidBoundAttributeWithInvalidUpperCase).FullName) + "DATA-SOMETHING") } }, }; @@ -1015,7 +833,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers [Theory] [MemberData(nameof(InvalidTagHelperAttributeDescriptorData))] - public void CreateDescriptor_DoesNotAllowDataDashAttributes( + public void CreateDescriptors_DoesNotAllowDataDashAttributes( Type type, IEnumerable expectedAttributeDescriptors, string[] expectedErrors) @@ -1035,7 +853,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers var actualError = actualErrors[i]; Assert.Equal(1, actualError.Length); Assert.Equal(SourceLocation.Zero, actualError.Location); - Assert.Equal(expectedErrors[i], actualError.Message); + Assert.Equal(expectedErrors[i], actualError.Message, StringComparer.Ordinal); } var actualDescriptor = Assert.Single(descriptors); @@ -1045,6 +863,584 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers CaseSensitiveTagHelperAttributeDescriptorComparer.Default); } + // tagTelperType, expectedAttributeDescriptors, expectedErrorMessages + public static TheoryData, string[]> TagHelperWithPrefixData + { + get + { + Func onError = (typeName, propertyName) => + $"Invalid tag helper bound property '{ typeName }.{ propertyName }'. " + + $"'{ nameof(HtmlAttributeNameAttribute) }." + + $"{ nameof(HtmlAttributeNameAttribute.DictionaryAttributePrefix) }' must be null unless " + + "property type implements 'IDictionary'."; + + // tagTelperType, expectedAttributeDescriptors, expectedErrorMessages + return new TheoryData, string[]> + { + { + typeof(DefaultValidHtmlAttributePrefix), + new[] + { + new TagHelperAttributeDescriptor( + name: "dictionary-property", + propertyName: nameof(DefaultValidHtmlAttributePrefix.DictionaryProperty), + typeName: typeof(IDictionary).FullName, + isIndexer: false, + isStringProperty: false), + new TagHelperAttributeDescriptor( + name: "dictionary-property-", + propertyName: nameof(DefaultValidHtmlAttributePrefix.DictionaryProperty), + typeName: typeof(string).FullName, + isIndexer: true, + isStringProperty: true), + }, + new string[0] + }, + { + typeof(SingleValidHtmlAttributePrefix), + new[] + { + new TagHelperAttributeDescriptor( + name: "valid-name", + propertyName: nameof(SingleValidHtmlAttributePrefix.DictionaryProperty), + typeName: typeof(IDictionary).FullName, + isIndexer: false, + isStringProperty: false), + new TagHelperAttributeDescriptor( + name: "valid-prefix", + propertyName: nameof(SingleValidHtmlAttributePrefix.DictionaryProperty), + typeName: typeof(string).FullName, + isIndexer: true, + isStringProperty: true), + }, + new string[0] + }, + { + typeof(MultipleValidHtmlAttributePrefix), + new[] + { + new TagHelperAttributeDescriptor( + name: "valid-name1", + propertyName: nameof(MultipleValidHtmlAttributePrefix.DictionaryProperty), + typeName: typeof(Dictionary).FullName, + isIndexer: false, + isStringProperty: false), + new TagHelperAttributeDescriptor( + name: "valid-name2", + propertyName: nameof(MultipleValidHtmlAttributePrefix.DictionarySubclassProperty), + typeName: typeof(DictionarySubclass).FullName, + isIndexer: false, + isStringProperty: false), + new TagHelperAttributeDescriptor( + name: "valid-name3", + propertyName: nameof(MultipleValidHtmlAttributePrefix.DictionaryWithoutParameterlessConstructorProperty), + typeName: typeof(DictionaryWithoutParameterlessConstructor).FullName, + isIndexer: false, + isStringProperty: false), + new TagHelperAttributeDescriptor( + name: "valid-name4", + propertyName: nameof(MultipleValidHtmlAttributePrefix.GenericDictionarySubclassProperty), + typeName: typeof(GenericDictionarySubclass).FullName, + isIndexer: false, + isStringProperty: false), + new TagHelperAttributeDescriptor( + name: "valid-name5", + propertyName: nameof(MultipleValidHtmlAttributePrefix.SortedDictionaryProperty), + typeName: typeof(SortedDictionary).FullName, + isIndexer: false, + isStringProperty: false), + new TagHelperAttributeDescriptor( + name: "valid-name6", + propertyName: nameof(MultipleValidHtmlAttributePrefix.StringProperty), + typeName: typeof(string).FullName, + isIndexer: false, + isStringProperty: true), + new TagHelperAttributeDescriptor( + name: "valid-prefix1-", + propertyName: nameof(MultipleValidHtmlAttributePrefix.DictionaryProperty), + typeName: typeof(object).FullName, + isIndexer: true, + isStringProperty: false), + new TagHelperAttributeDescriptor( + name: "valid-prefix2-", + propertyName: nameof(MultipleValidHtmlAttributePrefix.DictionarySubclassProperty), + typeName: typeof(string).FullName, + isIndexer: true, + isStringProperty: true), + new TagHelperAttributeDescriptor( + name: "valid-prefix3-", + propertyName: nameof(MultipleValidHtmlAttributePrefix.DictionaryWithoutParameterlessConstructorProperty), + typeName: typeof(string).FullName, + isIndexer: true, + isStringProperty: true), + new TagHelperAttributeDescriptor( + name: "valid-prefix4-", + propertyName: nameof(MultipleValidHtmlAttributePrefix.GenericDictionarySubclassProperty), + typeName: typeof(object).FullName, + isIndexer: true, + isStringProperty: false), + new TagHelperAttributeDescriptor( + name: "valid-prefix5-", + propertyName: nameof(MultipleValidHtmlAttributePrefix.SortedDictionaryProperty), + typeName: typeof(int).FullName, + isIndexer: true, + isStringProperty: false), + }, + new string[0] + }, + { + typeof(SingleInvalidHtmlAttributePrefix), + Enumerable.Empty(), + new[] + { + onError( + typeof(SingleInvalidHtmlAttributePrefix).FullName, + nameof(SingleInvalidHtmlAttributePrefix.StringProperty)), + } + }, + { + typeof(MultipleInvalidHtmlAttributePrefix), + new[] + { + new TagHelperAttributeDescriptor( + name: "valid-name1", + propertyName: nameof(MultipleInvalidHtmlAttributePrefix.LongProperty), + typeName: typeof(long).FullName, + isIndexer: false, + isStringProperty: false), + }, + new[] + { + onError( + typeof(MultipleInvalidHtmlAttributePrefix).FullName, + nameof(MultipleInvalidHtmlAttributePrefix.DictionaryOfIntProperty)), + onError( + typeof(MultipleInvalidHtmlAttributePrefix).FullName, + nameof(MultipleInvalidHtmlAttributePrefix.ReadOnlyDictionaryProperty)), + onError( + typeof(MultipleInvalidHtmlAttributePrefix).FullName, + nameof(MultipleInvalidHtmlAttributePrefix.IntProperty)), + onError( + typeof(MultipleInvalidHtmlAttributePrefix).FullName, + nameof(MultipleInvalidHtmlAttributePrefix.DictionaryOfIntSubclassProperty)), + } + }, + }; + } + } + + [Theory] + [MemberData(nameof(TagHelperWithPrefixData))] + public void CreateDescriptors_WithPrefixes_ReturnsExpectedAttributeDescriptors( + Type tagHelperType, + IEnumerable expectedAttributeDescriptors, + string[] expectedErrorMessages) + { + // Arrange + var errorSink = new ErrorSink(); + + // Act + var descriptors = TagHelperDescriptorFactory.CreateDescriptors(AssemblyName, tagHelperType, errorSink); + + // Assert + var errors = errorSink.Errors.ToArray(); + Assert.Equal(expectedErrorMessages.Length, errors.Length); + + for (var i = 0; i < errors.Length; i++) + { + Assert.Equal(1, errors[i].Length); + Assert.Equal(SourceLocation.Zero, errors[i].Location); + Assert.Equal(expectedErrorMessages[i], errors[i].Message, StringComparer.Ordinal); + } + + var descriptor = Assert.Single(descriptors); + Assert.Equal( + expectedAttributeDescriptors, + descriptor.Attributes, + CaseSensitiveTagHelperAttributeDescriptorComparer.Default); + } + + public static TheoryData ValidAttributeNameOrPrefixData + { + get + { + return new TheoryData + { + null, + string.Empty, + "data", + "dataa-", + "ValidName", + "valid-name", + "--valid--name--", + ",,--__..oddly.valid::;;", + }; + } + } + + [Theory] + [MemberData(nameof(ValidAttributeNameOrPrefixData))] + public void ValidateTagHelperAttributeDescriptor_WithValidName_ReturnsTrue(string name) + { + // Arrange + var descriptor = new TagHelperAttributeDescriptor( + name, + propertyName: "ValidProperty", + typeName: "PropertyType", + isIndexer: false); + var errorSink = new ErrorSink(); + + // Act + var result = TagHelperDescriptorFactory.ValidateTagHelperAttributeDescriptor( + descriptor, + typeof(MultiTagTagHelper), + errorSink); + + // Assert + Assert.True(result); + Assert.Empty(errorSink.Errors); + } + + [Theory] + [MemberData(nameof(ValidAttributeNameOrPrefixData))] + public void ValidateTagHelperAttributeDescriptor_WithValidPrefix_ReturnsTrue(string prefix) + { + // Arrange + var descriptor = new TagHelperAttributeDescriptor( + name: prefix, + propertyName: "ValidProperty", + typeName: "PropertyType", + isIndexer: true); + var errorSink = new ErrorSink(); + + // Act + var result = TagHelperDescriptorFactory.ValidateTagHelperAttributeDescriptor( + descriptor, + typeof(MultiTagTagHelper), + errorSink); + + // Assert + Assert.True(result); + Assert.Empty(errorSink.Errors); + } + + // name, expectedErrorMessages + public static TheoryData InvalidAttributeNameData + { + get + { + Func onNameError = (invalidText, invalidCharacter) => "Invalid tag helper " + + $"bound property '{ typeof(MultiTagTagHelper).FullName }.InvalidProperty'. Tag helpers cannot " + + $"bind to HTML attributes with name '{ invalidText }' because name contains a " + + $"'{ invalidCharacter }' character."; + var whitespaceErrorString = "Invalid tag helper bound property " + + $"'{ typeof(MultiTagTagHelper).FullName }.InvalidProperty'. Tag helpers cannot bind to HTML " + + "attributes with a whitespace name."; + Func onDataError = invalidText => "Invalid tag helper bound property " + + $"'{ typeof(MultiTagTagHelper).FullName }.InvalidProperty'. Tag helpers cannot bind to HTML " + + $"attributes with name '{ invalidText }' because name starts with 'data-'."; + + return GetInvalidNameOrPrefixData(onNameError, whitespaceErrorString, onDataError); + } + } + + [Theory] + [MemberData(nameof(InvalidAttributeNameData))] + public void ValidateTagHelperAttributeDescriptor_WithInvalidName_AddsExpectedErrors( + string name, + string[] expectedErrorMessages) + { + // Arrange + var descriptor = new TagHelperAttributeDescriptor( + name, + propertyName: "InvalidProperty", + typeName: "PropertyType", + isIndexer: false); + var errorSink = new ErrorSink(); + + // Act + var result = TagHelperDescriptorFactory.ValidateTagHelperAttributeDescriptor( + descriptor, + typeof(MultiTagTagHelper), + errorSink); + + // Assert + Assert.False(result); + + var errors = errorSink.Errors.ToArray(); + Assert.Equal(expectedErrorMessages.Length, errors.Length); + for (var i = 0; i < expectedErrorMessages.Length; i++) + { + Assert.Equal(1, errors[i].Length); + Assert.Equal(SourceLocation.Zero, errors[i].Location); + Assert.Equal(expectedErrorMessages[i], errors[i].Message, StringComparer.Ordinal); + } + } + + // prefix, expectedErrorMessages + public static TheoryData InvalidAttributePrefixData + { + get + { + Func onPrefixError = (invalidText, invalidCharacter) => "Invalid tag helper " + + $"bound property '{ typeof(MultiTagTagHelper).FullName }.InvalidProperty'. Tag helpers cannot " + + $"bind to HTML attributes with prefix '{ invalidText }' because prefix contains a " + + $"'{ invalidCharacter }' character."; + var whitespaceErrorString = "Invalid tag helper bound property " + + $"'{ typeof(MultiTagTagHelper).FullName }.InvalidProperty'. Tag helpers cannot bind to HTML " + + "attributes with a whitespace prefix."; + Func onDataError = invalidText => "Invalid tag helper bound property " + + $"'{ typeof(MultiTagTagHelper).FullName }.InvalidProperty'. Tag helpers cannot bind to HTML " + + $"attributes with prefix '{ invalidText }' because prefix starts with 'data-'."; + + return GetInvalidNameOrPrefixData(onPrefixError, whitespaceErrorString, onDataError); + } + } + + [Theory] + [MemberData(nameof(InvalidAttributePrefixData))] + public void ValidateTagHelperAttributeDescriptor_WithInvalidPrefix_AddsExpectedErrors( + string prefix, + string[] expectedErrorMessages) + { + // Arrange + var descriptor = new TagHelperAttributeDescriptor( + name: prefix, + propertyName: "InvalidProperty", + typeName: "ValuesType", + isIndexer: true); + var errorSink = new ErrorSink(); + + // Act + var result = TagHelperDescriptorFactory.ValidateTagHelperAttributeDescriptor( + descriptor, + typeof(MultiTagTagHelper), + errorSink); + + // Assert + Assert.False(result); + + var errors = errorSink.Errors.ToArray(); + Assert.Equal(expectedErrorMessages.Length, errors.Length); + for (var i = 0; i < expectedErrorMessages.Length; i++) + { + Assert.Equal(1, errors[i].Length); + Assert.Equal(SourceLocation.Zero, errors[i].Location); + Assert.Equal(expectedErrorMessages[i], errors[i].Message, StringComparer.Ordinal); + } + } + + private static TheoryData GetInvalidNameOrPrefixData( + Func onNameError, + string whitespaceErrorString, + Func onDataError) + { + // name, expectedErrorMessages + var data = new TheoryData + { + { "!", new[] { onNameError("!", "!") } }, + { "hello!", new[] { onNameError("hello!", "!") } }, + { "!hello", new[] { onNameError("!hello", "!") } }, + { "he!lo", new[] { onNameError("he!lo", "!") } }, + { + "!he!lo!", + new[] + { + onNameError("!he!lo!", "!"), + onNameError("!he!lo!", "!"), + onNameError("!he!lo!", "!"), + } + }, + { "@", new[] { onNameError("@", "@") } }, + { "hello@", new[] { onNameError("hello@", "@") } }, + { "@hello", new[] { onNameError("@hello", "@") } }, + { "he@lo", new[] { onNameError("he@lo", "@") } }, + { + "@he@lo@", + new[] + { + onNameError("@he@lo@", "@"), + onNameError("@he@lo@", "@"), + onNameError("@he@lo@", "@"), + } + }, + { "/", new[] { onNameError("/", "/") } }, + { "hello/", new[] { onNameError("hello/", "/") } }, + { "/hello", new[] { onNameError("/hello", "/") } }, + { "he/lo", new[] { onNameError("he/lo", "/") } }, + { + "/he/lo/", + new[] + { + onNameError("/he/lo/", "/"), + onNameError("/he/lo/", "/"), + onNameError("/he/lo/", "/"), + } + }, + { "<", new[] { onNameError("<", "<") } }, + { "hello<", new[] { onNameError("hello<", "<") } }, + { "", new[] { onNameError(">", ">") } }, + { "hello>", new[] { onNameError("hello>", ">") } }, + { ">hello", new[] { onNameError(">hello", ">") } }, + { "he>lo", new[] { onNameError("he>lo", ">") } }, + { + ">he>lo>", + new[] + { + onNameError(">he>lo>", ">"), + onNameError(">he>lo>", ">"), + onNameError(">he>lo>", ">"), + } + }, + { "]", new[] { onNameError("]", "]") } }, + { "hello]", new[] { onNameError("hello]", "]") } }, + { "]hello", new[] { onNameError("]hello", "]") } }, + { "he]lo", new[] { onNameError("he]lo", "]") } }, + { + "]he]lo]", + new[] + { + onNameError("]he]lo]", "]"), + onNameError("]he]lo]", "]"), + onNameError("]he]lo]", "]"), + } + }, + { "=", new[] { onNameError("=", "=") } }, + { "hello=", new[] { onNameError("hello=", "=") } }, + { "=hello", new[] { onNameError("=hello", "=") } }, + { "he=lo", new[] { onNameError("he=lo", "=") } }, + { + "=he=lo=", + new[] + { + onNameError("=he=lo=", "="), + onNameError("=he=lo=", "="), + onNameError("=he=lo=", "="), + } + }, + { "\"", new[] { onNameError("\"", "\"") } }, + { "hello\"", new[] { onNameError("hello\"", "\"") } }, + { "\"hello", new[] { onNameError("\"hello", "\"") } }, + { "he\"lo", new[] { onNameError("he\"lo", "\"") } }, + { + "\"he\"lo\"", + new[] + { + onNameError("\"he\"lo\"", "\""), + onNameError("\"he\"lo\"", "\""), + onNameError("\"he\"lo\"", "\""), + } + }, + { "'", new[] { onNameError("'", "'") } }, + { "hello'", new[] { onNameError("hello'", "'") } }, + { "'hello", new[] { onNameError("'hello", "'") } }, + { "he'lo", new[] { onNameError("he'lo", "'") } }, + { + "'he'lo'", + new[] + { + onNameError("'he'lo'", "'"), + onNameError("'he'lo'", "'"), + onNameError("'he'lo'", "'"), + } + }, + { Environment.NewLine, new[] { whitespaceErrorString } }, + { "\t", new[] { whitespaceErrorString } }, + { " \t ", new[] { whitespaceErrorString } }, + { " ", new[] { whitespaceErrorString } }, + { Environment.NewLine + " ", new[] { whitespaceErrorString } }, + { + "! \t\r\n@/<>?[]=\"'", + new[] + { + onNameError("! \t\r\n@/<>?[]=\"'", "!"), + onNameError("! \t\r\n@/<>?[]=\"'", " "), + onNameError("! \t\r\n@/<>?[]=\"'", "\t"), + onNameError("! \t\r\n@/<>?[]=\"'", "\r"), + onNameError("! \t\r\n@/<>?[]=\"'", "\n"), + onNameError("! \t\r\n@/<>?[]=\"'", "@"), + onNameError("! \t\r\n@/<>?[]=\"'", "/"), + onNameError("! \t\r\n@/<>?[]=\"'", "<"), + onNameError("! \t\r\n@/<>?[]=\"'", ">"), + onNameError("! \t\r\n@/<>?[]=\"'", "?"), + onNameError("! \t\r\n@/<>?[]=\"'", "["), + onNameError("! \t\r\n@/<>?[]=\"'", "]"), + onNameError("! \t\r\n@/<>?[]=\"'", "="), + onNameError("! \t\r\n@/<>?[]=\"'", "\""), + onNameError("! \t\r\n@/<>?[]=\"'", "'"), + } + }, + { + "! \tv\ra\nl@i/d<>?[]=\"'", + new[] + { + onNameError("! \tv\ra\nl@i/d<>?[]=\"'", "!"), + onNameError("! \tv\ra\nl@i/d<>?[]=\"'", " "), + onNameError("! \tv\ra\nl@i/d<>?[]=\"'", "\t"), + onNameError("! \tv\ra\nl@i/d<>?[]=\"'", "\r"), + onNameError("! \tv\ra\nl@i/d<>?[]=\"'", "\n"), + onNameError("! \tv\ra\nl@i/d<>?[]=\"'", "@"), + onNameError("! \tv\ra\nl@i/d<>?[]=\"'", "/"), + onNameError("! \tv\ra\nl@i/d<>?[]=\"'", "<"), + onNameError("! \tv\ra\nl@i/d<>?[]=\"'", ">"), + onNameError("! \tv\ra\nl@i/d<>?[]=\"'", "?"), + onNameError("! \tv\ra\nl@i/d<>?[]=\"'", "["), + onNameError("! \tv\ra\nl@i/d<>?[]=\"'", "]"), + onNameError("! \tv\ra\nl@i/d<>?[]=\"'", "="), + onNameError("! \tv\ra\nl@i/d<>?[]=\"'", "\""), + onNameError("! \tv\ra\nl@i/d<>?[]=\"'", "'"), + } + }, + }; + + if (onDataError != null) + { + data.Add("data-", new[] { onDataError("data-") }); + data.Add("data-something", new[] { onDataError("data-something") }); + data.Add("Data-Something", new[] { onDataError("Data-Something") }); + data.Add("DATA-SOMETHING", new[] { onDataError("DATA-SOMETHING") }); + } + + return data; + } + [TargetElement(Attributes = "class")] private class AttributeTargetingTagHelper : TagHelper { @@ -1232,5 +1628,81 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers [HtmlAttributeName("DATA-SOMETHING")] public string ValidSomething { get; set; } } + + private class DefaultValidHtmlAttributePrefix : TagHelper + { + public IDictionary DictionaryProperty { get; set; } + } + + private class SingleValidHtmlAttributePrefix : TagHelper + { + [HtmlAttributeName("valid-name", DictionaryAttributePrefix = "valid-prefix")] + public IDictionary DictionaryProperty { get; set; } + } + + private class MultipleValidHtmlAttributePrefix : TagHelper + { + [HtmlAttributeName("valid-name1", DictionaryAttributePrefix = "valid-prefix1-")] + public Dictionary DictionaryProperty { get; set; } + + [HtmlAttributeName("valid-name2", DictionaryAttributePrefix = "valid-prefix2-")] + public DictionarySubclass DictionarySubclassProperty { get; set; } + + [HtmlAttributeName("valid-name3", DictionaryAttributePrefix = "valid-prefix3-")] + public DictionaryWithoutParameterlessConstructor DictionaryWithoutParameterlessConstructorProperty { get; set; } + + [HtmlAttributeName("valid-name4", DictionaryAttributePrefix = "valid-prefix4-")] + public GenericDictionarySubclass GenericDictionarySubclassProperty { get; set; } + + [HtmlAttributeName("valid-name5", DictionaryAttributePrefix = "valid-prefix5-")] + public SortedDictionary SortedDictionaryProperty { get; set; } + + [HtmlAttributeName("valid-name6")] + public string StringProperty { get; set; } + } + + private class SingleInvalidHtmlAttributePrefix : TagHelper + { + [HtmlAttributeName("valid-name", DictionaryAttributePrefix = "valid-prefix")] + public string StringProperty { get; set; } + } + + private class MultipleInvalidHtmlAttributePrefix : TagHelper + { + [HtmlAttributeName("valid-name1")] + public long LongProperty { get; set; } + + [HtmlAttributeName("valid-name2", DictionaryAttributePrefix = "valid-prefix2-")] + public Dictionary DictionaryOfIntProperty { get; set; } + + [HtmlAttributeName("valid-name3", DictionaryAttributePrefix = "valid-prefix3-")] + public IReadOnlyDictionary ReadOnlyDictionaryProperty { get; set; } + + [HtmlAttributeName("valid-name4", DictionaryAttributePrefix = "valid-prefix4-")] + public int IntProperty { get; set; } + + [HtmlAttributeName("valid-name5", DictionaryAttributePrefix = "valid-prefix5-")] + public DictionaryOfIntSubclass DictionaryOfIntSubclassProperty { get; set; } + } + + private class DictionarySubclass : Dictionary + { + } + + private class DictionaryWithoutParameterlessConstructor : Dictionary + { + public DictionaryWithoutParameterlessConstructor(int count) + : base() + { + } + } + + private class DictionaryOfIntSubclass : Dictionary + { + } + + private class GenericDictionarySubclass : Dictionary + { + } } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.Razor.Test/Generator/CSharpTagHelperRenderingTest.cs b/test/Microsoft.AspNet.Razor.Test/Generator/CSharpTagHelperRenderingTest.cs index c3361aa4f6..c2cc94699f 100644 --- a/test/Microsoft.AspNet.Razor.Test/Generator/CSharpTagHelperRenderingTest.cs +++ b/test/Microsoft.AspNet.Razor.Test/Generator/CSharpTagHelperRenderingTest.cs @@ -34,7 +34,8 @@ namespace Microsoft.AspNet.Razor.Test.Generator new TagHelperAttributeDescriptor( "catchall-bound-string", "BoundRequiredString", - typeof(string).FullName), + typeof(string).FullName, + isIndexer: false), }, requiredAttributes: new[] { "catchall-unbound-required" }), new TagHelperDescriptor( @@ -46,11 +47,13 @@ namespace Microsoft.AspNet.Razor.Test.Generator new TagHelperAttributeDescriptor( "input-bound-required-string", "BoundRequiredString", - typeof(string).FullName), + typeof(string).FullName, + isIndexer: false), new TagHelperAttributeDescriptor( "input-bound-string", "BoundString", - typeof(string).FullName) + typeof(string).FullName, + isIndexer: false) }, requiredAttributes: new[] { "input-bound-required-string", "input-unbound-required" }), }; @@ -152,6 +155,80 @@ namespace Microsoft.AspNet.Razor.Test.Generator } } + private static IEnumerable 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).FullName, + isIndexer: false), + new TagHelperAttributeDescriptor( + name: "string-dictionary", + propertyName: "StringDictionaryProperty", + typeName: "Namespace.DictionaryWithoutParameterlessConstructor", + 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).FullName, + isIndexer: false), + new TagHelperAttributeDescriptor( + name: "string-dictionary", + propertyName: "StringDictionaryProperty", + typeName: "Namespace.DictionaryWithoutParameterlessConstructor", + isIndexer: false), + new TagHelperAttributeDescriptor( + name: "int-prefix-", + propertyName: "IntDictionaryProperty", + typeName: typeof(int).FullName, + isIndexer: true), + new TagHelperAttributeDescriptor( + name: "string-prefix-", + propertyName: "StringDictionaryProperty", + typeName: typeof(string).FullName, + isIndexer: true), + }), + }; + } + } + public static TheoryData TagHelperDescriptorFlowTestData { get @@ -498,6 +575,101 @@ namespace Microsoft.AspNet.Razor.Test.Generator contentLength: 4) } }, + { + "PrefixedAttributeTagHelpers", + "PrefixedAttributeTagHelpers.DesignTime", + PrefixedAttributeTagHelperDescriptors, + new List + { + BuildLineMapping( + documentAbsoluteIndex: 14, + documentLineIndex: 0, + generatedAbsoluteIndex: 499, + generatedLineIndex: 15, + characterOffsetIndex: 14, + contentLength: 17), + BuildLineMapping( + documentAbsoluteIndex: 37, + documentLineIndex: 2, + generatedAbsoluteIndex: 996, + generatedLineIndex: 34, + characterOffsetIndex: 2, + contentLength: 242), + BuildLineMapping( + documentAbsoluteIndex: 370, + documentLineIndex: 15, + generatedAbsoluteIndex: 1430, + generatedLineIndex: 50, + characterOffsetIndex: 43, + contentLength: 13), + BuildLineMapping( + documentAbsoluteIndex: 404, + documentLineIndex: 15, + generatedAbsoluteIndex: 1601, + generatedLineIndex: 55, + characterOffsetIndex: 77, + contentLength: 16), + BuildLineMapping( + documentAbsoluteIndex: 468, + documentLineIndex: 16, + generatedAbsoluteIndex: 2077, + generatedLineIndex: 64, + characterOffsetIndex: 43, + contentLength: 13), + BuildLineMapping( + documentAbsoluteIndex: 502, + documentLineIndex: 16, + generatedAbsoluteIndex: 2248, + generatedLineIndex: 69, + characterOffsetIndex: 77, + contentLength: 2), + BuildLineMapping( + documentAbsoluteIndex: 526, + documentLineIndex: 16, + generatedAbsoluteIndex: 2432, + generatedLineIndex: 74, + characterOffsetIndex: 101, + contentLength: 2), + BuildLineMapping( + documentAbsoluteIndex: 590, + documentLineIndex: 18, + documentCharacterOffsetIndex: 31, + generatedAbsoluteIndex: 2994, + generatedLineIndex: 84, + generatedCharacterOffsetIndex: 32, + contentLength: 2), + BuildLineMapping( + documentAbsoluteIndex: 611, + documentLineIndex: 18, + generatedAbsoluteIndex: 3129, + generatedLineIndex: 89, + characterOffsetIndex: 52, + contentLength: 2), + BuildLineMapping( + documentAbsoluteIndex: 634, + documentLineIndex: 18, + generatedAbsoluteIndex: 3287, + generatedLineIndex: 94, + characterOffsetIndex: 75, + contentLength: 2), + BuildLineMapping( + documentAbsoluteIndex: 783, + documentLineIndex: 20, + documentCharacterOffsetIndex: 42, + generatedAbsoluteIndex: 3521, + generatedLineIndex: 101, + generatedCharacterOffsetIndex: 6, + contentLength: 8), + BuildLineMapping( + documentAbsoluteIndex: 826, + documentLineIndex: 21, + documentCharacterOffsetIndex: 29, + generatedAbsoluteIndex: 4552, + generatedLineIndex: 115, + generatedCharacterOffsetIndex: 51, + contentLength: 2), + } + }, }; } } @@ -523,28 +695,36 @@ namespace Microsoft.AspNet.Razor.Test.Generator { // Test resource name, expected TagHelperDescriptors // Note: The baseline resource name is equivalent to the test resource name. - return new TheoryData> + return new TheoryData> { - { "SingleTagHelper", DefaultPAndInputTagHelperDescriptors }, - { "BasicTagHelpers", DefaultPAndInputTagHelperDescriptors }, - { "BasicTagHelpers.RemoveTagHelper", DefaultPAndInputTagHelperDescriptors }, - { "BasicTagHelpers.Prefixed", PrefixedPAndInputTagHelperDescriptors }, - { "ComplexTagHelpers", DefaultPAndInputTagHelperDescriptors }, - { "DuplicateTargetTagHelper", DuplicateTargetTagHelperDescriptors }, - { "EmptyAttributeTagHelpers", DefaultPAndInputTagHelperDescriptors }, - { "EscapedTagHelpers", DefaultPAndInputTagHelperDescriptors }, - { "AttributeTargetingTagHelpers", AttributeTargetingTagHelperDescriptors }, + { "SingleTagHelper", null, DefaultPAndInputTagHelperDescriptors }, + { "BasicTagHelpers", null, DefaultPAndInputTagHelperDescriptors }, + { "BasicTagHelpers.RemoveTagHelper", null, DefaultPAndInputTagHelperDescriptors }, + { "BasicTagHelpers.Prefixed", null, PrefixedPAndInputTagHelperDescriptors }, + { "ComplexTagHelpers", null, DefaultPAndInputTagHelperDescriptors }, + { "DuplicateTargetTagHelper", null, DuplicateTargetTagHelperDescriptors }, + { "EmptyAttributeTagHelpers", null, DefaultPAndInputTagHelperDescriptors }, + { "EscapedTagHelpers", null, DefaultPAndInputTagHelperDescriptors }, + { "AttributeTargetingTagHelpers", null, AttributeTargetingTagHelperDescriptors }, + { "PrefixedAttributeTagHelpers", null, PrefixedAttributeTagHelperDescriptors }, + { + "PrefixedAttributeTagHelpers", + "PrefixedAttributeTagHelpers.Reversed", + PrefixedAttributeTagHelperDescriptors.Reverse() + }, }; } } [Theory] [MemberData(nameof(RuntimeTimeTagHelperTestData))] - public void TagHelpers_GenerateExpectedRuntimeOutput(string testName, - IEnumerable tagHelperDescriptors) + public void TagHelpers_GenerateExpectedRuntimeOutput( + string testName, + string baseLineName, + IEnumerable tagHelperDescriptors) { - // Act & Assert - RunTagHelperTest(testName, tagHelperDescriptors: tagHelperDescriptors); + // Arrange & Act & Assert + RunTagHelperTest(testName, baseLineName, tagHelperDescriptors: tagHelperDescriptors); } [Fact] diff --git a/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperBlockRewriterTest.cs b/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperBlockRewriterTest.cs index 7860db7af0..42aeb729c0 100644 --- a/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperBlockRewriterTest.cs +++ b/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperBlockRewriterTest.cs @@ -181,6 +181,8 @@ namespace Microsoft.AspNet.Razor.TagHelpers var noErrors = new RazorError[0]; var errorFormat = "Attribute '{0}' on tag helper element '{1}' requires a value. Tag helper bound " + "attributes of type '{2}' cannot be empty or contain only whitespace."; + var emptyKeyFormat = "The tag helper attribute '{0}' in element '{1}' is missing a key. The " + + "syntax is '<{1} {0}{{ key }}=\"value\">'."; var stringType = typeof(string).FullName; var intType = typeof(int).FullName; var expressionString = "@DateTime.Now + 1"; @@ -269,6 +271,196 @@ namespace Microsoft.AspNet.Razor.TagHelpers })), new[] { new RazorError(string.Format(errorFormat, "bound-int", "p", intType), 3, 0, 3, 9) } }, + { + "", + new MarkupBlock( + new MarkupTagHelperBlock( + "input", + selfClosing: true, + attributes: new List> + { + new KeyValuePair("int-dictionary", null), + })), + new[] + { + new RazorError( + string.Format(errorFormat, "int-dictionary", "input", typeof(IDictionary).FullName), + absoluteIndex: 7, + lineIndex: 0, + columnIndex: 7, + length: 14), + } + }, + { + "", + new MarkupBlock( + new MarkupTagHelperBlock( + "input", + selfClosing: true, + attributes: new List> + { + new KeyValuePair("string-dictionary", null), + })), + new[] + { + new RazorError( + string.Format(errorFormat, "string-dictionary", "input", typeof(IDictionary).FullName), + absoluteIndex: 7, + lineIndex: 0, + columnIndex: 7, + length: 17), + } + }, + { + "", + new MarkupBlock( + new MarkupTagHelperBlock( + "input", + selfClosing: true, + attributes: new List> + { + new KeyValuePair("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), + } + }, + { + "", + new MarkupBlock( + new MarkupTagHelperBlock( + "input", + selfClosing: true, + attributes: new List> + { + new KeyValuePair("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), + } + }, + { + "", + new MarkupBlock( + new MarkupTagHelperBlock( + "input", + selfClosing: true, + attributes: new List> + { + new KeyValuePair("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), + } + }, + { + "", + new MarkupBlock( + new MarkupTagHelperBlock( + "input", + selfClosing: true, + attributes: new List> + { + new KeyValuePair("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), + } + }, + { + "", + new MarkupBlock( + new MarkupTagHelperBlock( + "input", + selfClosing: true, + attributes: new List> + { + new KeyValuePair("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), + } + }, + { + "", + new MarkupBlock( + new MarkupTagHelperBlock( + "input", + selfClosing: true, + attributes: new List> + { + new KeyValuePair("string-prefix-value", new MarkupBlock()), + })), + new RazorError[0] + }, + { + "", + new MarkupBlock( + new MarkupTagHelperBlock( + "input", + selfClosing: true, + attributes: new List> + { + new KeyValuePair("int-prefix-value", factory.CodeMarkup("3")), + })), + new RazorError[0] + }, + { + "", + new MarkupBlock( + new MarkupTagHelperBlock( + "input", + selfClosing: true, + attributes: new List> + { + new KeyValuePair("string-prefix-value", new MarkupBlock( + factory.Markup("some"), + factory.Markup(" string"))), + })), + new RazorError[0] + }, { "", new MarkupBlock( @@ -878,7 +1070,8 @@ namespace Microsoft.AspNet.Razor.TagHelpers new TagHelperAttributeDescriptor( "bound-required-string", "BoundRequiredString", - typeof(string).FullName) + typeof(string).FullName, + isIndexer: false) }, requiredAttributes: new[] { "unbound-required" }), new TagHelperDescriptor( @@ -890,7 +1083,8 @@ namespace Microsoft.AspNet.Razor.TagHelpers new TagHelperAttributeDescriptor( "bound-required-string", "BoundRequiredString", - typeof(string).FullName) + typeof(string).FullName, + isIndexer: false) }, requiredAttributes: new[] { "bound-required-string" }), new TagHelperDescriptor( @@ -902,9 +1096,38 @@ namespace Microsoft.AspNet.Razor.TagHelpers new TagHelperAttributeDescriptor( "bound-required-int", "BoundRequiredInt", - typeof(int).FullName) + typeof(int).FullName, + isIndexer: false) }, requiredAttributes: new[] { "bound-required-int" }), + new TagHelperDescriptor( + tagName: "input", + typeName: "InputTagHelper3", + assemblyName: "SomeAssembly", + attributes: new[] + { + new TagHelperAttributeDescriptor( + "int-dictionary", + "DictionaryOfIntProperty", + typeof(IDictionary).FullName, + isIndexer: false), + new TagHelperAttributeDescriptor( + "string-dictionary", + "DictionaryOfStringProperty", + typeof(IDictionary).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()), new TagHelperDescriptor( tagName: "p", typeName: "PTagHelper", @@ -914,11 +1137,13 @@ namespace Microsoft.AspNet.Razor.TagHelpers new TagHelperAttributeDescriptor( "bound-string", "BoundRequiredString", - typeof(string).FullName), + typeof(string).FullName, + isIndexer: false), new TagHelperAttributeDescriptor( "bound-int", "BoundRequiredString", - typeof(int).FullName) + typeof(int).FullName, + isIndexer: false) }, requiredAttributes: Enumerable.Empty()), }; diff --git a/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperDescriptorTest.cs b/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperDescriptorTest.cs index 0e00c32cec..ab545373c5 100644 --- a/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperDescriptorTest.cs +++ b/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperDescriptorTest.cs @@ -52,11 +52,13 @@ namespace Microsoft.AspNet.Razor.TagHelpers new TagHelperAttributeDescriptor( name: "attribute one", propertyName: "property name", - typeName: "property type name"), + typeName: "property type name", + isIndexer: false), new TagHelperAttributeDescriptor( name: "attribute two", propertyName: "property name", - typeName: typeof(string).FullName), + typeName: typeof(string).FullName, + isIndexer: false), }, requiredAttributes: Enumerable.Empty()); var expectedSerializedDescriptor = @@ -66,11 +68,62 @@ namespace Microsoft.AspNet.Razor.TagHelpers $"\"{ nameof(TagHelperDescriptor.TypeName) }\":\"type name\"," + $"\"{ nameof(TagHelperDescriptor.AssemblyName) }\":\"assembly name\"," + $"\"{ nameof(TagHelperDescriptor.Attributes) }\":[" + - $"{{\"{ nameof(TagHelperAttributeDescriptor.IsStringProperty) }\":false," + + $"{{\"{ nameof(TagHelperAttributeDescriptor.IsIndexer) }\":false," + + $"\"{ nameof(TagHelperAttributeDescriptor.IsStringProperty) }\":false," + $"\"{ nameof(TagHelperAttributeDescriptor.Name) }\":\"attribute one\"," + $"\"{ nameof(TagHelperAttributeDescriptor.PropertyName) }\":\"property name\"," + $"\"{ nameof(TagHelperAttributeDescriptor.TypeName) }\":\"property type name\"}}," + - $"{{\"{ nameof(TagHelperAttributeDescriptor.IsStringProperty) }\":true," + + $"{{\"{ nameof(TagHelperAttributeDescriptor.IsIndexer) }\":false," + + $"\"{ nameof(TagHelperAttributeDescriptor.IsStringProperty) }\":true," + + $"\"{ nameof(TagHelperAttributeDescriptor.Name) }\":\"attribute two\"," + + $"\"{ nameof(TagHelperAttributeDescriptor.PropertyName) }\":\"property name\"," + + $"\"{ nameof(TagHelperAttributeDescriptor.TypeName) }\":\"{ typeof(string).FullName }\"}}]," + + $"\"{ nameof(TagHelperDescriptor.RequiredAttributes) }\":[]}}"; + + // Act + var serializedDescriptor = JsonConvert.SerializeObject(descriptor); + + // Assert + Assert.Equal(expectedSerializedDescriptor, serializedDescriptor, StringComparer.Ordinal); + } + + [Fact] + public void TagHelperDescriptor_WithIndexerAttributes_CanBeSerialized() + { + // Arrange + var descriptor = new TagHelperDescriptor( + prefix: "prefix:", + tagName: "tag name", + typeName: "type name", + assemblyName: "assembly name", + attributes: new[] + { + new TagHelperAttributeDescriptor( + name: "attribute one", + propertyName: "property name", + typeName: "property type name", + isIndexer: true), + new TagHelperAttributeDescriptor( + name: "attribute two", + propertyName: "property name", + typeName: typeof(string).FullName, + isIndexer: true), + }, + requiredAttributes: Enumerable.Empty()); + var expectedSerializedDescriptor = + $"{{\"{ nameof(TagHelperDescriptor.Prefix) }\":\"prefix:\"," + + $"\"{ nameof(TagHelperDescriptor.TagName) }\":\"tag name\"," + + $"\"{ nameof(TagHelperDescriptor.FullTagName) }\":\"prefix:tag name\"," + + $"\"{ nameof(TagHelperDescriptor.TypeName) }\":\"type name\"," + + $"\"{ nameof(TagHelperDescriptor.AssemblyName) }\":\"assembly name\"," + + $"\"{ nameof(TagHelperDescriptor.Attributes) }\":[" + + $"{{\"{ nameof(TagHelperAttributeDescriptor.IsIndexer) }\":true," + + $"\"{ nameof(TagHelperAttributeDescriptor.IsStringProperty) }\":false," + + $"\"{ nameof(TagHelperAttributeDescriptor.Name) }\":\"attribute one\"," + + $"\"{ nameof(TagHelperAttributeDescriptor.PropertyName) }\":\"property name\"," + + $"\"{ nameof(TagHelperAttributeDescriptor.TypeName) }\":\"property type name\"}}," + + $"{{\"{ nameof(TagHelperAttributeDescriptor.IsIndexer) }\":true," + + $"\"{ nameof(TagHelperAttributeDescriptor.IsStringProperty) }\":true," + $"\"{ nameof(TagHelperAttributeDescriptor.Name) }\":\"attribute two\"," + $"\"{ nameof(TagHelperAttributeDescriptor.PropertyName) }\":\"property name\"," + $"\"{ nameof(TagHelperAttributeDescriptor.TypeName) }\":\"{ typeof(string).FullName }\"}}]," + @@ -129,11 +182,13 @@ namespace Microsoft.AspNet.Razor.TagHelpers $"\"{ nameof(TagHelperDescriptor.TypeName) }\":\"type name\"," + $"\"{ nameof(TagHelperDescriptor.AssemblyName) }\":\"assembly name\"," + $"\"{ nameof(TagHelperDescriptor.Attributes) }\":[" + - $"{{\"{ nameof(TagHelperAttributeDescriptor.IsStringProperty) }\":false," + + $"{{\"{ nameof(TagHelperAttributeDescriptor.IsIndexer) }\":false," + + $"\"{ nameof(TagHelperAttributeDescriptor.IsStringProperty) }\":false," + $"\"{ nameof(TagHelperAttributeDescriptor.Name) }\":\"attribute one\"," + $"\"{ nameof(TagHelperAttributeDescriptor.PropertyName) }\":\"property name\"," + $"\"{ nameof(TagHelperAttributeDescriptor.TypeName) }\":\"property type name\"}}," + - $"{{\"{ nameof(TagHelperAttributeDescriptor.IsStringProperty) }\":true," + + $"{{\"{ nameof(TagHelperAttributeDescriptor.IsIndexer) }\":false," + + $"\"{ nameof(TagHelperAttributeDescriptor.IsStringProperty) }\":true," + $"\"{ nameof(TagHelperAttributeDescriptor.Name) }\":\"attribute two\"," + $"\"{ nameof(TagHelperAttributeDescriptor.PropertyName) }\":\"property name\"," + $"\"{ nameof(TagHelperAttributeDescriptor.TypeName) }\":\"{ typeof(string).FullName }\"}}]," + @@ -148,11 +203,13 @@ namespace Microsoft.AspNet.Razor.TagHelpers new TagHelperAttributeDescriptor( name: "attribute one", propertyName: "property name", - typeName: "property type name"), + typeName: "property type name", + isIndexer: false), new TagHelperAttributeDescriptor( name: "attribute two", propertyName: "property name", - typeName: typeof(string).FullName), + typeName: typeof(string).FullName, + isIndexer: false), }, requiredAttributes: Enumerable.Empty()); @@ -166,7 +223,9 @@ namespace Microsoft.AspNet.Razor.TagHelpers Assert.Equal(expectedDescriptor.FullTagName, descriptor.FullTagName, StringComparer.Ordinal); Assert.Equal(expectedDescriptor.TypeName, descriptor.TypeName, StringComparer.Ordinal); Assert.Equal(expectedDescriptor.AssemblyName, descriptor.AssemblyName, StringComparer.Ordinal); + Assert.Equal(2, descriptor.Attributes.Count); + Assert.Equal(expectedDescriptor.Attributes[0].IsIndexer, descriptor.Attributes[0].IsIndexer); Assert.Equal(expectedDescriptor.Attributes[0].IsStringProperty, descriptor.Attributes[0].IsStringProperty); Assert.Equal(expectedDescriptor.Attributes[0].Name, descriptor.Attributes[0].Name, StringComparer.Ordinal); Assert.Equal( @@ -177,6 +236,8 @@ namespace Microsoft.AspNet.Razor.TagHelpers expectedDescriptor.Attributes[0].TypeName, descriptor.Attributes[0].TypeName, StringComparer.Ordinal); + + Assert.Equal(expectedDescriptor.Attributes[1].IsIndexer, descriptor.Attributes[1].IsIndexer); Assert.Equal(expectedDescriptor.Attributes[1].IsStringProperty, descriptor.Attributes[1].IsStringProperty); Assert.Equal(expectedDescriptor.Attributes[1].Name, descriptor.Attributes[1].Name, StringComparer.Ordinal); Assert.Equal( @@ -187,6 +248,88 @@ namespace Microsoft.AspNet.Razor.TagHelpers expectedDescriptor.Attributes[1].TypeName, descriptor.Attributes[1].TypeName, StringComparer.Ordinal); + + Assert.Empty(descriptor.RequiredAttributes); + } + + [Fact] + public void TagHelperDescriptor_WithIndexerAttributes_CanBeDeserialized() + { + // Arrange + var serializedDescriptor = + $"{{\"{ nameof(TagHelperDescriptor.Prefix) }\":\"prefix:\"," + + $"\"{ nameof(TagHelperDescriptor.TagName) }\":\"tag name\"," + + $"\"{ nameof(TagHelperDescriptor.FullTagName) }\":\"prefix:tag name\"," + + $"\"{ nameof(TagHelperDescriptor.TypeName) }\":\"type name\"," + + $"\"{ nameof(TagHelperDescriptor.AssemblyName) }\":\"assembly name\"," + + $"\"{ nameof(TagHelperDescriptor.Attributes) }\":[" + + $"{{\"{ nameof(TagHelperAttributeDescriptor.IsIndexer) }\":true," + + $"\"{ nameof(TagHelperAttributeDescriptor.IsStringProperty) }\":false," + + $"\"{ nameof(TagHelperAttributeDescriptor.Name) }\":\"attribute one\"," + + $"\"{ nameof(TagHelperAttributeDescriptor.PropertyName) }\":\"property name\"," + + $"\"{ nameof(TagHelperAttributeDescriptor.TypeName) }\":\"property type name\"}}," + + $"{{\"{ nameof(TagHelperAttributeDescriptor.IsIndexer) }\":true," + + $"\"{ nameof(TagHelperAttributeDescriptor.IsStringProperty) }\":true," + + $"\"{ nameof(TagHelperAttributeDescriptor.Name) }\":\"attribute two\"," + + $"\"{ nameof(TagHelperAttributeDescriptor.PropertyName) }\":\"property name\"," + + $"\"{ nameof(TagHelperAttributeDescriptor.TypeName) }\":\"{ typeof(string).FullName }\"}}]," + + $"\"{ nameof(TagHelperDescriptor.RequiredAttributes) }\":[]}}"; + var expectedDescriptor = new TagHelperDescriptor( + prefix: "prefix:", + tagName: "tag name", + typeName: "type name", + assemblyName: "assembly name", + attributes: new[] + { + new TagHelperAttributeDescriptor( + name: "attribute one", + propertyName: "property name", + typeName: "property type name", + isIndexer: true), + new TagHelperAttributeDescriptor( + name: "attribute two", + propertyName: "property name", + typeName: typeof(string).FullName, + isIndexer: true), + }, + requiredAttributes: Enumerable.Empty()); + + // Act + var descriptor = JsonConvert.DeserializeObject(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); } } diff --git a/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperParseTreeRewriterTest.cs b/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperParseTreeRewriterTest.cs index ab0d98c838..634d16f649 100644 --- a/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperParseTreeRewriterTest.cs +++ b/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperParseTreeRewriterTest.cs @@ -921,7 +921,8 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers new TagHelperAttributeDescriptor( name: "bound", propertyName: "Bound", - typeName: typeof(bool).FullName), + typeName: typeof(bool).FullName, + isIndexer: false), }, requiredAttributes: Enumerable.Empty()) }; @@ -944,7 +945,8 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers new TagHelperAttributeDescriptor( name: "bound", propertyName: "Bound", - typeName: typeof(bool).FullName), + typeName: typeof(bool).FullName, + isIndexer: false), }, requiredAttributes: Enumerable.Empty()) }; @@ -1508,11 +1510,13 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers new TagHelperAttributeDescriptor( name: "bound", propertyName: "Bound", - typeName: typeof(bool).FullName), + typeName: typeof(bool).FullName, + isIndexer: false), new TagHelperAttributeDescriptor( name: "name", propertyName: "Name", - typeName: typeof(string).FullName) + typeName: typeof(string).FullName, + isIndexer: false) }) }; var descriptorProvider = new TagHelperDescriptorProvider(descriptors); @@ -3396,7 +3400,7 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers string.Format(CultureInfo.InvariantCulture, errorFormatUnclosed, "p"), SourceLocation.Zero), new RazorError( - "TagHelper attributes must be welformed.", + "TagHelper attributes must be well-formed.", absoluteIndex: 12, lineIndex: 0, columnIndex: 12) @@ -3962,9 +3966,13 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers new TagHelperDescriptor("person", "PersonTagHelper", "personAssembly", attributes: new[] { - new TagHelperAttributeDescriptor("age", "Age", typeof(int).FullName), - new TagHelperAttributeDescriptor("birthday", "BirthDay", typeof(DateTime).FullName), - new TagHelperAttributeDescriptor("name", "Name", typeof(string).FullName), + new TagHelperAttributeDescriptor("age", "Age", typeof(int).FullName, isIndexer: false), + new TagHelperAttributeDescriptor( + "birthday", + "BirthDay", + typeof(DateTime).FullName, + isIndexer: false), + new TagHelperAttributeDescriptor("name", "Name", typeof(string).FullName, isIndexer: false), }) }; var providerContext = new TagHelperDescriptorProvider(descriptors); diff --git a/test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/CS/Output/PrefixedAttributeTagHelpers.DesignTime.cs b/test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/CS/Output/PrefixedAttributeTagHelpers.DesignTime.cs new file mode 100644 index 0000000000..86282df3b5 --- /dev/null +++ b/test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/CS/Output/PrefixedAttributeTagHelpers.DesignTime.cs @@ -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 + { + { "three", 3 }, + }; + var stringDictionary = new SortedDictionary + { + { "name", "value" }, + }; + +#line default +#line hidden + + __InputTagHelper1 = CreateTagHelper(); +#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.IntDictionaryProperty = __InputTagHelper1.IntDictionaryProperty; + __InputTagHelper2.StringDictionaryProperty = __InputTagHelper1.StringDictionaryProperty; + __InputTagHelper1 = CreateTagHelper(); +#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.IntDictionaryProperty = __InputTagHelper1.IntDictionaryProperty; + __InputTagHelper2.IntDictionaryProperty["garlic"] = __InputTagHelper1.IntDictionaryProperty["garlic"]; + __InputTagHelper2.IntDictionaryProperty["grabber"] = __InputTagHelper1.IntProperty; + __InputTagHelper1 = CreateTagHelper(); +#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.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(); +#line 22 "PrefixedAttributeTagHelpers.cshtml" +__InputTagHelper1.IntDictionaryProperty["value"] = 37; + +#line default +#line hidden + __InputTagHelper1.StringDictionaryProperty["thyme"] = "string"; + __InputTagHelper2 = CreateTagHelper(); + __InputTagHelper2.IntDictionaryProperty["value"] = __InputTagHelper1.IntDictionaryProperty["value"]; + __InputTagHelper2.StringDictionaryProperty["thyme"] = __InputTagHelper1.StringDictionaryProperty["thyme"]; + } + #pragma warning restore 1998 + } +} diff --git a/test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/CS/Output/PrefixedAttributeTagHelpers.Reversed.cs b/test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/CS/Output/PrefixedAttributeTagHelpers.Reversed.cs new file mode 100644 index 0000000000..9849fcec09 --- /dev/null +++ b/test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/CS/Output/PrefixedAttributeTagHelpers.Reversed.cs @@ -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 + { + { "three", 3 }, + }; + var stringDictionary = new SortedDictionary + { + { "name", "value" }, + }; + +#line default +#line hidden + + Instrumentation.BeginContext(280, 51, true); + WriteLiteral("\r\n\r\n
\r\n "); + Instrumentation.EndContext(); + __tagHelperExecutionContext = __tagHelperScopeManager.Begin("input", true, "test", async() => { + } + , StartTagHelperWritingScope, EndTagHelperWritingScope); + __InputTagHelper2 = CreateTagHelper(); + __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(); + __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(); + __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(); + __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(); + __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(); + __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(); + __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(); + __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
"); + Instrumentation.EndContext(); + } + #pragma warning restore 1998 + } +} diff --git a/test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/CS/Output/PrefixedAttributeTagHelpers.cs b/test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/CS/Output/PrefixedAttributeTagHelpers.cs new file mode 100644 index 0000000000..754755d444 --- /dev/null +++ b/test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/CS/Output/PrefixedAttributeTagHelpers.cs @@ -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 + { + { "three", 3 }, + }; + var stringDictionary = new SortedDictionary + { + { "name", "value" }, + }; + +#line default +#line hidden + + Instrumentation.BeginContext(280, 51, true); + WriteLiteral("\r\n\r\n
\r\n "); + Instrumentation.EndContext(); + __tagHelperExecutionContext = __tagHelperScopeManager.Begin("input", true, "test", async() => { + } + , StartTagHelperWritingScope, EndTagHelperWritingScope); + __InputTagHelper1 = CreateTagHelper(); + __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(); + __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(); + __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(); + __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(); + __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(); + __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(); + __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(); + __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
"); + Instrumentation.EndContext(); + } + #pragma warning restore 1998 + } +} diff --git a/test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/CS/Source/PrefixedAttributeTagHelpers.cshtml b/test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/CS/Source/PrefixedAttributeTagHelpers.cshtml new file mode 100644 index 0000000000..0c21d013c1 --- /dev/null +++ b/test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/CS/Source/PrefixedAttributeTagHelpers.cshtml @@ -0,0 +1,23 @@ +@addTagHelper "something, nice" + +@{ + var literate = "or illiterate"; + var intDictionary = new Dictionary + { + { "three", 3 }, + }; + var stringDictionary = new SortedDictionary + { + { "name", "value" }, + }; +} + +
+ + + + +
\ No newline at end of file