[Perf] Avoid ViewBuffers for writing bound TagHelper attribute values. Fixes aspnet/Razor#717

This commit is contained in:
mnltejaswini 2016-05-24 11:01:56 -07:00
parent f54a964815
commit b96851ec20
6 changed files with 136 additions and 1 deletions

View File

@ -106,6 +106,8 @@ namespace Microsoft.AspNetCore.Mvc.Razor
FormatInvalidIndexerAssignmentMethodName = "InvalidTagHelperIndexerAssignment",
StartTagHelperWritingScopeMethodName = "StartTagHelperWritingScope",
EndTagHelperWritingScopeMethodName = "EndTagHelperWritingScope",
BeginWriteTagHelperAttributeMethodName = "BeginWriteTagHelperAttribute",
EndWriteTagHelperAttributeMethodName = "EndWriteTagHelperAttribute",
// Can't use nameof because IHtmlHelper is (also) not accessible here.
MarkAsHtmlEncodedMethodName = HtmlHelperPropertyName + ".Raw",

View File

@ -478,6 +478,22 @@ namespace Microsoft.AspNetCore.Mvc.Razor
return string.Format(CultureInfo.CurrentCulture, GetString("ViewLocationFormatsIsRequired"), p0);
}
/// <summary>
/// Nesting of TagHelper attribute writing scopes is not supported.
/// </summary>
internal static string RazorPage_NestingAttributeWritingScopesNotSupported
{
get { return GetString("RazorPage_NestingAttributeWritingScopesNotSupported"); }
}
/// <summary>
/// Nesting of TagHelper attribute writing scopes is not supported.
/// </summary>
internal static string FormatRazorPage_NestingAttributeWritingScopesNotSupported()
{
return GetString("RazorPage_NestingAttributeWritingScopesNotSupported");
}
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);

View File

@ -40,6 +40,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor
private IViewBufferScope _bufferScope;
private bool _ignoreBody;
private HashSet<string> _ignoredSections;
private TextWriter _pageWriter;
public RazorPage()
{
@ -231,6 +232,61 @@ namespace Microsoft.AspNetCore.Mvc.Razor
return tagHelperContent;
}
/// <summary>
/// Starts a new scope for writing <see cref="ITagHelper"/> attribute values.
/// </summary>
/// <remarks>
/// All writes to the <see cref="Output"/> or <see cref="ViewContext.Writer"/> after calling this method will
/// be buffered until <see cref="EndWriteTagHelperAttribute"/> is called.
/// The content will be buffered using a shared <see cref="StringWriter"/> within this <see cref="RazorPage"/>
/// Nesting of <see cref="BeginWriteTagHelperAttribute"/> and <see cref="EndWriteTagHelperAttribute"/> method calls
/// is not supported.
/// </remarks>
public void BeginWriteTagHelperAttribute()
{
if (_pageWriter != null)
{
throw new InvalidOperationException(Resources.RazorPage_NestingAttributeWritingScopesNotSupported);
}
_pageWriter = ViewContext.Writer;
if (_valueBuffer == null)
{
_valueBuffer = new StringWriter();
}
// We need to replace the ViewContext's Writer to ensure that all content (including content written
// from HTML helpers) is redirected.
ViewContext.Writer = _valueBuffer;
}
/// <summary>
/// Ends the current writing scope that was started by calling <see cref="BeginWriteTagHelperAttribute"/>.
/// </summary>
/// <returns>The content buffered by the shared <see cref="StringWriter"/> of this <see cref="RazorPage"/>.</returns>
/// <remarks>
/// This method assumes that there will be no nesting of <see cref="BeginWriteTagHelperAttribute"/>
/// and <see cref="EndWriteTagHelperAttribute"/> method calls.
/// </remarks>
public string EndWriteTagHelperAttribute()
{
if (_pageWriter == null)
{
throw new InvalidOperationException(Resources.RazorPage_ThereIsNoActiveWritingScopeToEnd);
}
var content = _valueBuffer.ToString();
_valueBuffer.GetStringBuilder().Clear();
// Restore previous writer.
ViewContext.Writer = _pageWriter;
_pageWriter = null;
return content;
}
/// <summary>
/// Writes the specified <paramref name="value"/> with HTML encoding to <see cref="Output"/>.
/// </summary>

View File

@ -206,4 +206,7 @@
<data name="ViewLocationFormatsIsRequired" xml:space="preserve">
<value>'{0}' cannot be empty. These locations are required to locate a view for rendering.</value>
</data>
<data name="RazorPage_NestingAttributeWritingScopesNotSupported" xml:space="preserve">
<value>Nesting of TagHelper attribute writing scopes is not supported.</value>
</data>
</root>

View File

@ -13,7 +13,7 @@ namespace AspNetCore
{
#line hidden
#pragma warning disable 0414
private global::Microsoft.AspNetCore.Razor.TagHelpers.TagHelperContent __tagHelperStringValueBuffer = null;
private string __tagHelperStringValueBuffer = null;
#pragma warning restore 0414
private global::Microsoft.AspNetCore.Razor.Runtime.TagHelpers.TagHelperExecutionContext __tagHelperExecutionContext = null;
private global::Microsoft.AspNetCore.Razor.Runtime.TagHelpers.TagHelperRunner __tagHelperRunner = null;

View File

@ -225,6 +225,64 @@ namespace Microsoft.AspNetCore.Mvc.Razor
await page.ExecuteAsync();
}
[Fact]
public async Task EndWriteTagHelperAttribute_RestoresPageWriter()
{
// Arrange
var page = CreatePage(v =>
{
v.BeginWriteTagHelperAttribute();
v.Write("Hello World!");
v.EndWriteTagHelperAttribute();
});
var originalWriter = page.Output;
// Act
await page.ExecuteAsync();
// Assert
Assert.NotNull(originalWriter);
Assert.Same(originalWriter, page.Output);
}
[Fact]
public async Task EndWriteTagHelperAttribute_ReturnsAppropriateContent()
{
// Arrange
var viewContext = CreateViewContext();
var page = CreatePage(v =>
{
v.HtmlEncoder = new HtmlTestEncoder();
v.BeginWriteTagHelperAttribute();
v.Write("Hello World!");
var returnValue = v.EndWriteTagHelperAttribute();
// Assert
var content = Assert.IsType<string>(returnValue);
Assert.Equal("HtmlEncode[[Hello World!]]", content);
});
// Act & Assert
await page.ExecuteAsync();
}
[Fact]
public async Task BeginWriteTagHelperAttribute_NestingWritingScopesThrows()
{
// Arrange
var viewContext = CreateViewContext();
var page = CreatePage(v =>
{
v.BeginWriteTagHelperAttribute();
v.BeginWriteTagHelperAttribute();
v.Write("Hello World!");
});
// Act & Assert
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => page.ExecuteAsync());
Assert.Equal("Nesting of TagHelper attribute writing scopes is not supported.", ex.Message);
}
// This is an integration test for ensuring that ViewBuffer segments used by
// TagHelpers can be merged back into the 'main' segment where possible.
[Fact]