From b96851ec20606a83707ee293620a359439eaf8b2 Mon Sep 17 00:00:00 2001 From: mnltejaswini Date: Tue, 24 May 2016 11:01:56 -0700 Subject: [PATCH] [Perf] Avoid ViewBuffers for writing bound TagHelper attribute values. Fixes aspnet/Razor#717 --- .../MvcRazorHost.cs | 2 + .../Properties/Resources.Designer.cs | 16 +++++ .../RazorPage.cs | 56 ++++++++++++++++++ .../Resources.resx | 3 + .../Runtime/ModelExpressionTagHelper.cs | 2 +- .../RazorPageTest.cs | 58 +++++++++++++++++++ 6 files changed, 136 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.AspNetCore.Mvc.Razor.Host/MvcRazorHost.cs b/src/Microsoft.AspNetCore.Mvc.Razor.Host/MvcRazorHost.cs index 9ea649538f..139b80304f 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor.Host/MvcRazorHost.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor.Host/MvcRazorHost.cs @@ -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", diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/Properties/Resources.Designer.cs b/src/Microsoft.AspNetCore.Mvc.Razor/Properties/Resources.Designer.cs index 8c460c1ceb..fe96da1030 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor/Properties/Resources.Designer.cs @@ -478,6 +478,22 @@ namespace Microsoft.AspNetCore.Mvc.Razor return string.Format(CultureInfo.CurrentCulture, GetString("ViewLocationFormatsIsRequired"), p0); } + /// + /// Nesting of TagHelper attribute writing scopes is not supported. + /// + internal static string RazorPage_NestingAttributeWritingScopesNotSupported + { + get { return GetString("RazorPage_NestingAttributeWritingScopesNotSupported"); } + } + + /// + /// Nesting of TagHelper attribute writing scopes is not supported. + /// + internal static string FormatRazorPage_NestingAttributeWritingScopesNotSupported() + { + return GetString("RazorPage_NestingAttributeWritingScopesNotSupported"); + } + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/RazorPage.cs b/src/Microsoft.AspNetCore.Mvc.Razor/RazorPage.cs index 2795e37a44..0e5581ee75 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor/RazorPage.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor/RazorPage.cs @@ -40,6 +40,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor private IViewBufferScope _bufferScope; private bool _ignoreBody; private HashSet _ignoredSections; + private TextWriter _pageWriter; public RazorPage() { @@ -231,6 +232,61 @@ namespace Microsoft.AspNetCore.Mvc.Razor return tagHelperContent; } + /// + /// Starts a new scope for writing attribute values. + /// + /// + /// All writes to the or after calling this method will + /// be buffered until is called. + /// The content will be buffered using a shared within this + /// Nesting of and method calls + /// is not supported. + /// + 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; + + } + + /// + /// Ends the current writing scope that was started by calling . + /// + /// The content buffered by the shared of this . + /// + /// This method assumes that there will be no nesting of + /// and method calls. + /// + 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; + } + /// /// Writes the specified with HTML encoding to . /// diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/Resources.resx b/src/Microsoft.AspNetCore.Mvc.Razor/Resources.resx index 762d5356b5..5d8203d3da 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor/Resources.resx +++ b/src/Microsoft.AspNetCore.Mvc.Razor/Resources.resx @@ -206,4 +206,7 @@ '{0}' cannot be empty. These locations are required to locate a view for rendering. + + Nesting of TagHelper attribute writing scopes is not supported. + \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Host.Test/TestFiles/Output/Runtime/ModelExpressionTagHelper.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Host.Test/TestFiles/Output/Runtime/ModelExpressionTagHelper.cs index 1d2e2bc004..43acb22deb 100644 --- a/test/Microsoft.AspNetCore.Mvc.Razor.Host.Test/TestFiles/Output/Runtime/ModelExpressionTagHelper.cs +++ b/test/Microsoft.AspNetCore.Mvc.Razor.Host.Test/TestFiles/Output/Runtime/ModelExpressionTagHelper.cs @@ -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; diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorPageTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorPageTest.cs index e4812c5605..011e07ee7f 100644 --- a/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorPageTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorPageTest.cs @@ -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(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(() => 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]