diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/TagHelpers/UrlResolutionTagHelper.cs b/src/Microsoft.AspNetCore.Mvc.Razor/TagHelpers/UrlResolutionTagHelper.cs
index 1ec061f64b..0bd0450735 100644
--- a/src/Microsoft.AspNetCore.Mvc.Razor/TagHelpers/UrlResolutionTagHelper.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Razor/TagHelpers/UrlResolutionTagHelper.cs
@@ -176,7 +176,10 @@ namespace Microsoft.AspNetCore.Mvc.Razor.TagHelpers
string resolvedUrl;
if (TryResolveUrl(stringValue, resolvedUrl: out resolvedUrl))
{
- output.Attributes[i] = new TagHelperAttribute(attribute.Name, resolvedUrl);
+ output.Attributes[i] = new TagHelperAttribute(
+ attribute.Name,
+ resolvedUrl,
+ attribute.ValueStyle);
}
}
else
@@ -202,12 +205,18 @@ namespace Microsoft.AspNetCore.Mvc.Razor.TagHelpers
IHtmlContent resolvedUrl;
if (TryResolveUrl(stringValue, resolvedUrl: out resolvedUrl))
{
- output.Attributes[i] = new TagHelperAttribute(attribute.Name, resolvedUrl);
+ output.Attributes[i] = new TagHelperAttribute(
+ attribute.Name,
+ resolvedUrl,
+ attribute.ValueStyle);
}
else if (htmlString == null)
{
// Not a ~/ URL. Just avoid re-encoding the attribute value later.
- output.Attributes[i] = new TagHelperAttribute(attribute.Name, new HtmlString(stringValue));
+ output.Attributes[i] = new TagHelperAttribute(
+ attribute.Name,
+ new HtmlString(stringValue),
+ attribute.ValueStyle);
}
}
}
diff --git a/src/Microsoft.AspNetCore.Mvc.TagHelpers/CacheTagHelper.cs b/src/Microsoft.AspNetCore.Mvc.TagHelpers/CacheTagHelper.cs
index 6d484b495e..f25bd690ae 100644
--- a/src/Microsoft.AspNetCore.Mvc.TagHelpers/CacheTagHelper.cs
+++ b/src/Microsoft.AspNetCore.Mvc.TagHelpers/CacheTagHelper.cs
@@ -2,7 +2,6 @@
// 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.IO;
using System.Text;
using System.Text.Encodings.Web;
@@ -94,8 +93,8 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
try
{
- // The entry is set instead of assigning a value to the
- // task so that the expiration options are are not impacted
+ // The entry is set instead of assigning a value to the
+ // task so that the expiration options are are not impacted
// by the time it took to compute it.
using (var entry = MemoryCache.CreateEntry(cacheKey))
@@ -118,7 +117,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
}
finally
{
- // If an exception occurs, ensure the other awaiters
+ // If an exception occurs, ensure the other awaiters
// render the output by themselves.
tcs.SetResult(null);
}
diff --git a/src/Microsoft.AspNetCore.Mvc.TagHelpers/LinkTagHelper.cs b/src/Microsoft.AspNetCore.Mvc.TagHelpers/LinkTagHelper.cs
index 72ee591204..5940e672cc 100644
--- a/src/Microsoft.AspNetCore.Mvc.TagHelpers/LinkTagHelper.cs
+++ b/src/Microsoft.AspNetCore.Mvc.TagHelpers/LinkTagHelper.cs
@@ -268,9 +268,11 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
if (Href != null)
{
var index = output.Attributes.IndexOfName(HrefAttributeName);
+ var existingAttribute = output.Attributes[index];
output.Attributes[index] = new TagHelperAttribute(
- HrefAttributeName,
- _fileVersionProvider.AddFileVersionToPath(Href));
+ existingAttribute.Name,
+ _fileVersionProvider.AddFileVersionToPath(Href),
+ existingAttribute.ValueStyle);
}
}
diff --git a/src/Microsoft.AspNetCore.Mvc.TagHelpers/ScriptTagHelper.cs b/src/Microsoft.AspNetCore.Mvc.TagHelpers/ScriptTagHelper.cs
index afec945db1..d19a0d42f5 100644
--- a/src/Microsoft.AspNetCore.Mvc.TagHelpers/ScriptTagHelper.cs
+++ b/src/Microsoft.AspNetCore.Mvc.TagHelpers/ScriptTagHelper.cs
@@ -234,10 +234,11 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
if (Src != null)
{
var index = output.Attributes.IndexOfName(SrcAttributeName);
+ var existingAttribute = output.Attributes[index];
output.Attributes[index] = new TagHelperAttribute(
- SrcAttributeName,
+ existingAttribute.Name,
_fileVersionProvider.AddFileVersionToPath(Src),
- output.Attributes[index].ValueStyle);
+ existingAttribute.ValueStyle);
}
}
diff --git a/src/Microsoft.AspNetCore.Mvc.TagHelpers/TagHelperOutputExtensions.cs b/src/Microsoft.AspNetCore.Mvc.TagHelpers/TagHelperOutputExtensions.cs
index 89168a6497..f9cc053ae8 100644
--- a/src/Microsoft.AspNetCore.Mvc.TagHelpers/TagHelperOutputExtensions.cs
+++ b/src/Microsoft.AspNetCore.Mvc.TagHelpers/TagHelperOutputExtensions.cs
@@ -3,7 +3,11 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
using System.Linq;
+using System.Text.Encodings.Web;
+using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Razor.TagHelpers;
@@ -90,9 +94,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
/// The to merge attributes from.
/// Existing on the given
/// are not overridden; "class" attributes are merged with spaces.
- public static void MergeAttributes(
- this TagHelperOutput tagHelperOutput,
- TagBuilder tagBuilder)
+ public static void MergeAttributes(this TagHelperOutput tagHelperOutput, TagBuilder tagBuilder)
{
if (tagHelperOutput == null)
{
@@ -110,18 +112,18 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
{
tagHelperOutput.Attributes.Add(attribute.Key, attribute.Value);
}
- else if (attribute.Key.Equals("class", StringComparison.OrdinalIgnoreCase))
+ else if (string.Equals(attribute.Key, "class", StringComparison.OrdinalIgnoreCase))
{
TagHelperAttribute classAttribute;
+ var found = tagHelperOutput.Attributes.TryGetAttribute("class", out classAttribute);
+ Debug.Assert(found);
- if (tagHelperOutput.Attributes.TryGetAttribute("class", out classAttribute))
- {
- tagHelperOutput.Attributes.SetAttribute("class", classAttribute.Value + " " + attribute.Value);
- }
- else
- {
- tagHelperOutput.Attributes.Add("class", attribute.Value);
- }
+ var newAttribute = new TagHelperAttribute(
+ classAttribute.Name,
+ new ClassAttributeHtmlContent(classAttribute.Value, attribute.Value),
+ classAttribute.ValueStyle);
+
+ tagHelperOutput.Attributes.SetAttribute(newAttribute);
}
}
}
@@ -201,5 +203,63 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
return -1;
}
+
+ private class ClassAttributeHtmlContent : IHtmlContent
+ {
+ private readonly object _left;
+ private readonly string _right;
+
+ public ClassAttributeHtmlContent(object left, string right)
+ {
+ _left = left;
+ _right = right;
+ }
+
+ public void WriteTo(TextWriter writer, HtmlEncoder encoder)
+ {
+ if (writer == null)
+ {
+ throw new ArgumentNullException(nameof(writer));
+ }
+
+ if (encoder == null)
+ {
+ throw new ArgumentNullException(nameof(encoder));
+ }
+
+ // Write out "{left} {right}" in the common nothing-empty case.
+ var wroteLeft = false;
+ if (_left != null)
+ {
+ var htmlContent = _left as IHtmlContent;
+ if (htmlContent != null)
+ {
+ // Ignore case where htmlContent is HtmlString.Empty. At worst, will add a leading space to the
+ // generated attribute value.
+ htmlContent.WriteTo(writer, encoder);
+ wroteLeft = true;
+ }
+ else
+ {
+ var stringValue = _left.ToString();
+ if (!string.IsNullOrEmpty(stringValue))
+ {
+ encoder.Encode(writer, stringValue);
+ wroteLeft = true;
+ }
+ }
+ }
+
+ if (!string.IsNullOrEmpty(_right))
+ {
+ if (wroteLeft)
+ {
+ writer.Write(' ');
+ }
+
+ encoder.Encode(writer, _right);
+ }
+ }
+ }
}
}
\ No newline at end of file
diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Order.Encoded.html b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Order.Encoded.html
index e3a28d41de..700c9b1e45 100644
--- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Order.Encoded.html
+++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Order.Encoded.html
@@ -74,7 +74,7 @@
Female
-
+
diff --git a/test/Microsoft.AspNetCore.Mvc.TagHelpers.Test/CaseSensitiveTagHelperAttributeComparer.cs b/test/Microsoft.AspNetCore.Mvc.TagHelpers.Test/CaseSensitiveTagHelperAttributeComparer.cs
new file mode 100644
index 0000000000..c5c4a22cfb
--- /dev/null
+++ b/test/Microsoft.AspNetCore.Mvc.TagHelpers.Test/CaseSensitiveTagHelperAttributeComparer.cs
@@ -0,0 +1,64 @@
+// 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.IO;
+using Microsoft.AspNetCore.Html;
+using Microsoft.AspNetCore.Razor.TagHelpers;
+
+namespace Microsoft.AspNetCore.Mvc.TagHelpers
+{
+ public class CaseSensitiveTagHelperAttributeComparer : IEqualityComparer
+ {
+ public readonly static CaseSensitiveTagHelperAttributeComparer Default =
+ new CaseSensitiveTagHelperAttributeComparer();
+
+ private CaseSensitiveTagHelperAttributeComparer()
+ {
+ }
+
+ public bool Equals(TagHelperAttribute attributeX, TagHelperAttribute attributeY)
+ {
+ if (attributeX == attributeY)
+ {
+ return true;
+ }
+
+ // Normal comparer (TagHelperAttribute.Equals()) doesn't care about the Name case, in tests we do.
+ return attributeX != null &&
+ string.Equals(attributeX.Name, attributeY.Name, StringComparison.Ordinal) &&
+ attributeX.ValueStyle == attributeY.ValueStyle &&
+ (attributeX.ValueStyle == HtmlAttributeValueStyle.Minimized ||
+ string.Equals(GetString(attributeX.Value), GetString(attributeY.Value)));
+ }
+
+ public int GetHashCode(TagHelperAttribute attribute)
+ {
+ // Manually combine hash codes here. We can't reference HashCodeCombiner because we have internals visible
+ // from Mvc.Core and Mvc.TagHelpers; both of which reference HashCodeCombiner.
+ var baseHashCode = 0x1505L;
+ var attributeHashCode = attribute.GetHashCode();
+ var combinedHash = ((baseHashCode << 5) + baseHashCode) ^ attributeHashCode;
+ var nameHashCode = StringComparer.Ordinal.GetHashCode(attribute.Name);
+ combinedHash = ((combinedHash << 5) + combinedHash) ^ nameHashCode;
+
+ return combinedHash.GetHashCode();
+ }
+
+ private string GetString(object value)
+ {
+ var htmlContent = value as IHtmlContent;
+ if (htmlContent != null)
+ {
+ using (var writer = new StringWriter())
+ {
+ htmlContent.WriteTo(writer, NullHtmlEncoder.Default);
+ return writer.ToString();
+ }
+ }
+
+ return value?.ToString() ?? string.Empty;
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/Microsoft.AspNetCore.Mvc.TagHelpers.Test/InputTagHelperTest.cs b/test/Microsoft.AspNetCore.Mvc.TagHelpers.Test/InputTagHelperTest.cs
index be2ee35a4f..e4a4831239 100644
--- a/test/Microsoft.AspNetCore.Mvc.TagHelpers.Test/InputTagHelperTest.cs
+++ b/test/Microsoft.AspNetCore.Mvc.TagHelpers.Test/InputTagHelperTest.cs
@@ -489,7 +489,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
htmlGenerator.Verify();
Assert.Equal(TagMode.StartTagOnly, output.TagMode);
- Assert.Equal(expectedAttributes, output.Attributes);
+ Assert.Equal(expectedAttributes, output.Attributes, CaseSensitiveTagHelperAttributeComparer.Default);
Assert.Equal(expectedPreContent, output.PreContent.GetContent());
Assert.Equal(expectedContent, output.Content.GetContent());
Assert.Equal(expectedPostContent, output.PostContent.GetContent());
@@ -590,7 +590,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
htmlGenerator.Verify();
Assert.Equal(TagMode.StartTagOnly, output.TagMode);
- Assert.Equal(expectedAttributes, output.Attributes);
+ Assert.Equal(expectedAttributes, output.Attributes, CaseSensitiveTagHelperAttributeComparer.Default);
Assert.Equal(expectedPreContent, output.PreContent.GetContent());
Assert.Equal(expectedContent, output.Content.GetContent());
Assert.Equal(expectedPostContent, output.PostContent.GetContent());
@@ -684,7 +684,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
htmlGenerator.Verify();
Assert.Equal(TagMode.StartTagOnly, output.TagMode);
- Assert.Equal(expectedAttributes, output.Attributes);
+ Assert.Equal(expectedAttributes, output.Attributes, CaseSensitiveTagHelperAttributeComparer.Default);
Assert.Equal(expectedPreContent, output.PreContent.GetContent());
Assert.Equal(expectedContent, output.Content.GetContent());
Assert.Equal(expectedPostContent, output.PostContent.GetContent());
@@ -801,7 +801,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
htmlGenerator.Verify();
Assert.Equal(TagMode.StartTagOnly, output.TagMode);
- Assert.Equal(expectedAttributes, output.Attributes);
+ Assert.Equal(expectedAttributes, output.Attributes, CaseSensitiveTagHelperAttributeComparer.Default);
Assert.Equal(expectedPreContent, output.PreContent.GetContent());
Assert.Equal(expectedContent, output.Content.GetContent());
Assert.Equal(expectedPostContent, output.PostContent.GetContent());
diff --git a/test/Microsoft.AspNetCore.Mvc.TagHelpers.Test/TagHelperOutputExtensionsTest.cs b/test/Microsoft.AspNetCore.Mvc.TagHelpers.Test/TagHelperOutputExtensionsTest.cs
index eea6e30df0..0011cbcf0c 100644
--- a/test/Microsoft.AspNetCore.Mvc.TagHelpers.Test/TagHelperOutputExtensionsTest.cs
+++ b/test/Microsoft.AspNetCore.Mvc.TagHelpers.Test/TagHelperOutputExtensionsTest.cs
@@ -676,7 +676,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
},
new TagHelperAttributeList
{
- { "class", "btn2 btn" }
+ { "clASS", "btn2 btn" }
}
},
{
@@ -691,7 +691,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
},
new TagHelperAttributeList
{
- { "class", "btn2 btn" }
+ { "clASS", "btn2 btn" }
}
},
{
@@ -706,7 +706,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
},
new TagHelperAttributeList
{
- { "class", "btn2 btn" }
+ { "clASS", "btn2 btn" }
}
},
{
@@ -725,7 +725,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
new TagHelperAttributeList
{
{ "before", "before value" },
- { "class", "btn2 btn" },
+ { "clASS", "btn2 btn" },
{ "mid", "mid value" },
{ "after", "after value" },
}
@@ -748,7 +748,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
new TagHelperAttributeList
{
{ "before", "before value" },
- { "class", "btn2 btn" },
+ { "clASS", "btn2 btn" },
{ "mid", "mid value" },
{ "after", "after value" },
{ "A", "A Value" },
@@ -833,7 +833,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
// Assert
var attribute = Assert.Single(tagHelperOutput.Attributes);
- Assert.Equal(expectedAttribute, attribute);
+ Assert.Equal(expectedAttribute, attribute, CaseSensitiveTagHelperAttributeComparer.Default);
}
[Theory]
@@ -859,7 +859,10 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
// Assert
var attribute = Assert.Single(tagHelperOutput.Attributes);
- Assert.Equal(new TagHelperAttribute(originalName, "Hello btn"), attribute);
+ Assert.Equal(
+ new TagHelperAttribute(originalName, "Hello btn"),
+ attribute,
+ CaseSensitiveTagHelperAttributeComparer.Default);
}
[Fact]
@@ -959,42 +962,5 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
attribute = Assert.Single(tagHelperOutput.Attributes, attr => attr.Name.Equals("for"));
Assert.Equal(expectedBuilderAttribute.Value, attribute.Value);
}
-
- private class CaseSensitiveTagHelperAttributeComparer : IEqualityComparer
- {
- public readonly static CaseSensitiveTagHelperAttributeComparer Default =
- new CaseSensitiveTagHelperAttributeComparer();
-
- private CaseSensitiveTagHelperAttributeComparer()
- {
- }
-
- public bool Equals(TagHelperAttribute attributeX, TagHelperAttribute attributeY)
- {
- if (attributeX == attributeY)
- {
- return true;
- }
-
- // Normal comparer (TagHelperAttribute.Equals()) doesn't care about the Name case, in tests we do.
- return attributeX != null &&
- string.Equals(attributeX.Name, attributeY.Name, StringComparison.Ordinal) &&
- attributeX.ValueStyle == attributeY.ValueStyle &&
- (attributeX.ValueStyle == HtmlAttributeValueStyle.Minimized || Equals(attributeX.Value, attributeY.Value));
- }
-
- public int GetHashCode(TagHelperAttribute attribute)
- {
- // Manually combine hash codes here. We can't reference HashCodeCombiner because we have internals visible
- // from Mvc.Core and Mvc.TagHelpers; both of which reference HashCodeCombiner.
- var baseHashCode = 0x1505L;
- var attributeHashCode = attribute.GetHashCode();
- var combinedHash = ((baseHashCode << 5) + baseHashCode) ^ attributeHashCode;
- var nameHashCode = StringComparer.Ordinal.GetHashCode(attribute.Name);
- combinedHash = ((combinedHash << 5) + combinedHash) ^ nameHashCode;
-
- return combinedHash.GetHashCode();
- }
- }
}
}
diff --git a/test/Microsoft.AspNetCore.Mvc.TagHelpers.Test/ValidationSummaryTagHelperTest.cs b/test/Microsoft.AspNetCore.Mvc.TagHelpers.Test/ValidationSummaryTagHelperTest.cs
index 6767d6d051..f55591af43 100644
--- a/test/Microsoft.AspNetCore.Mvc.TagHelpers.Test/ValidationSummaryTagHelperTest.cs
+++ b/test/Microsoft.AspNetCore.Mvc.TagHelpers.Test/ValidationSummaryTagHelperTest.cs
@@ -47,6 +47,11 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
var expectedTagName = "not-div";
var metadataProvider = new TestModelMetadataProvider();
var htmlGenerator = new TestableHtmlGenerator(metadataProvider);
+ var expectedAttributes = new TagHelperAttributeList
+ {
+ new TagHelperAttribute("class", "form-control validation-summary-valid"),
+ new TagHelperAttribute("data-valmsg-summary", "true"),
+ };
var expectedPreContent = "original pre-content";
var expectedContent = "original content";
@@ -80,18 +85,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
await validationSummaryTagHelper.ProcessAsync(tagHelperContext, output);
// Assert
- Assert.Collection(
- output.Attributes,
- attribute =>
- {
- Assert.Equal("class", attribute.Name);
- Assert.Equal("form-control validation-summary-valid", attribute.Value);
- },
- attribute =>
- {
- Assert.Equal("data-valmsg-summary", attribute.Name);
- Assert.Equal("true", attribute.Value);
- });
+ Assert.Equal(expectedAttributes, output.Attributes, CaseSensitiveTagHelperAttributeComparer.Default);
Assert.Equal(expectedPreContent, output.PreContent.GetContent());
Assert.Equal(expectedContent, output.Content.GetContent());
Assert.Equal(
@@ -266,7 +260,11 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
// Assert
Assert.InRange(output.Attributes.Count, low: 1, high: 2);
var attribute = Assert.Single(output.Attributes, attr => attr.Name.Equals("class"));
- Assert.Equal("form-control validation-summary-errors", attribute.Value);
+ Assert.Equal(
+ new TagHelperAttribute("class", "form-control validation-summary-errors"),
+ attribute,
+ CaseSensitiveTagHelperAttributeComparer.Default);
+
Assert.Equal(expectedPreContent, output.PreContent.GetContent());
Assert.Equal(expectedContent, output.Content.GetContent());
Assert.Equal(
@@ -282,9 +280,14 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
var expectedError0 = "I am an error.";
var expectedError2 = "I am also an error.";
var expectedTagName = "not-div";
+ var expectedAttributes = new TagHelperAttributeList
+ {
+ new TagHelperAttribute("class", "form-control validation-summary-errors"),
+ new TagHelperAttribute("data-valmsg-summary", "true"),
+ };
+
var metadataProvider = new TestModelMetadataProvider();
var htmlGenerator = new TestableHtmlGenerator(metadataProvider);
-
var validationSummaryTagHelper = new ValidationSummaryTagHelper(htmlGenerator)
{
ValidationSummary = ValidationSummary.All,
@@ -325,18 +328,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
await validationSummaryTagHelper.ProcessAsync(tagHelperContext, output);
// Assert
- Assert.Collection(
- output.Attributes,
- attribute =>
- {
- Assert.Equal("class", attribute.Name);
- Assert.Equal("form-control validation-summary-errors", attribute.Value);
- },
- attribute =>
- {
- Assert.Equal("data-valmsg-summary", attribute.Name);
- Assert.Equal("true", attribute.Value);
- });
+ Assert.Equal(expectedAttributes, output.Attributes, CaseSensitiveTagHelperAttributeComparer.Default);
Assert.Equal(expectedPreContent, output.PreContent.GetContent());
Assert.Equal(expectedContent, output.Content.GetContent());
Assert.Equal(