diff --git a/src/Microsoft.AspNetCore.Razor.Runtime/TagHelpers/TagHelperOutput.cs b/src/Microsoft.AspNetCore.Razor.Runtime/TagHelpers/TagHelperOutput.cs index 8eaa47bfe6..e1e5bea4ac 100644 --- a/src/Microsoft.AspNetCore.Razor.Runtime/TagHelpers/TagHelperOutput.cs +++ b/src/Microsoft.AspNetCore.Razor.Runtime/TagHelpers/TagHelperOutput.cs @@ -283,14 +283,14 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers if (!isTagNameNullOrWhitespace) { - writer.Write('<'); + writer.Write("<"); writer.Write(TagName); // Perf: Avoid allocating enumerator for (var i = 0; i < Attributes.Count; i++) { var attribute = Attributes[i]; - writer.Write(' '); + writer.Write(" "); writer.Write(attribute.Name); if (attribute.Minimized) @@ -309,10 +309,9 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers using (var stringWriter = new StringWriter()) { htmlContent.WriteTo(stringWriter, encoder); + stringWriter.GetStringBuilder().Replace("\"", """); var stringValue = stringWriter.ToString(); - stringValue = stringValue.Replace("\"", """); - writer.Write(stringValue); } } @@ -321,7 +320,7 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers encoder.Encode(writer, value.ToString()); } - writer.Write('"'); + writer.Write("\""); } if (TagMode == TagMode.SelfClosing) @@ -329,7 +328,7 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers writer.Write(" /"); } - writer.Write('>'); + writer.Write(">"); } if (isTagNameNullOrWhitespace || TagMode == TagMode.StartTagAndEndTag) diff --git a/test/Microsoft.AspNetCore.Razor.Runtime.Test/TagHelpers/TagHelperOutputTest.cs b/test/Microsoft.AspNetCore.Razor.Runtime.Test/TagHelpers/TagHelperOutputTest.cs index aacd53708d..02d02a5012 100644 --- a/test/Microsoft.AspNetCore.Razor.Runtime.Test/TagHelpers/TagHelperOutputTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Runtime.Test/TagHelpers/TagHelperOutputTest.cs @@ -4,8 +4,10 @@ using System; using System.Collections.Generic; using System.IO; +using System.Text; using System.Text.Encodings.Web; using System.Threading.Tasks; +using Microsoft.AspNetCore.Html; using Microsoft.AspNetCore.Razor.Runtime.TagHelpers; using Microsoft.Extensions.WebEncoders.Testing; using Xunit; @@ -990,6 +992,34 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers Assert.Equal(expected, writer.ToString(), StringComparer.Ordinal); } + // This tests a separate code path that's used by THO when the writer is an HtmlTextWriter. + // The output should be the same, but we do some specific perf optimizations on this path. + [Theory] + [MemberData(nameof(WriteTagHelper_InputData))] + public void WriteTo_WritesFormattedTagHelper_HtmlTextWriter(TagHelperOutput output, string expected) + { + // Arrange + var inner = new StringWriter(); + var testEncoder = new HtmlTestEncoder(); + var writer = new MockHtmlTextWriter(inner, testEncoder); + + var tagHelperExecutionContext = new TagHelperExecutionContext( + tagName: output.TagName, + tagMode: output.TagMode, + items: new Dictionary(), + uniqueId: string.Empty, + executeChildContentAsync: () => Task.FromResult(result: true), + startTagHelperWritingScope: _ => { }, + endTagHelperWritingScope: () => new DefaultTagHelperContent()); + tagHelperExecutionContext.Output = output; + + // Act + output.WriteTo(writer, testEncoder); + + // Assert + Assert.Equal(expected, inner.ToString(), StringComparer.Ordinal); + } + private static TagHelperOutput GetTagHelperOutput( string tagName, TagHelperAttributeList attributes, @@ -1036,5 +1066,29 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers return output; } + + private class MockHtmlTextWriter : HtmlTextWriter + { + private readonly HtmlEncoder _encoder; + private readonly TextWriter _inner; + + public MockHtmlTextWriter(TextWriter inner, HtmlEncoder encoder) + { + _inner = inner; + _encoder = encoder; + } + + public override Encoding Encoding => _inner.Encoding; + + public override void Write(IHtmlContent value) + { + value.WriteTo(_inner, _encoder); + } + + public override void Write(char value) + { + _inner.Write(value); + } + } } } \ No newline at end of file