diff --git a/src/Microsoft.AspNetCore.Razor.Runtime/TagHelpers/DefaultTagHelperContent.cs b/src/Microsoft.AspNetCore.Razor.Runtime/TagHelpers/DefaultTagHelperContent.cs
index f84f825297..b92607a290 100644
--- a/src/Microsoft.AspNetCore.Razor.Runtime/TagHelpers/DefaultTagHelperContent.cs
+++ b/src/Microsoft.AspNetCore.Razor.Runtime/TagHelpers/DefaultTagHelperContent.cs
@@ -148,6 +148,84 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers
return this;
}
+ ///
+ public override void CopyTo(IHtmlContentBuilder destination)
+ {
+ if (destination == null)
+ {
+ throw new ArgumentNullException(nameof(destination));
+ }
+
+ if (_buffer == null)
+ {
+ return;
+ }
+
+ for (var i = 0; i < Buffer.Count; i++)
+ {
+ var entry = Buffer[i];
+ if (entry == null)
+ {
+ continue;
+ }
+
+ string entryAsString;
+ IHtmlContentContainer entryAsContainer;
+ if ((entryAsString = entry as string) != null)
+ {
+ destination.Append(entryAsString);
+ }
+ else if ((entryAsContainer = entry as IHtmlContentContainer) != null)
+ {
+ entryAsContainer.CopyTo(destination);
+ }
+ else
+ {
+ destination.AppendHtml((IHtmlContent)entry);
+ }
+ }
+ }
+
+ ///
+ public override void MoveTo(IHtmlContentBuilder destination)
+ {
+ if (destination == null)
+ {
+ throw new ArgumentNullException(nameof(destination));
+ }
+
+ if (_buffer == null)
+ {
+ return;
+ }
+
+ for (var i = 0; i < Buffer.Count; i++)
+ {
+ var entry = Buffer[i];
+ if (entry == null)
+ {
+ continue;
+ }
+
+ string entryAsString;
+ IHtmlContentContainer entryAsContainer;
+ if ((entryAsString = entry as string) != null)
+ {
+ destination.Append(entryAsString);
+ }
+ else if ((entryAsContainer = entry as IHtmlContentContainer) != null)
+ {
+ entryAsContainer.MoveTo(destination);
+ }
+ else
+ {
+ destination.AppendHtml((IHtmlContent)entry);
+ }
+ }
+
+ Buffer.Clear();
+ }
+
///
public override TagHelperContent Clear()
{
diff --git a/src/Microsoft.AspNetCore.Razor.Runtime/TagHelpers/TagHelperContent.cs b/src/Microsoft.AspNetCore.Razor.Runtime/TagHelpers/TagHelperContent.cs
index 45a8160444..507ddbf535 100644
--- a/src/Microsoft.AspNetCore.Razor.Runtime/TagHelpers/TagHelperContent.cs
+++ b/src/Microsoft.AspNetCore.Razor.Runtime/TagHelpers/TagHelperContent.cs
@@ -128,6 +128,12 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers
/// A reference to this instance after the clear operation has completed.
public abstract TagHelperContent Clear();
+ ///
+ public abstract void CopyTo(IHtmlContentBuilder destination);
+
+ ///
+ public abstract void MoveTo(IHtmlContentBuilder destination);
+
///
/// Gets the content.
///
diff --git a/src/Microsoft.AspNetCore.Razor.Runtime/TagHelpers/TagHelperOutput.cs b/src/Microsoft.AspNetCore.Razor.Runtime/TagHelpers/TagHelperOutput.cs
index 029ce1a9c0..3baadfdcd5 100644
--- a/src/Microsoft.AspNetCore.Razor.Runtime/TagHelpers/TagHelperOutput.cs
+++ b/src/Microsoft.AspNetCore.Razor.Runtime/TagHelpers/TagHelperOutput.cs
@@ -12,7 +12,7 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers
///
/// Class used to represent the output of an .
///
- public class TagHelperOutput : IHtmlContent
+ public class TagHelperOutput : IHtmlContentContainer
{
private readonly Func> _getChildContentAsync;
private TagHelperAttributeList _attributes;
@@ -276,7 +276,100 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers
return _getChildContentAsync(useCachedResult, encoder);
}
- ///
+ void IHtmlContentContainer.CopyTo(IHtmlContentBuilder destination)
+ {
+ if (destination == null)
+ {
+ throw new ArgumentNullException(nameof(destination));
+ }
+
+ _preElement?.CopyTo(destination);
+
+ var isTagNameNullOrWhitespace = string.IsNullOrWhiteSpace(TagName);
+
+ if (!isTagNameNullOrWhitespace)
+ {
+ destination.AppendHtml("<");
+ destination.AppendHtml(TagName);
+
+ CopyAttributesTo(destination);
+
+ if (TagMode == TagMode.SelfClosing)
+ {
+ destination.AppendHtml(" /");
+ }
+
+ destination.AppendHtml(">");
+ }
+
+ if (isTagNameNullOrWhitespace || TagMode == TagMode.StartTagAndEndTag)
+ {
+ _preContent?.CopyTo(destination);
+
+ _content?.CopyTo(destination);
+
+ _postContent?.CopyTo(destination);
+ }
+
+ if (!isTagNameNullOrWhitespace && TagMode == TagMode.StartTagAndEndTag)
+ {
+ destination.AppendHtml("");
+ destination.AppendHtml(TagName);
+ destination.AppendHtml(">");
+ }
+
+ _postElement?.CopyTo(destination);
+ }
+
+ void IHtmlContentContainer.MoveTo(IHtmlContentBuilder destination)
+ {
+ if (destination == null)
+ {
+ throw new ArgumentNullException(nameof(destination));
+ }
+
+ _preElement?.MoveTo(destination);
+
+ var isTagNameNullOrWhitespace = string.IsNullOrWhiteSpace(TagName);
+
+ if (!isTagNameNullOrWhitespace)
+ {
+ destination.AppendHtml("<");
+ destination.AppendHtml(TagName);
+
+ CopyAttributesTo(destination);
+
+ if (TagMode == TagMode.SelfClosing)
+ {
+ destination.AppendHtml(" /");
+ }
+
+ destination.AppendHtml(">");
+ }
+
+ if (isTagNameNullOrWhitespace || TagMode == TagMode.StartTagAndEndTag)
+ {
+ _preContent?.MoveTo(destination);
+ _content?.MoveTo(destination);
+ _postContent?.MoveTo(destination);
+ }
+
+ if (!isTagNameNullOrWhitespace && TagMode == TagMode.StartTagAndEndTag)
+ {
+ destination.AppendHtml("");
+ destination.AppendHtml(TagName);
+ destination.AppendHtml(">");
+ }
+
+ _postElement?.MoveTo(destination);
+
+ // Depending on the code path we took, these might need to be cleared.
+ _preContent?.Clear();
+ _content?.Clear();
+ _postContent?.Clear();
+ _attributes?.Clear();
+ }
+
public void WriteTo(TextWriter writer, HtmlEncoder encoder)
{
if (writer == null)
@@ -284,6 +377,11 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers
throw new ArgumentNullException(nameof(writer));
}
+ if (encoder == null)
+ {
+ throw new ArgumentNullException(nameof(encoder));
+ }
+
_preElement?.WriteTo(writer, encoder);
var isTagNameNullOrWhitespace = string.IsNullOrWhiteSpace(TagName);
@@ -310,16 +408,25 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers
var htmlContent = value as IHtmlContent;
if (htmlContent != null)
{
- // There's no way of tracking the attribute value quotations in the Razor source. Therefore, we
- // must escape any IHtmlContent double quote values in the case that a user wrote:
- //
- using (var stringWriter = new StringWriter())
+ // Perf: static text in a bound attribute go down this path. Avoid allocating if possible (common case).
+ var htmlEncodedString = value as HtmlEncodedString;
+ if (htmlEncodedString != null && !htmlEncodedString.Value.Contains("\""))
{
- htmlContent.WriteTo(stringWriter, encoder);
- stringWriter.GetStringBuilder().Replace("\"", """);
+ writer.Write(htmlEncodedString.Value);
+ }
+ else
+ {
+ // There's no way of tracking the attribute value quotations in the Razor source. Therefore, we
+ // must escape any IHtmlContent double quote values in the case that a user wrote:
+ //
+ using (var stringWriter = new StringWriter())
+ {
+ htmlContent.WriteTo(stringWriter, encoder);
+ stringWriter.GetStringBuilder().Replace("\"", """);
- var stringValue = stringWriter.ToString();
- writer.Write(stringValue);
+ var stringValue = stringWriter.ToString();
+ writer.Write(stringValue);
+ }
}
}
else if (value != null)
@@ -356,5 +463,76 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers
_postElement?.WriteTo(writer, encoder);
}
+
+ private void CopyAttributesTo(IHtmlContentBuilder destination)
+ {
+ StringWriter stringWriter = null;
+
+ // Perf: Avoid allocating enumerator
+ for (var i = 0; i < (_attributes?.Count ?? 0); i++)
+ {
+ var attribute = _attributes[i];
+ destination.AppendHtml(" ");
+ destination.AppendHtml(attribute.Name);
+
+ if (attribute.Minimized)
+ {
+ continue;
+ }
+
+ destination.AppendHtml("=\"");
+ var value = attribute.Value;
+ var htmlContent = value as IHtmlContent;
+ if (htmlContent != null)
+ {
+ // Perf: static text in a bound attribute go down this path. Avoid allocating if possible (common case).
+ var htmlEncodedString = value as HtmlEncodedString;
+ if (htmlEncodedString != null && !htmlEncodedString.Value.Contains("\""))
+ {
+ destination.AppendHtml(htmlEncodedString);
+ }
+ else
+ {
+ // Perf: We'll share this writer implementation for all attributes since
+ // they can't nest.
+ stringWriter = stringWriter ?? new StringWriter();
+
+ destination.AppendHtml(new AttributeContent(htmlContent, stringWriter));
+ }
+ }
+ else if (value != null)
+ {
+ destination.Append(value.ToString());
+ }
+
+ destination.AppendHtml("\"");
+ }
+ }
+
+ private class AttributeContent : IHtmlContent
+ {
+ private readonly IHtmlContent _inner;
+ private readonly StringWriter _stringWriter;
+
+ public AttributeContent(IHtmlContent inner, StringWriter stringWriter)
+ {
+ _inner = inner;
+ _stringWriter = stringWriter;
+ }
+
+ public void WriteTo(TextWriter writer, HtmlEncoder encoder)
+ {
+ // There's no way of tracking the attribute value quotations in the Razor source. Therefore, we
+ // must escape any IHtmlContent double quote values in the case that a user wrote:
+ //
+ _inner.WriteTo(_stringWriter, encoder);
+ _stringWriter.GetStringBuilder().Replace("\"", """);
+
+ var stringValue = _stringWriter.ToString();
+ writer.Write(stringValue);
+
+ _stringWriter.GetStringBuilder().Clear();
+ }
+ }
}
}
diff --git a/test/Microsoft.AspNetCore.Razor.Runtime.Test/TagHelpers/DefaultTagHelperContentTest.cs b/test/Microsoft.AspNetCore.Razor.Runtime.Test/TagHelpers/DefaultTagHelperContentTest.cs
index 936c72014a..827983a31c 100644
--- a/test/Microsoft.AspNetCore.Razor.Runtime.Test/TagHelpers/DefaultTagHelperContentTest.cs
+++ b/test/Microsoft.AspNetCore.Razor.Runtime.Test/TagHelpers/DefaultTagHelperContentTest.cs
@@ -1,8 +1,10 @@
// 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.Collections.Generic;
using System.Globalization;
using System.IO;
+using Microsoft.AspNetCore.Html;
using Microsoft.Extensions.WebEncoders.Testing;
using Xunit;
@@ -83,6 +85,107 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers
Assert.Equal(expected, copiedTagHelperContent.GetContent(new HtmlTestEncoder()));
}
+ [Fact]
+ public void CopyTo_CopiesAllItems()
+ {
+ // Arrange
+ var source = new DefaultTagHelperContent();
+ source.AppendHtml(new HtmlEncodedString("hello"));
+ source.Append("Test");
+
+ var items = new List