From 27e4822a7b0710e05569aa15c7b642b521192fbe Mon Sep 17 00:00:00 2001 From: Doug Bunting Date: Fri, 23 Sep 2016 12:01:48 -0700 Subject: [PATCH] Preserve `class` casing when merging a `TagBuilder` into a `TagHelperOutput` - #5313 Also: - preserve existing `TagHelperAttribute.ValueStyle` - fix this in `UrlResolutionTagHelper`, `LinkTagHelper`, and `ScriptTagHelper` as well - correct handling of non-`string` `classAttribute.Value`s in `TagHelperOutputExtensions` - relates to #3918 because new `ClassAttributeHtmlContent` is smaller than any concatenated attribute value nit: clean up `CacheTagHelper` whitespace and `using`s --- .../TagHelpers/UrlResolutionTagHelper.cs | 15 +++- .../CacheTagHelper.cs | 7 +- .../LinkTagHelper.cs | 6 +- .../ScriptTagHelper.cs | 5 +- .../TagHelperOutputExtensions.cs | 84 ++++++++++++++++--- ...ite.HtmlGeneration_Home.Order.Encoded.html | 2 +- ...CaseSensitiveTagHelperAttributeComparer.cs | 64 ++++++++++++++ .../InputTagHelperTest.cs | 8 +- .../TagHelperOutputExtensionsTest.cs | 54 +++--------- .../ValidationSummaryTagHelperTest.cs | 44 ++++------ 10 files changed, 191 insertions(+), 98 deletions(-) create mode 100644 test/Microsoft.AspNetCore.Mvc.TagHelpers.Test/CaseSensitiveTagHelperAttributeComparer.cs 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(