From 0b7035ddcf431cb9a150432461824d39d3feccf9 Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Fri, 12 Feb 2016 18:12:08 -0800 Subject: [PATCH] Implement MoveTo semantics in Mvc - Simplify things that used to rely on HtmlTextWriter. This behavior is now the default. - Simplify a whole mess of Razor TextWriter code. - Integration test for merging of TagHelper buffers back into the 'main' buffer. --- global.json | 2 +- .../LocalizedHtmlString.cs | 10 +- .../RazorPage.cs | 60 +- .../RazorTextWriter.cs | 209 ------- .../RazorView.cs | 57 +- .../TagHelpers/UrlResolutionTagHelper.cs | 12 +- .../CacheTagHelper.cs | 7 - .../Internal/HtmlContentWrapperTextWriter.cs | 176 ------ .../Internal/IViewBufferScope.cs | 9 +- .../Internal/MemoryPoolViewBufferScope.cs | 17 +- .../Internal/PagedBufferedTextWriter.cs | 211 +++++++ .../Internal/ViewBuffer.cs | 248 ++++++--- .../Internal/ViewBufferTextWriter.cs | 522 +++++++++++++----- .../Rendering/MvcForm.cs | 45 +- .../Rendering/TagBuilder.cs | 13 +- .../DefaultViewComponentHelper.cs | 6 +- .../HtmlContentViewComponentResult.cs | 10 +- .../ViewFeatures/AntiforgeryExtensions.cs | 8 - .../ViewFeatures/HtmlHelper.cs | 8 +- .../ViewFeatures/StringHtmlContent.cs | 12 +- .../ViewFeatures/TemplateRenderer.cs | 6 +- .../RazorPageTest.cs | 165 +++++- .../RazorViewTest.cs | 4 +- .../SpanFactory.cs | 1 - .../HtmlContentWrapperTextWriterTest.cs | 186 ------- .../PagedBufferedStringWriterTest.cs} | 16 +- .../Internal/TestViewBufferScope.cs | 19 +- .../Internal/ViewBufferTest.cs | 209 +++---- .../Internal/ViewBufferTextWriterTest.cs} | 88 ++- .../Rendering/ViewContextTests.cs | 2 +- 30 files changed, 1245 insertions(+), 1093 deletions(-) delete mode 100644 src/Microsoft.AspNetCore.Mvc.Razor/RazorTextWriter.cs delete mode 100644 src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/HtmlContentWrapperTextWriter.cs create mode 100644 src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/PagedBufferedTextWriter.cs delete mode 100644 test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/HtmlContentWrapperTextWriterTest.cs rename test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/{ViewBufferTextWriterTest.cs => Internal/PagedBufferedStringWriterTest.cs} (90%) rename test/{Microsoft.AspNetCore.Mvc.Razor.Test/RazorTextWriterTest.cs => Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/ViewBufferTextWriterTest.cs} (66%) diff --git a/global.json b/global.json index fad3dfeab0..b488354b34 100644 --- a/global.json +++ b/global.json @@ -1,3 +1,3 @@ { - "projects": ["src", "test/WebSites", "samples"] + "projects": ["src", "test/WebSites", "samples", "d:\\k\\HtmlAbstractions\\src", "d:\\k\\Razor\\src"] } diff --git a/src/Microsoft.AspNetCore.Mvc.Localization/LocalizedHtmlString.cs b/src/Microsoft.AspNetCore.Mvc.Localization/LocalizedHtmlString.cs index b6322850d9..a16d52405a 100644 --- a/src/Microsoft.AspNetCore.Mvc.Localization/LocalizedHtmlString.cs +++ b/src/Microsoft.AspNetCore.Mvc.Localization/LocalizedHtmlString.cs @@ -100,15 +100,7 @@ namespace Microsoft.AspNetCore.Mvc.Localization throw new ArgumentNullException(nameof(encoder)); } - var htmlTextWriter = writer as HtmlTextWriter; - if (htmlTextWriter == null) - { - FormatValue(writer, encoder, Value, _arguments); - } - else - { - htmlTextWriter.Write(this); - } + FormatValue(writer, encoder, Value, _arguments); } private static void FormatValue( diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/RazorPage.cs b/src/Microsoft.AspNetCore.Mvc.Razor/RazorPage.cs index 2002ca9718..79b86d291a 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor/RazorPage.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor/RazorPage.cs @@ -37,7 +37,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor private bool _renderedBody; private AttributeInfo _attributeInfo; private TagHelperAttributeInfo _tagHelperAttributeInfo; - private HtmlContentWrapperTextWriter _valueBuffer; + private StringWriter _valueBuffer; private IViewBufferScope _bufferScope; private bool _ignoreBody; private HashSet _ignoredSections; @@ -214,7 +214,8 @@ namespace Microsoft.AspNetCore.Mvc.Razor /// public void StartTagHelperWritingScope(HtmlEncoder encoder) { - _tagHelperScopes.Push(new TagHelperScopeInfo(HtmlEncoder, ViewContext.Writer)); + var buffer = new ViewBuffer(BufferScope, Path, ViewBuffer.TagHelperPageSize); + _tagHelperScopes.Push(new TagHelperScopeInfo(buffer, HtmlEncoder, ViewContext.Writer)); // If passed an HtmlEncoder, override the property. if (encoder != null) @@ -224,9 +225,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor // We need to replace the ViewContext's Writer to ensure that all content (including content written // from HTML helpers) is redirected. - var buffer = new ViewBuffer(BufferScope, Path); - var writer = new HtmlContentWrapperTextWriter(buffer, ViewContext.Writer.Encoding); - ViewContext.Writer = writer; + ViewContext.Writer = new ViewBufferTextWriter(buffer, ViewContext.Writer.Encoding); } /// @@ -240,13 +239,13 @@ namespace Microsoft.AspNetCore.Mvc.Razor throw new InvalidOperationException(Resources.RazorPage_ThereIsNoActiveWritingScopeToEnd); } + var scopeInfo = _tagHelperScopes.Pop(); + // Get the content written during the current scope. - var writer = ViewContext.Writer as HtmlContentWrapperTextWriter; var tagHelperContent = new DefaultTagHelperContent(); - tagHelperContent.AppendHtml(writer?.ContentBuilder); + tagHelperContent.AppendHtml(scopeInfo.Buffer); // Restore previous scope. - var scopeInfo = _tagHelperScopes.Pop(); HtmlEncoder = scopeInfo.Encoder; ViewContext.Writer = scopeInfo.Writer; @@ -317,16 +316,25 @@ namespace Microsoft.AspNetCore.Mvc.Razor var htmlContent = value as IHtmlContent; if (htmlContent != null) { - var htmlTextWriter = writer as HtmlTextWriter; - if (htmlTextWriter == null) + var bufferedWriter = writer as ViewBufferTextWriter; + if (bufferedWriter == null || !bufferedWriter.IsBuffering) { htmlContent.WriteTo(writer, encoder); } else { - // This special case allows us to keep buffering as IHtmlContent until we get to the 'final' - // TextWriter. - htmlTextWriter.Write(htmlContent); + var htmlContentContainer = value as IHtmlContentContainer; + if (htmlContentContainer != null) + { + // This is likely another ViewBuffer. + htmlContentContainer.MoveTo(bufferedWriter.Buffer); + } + else + { + // Perf: This is the common case for IHtmlContent, ViewBufferTextWriter is inefficient + // for writing character by character. + bufferedWriter.Buffer.AppendHtml(htmlContent); + } } return; @@ -354,7 +362,10 @@ namespace Microsoft.AspNetCore.Mvc.Razor { if (!string.IsNullOrEmpty(value)) { - encoder.Encode(writer, value); + // Perf: Encode right away instead of writing it character-by-character. + // character-by-character isn't efficient when using a writer backed by a ViewBuffer. + var encoded = encoder.Encode(value); + writer.Write(encoded); } } @@ -529,7 +540,6 @@ namespace Microsoft.AspNetCore.Mvc.Razor int attributeValuesCount) { _tagHelperAttributeInfo = new TagHelperAttributeInfo(executionContext, attributeName, attributeValuesCount); - _valueBuffer = null; } public void AddHtmlAttributeValue( @@ -567,10 +577,11 @@ namespace Microsoft.AspNetCore.Mvc.Razor if (value != null) { + // Perf: We'll use this buffer for all of the attribute values and then clear it to + // reduce allocations. if (_valueBuffer == null) { - var buffer = new ViewBuffer(BufferScope, Path); - _valueBuffer = new HtmlContentWrapperTextWriter(buffer, Output.Encoding); + _valueBuffer = new StringWriter(); } if (!string.IsNullOrEmpty(prefix)) @@ -586,10 +597,12 @@ namespace Microsoft.AspNetCore.Mvc.Razor { if (!_tagHelperAttributeInfo.Suppressed) { - executionContext.AddHtmlAttribute( - _tagHelperAttributeInfo.Name, - (IHtmlContent)_valueBuffer?.ContentBuilder ?? HtmlString.Empty); - _valueBuffer = null; + // Perf: _valueBuffer might be null if nothing was written. If it is set, clear it so + // it is reset for the next value. + var content = _valueBuffer == null ? HtmlString.Empty : new HtmlString(_valueBuffer.ToString()); + _valueBuffer?.GetStringBuilder().Clear(); + + executionContext.AddHtmlAttribute(_tagHelperAttributeInfo.Name, content); } } @@ -1053,12 +1066,15 @@ namespace Microsoft.AspNetCore.Mvc.Razor private struct TagHelperScopeInfo { - public TagHelperScopeInfo(HtmlEncoder encoder, TextWriter writer) + public TagHelperScopeInfo(ViewBuffer buffer, HtmlEncoder encoder, TextWriter writer) { + Buffer = buffer; Encoder = encoder; Writer = writer; } + public ViewBuffer Buffer { get; } + public HtmlEncoder Encoder { get; } public TextWriter Writer { get; } diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/RazorTextWriter.cs b/src/Microsoft.AspNetCore.Mvc.Razor/RazorTextWriter.cs deleted file mode 100644 index e82f690c6e..0000000000 --- a/src/Microsoft.AspNetCore.Mvc.Razor/RazorTextWriter.cs +++ /dev/null @@ -1,209 +0,0 @@ -// 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.IO; -using System.Text; -using System.Text.Encodings.Web; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Html; -using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal; - -namespace Microsoft.AspNetCore.Mvc.Razor -{ - /// - /// An that is backed by a unbuffered writer (over the Response stream) and a buffered - /// . When Flush or FlushAsync is invoked, the writer - /// copies all content from the buffered writer to the unbuffered one and switches to writing to the unbuffered - /// writer for all further write operations. - /// - public class RazorTextWriter : HtmlTextWriter - { - /// - /// Creates a new instance of . - /// - /// The to write output to when this instance - /// is no longer buffering. - /// The to buffer output to. - /// The HTML encoder. - public RazorTextWriter(TextWriter unbufferedWriter, ViewBuffer buffer, HtmlEncoder encoder) - { - UnbufferedWriter = unbufferedWriter; - Buffer = buffer; - HtmlEncoder = encoder; - - BufferedWriter = new HtmlContentWrapperTextWriter(buffer, unbufferedWriter.Encoding); - TargetWriter = BufferedWriter; - } - - /// - public override Encoding Encoding - { - get { return BufferedWriter.Encoding; } - } - - /// - public bool IsBuffering { get; private set; } = true; - - /// - /// Gets the buffered content. - /// - public ViewBuffer Buffer { get; } - - // Internal for unit testing - internal HtmlContentWrapperTextWriter BufferedWriter { get; } - - private TextWriter UnbufferedWriter { get; } - - private TextWriter TargetWriter { get; set; } - - private HtmlEncoder HtmlEncoder { get; } - - /// - public override void Write(char value) - { - TargetWriter.Write(value); - } - - /// - public override void Write(char[] buffer, int index, int count) - { - if (buffer == null) - { - throw new ArgumentNullException(nameof(buffer)); - } - - if (index < 0) - { - throw new ArgumentOutOfRangeException(nameof(index)); - } - - if (count < 0 || (index + count > buffer.Length)) - { - throw new ArgumentOutOfRangeException(nameof(count)); - } - - TargetWriter.Write(buffer, index, count); - } - - /// - public override void Write(string value) - { - if (!string.IsNullOrEmpty(value)) - { - TargetWriter.Write(value); - } - } - - /// - public override void Write(IHtmlContent value) - { - // Perf: We don't special case 'TargetWriter is HtmlTextWriter' here, because want to delegate to the - // IHtmlContent if it wants to 'flatten' itself or not. This is an important optimization for TagHelpers. - value.WriteTo(TargetWriter, HtmlEncoder); - } - - /// - public override Task WriteAsync(char value) - { - return TargetWriter.WriteAsync(value); - } - - /// - public override Task WriteAsync(char[] buffer, int index, int count) - { - if (buffer == null) - { - throw new ArgumentNullException(nameof(buffer)); - } - - if (index < 0) - { - throw new ArgumentOutOfRangeException(nameof(index)); - } - if (count < 0 || (buffer.Length - index < count)) - { - throw new ArgumentOutOfRangeException(nameof(count)); - } - - return TargetWriter.WriteAsync(buffer, index, count); - } - - /// - public override Task WriteAsync(string value) - { - return TargetWriter.WriteAsync(value); - } - - /// - public override void WriteLine() - { - TargetWriter.WriteLine(); - } - - /// - public override void WriteLine(string value) - { - TargetWriter.WriteLine(value); - } - - /// - public override Task WriteLineAsync(char value) - { - return TargetWriter.WriteLineAsync(value); - } - - /// - public override Task WriteLineAsync(char[] value, int start, int offset) - { - return TargetWriter.WriteLineAsync(value, start, offset); - } - - /// - public override Task WriteLineAsync(string value) - { - return TargetWriter.WriteLineAsync(value); - } - - /// - public override Task WriteLineAsync() - { - return TargetWriter.WriteLineAsync(); - } - - /// - /// Copies the buffered content to the unbuffered writer and invokes flush on it. - /// Additionally causes this instance to no longer buffer and direct all write operations - /// to the unbuffered writer. - /// - public override void Flush() - { - if (IsBuffering) - { - IsBuffering = false; - TargetWriter = UnbufferedWriter; - Buffer.WriteTo(UnbufferedWriter, HtmlEncoder); - } - - UnbufferedWriter.Flush(); - } - - /// - /// Copies the buffered content to the unbuffered writer and invokes flush on it. - /// Additionally causes this instance to no longer buffer and direct all write operations - /// to the unbuffered writer. - /// - /// A that represents the asynchronous copy and flush operations. - public override Task FlushAsync() - { - if (IsBuffering) - { - IsBuffering = false; - TargetWriter = UnbufferedWriter; - Buffer.WriteTo(UnbufferedWriter, HtmlEncoder); - } - - return UnbufferedWriter.FlushAsync(); - } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/RazorView.cs b/src/Microsoft.AspNetCore.Mvc.Razor/RazorView.cs index d823f62370..cc322ce1f8 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor/RazorView.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor/RazorView.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.Buffers; using System.Collections.Generic; using System.Diagnostics; using System.Linq; @@ -103,20 +102,35 @@ namespace Microsoft.AspNetCore.Mvc.Razor await RenderLayoutAsync(context, bodyWriter); } - private async Task RenderPageAsync( + private async Task RenderPageAsync( IRazorPage page, ViewContext context, bool invokeViewStarts) { - Debug.Assert(_bufferScope != null); - var buffer = new ViewBuffer(_bufferScope, page.Path); - var razorTextWriter = new RazorTextWriter(context.Writer, buffer, _htmlEncoder); + var writer = context.Writer as ViewBufferTextWriter; + if (writer == null) + { + Debug.Assert(_bufferScope != null); + + // If we get here, this is likely the top-level page (not a partial) - this means + // that context.Writer is wrapping the output stream. We need to buffer, so create a buffered writer. + var buffer = new ViewBuffer(_bufferScope, page.Path, ViewBuffer.ViewPageSize); + writer = new ViewBufferTextWriter(buffer, context.Writer.Encoding, _htmlEncoder, context.Writer); + } + else + { + // This means we're writing something like a partial, where the output needs to be buffered. + // Create a new buffer, but without the ability to flush. + var buffer = new ViewBuffer(_bufferScope, page.Path, ViewBuffer.ViewPageSize); + writer = new ViewBufferTextWriter(buffer, context.Writer.Encoding); + } // The writer for the body is passed through the ViewContext, allowing things like HtmlHelpers // and ViewComponents to reference it. var oldWriter = context.Writer; var oldFilePath = context.ExecutingFilePath; - context.Writer = razorTextWriter; + + context.Writer = writer; context.ExecutingFilePath = page.Path; try @@ -128,13 +142,12 @@ namespace Microsoft.AspNetCore.Mvc.Razor } await RenderPageCoreAsync(page, context); - return razorTextWriter; + return writer; } finally { context.Writer = oldWriter; context.ExecutingFilePath = oldFilePath; - razorTextWriter.Dispose(); } } @@ -184,12 +197,16 @@ namespace Microsoft.AspNetCore.Mvc.Razor private async Task RenderLayoutAsync( ViewContext context, - RazorTextWriter bodyWriter) + ViewBufferTextWriter bodyWriter) { // A layout page can specify another layout page. We'll need to continue // looking for layout pages until they're no longer specified. var previousPage = RazorPage; var renderedLayouts = new List(); + + // This loop will execute Layout pages from the inside to the outside. With each + // iteration, bodyWriter is replaced with the aggregate of all the "body" content + // (including the layout page we just rendered). while (!string.IsNullOrEmpty(previousPage.Layout)) { if (!bodyWriter.IsBuffering) @@ -225,6 +242,8 @@ namespace Microsoft.AspNetCore.Mvc.Razor previousPage = layoutPage; } + // Now we've reached and rendered the outer-most layout page. Nothing left to execute. + // Ensure all defined sections were rendered or RenderBody was invoked for page without defined sections. foreach (var layoutPage in renderedLayouts) { @@ -233,10 +252,24 @@ namespace Microsoft.AspNetCore.Mvc.Razor if (bodyWriter.IsBuffering) { - // Only copy buffered content to the Output if we're currently buffering. - using (var writer = _bufferScope.CreateWriter(context.Writer)) + // If IsBuffering - then we've got a bunch of content in the view buffer. How to best deal with it + // really depends on whether or not we're writing directly to the output or if we're writing to + // another buffer. + var viewBufferTextWriter = context.Writer as ViewBufferTextWriter; + if (viewBufferTextWriter == null || !viewBufferTextWriter.IsBuffering) { - await bodyWriter.Buffer.WriteToAsync(writer, _htmlEncoder); + // This means we're writing to a 'real' writer, probably to the actual output stream. + // We're using PagedBufferedTextWriter here to 'smooth' synchronous writes of IHtmlContent values. + using (var writer = _bufferScope.CreateWriter(context.Writer)) + { + await bodyWriter.Buffer.WriteToAsync(writer, _htmlEncoder); + } + } + else + { + // This means we're writing to another buffer. Use MoveTo to combine them. + bodyWriter.Buffer.MoveTo(viewBufferTextWriter.Buffer); + return; } } } diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/TagHelpers/UrlResolutionTagHelper.cs b/src/Microsoft.AspNetCore.Mvc.Razor/TagHelpers/UrlResolutionTagHelper.cs index 75c0f7dbc4..583e5229bb 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor/TagHelpers/UrlResolutionTagHelper.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor/TagHelpers/UrlResolutionTagHelper.cs @@ -354,16 +354,8 @@ namespace Microsoft.AspNetCore.Mvc.Razor.TagHelpers public void WriteTo(TextWriter writer, HtmlEncoder encoder) { - var htmlTextWriter = writer as HtmlTextWriter; - if (htmlTextWriter != null) - { - htmlTextWriter.Write(this); - } - else - { - encoder.Encode(writer, _firstSegment, 0, _firstSegmentLength); - writer.Write(_secondSegment); - } + encoder.Encode(writer, _firstSegment, 0, _firstSegmentLength); + writer.Write(_secondSegment); } } } diff --git a/src/Microsoft.AspNetCore.Mvc.TagHelpers/CacheTagHelper.cs b/src/Microsoft.AspNetCore.Mvc.TagHelpers/CacheTagHelper.cs index 590bab435b..f6f455f12b 100644 --- a/src/Microsoft.AspNetCore.Mvc.TagHelpers/CacheTagHelper.cs +++ b/src/Microsoft.AspNetCore.Mvc.TagHelpers/CacheTagHelper.cs @@ -400,13 +400,6 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers public void WriteTo(TextWriter writer, HtmlEncoder encoder) { - var htmlTextWriter = writer as HtmlTextWriter; - if (htmlTextWriter != null) - { - htmlTextWriter.Write(this); - return; - } - for (var i = 0; i < _builder.Length; i++) { writer.Write(_builder[i]); diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/HtmlContentWrapperTextWriter.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/HtmlContentWrapperTextWriter.cs deleted file mode 100644 index 6fba2af406..0000000000 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/HtmlContentWrapperTextWriter.cs +++ /dev/null @@ -1,176 +0,0 @@ -// 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.Text; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Html; -using Microsoft.AspNetCore.Mvc.Internal; - -namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal -{ - /// - /// implementation which writes to an instance. - /// - public class HtmlContentWrapperTextWriter : HtmlTextWriter - { - private const int MaxCharToStringLength = 1024; - - /// - /// Initializes a new instance of the class. - /// - /// The to write to. - /// The in which output is written. - public HtmlContentWrapperTextWriter(IHtmlContentBuilder contentBuilder, Encoding encoding) - { - if (contentBuilder == null) - { - throw new ArgumentNullException(nameof(contentBuilder)); - } - - if (encoding == null) - { - throw new ArgumentNullException(nameof(encoding)); - } - - ContentBuilder = contentBuilder; - Encoding = encoding; - } - - /// - /// The this writes to. - /// - public IHtmlContentBuilder ContentBuilder { get; } - - /// - public override Encoding Encoding { get; } - - /// - public override void Write(char value) - { - Write(value.ToString()); - } - - /// - public override void Write(char[] buffer, int index, int count) - { - if (buffer == null) - { - throw new ArgumentNullException(nameof(buffer)); - } - - if (index < 0) - { - throw new ArgumentOutOfRangeException(nameof(index)); - } - - if (count < 0 || (index + count > buffer.Length)) - { - throw new ArgumentOutOfRangeException(nameof(count)); - } - - while (count > 0) - { - // Split large char arrays into 1KB strings. - var currentCount = count; - if (MaxCharToStringLength < currentCount) - { - currentCount = MaxCharToStringLength; - } - - Write(new string(buffer, index, currentCount)); - index += currentCount; - count -= currentCount; - } - } - - /// - public override void Write(string value) - { - if (string.IsNullOrEmpty(value)) - { - return; - } - - ContentBuilder.Append(value); - } - - /// - public override void Write(IHtmlContent value) - { - if (value == null) - { - throw new ArgumentNullException(nameof(value)); - } - - ContentBuilder.AppendHtml(value); - } - - /// - public override Task WriteAsync(char value) - { - Write(value.ToString()); - return TaskCache.CompletedTask; - } - - /// - public override Task WriteAsync(char[] buffer, int index, int count) - { - if (buffer == null) - { - throw new ArgumentNullException(nameof(buffer)); - } - - Write(buffer, index, count); - return TaskCache.CompletedTask; - } - - /// - public override Task WriteAsync(string value) - { - Write(value); - return TaskCache.CompletedTask; - } - - /// - public override void WriteLine() - { - Write(Environment.NewLine); - } - - /// - public override void WriteLine(string value) - { - Write(value); - WriteLine(); - } - - /// - public override Task WriteLineAsync(char value) - { - WriteLine(value); - return TaskCache.CompletedTask; - } - - /// - public override Task WriteLineAsync(char[] value, int start, int offset) - { - WriteLine(value, start, offset); - return TaskCache.CompletedTask; - } - - /// - public override Task WriteLineAsync(string value) - { - WriteLine(value); - return TaskCache.CompletedTask; - } - - /// - public override Task WriteLineAsync() - { - WriteLine(); - return TaskCache.CompletedTask; - } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/IViewBufferScope.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/IViewBufferScope.cs index 2250771fb4..2b6f7ee37d 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/IViewBufferScope.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/IViewBufferScope.cs @@ -13,8 +13,9 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal /// /// Gets a . /// + /// The minimum size of the segment. /// The . - ViewBufferValue[] GetSegment(); + ViewBufferValue[] GetPage(int pageSize); /// /// Returns a that can be reused. @@ -23,11 +24,11 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal void ReturnSegment(ViewBufferValue[] segment); /// - /// Creates a that will delegate to the provided + /// Creates a that will delegate to the provided /// . /// /// The . - /// A . - ViewBufferTextWriter CreateWriter(TextWriter writer); + /// A . + PagedBufferedTextWriter CreateWriter(TextWriter writer); } } diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/MemoryPoolViewBufferScope.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/MemoryPoolViewBufferScope.cs index d0613afe91..10a39dcd5c 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/MemoryPoolViewBufferScope.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/MemoryPoolViewBufferScope.cs @@ -13,7 +13,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal /// public class MemoryPoolViewBufferScope : IViewBufferScope, IDisposable { - public static readonly int SegmentSize = 512; + public static readonly int MinimumSize = 16; private readonly ArrayPool _viewBufferPool; private readonly ArrayPool _charPool; private List _available; @@ -27,7 +27,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal /// The for creating instances. /// /// - /// The for creating instances. + /// The for creating instances. /// public MemoryPoolViewBufferScope(ArrayPool viewBufferPool, ArrayPool charPool) { @@ -36,8 +36,13 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal } /// - public ViewBufferValue[] GetSegment() + public ViewBufferValue[] GetPage(int pageSize) { + if (pageSize <= 0) + { + throw new ArgumentOutOfRangeException(nameof(pageSize)); + } + if (_disposed) { throw new ObjectDisposedException(typeof(MemoryPoolViewBufferScope).FullName); @@ -60,7 +65,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal try { - segment = _viewBufferPool.Rent(SegmentSize); + segment = _viewBufferPool.Rent(Math.Max(pageSize, MinimumSize)); _leased.Add(segment); } catch when (segment != null) @@ -91,14 +96,14 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal } /// - public ViewBufferTextWriter CreateWriter(TextWriter writer) + public PagedBufferedTextWriter CreateWriter(TextWriter writer) { if (writer == null) { throw new ArgumentNullException(nameof(writer)); } - return new ViewBufferTextWriter(_charPool, writer); + return new PagedBufferedTextWriter(_charPool, writer); } /// diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/PagedBufferedTextWriter.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/PagedBufferedTextWriter.cs new file mode 100644 index 0000000000..93c26b01e9 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/PagedBufferedTextWriter.cs @@ -0,0 +1,211 @@ +// 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.Buffers; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal +{ + public class PagedBufferedTextWriter : TextWriter + { + public const int PageSize = 1024; + + private readonly TextWriter _inner; + private readonly List _pages; + private readonly ArrayPool _pool; + + private int _currentPage; + private int _currentIndex; // The next 'free' character + + public PagedBufferedTextWriter(ArrayPool pool, TextWriter inner) + { + _pool = pool; + _inner = inner; + _pages = new List(); + } + + public override Encoding Encoding => _inner.Encoding; + + public override void Flush() + { + // Don't do anything. We'll call FlushAsync. + } + + public override async Task FlushAsync() + { + if (_pages.Count == 0) + { + return; + } + + for (var i = 0; i <= _currentPage; i++) + { + var page = _pages[i]; + + var count = i == _currentPage ? _currentIndex : page.Length; + if (count > 0) + { + await _inner.WriteAsync(page, 0, count); + } + } + + // Return all but one of the pages. This way if someone writes a large chunk of + // content, we can return those buffers and avoid holding them for the whole + // page's lifetime. + for (var i = _pages.Count - 1; i > 0; i--) + { + var page = _pages[i]; + + try + { + _pages.RemoveAt(i); + } + finally + { + _pool.Return(page); + } + } + + _currentPage = 0; + _currentIndex = 0; + } + + public override void Write(char value) + { + var page = GetCurrentPage(); + page[_currentIndex++] = value; + } + + public override void Write(char[] buffer) + { + Write(buffer, 0, buffer.Length); + } + + public override void Write(char[] buffer, int index, int count) + { + while (count > 0) + { + var page = GetCurrentPage(); + var copyLength = Math.Min(count, page.Length - _currentIndex); + Debug.Assert(copyLength > 0); + + Array.Copy( + buffer, + index, + page, + _currentIndex, + copyLength); + + _currentIndex += copyLength; + index += copyLength; + + count -= copyLength; + } + } + + public override void Write(string value) + { + var index = 0; + var count = value.Length; + + while (count > 0) + { + var page = GetCurrentPage(); + var copyLength = Math.Min(count, page.Length - _currentIndex); + Debug.Assert(copyLength > 0); + + value.CopyTo( + index, + page, + _currentIndex, + copyLength); + + _currentIndex += copyLength; + index += copyLength; + + count -= copyLength; + } + } + + public override Task WriteAsync(char value) + { + return _inner.WriteAsync(value); + } + + public override Task WriteAsync(char[] buffer, int index, int count) + { + return _inner.WriteAsync(buffer, index, count); + } + + public override Task WriteAsync(string value) + { + return _inner.WriteAsync(value); + } + + private char[] GetCurrentPage() + { + char[] page = null; + if (_pages.Count == 0) + { + Debug.Assert(_currentPage == 0); + Debug.Assert(_currentIndex == 0); + + try + { + page = _pool.Rent(PageSize); + _pages.Add(page); + } + catch when (page != null) + { + _pool.Return(page); + throw; + } + + return page; + } + + Debug.Assert(_pages.Count > _currentPage); + page = _pages[_currentPage]; + + if (_currentIndex == page.Length) + { + // Current page is full. + _currentPage++; + _currentIndex = 0; + + if (_pages.Count == _currentPage) + { + try + { + page = _pool.Rent(PageSize); + _pages.Add(page); + } + catch when (page != null) + { + _pool.Return(page); + throw; + } + } + } + + return page; + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + for (var i = 0; i < _pages.Count; i++) + { + _pool.Return(_pages[i]); + } + + _pages.Clear(); + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/ViewBuffer.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/ViewBuffer.cs index b6fcb0ed1f..6ad1e02cc9 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/ViewBuffer.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/ViewBuffer.cs @@ -2,15 +2,12 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Buffers; using System.Collections.Generic; using System.Diagnostics; using System.IO; -using System.Text; using System.Text.Encodings.Web; using System.Threading.Tasks; using Microsoft.AspNetCore.Html; -using Microsoft.AspNetCore.Mvc.Rendering; namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal { @@ -20,23 +17,36 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal [DebuggerDisplay("{DebuggerToString()}")] public class ViewBuffer : IHtmlContentBuilder { + public static readonly int PartialViewPageSize = 32; + public static readonly int TagHelperPageSize = 32; + public static readonly int ViewComponentPageSize = 32; + public static readonly int ViewPageSize = 256; + private readonly IViewBufferScope _bufferScope; private readonly string _name; + private readonly int _pageSize; /// /// Initializes a new instance of . /// /// The . /// A name to identify this instance. - public ViewBuffer(IViewBufferScope bufferScope, string name) + /// The size of buffer pages. + public ViewBuffer(IViewBufferScope bufferScope, string name, int pageSize) { if (bufferScope == null) { throw new ArgumentNullException(nameof(bufferScope)); } + if (pageSize <= 0) + { + throw new ArgumentOutOfRangeException(nameof(pageSize)); + } + _bufferScope = bufferScope; _name = name; + _pageSize = pageSize; Pages = new List(); } @@ -54,7 +64,9 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal return this; } - AppendValue(new ViewBufferValue(unencoded)); + // Text that needs encoding is the uncommon case in views, which is why it + // creates a wrapper and pre-encoded text does not. + AppendValue(new ViewBufferValue(new EncodingWrapper(unencoded))); return this; } @@ -66,48 +78,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal return this; } - // Perf: special case ViewBuffers so we can 'combine' them. - var otherBuffer = content as ViewBuffer; - if (otherBuffer == null) - { - AppendValue(new ViewBufferValue(content)); - return this; - } - - for (var i = 0; i < otherBuffer.Pages.Count; i++) - { - var otherPage = otherBuffer.Pages[i]; - var currentPage = Pages.Count == 0 ? null : Pages[Pages.Count - 1]; - - // If the other page is less or equal to than half full, let's copy it's to the current page if - // possible. - var isLessThanHalfFull = 2 * otherPage.Count <= otherPage.Capacity; - if (isLessThanHalfFull && - currentPage != null && - currentPage.Capacity - currentPage.Count >= otherPage.Count) - { - // We have room, let's copy the items. - Array.Copy( - sourceArray: otherPage.Buffer, - sourceIndex: 0, - destinationArray: currentPage.Buffer, - destinationIndex: currentPage.Count, - length: otherPage.Count); - - currentPage.Count += otherPage.Count; - - // Now we can return this page, and it can be reused in the scope of this request. - _bufferScope.ReturnSegment(otherPage.Buffer); - } - else - { - // Otherwise, let's just take the the page from the other buffer. - Pages.Add(otherPage); - } - - } - - otherBuffer.Clear(); + AppendValue(new ViewBufferValue(content)); return this; } @@ -118,9 +89,8 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal { return this; } - - var value = new HtmlString(encoded); - AppendValue(new ViewBufferValue(value)); + + AppendValue(new ViewBufferValue(encoded)); return this; } @@ -135,7 +105,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal ViewBufferPage page; if (Pages.Count == 0) { - page = new ViewBufferPage(_bufferScope.GetSegment()); + page = new ViewBufferPage(_bufferScope.GetPage(_pageSize)); Pages.Add(page); } else @@ -143,7 +113,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal page = Pages[Pages.Count - 1]; if (page.IsFull) { - page = new ViewBufferPage(_bufferScope.GetSegment()); + page = new ViewBufferPage(_bufferScope.GetPage(_pageSize)); Pages.Add(page); } } @@ -165,15 +135,18 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal /// public void WriteTo(TextWriter writer, HtmlEncoder encoder) { - if (Pages == null) + if (writer == null) { - return; + throw new ArgumentNullException(nameof(writer)); } - var htmlTextWriter = writer as HtmlTextWriter; - if (htmlTextWriter != null) + if (encoder == null) + { + throw new ArgumentNullException(nameof(encoder)); + } + + if (Pages == null) { - htmlTextWriter.Write(this); return; } @@ -209,15 +182,18 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal /// A which will complete once content has been written. public async Task WriteToAsync(TextWriter writer, HtmlEncoder encoder) { - if (Pages == null) + if (writer == null) { - return; + throw new ArgumentNullException(nameof(writer)); } - var htmlTextWriter = writer as HtmlTextWriter; - if (htmlTextWriter != null) + if (encoder == null) + { + throw new ArgumentNullException(nameof(encoder)); + } + + if (Pages == null) { - htmlTextWriter.Write(this); return; } @@ -254,5 +230,153 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal } private string DebuggerToString() => _name; + + public void CopyTo(IHtmlContentBuilder destination) + { + if (destination == null) + { + throw new ArgumentNullException(nameof(destination)); + } + + if (Pages == null) + { + return; + } + + for (var i = 0; i < Pages.Count; i++) + { + var page = Pages[i]; + for (var j = 0; j < page.Count; j++) + { + var value = page.Buffer[j]; + + string valueAsString; + IHtmlContentContainer valueAsContainer; + if ((valueAsString = value.Value as string) != null) + { + destination.AppendHtml(valueAsString); + } + else if ((valueAsContainer = value.Value as IHtmlContentContainer) != null) + { + valueAsContainer.CopyTo(destination); + } + else + { + destination.AppendHtml((IHtmlContent)value.Value); + } + } + } + } + + public void MoveTo(IHtmlContentBuilder destination) + { + if (destination == null) + { + throw new ArgumentNullException(nameof(destination)); + } + + if (Pages == null) + { + return; + } + + // Perf: We have an efficient implementation when the destination is another view buffer, + // we can just insert our pages as-is. + var other = destination as ViewBuffer; + if (other != null) + { + MoveTo(other); + return; + } + + for (var i = 0; i < Pages.Count; i++) + { + var page = Pages[i]; + for (var j = 0; j < page.Count; j++) + { + var value = page.Buffer[j]; + + string valueAsString; + IHtmlContentContainer valueAsContainer; + if ((valueAsString = value.Value as string) != null) + { + destination.AppendHtml(valueAsString); + } + else if ((valueAsContainer = value.Value as IHtmlContentContainer) != null) + { + valueAsContainer.MoveTo(destination); + } + else + { + destination.AppendHtml((IHtmlContent)value.Value); + } + } + } + + for (var i = 0; i < Pages.Count; i++) + { + var page = Pages[i]; + Array.Clear(page.Buffer, 0, page.Count); + _bufferScope.ReturnSegment(page.Buffer); + } + + Pages.Clear(); + } + + private void MoveTo(ViewBuffer destination) + { + for (var i = 0; i < Pages.Count; i++) + { + var page = Pages[i]; + + var destinationPage = destination.Pages.Count == 0 ? null : destination.Pages[destination.Pages.Count - 1]; + + // If the source page is less or equal to than half full, let's copy it's content to the destination + // page if possible. + var isLessThanHalfFull = 2 * page.Count <= page.Capacity; + if (isLessThanHalfFull && + destinationPage != null && + destinationPage.Capacity - destinationPage.Count >= page.Count) + { + // We have room, let's copy the items. + Array.Copy( + sourceArray: page.Buffer, + sourceIndex: 0, + destinationArray: destinationPage.Buffer, + destinationIndex: destinationPage.Count, + length: page.Count); + + destinationPage.Count += page.Count; + + // Now we can return the source page, and it can be reused in the scope of this request. + Array.Clear(page.Buffer, 0, page.Count); + _bufferScope.ReturnSegment(page.Buffer); + + } + else + { + // Otherwise, let's just add the source page to the other buffer. + destination.Pages.Add(page); + } + + } + + Pages.Clear(); + } + + private class EncodingWrapper : IHtmlContent + { + private readonly string _unencoded; + + public EncodingWrapper(string unencoded) + { + _unencoded = unencoded; + } + + public void WriteTo(TextWriter writer, HtmlEncoder encoder) + { + encoder.Encode(writer, _unencoded); + } + } } } diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/ViewBufferTextWriter.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/ViewBufferTextWriter.cs index 43110e459d..f011296fbe 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/ViewBufferTextWriter.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/ViewBufferTextWriter.cs @@ -1,211 +1,437 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// 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.Buffers; -using System.Collections.Generic; -using System.Diagnostics; using System.IO; using System.Text; +using System.Text.Encodings.Web; using System.Threading.Tasks; +using Microsoft.AspNetCore.Html; +using Microsoft.AspNetCore.Mvc.Internal; namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal { + /// + /// + /// A that is backed by a unbuffered writer (over the Response stream) and/or a + /// + /// + /// + /// When Flush or FlushAsync is invoked, the writer copies all content from the buffer to + /// the writer and switches to writing to the unbuffered writer for all further write operations. + /// + /// public class ViewBufferTextWriter : TextWriter { - public const int PageSize = 1024; - private readonly TextWriter _inner; - private readonly List _pages; - private readonly ArrayPool _pool; + private readonly HtmlEncoder _htmlEncoder; - private int _currentPage; - private int _currentIndex; // The next 'free' character - - public ViewBufferTextWriter(ArrayPool pool, TextWriter inner) + /// + /// Creates a new instance of . + /// + /// The for buffered output. + /// The . + public ViewBufferTextWriter(ViewBuffer buffer, Encoding encoding) { - _pool = pool; + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + + if (encoding == null) + { + throw new ArgumentNullException(nameof(encoding)); + } + + Buffer = buffer; + Encoding = encoding; + } + + /// + /// Creates a new instance of . + /// + /// The for buffered output. + /// The . + /// The HTML encoder. + /// + /// The inner to write output to when this instance is no longer buffering. + /// + public ViewBufferTextWriter(ViewBuffer buffer, Encoding encoding, HtmlEncoder htmlEncoder, TextWriter inner) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + + if (encoding == null) + { + throw new ArgumentNullException(nameof(encoding)); + } + + if (htmlEncoder == null) + { + throw new ArgumentNullException(nameof(htmlEncoder)); + } + + if (inner == null) + { + throw new ArgumentNullException(nameof(inner)); + } + + Buffer = buffer; + Encoding = encoding; + _htmlEncoder = htmlEncoder; _inner = inner; - _pages = new List(); } - public override Encoding Encoding => _inner.Encoding; + /// + public override Encoding Encoding { get; } - public override void Flush() + /// + public bool IsBuffering { get; private set; } = true; + + /// + /// Gets the . + /// + public ViewBuffer Buffer { get; } + + /// + public override void Write(char value) { - // Don't do anything. We'll call FlushAsync. + if (IsBuffering) + { + Buffer.AppendHtml(value.ToString()); + } + else + { + _inner.Write(value); + } } - public override async Task FlushAsync() + /// + public override void Write(char[] buffer, int index, int count) { - if (_pages.Count == 0) + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + + if (index < 0 || index >= buffer.Length) + { + throw new ArgumentOutOfRangeException(nameof(index)); + } + + if (count < 0 || (buffer.Length - index < count)) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + + if (IsBuffering) + { + Buffer.AppendHtml(new string(buffer, index, count)); + } + else + { + _inner.Write(buffer, index, count); + } + } + + /// + public override void Write(string value) + { + if (string.IsNullOrEmpty(value)) { return; } - for (var i = 0; i <= _currentPage; i++) + if (IsBuffering) { - var page = _pages[i]; - - var count = i == _currentPage ? _currentIndex : page.Length; - if (count > 0) - { - await _inner.WriteAsync(page, 0, count); - } + Buffer.AppendHtml(value); } - - // Return all but one of the pages. This way if someone writes a large chunk of - // content, we can return those buffers and avoid holding them for the whole - // page's lifetime. - for (var i = _pages.Count - 1; i > 0; i--) + else { - var page = _pages[i]; - - try - { - _pages.RemoveAt(i); - } - finally - { - _pool.Return(page); - } - } - - _currentPage = 0; - _currentIndex = 0; - } - - public override void Write(char value) - { - var page = GetCurrentPage(); - page[_currentIndex++] = value; - } - - public override void Write(char[] buffer) - { - Write(buffer, 0, buffer.Length); - } - - public override void Write(char[] buffer, int index, int count) - { - while (count > 0) - { - var page = GetCurrentPage(); - var copyLength = Math.Min(count, page.Length - _currentIndex); - Debug.Assert(copyLength > 0); - - Array.Copy( - buffer, - index, - page, - _currentIndex, - copyLength); - - _currentIndex += copyLength; - index += copyLength; - - count -= copyLength; + _inner.Write(value); } } - public override void Write(string value) + /// + public override void Write(object value) { - var index = 0; - var count = value.Length; - - while (count > 0) + if (value == null) { - var page = GetCurrentPage(); - var copyLength = Math.Min(count, page.Length - _currentIndex); - Debug.Assert(copyLength > 0); + return; + } - value.CopyTo( - index, - page, - _currentIndex, - copyLength); - - _currentIndex += copyLength; - index += copyLength; - - count -= copyLength; + IHtmlContentContainer container; + IHtmlContent content; + if ((container = value as IHtmlContentContainer) != null) + { + Write(container); + } + else if ((content = value as IHtmlContent) != null) + { + Write(content); + } + else + { + Write(value.ToString()); } } + /// + /// Writes an value. + /// + /// The value. + public void Write(IHtmlContent value) + { + if (value == null) + { + return; + } + + if (IsBuffering) + { + Buffer.AppendHtml(value); + } + else + { + value.WriteTo(_inner, _htmlEncoder); + } + } + + /// + /// Writes an value. + /// + /// The value. + public void Write(IHtmlContentContainer value) + { + if (value == null) + { + return; + } + + if (IsBuffering) + { + value.MoveTo(Buffer); + } + else + { + value.WriteTo(_inner, _htmlEncoder); + } + } + + /// + public override void WriteLine(object value) + { + if (value == null) + { + return; + } + + IHtmlContentContainer container; + IHtmlContent content; + if ((container = value as IHtmlContentContainer) != null) + { + Write(container); + Write(NewLine); + } + else if ((content = value as IHtmlContent) != null) + { + Write(content); + Write(NewLine); + } + else + { + Write(value.ToString()); + Write(NewLine); + } + } + + /// public override Task WriteAsync(char value) { - return _inner.WriteAsync(value); + if (IsBuffering) + { + Buffer.AppendHtml(value.ToString()); + return TaskCache.CompletedTask; + } + else + { + return _inner.WriteAsync(value); + } } + /// public override Task WriteAsync(char[] buffer, int index, int count) { - return _inner.WriteAsync(buffer, index, count); + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + + if (index < 0) + { + throw new ArgumentOutOfRangeException(nameof(index)); + } + if (count < 0 || (buffer.Length - index < count)) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + + if (IsBuffering) + { + Buffer.AppendHtml(new string(buffer, index, count)); + return TaskCache.CompletedTask; + } + else + { + return _inner.WriteAsync(buffer, index, count); + } } + /// public override Task WriteAsync(string value) { - return _inner.WriteAsync(value); + if (IsBuffering) + { + Buffer.AppendHtml(value); + return TaskCache.CompletedTask; + } + else + { + return _inner.WriteAsync(value); + } } - private char[] GetCurrentPage() + /// + public override void WriteLine() { - char[] page = null; - if (_pages.Count == 0) + if (IsBuffering) { - Debug.Assert(_currentPage == 0); - Debug.Assert(_currentIndex == 0); - - try - { - page = _pool.Rent(PageSize); - _pages.Add(page); - } - catch when (page != null) - { - _pool.Return(page); - throw; - } - - return page; + Buffer.AppendHtml(NewLine); } - - Debug.Assert(_pages.Count > _currentPage); - page = _pages[_currentPage]; - - if (_currentIndex == page.Length) + else { - // Current page is full. - _currentPage++; - _currentIndex = 0; - - if (_pages.Count == _currentPage) - { - try - { - page = _pool.Rent(PageSize); - _pages.Add(page); - } - catch when (page != null) - { - _pool.Return(page); - throw; - } - } + _inner.WriteLine(); } - - return page; } - protected override void Dispose(bool disposing) + /// + public override void WriteLine(string value) { - base.Dispose(disposing); - - for (var i = 0; i < _pages.Count; i++) + if (IsBuffering) { - _pool.Return(_pages[i]); + Buffer.AppendHtml(value); + Buffer.AppendHtml(NewLine); + } + else + { + _inner.WriteLine(value); + } + } + + /// + public override Task WriteLineAsync(char value) + { + if (IsBuffering) + { + Buffer.AppendHtml(value.ToString()); + Buffer.AppendHtml(NewLine); + return TaskCache.CompletedTask; + } + else + { + return _inner.WriteLineAsync(value); + } + } + + /// + public override Task WriteLineAsync(char[] value, int start, int offset) + { + if (IsBuffering) + { + Buffer.AppendHtml(new string(value, start, offset)); + Buffer.AppendHtml(NewLine); + return TaskCache.CompletedTask; + } + else + { + return _inner.WriteLineAsync(value, start, offset); + } + } + + /// + public override Task WriteLineAsync(string value) + { + if (IsBuffering) + { + Buffer.AppendHtml(value); + Buffer.AppendHtml(NewLine); + return TaskCache.CompletedTask; + } + else + { + return _inner.WriteLineAsync(value); + } + } + + /// + public override Task WriteLineAsync() + { + if (IsBuffering) + { + Buffer.AppendHtml(NewLine); + return TaskCache.CompletedTask; + } + else + { + return _inner.WriteLineAsync(); + } + } + + /// + /// Copies the buffered content to the unbuffered writer and invokes flush on it. + /// Additionally causes this instance to no longer buffer and direct all write operations + /// to the unbuffered writer. + /// + public override void Flush() + { + if (_inner == null || _inner is ViewBufferTextWriter) + { + return; } - _pages.Clear(); + if (IsBuffering) + { + IsBuffering = false; + Buffer.WriteTo(_inner, _htmlEncoder); + Buffer.Clear(); + } + + _inner.Flush(); + } + + /// + /// Copies the buffered content to the unbuffered writer and invokes flush on it. + /// Additionally causes this instance to no longer buffer and direct all write operations + /// to the unbuffered writer. + /// + /// A that represents the asynchronous copy and flush operations. + public override async Task FlushAsync() + { + if (_inner == null || _inner is ViewBufferTextWriter) + { + return; + } + + if (IsBuffering) + { + IsBuffering = false; + await Buffer.WriteToAsync(_inner, _htmlEncoder); + Buffer.Clear(); + } + + await _inner.FlushAsync(); } } -} +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Rendering/MvcForm.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Rendering/MvcForm.cs index 9e6b2fbfd1..78837e1b23 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Rendering/MvcForm.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Rendering/MvcForm.cs @@ -3,9 +3,8 @@ using System; using System.Text.Encodings.Web; -using Microsoft.AspNetCore.Html; using Microsoft.AspNetCore.Mvc.ViewFeatures; -using Microsoft.Extensions.DependencyInjection; +using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal; namespace Microsoft.AspNetCore.Mvc.Rendering { @@ -15,20 +14,29 @@ namespace Microsoft.AspNetCore.Mvc.Rendering public class MvcForm : IDisposable { private readonly ViewContext _viewContext; + private readonly HtmlEncoder _htmlEncoder; + private bool _disposed; /// /// Initializes a new instance of . /// /// The . - public MvcForm(ViewContext viewContext) + /// The . + public MvcForm(ViewContext viewContext, HtmlEncoder htmlEncoder) { if (viewContext == null) { throw new ArgumentNullException(nameof(viewContext)); } + if (htmlEncoder == null) + { + throw new ArgumentNullException(nameof(htmlEncoder)); + } + _viewContext = viewContext; + _htmlEncoder = htmlEncoder; } /// @@ -63,27 +71,24 @@ namespace Microsoft.AspNetCore.Mvc.Rendering private void RenderEndOfFormContent() { var formContext = _viewContext.FormContext; - if (formContext.HasEndOfFormContent) + if (!formContext.HasEndOfFormContent) { - var writer = _viewContext.Writer; - var htmlWriter = writer as HtmlTextWriter; - - HtmlEncoder htmlEncoder = null; - if (htmlWriter == null) - { - htmlEncoder = _viewContext.HttpContext.RequestServices.GetRequiredService(); - } + return; + } + var viewBufferWriter = _viewContext.Writer as ViewBufferTextWriter; + if (viewBufferWriter == null) + { foreach (var content in formContext.EndOfFormContent) { - if (htmlWriter == null) - { - content.WriteTo(writer, htmlEncoder); - } - else - { - htmlWriter.Write(content); - } + content.WriteTo(_viewContext.Writer, _htmlEncoder); + } + } + else + { + foreach (var content in formContext.EndOfFormContent) + { + viewBufferWriter.Write(content); } } } diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Rendering/TagBuilder.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Rendering/TagBuilder.cs index dd7585721f..1af2ee2c6f 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Rendering/TagBuilder.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Rendering/TagBuilder.cs @@ -251,13 +251,14 @@ namespace Microsoft.AspNetCore.Mvc.Rendering /// public void WriteTo(TextWriter writer, HtmlEncoder encoder) { - var htmlTextWriter = writer as HtmlTextWriter; - if (htmlTextWriter != null) + if (writer == null) { - // As a perf optimization, we can buffer this output rather than writing it - // out character by character. - htmlTextWriter.Write(this); - return; + throw new ArgumentNullException(nameof(writer)); + } + + if (encoder == null) + { + throw new ArgumentNullException(nameof(encoder)); } switch (TagRenderMode) diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewComponents/DefaultViewComponentHelper.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewComponents/DefaultViewComponentHelper.cs index 720c0a533f..b5e6adfd84 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewComponents/DefaultViewComponentHelper.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewComponents/DefaultViewComponentHelper.cs @@ -134,8 +134,8 @@ namespace Microsoft.AspNetCore.Mvc.ViewComponents ViewComponentDescriptor descriptor, object arguments) { - var viewBuffer = new ViewBuffer(_viewBufferScope, descriptor.FullName); - using (var writer = new HtmlContentWrapperTextWriter(viewBuffer, _viewContext.Writer.Encoding)) + var viewBuffer = new ViewBuffer(_viewBufferScope, descriptor.FullName, ViewBuffer.ViewComponentPageSize); + using (var writer = new ViewBufferTextWriter(viewBuffer, _viewContext.Writer.Encoding)) { var context = new ViewComponentContext( descriptor, @@ -152,7 +152,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewComponents } await invoker.InvokeAsync(context); - return writer.ContentBuilder; + return viewBuffer; } } } diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewComponents/HtmlContentViewComponentResult.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewComponents/HtmlContentViewComponentResult.cs index 73bfa39fb4..e77fa66653 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewComponents/HtmlContentViewComponentResult.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewComponents/HtmlContentViewComponentResult.cs @@ -46,15 +46,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewComponents throw new ArgumentNullException(nameof(context)); } - var htmlWriter = context.Writer as HtmlTextWriter; - if (htmlWriter == null) - { - EncodedContent.WriteTo(context.Writer, context.HtmlEncoder); - } - else - { - htmlWriter.Write(EncodedContent); - } + context.Writer.Write(EncodedContent); } /// diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/AntiforgeryExtensions.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/AntiforgeryExtensions.cs index 611fed18d6..42470db504 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/AntiforgeryExtensions.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/AntiforgeryExtensions.cs @@ -57,14 +57,6 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures // _fieldName containing almost any character. public void WriteTo(TextWriter writer, HtmlEncoder encoder) { - var htmlTextWriter = writer as HtmlTextWriter; - if (htmlTextWriter != null) - { - // If possible, defer encoding until we're writing to the response. - htmlTextWriter.Write(this); - return; - } - writer.Write("A new instance. protected virtual MvcForm CreateForm() { - return new MvcForm(ViewContext); + return new MvcForm(ViewContext, _htmlEncoder); } protected virtual IHtmlContent GenerateCheckBox( diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/StringHtmlContent.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/StringHtmlContent.cs index de3c2d3022..38715b89fd 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/StringHtmlContent.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/StringHtmlContent.cs @@ -39,17 +39,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures throw new ArgumentNullException(nameof(encoder)); } - var htmlTextWriter = writer as HtmlTextWriter; - if (htmlTextWriter != null) - { - // As a perf optimization, we can buffer this output rather than writing it - // out character by character. - htmlTextWriter.Write(this); - } - else - { - encoder.Encode(writer, _input); - } + encoder.Encode(writer, _input); } private string DebuggerToString() diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/TemplateRenderer.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/TemplateRenderer.cs index ed9c904de5..0344b4ee97 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/TemplateRenderer.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/TemplateRenderer.cs @@ -126,8 +126,8 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal if (viewEngineResult.Success) { - var viewBuffer = new ViewBuffer(_bufferScope, viewName); - using (var writer = new HtmlContentWrapperTextWriter(viewBuffer, _viewContext.Writer.Encoding)) + var viewBuffer = new ViewBuffer(_bufferScope, viewName, ViewBuffer.PartialViewPageSize); + using (var writer = new ViewBufferTextWriter(viewBuffer, _viewContext.Writer.Encoding)) { // Forcing synchronous behavior so users don't have to await templates. var view = viewEngineResult.View; @@ -136,7 +136,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal var viewContext = new ViewContext(_viewContext, viewEngineResult.View, _viewData, writer); var renderTask = viewEngineResult.View.RenderAsync(viewContext); renderTask.GetAwaiter().GetResult(); - return writer.ContentBuilder; + return viewBuffer; } } } diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorPageTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorPageTest.cs index 3779180b14..a9712b2cba 100644 --- a/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorPageTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorPageTest.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; +using System.Text; using System.Text.Encodings.Web; using System.Threading.Tasks; using Microsoft.AspNetCore.Html; @@ -52,7 +53,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor // Act await page.ExecuteAsync(); - var pageOutput = page.Output.ToString(); + var pageOutput = page.RenderedContent; // Assert Assert.Equal( @@ -79,7 +80,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor // Act await page.ExecuteAsync(); - var pageOutput = page.Output.ToString(); + var pageOutput = page.RenderedContent; // Assert Assert.Equal("HtmlEncode[[Hello Prefix]]HtmlEncode[[From Scope: ]]HtmlEncode[[Hello In Scope]]", pageOutput); @@ -113,7 +114,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor await page.ExecuteAsync(); // Assert - var pageOutput = page.Output.ToString(); + var pageOutput = page.RenderedContent; Assert.Equal( "HtmlEncode[[Hello Prefix]]HtmlEncode[[From Scopes: ]]HtmlEncode[[Hello In Scope Pre Nest]]" + "HtmlEncode[[Hello In Scope Post Nest]]HtmlEncode[[Hello In Nested Scope]]", @@ -224,6 +225,138 @@ namespace Microsoft.AspNetCore.Mvc.Razor await page.ExecuteAsync(); } + // This is an integration test for ensuring that ViewBuffer segments used by + // TagHelpers can be merged back into the 'main' segment where possible. + [Fact] + public async Task TagHelperScopes_ViewBuffersCanCombine() + { + // Arrange + var bufferScope = new TestViewBufferScope(); + var viewContext = CreateViewContext(bufferScope: bufferScope); + + var page = CreatePage(async v => + { + Assert.Equal(0, bufferScope.CreatedBuffers.Count); + v.Write("Level:0"); // Creates a 'top-level' buffer. + Assert.Equal(1, bufferScope.CreatedBuffers.Count); + + // Run a TagHelper + { + v.StartTagHelperWritingScope(encoder: null); + + Assert.Equal(1, bufferScope.CreatedBuffers.Count); + Assert.Equal(0, bufferScope.ReturnedBuffers.Count); + v.Write("Level:1-A"); // Creates a new buffer for the taghelper. + Assert.Equal(2, bufferScope.CreatedBuffers.Count); + Assert.Equal(0, bufferScope.ReturnedBuffers.Count); + + TagHelperContent innerContentLevel1 = null; + var outputLevel1 = new TagHelperOutput("t1", new TagHelperAttributeList(), (_, encoder) => + { + return Task.FromResult(innerContentLevel1); + }); + + innerContentLevel1 = v.EndTagHelperWritingScope(); + outputLevel1.Content = await outputLevel1.GetChildContentAsync(); + + Assert.Equal(2, bufferScope.CreatedBuffers.Count); + Assert.Equal(0, bufferScope.ReturnedBuffers.Count); + v.Write(outputLevel1); // Writing the taghelper to output returns a buffer. + Assert.Equal(2, bufferScope.CreatedBuffers.Count); + Assert.Equal(1, bufferScope.ReturnedBuffers.Count); + } + + Assert.Equal(2, bufferScope.CreatedBuffers.Count); + Assert.Equal(1, bufferScope.ReturnedBuffers.Count); + v.Write("Level:0"); // Already have a buffer for this scope. + Assert.Equal(2, bufferScope.CreatedBuffers.Count); + Assert.Equal(1, bufferScope.ReturnedBuffers.Count); + + // Run another TagHelper + { + v.StartTagHelperWritingScope(encoder: null); + + Assert.Equal(2, bufferScope.CreatedBuffers.Count); + Assert.Equal(1, bufferScope.ReturnedBuffers.Count); + v.Write("Level:1-B"); // Creates a new buffer for the taghelper. + Assert.Equal(3, bufferScope.CreatedBuffers.Count); + Assert.Equal(1, bufferScope.ReturnedBuffers.Count); + + TagHelperContent innerContentLevel1 = null; + var outputLevel1 = new TagHelperOutput("t2", new TagHelperAttributeList(), (_, encoder) => + { + return Task.FromResult(innerContentLevel1); + }); + + // Run a nested TagHelper + { + v.StartTagHelperWritingScope(encoder: null); + + Assert.Equal(3, bufferScope.CreatedBuffers.Count); + Assert.Equal(1, bufferScope.ReturnedBuffers.Count); + v.Write("Level:2"); // Creates a new buffer for the taghelper. + Assert.Equal(4, bufferScope.CreatedBuffers.Count); + Assert.Equal(1, bufferScope.ReturnedBuffers.Count); + + TagHelperContent innerContentLevel2 = null; + var outputLevel2 = new TagHelperOutput("t3", new TagHelperAttributeList(), (_, encoder) => + { + return Task.FromResult(innerContentLevel2); + }); + + innerContentLevel2 = v.EndTagHelperWritingScope(); + outputLevel2.Content = await outputLevel2.GetChildContentAsync(); + + Assert.Equal(4, bufferScope.CreatedBuffers.Count); + Assert.Equal(1, bufferScope.ReturnedBuffers.Count); + v.Write(outputLevel2); // Writing the taghelper to output returns a buffer. + Assert.Equal(4, bufferScope.CreatedBuffers.Count); + Assert.Equal(2, bufferScope.ReturnedBuffers.Count); + } + + Assert.Equal(4, bufferScope.CreatedBuffers.Count); + Assert.Equal(2, bufferScope.ReturnedBuffers.Count); + v.Write("Level:1-B"); // Already have a buffer for this scope. + Assert.Equal(4, bufferScope.CreatedBuffers.Count); + Assert.Equal(2, bufferScope.ReturnedBuffers.Count); + + innerContentLevel1 = v.EndTagHelperWritingScope(); + outputLevel1.Content = await outputLevel1.GetChildContentAsync(); + + Assert.Equal(4, bufferScope.CreatedBuffers.Count); + Assert.Equal(2, bufferScope.ReturnedBuffers.Count); + v.Write(outputLevel1); // Writing the taghelper to output returns a buffer. + Assert.Equal(4, bufferScope.CreatedBuffers.Count); + Assert.Equal(3, bufferScope.ReturnedBuffers.Count); + } + + Assert.Equal(4, bufferScope.CreatedBuffers.Count); + Assert.Equal(3, bufferScope.ReturnedBuffers.Count); + v.Write("Level:0"); // Already have a buffer for this scope. + Assert.Equal(4, bufferScope.CreatedBuffers.Count); + Assert.Equal(3, bufferScope.ReturnedBuffers.Count); + + }, viewContext); + + // Act & Assert + await page.ExecuteAsync(); + Assert.Equal( + "HtmlEncode[[Level:0]]" + + "" + + "HtmlEncode[[Level:1-A]]" + + "" + + "HtmlEncode[[Level:0]]" + + "" + + "HtmlEncode[[Level:1-B]]" + + "" + + "HtmlEncode[[Level:2]]" + + "" + + "HtmlEncode[[Level:1-B]]" + + "" + + "HtmlEncode[[Level:0]]", + page.RenderedContent); + } + [Fact] public async Task DefineSection_ThrowsIfSectionIsAlreadyDefined() { @@ -1237,8 +1370,8 @@ namespace Microsoft.AspNetCore.Mvc.Razor public async Task Write_WithHtmlString_WritesValueWithoutEncoding() { // Arrange - var buffer = new ViewBuffer(new TestViewBufferScope(), string.Empty); - var writer = new RazorTextWriter(TextWriter.Null, buffer, new HtmlTestEncoder()); + var buffer = new ViewBuffer(new TestViewBufferScope(), string.Empty, pageSize: 32); + var writer = new ViewBufferTextWriter(buffer, Encoding.UTF8); var page = CreatePage(p => { @@ -1250,7 +1383,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor await page.ExecuteAsync(); // Assert - Assert.Equal("Hello world", HtmlContentUtilities.HtmlContentToString(writer.Buffer)); + Assert.Equal("Hello world", HtmlContentUtilities.HtmlContentToString(buffer)); } private static TestableRazorPage CreatePage( @@ -1284,12 +1417,18 @@ namespace Microsoft.AspNetCore.Mvc.Razor return view.Object; } - private static ViewContext CreateViewContext(TextWriter writer = null, string viewPath = null) + private static ViewContext CreateViewContext( + TextWriter writer = null, + IViewBufferScope bufferScope = null, + string viewPath = null) { - writer = writer ?? new StringWriter(); + bufferScope = bufferScope ?? new TestViewBufferScope(); + var buffer = new ViewBuffer(bufferScope, viewPath ?? "TEST", 32); + writer = writer ?? new ViewBufferTextWriter(buffer, Encoding.UTF8); + var httpContext = new DefaultHttpContext(); var serviceProvider = new ServiceCollection() - .AddSingleton() + .AddSingleton(bufferScope) .BuildServiceProvider(); httpContext.RequestServices = serviceProvider; var actionContext = new ActionContext( @@ -1321,8 +1460,12 @@ namespace Microsoft.AspNetCore.Mvc.Razor { get { - var writer = Assert.IsType(Output); - return writer.ToString(); + var bufferedWriter = Assert.IsType(Output); + using (var stringWriter = new StringWriter()) + { + bufferedWriter.Buffer.WriteTo(stringWriter, HtmlEncoder); + return stringWriter.ToString(); + } } } diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorViewTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorViewTest.cs index 08308b505d..5b7be35ddf 100644 --- a/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorViewTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorViewTest.cs @@ -54,7 +54,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor // Assert Assert.NotSame(expected, actual); - Assert.IsType(actual); + Assert.IsType(actual); Assert.Equal("HtmlEncode[[Hello world]]", viewContext.Writer.ToString()); } @@ -248,7 +248,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor await view.RenderAsync(viewContext); // Assert - Assert.IsType(actual); + Assert.IsType(actual); Assert.NotSame(original, actual); } diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Test/SpanFactory.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Test/SpanFactory.cs index f5179a7288..ad5487ce5c 100644 --- a/test/Microsoft.AspNetCore.Mvc.Razor.Test/SpanFactory.cs +++ b/test/Microsoft.AspNetCore.Mvc.Razor.Test/SpanFactory.cs @@ -8,7 +8,6 @@ using System.Linq; using Microsoft.AspNetCore.Razor; using Microsoft.AspNetCore.Razor.Chunks.Generators; using Microsoft.AspNetCore.Razor.Editor; -using Microsoft.AspNetCore.Razor.Generator; using Microsoft.AspNetCore.Razor.Parser.SyntaxTree; using Microsoft.AspNetCore.Razor.Text; using Microsoft.AspNetCore.Razor.Tokenizer; diff --git a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/HtmlContentWrapperTextWriterTest.cs b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/HtmlContentWrapperTextWriterTest.cs deleted file mode 100644 index 0ad44ff6cf..0000000000 --- a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/HtmlContentWrapperTextWriterTest.cs +++ /dev/null @@ -1,186 +0,0 @@ -// 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 System.Linq; -using System.Text; -using System.Text.Encodings.Web; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Html; -using Microsoft.AspNetCore.Mvc.Rendering; -using Xunit; - -namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal -{ - public class HtmlContentWrapperTextWriterTest - { - [Fact] - public async Task Write_WritesCharBuffer() - { - // Arrange - var input1 = new ArraySegment(new char[] { 'a', 'b', 'c', 'd' }, 1, 3); - var input2 = new ArraySegment(new char[] { 'e', 'f' }, 0, 2); - var input3 = new ArraySegment(new char[] { 'g', 'h', 'i', 'j' }, 3, 1); - var buffer = new TestHtmlContentBuilder(); - var writer = new HtmlContentWrapperTextWriter(buffer, Encoding.UTF8); - - // Act - writer.Write(input1.Array, input1.Offset, input1.Count); - await writer.WriteAsync(input2.Array, input2.Offset, input2.Count); - await writer.WriteLineAsync(input3.Array, input3.Offset, input3.Count); - - // Assert - Assert.Collection(buffer.Values, - value => Assert.Equal("bcd", value), - value => Assert.Equal("ef", value), - value => Assert.Equal("j", value), - value => Assert.Equal(Environment.NewLine, value)); - } - - [Fact] - public void Write_SplitsCharBuffer_Into1kbStrings() - { - // Arrange - var charArray = Enumerable.Range(0, 2050).Select(_ => 'a').ToArray(); - var buffer = new TestHtmlContentBuilder(); - var writer = new HtmlContentWrapperTextWriter(buffer, Encoding.UTF8); - - // Act - writer.Write(charArray); - - // Assert - Assert.Collection( - buffer.Values, - value => Assert.Equal(new string('a', 1024), value), - value => Assert.Equal(new string('a', 1024), value), - value => Assert.Equal("aa", value)); - } - - [Fact] - public void Write_HtmlContent_AddsToEntries() - { - // Arrange - var buffer = new TestHtmlContentBuilder(); - var writer = new HtmlContentWrapperTextWriter(buffer, Encoding.UTF8); - var content = new HtmlString("Hello, world!"); - - // Act - writer.Write(content); - - // Assert - Assert.Collection( - buffer.Values, - item => Assert.Same(content, item)); - } - - [Fact] - public void Write_Object_HtmlContent_AddsToEntries() - { - // Arrange - var buffer = new TestHtmlContentBuilder(); - var writer = new HtmlContentWrapperTextWriter(buffer, Encoding.UTF8); - var content = new HtmlString("Hello, world!"); - - // Act - writer.Write((object)content); - - // Assert - Assert.Collection( - buffer.Values, - item => Assert.Same(content, item)); - } - - [Fact] - public void WriteLine_Object_HtmlContent_AddsToEntries() - { - // Arrange - var buffer = new TestHtmlContentBuilder(); - var writer = new HtmlContentWrapperTextWriter(buffer, Encoding.UTF8); - var content = new HtmlString("Hello, world!"); - - // Act - writer.WriteLine(content); - - // Assert - Assert.Collection( - buffer.Values, - item => Assert.Same(content, item), - item => Assert.Equal(Environment.NewLine, item)); - } - - [Fact] - public async Task Write_WritesStringBuffer() - { - // Arrange - var newLine = Environment.NewLine; - var input1 = "Hello"; - var input2 = "from"; - var input3 = "ASP"; - var input4 = ".Net"; - var buffer = new TestHtmlContentBuilder(); - var writer = new HtmlContentWrapperTextWriter(buffer, Encoding.UTF8); - - // Act - writer.Write(input1); - writer.WriteLine(input2); - await writer.WriteAsync(input3); - await writer.WriteLineAsync(input4); - - // Assert - Assert.Equal(new[] { input1, input2, newLine, input3, input4, newLine }, buffer.Values); - } - - [Fact] - public void Write_HtmlContent_WritesToBuffer() - { - // Arrange - var buffer = new TestHtmlContentBuilder(); - var writer = new HtmlContentWrapperTextWriter(buffer, Encoding.UTF8); - var content = new HtmlString("Hello, world!"); - - // Act - writer.Write(content); - - // Assert - Assert.Collection( - buffer.Values, - item => Assert.Same(content, item)); - } - - private class TestHtmlContentBuilder : IHtmlContentBuilder - { - public List Values { get; } = new List(); - - public IHtmlContentBuilder Append(string unencoded) - { - Values.Add(unencoded); - return this; - } - - public IHtmlContentBuilder AppendHtml(IHtmlContent content) - { - Values.Add(content); - return this; - } - - public IHtmlContentBuilder AppendHtml(string encoded) - { - Values.Add(new HtmlString(encoded)); - return this; - } - - public IHtmlContentBuilder Clear() - { - Values.Clear(); - return this; - } - - public void WriteTo(TextWriter writer, HtmlEncoder encoder) - { - throw new NotSupportedException(); - } - } - } -} diff --git a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ViewBufferTextWriterTest.cs b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/PagedBufferedStringWriterTest.cs similarity index 90% rename from test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ViewBufferTextWriterTest.cs rename to test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/PagedBufferedStringWriterTest.cs index 0ff27fc1c1..f22f0d6ed1 100644 --- a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ViewBufferTextWriterTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/PagedBufferedStringWriterTest.cs @@ -9,13 +9,13 @@ using Xunit; namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal { - public class ViewBufferTextWriterTest + public class PagedBufferedStringWriterTest { private static readonly char[] Content; - static ViewBufferTextWriterTest() + static PagedBufferedStringWriterTest() { - Content = new char[4 * ViewBufferTextWriter.PageSize]; + Content = new char[4 * PagedBufferedTextWriter.PageSize]; for (var i = 0; i < Content.Length; i++) { Content[i] = (char)((i % 26) + 'A'); @@ -29,7 +29,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal var pool = new TestArrayPool(); var inner = new StringWriter(); - var writer = new ViewBufferTextWriter(pool, inner); + var writer = new PagedBufferedTextWriter(pool, inner); // Act for (var i = 0; i < Content.Length; i++) @@ -50,7 +50,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal var pool = new TestArrayPool(); var inner = new StringWriter(); - var writer = new ViewBufferTextWriter(pool, inner); + var writer = new PagedBufferedTextWriter(pool, inner); // These numbers chosen to hit boundary conditions in buffer lengths Assert.Equal(4096, Content.Length); // Update these numbers if this changes. @@ -89,7 +89,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal var pool = new TestArrayPool(); var inner = new StringWriter(); - var writer = new ViewBufferTextWriter(pool, inner); + var writer = new PagedBufferedTextWriter(pool, inner); // These numbers chosen to hit boundary conditions in buffer lengths Assert.Equal(4096, Content.Length); // Update these numbers if this changes. @@ -116,7 +116,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal var pool = new TestArrayPool(); var inner = new StringWriter(); - var writer = new ViewBufferTextWriter(pool, inner); + var writer = new PagedBufferedTextWriter(pool, inner); // These numbers chosen to hit boundary conditions in buffer lengths Assert.Equal(4096, Content.Length); // Update these numbers if this changes. @@ -144,7 +144,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal var pool = new TestArrayPool(); var inner = new StringWriter(); - var writer = new ViewBufferTextWriter(pool, inner); + var writer = new PagedBufferedTextWriter(pool, inner); for (var i = 0; i < Content.Length; i++) { diff --git a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/TestViewBufferScope.cs b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/TestViewBufferScope.cs index c9163e9d5a..4497a592d4 100644 --- a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/TestViewBufferScope.cs +++ b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/TestViewBufferScope.cs @@ -9,26 +9,25 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal { public class TestViewBufferScope : IViewBufferScope { - public const int DefaultBufferSize = 128; - private readonly int _bufferSize; - - public TestViewBufferScope(int bufferSize = DefaultBufferSize) - { - _bufferSize = bufferSize; - } + public IList CreatedBuffers { get; } = new List(); public IList ReturnedBuffers { get; } = new List(); - public ViewBufferValue[] GetSegment() => new ViewBufferValue[_bufferSize]; + public ViewBufferValue[] GetPage(int size) + { + var buffer = new ViewBufferValue[size]; + CreatedBuffers.Add(buffer); + return buffer; + } public void ReturnSegment(ViewBufferValue[] segment) { ReturnedBuffers.Add(segment); } - public ViewBufferTextWriter CreateWriter(TextWriter writer) + public PagedBufferedTextWriter CreateWriter(TextWriter writer) { - return new ViewBufferTextWriter(ArrayPool.Shared, writer); + return new PagedBufferedTextWriter(ArrayPool.Shared, writer); } } } diff --git a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/ViewBufferTest.cs b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/ViewBufferTest.cs index f46f385b29..ad329342ee 100644 --- a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/ViewBufferTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/ViewBufferTest.cs @@ -16,10 +16,10 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal public class ViewBufferTest { [Fact] - public void Append_AddsStringRazorValue() + public void Append_AddsEncodingWrapper() { // Arrange - var buffer = new ViewBuffer(new TestViewBufferScope(), "some-name"); + var buffer = new ViewBuffer(new TestViewBufferScope(), "some-name", pageSize: 32); // Act buffer.Append("Hello world"); @@ -27,14 +27,14 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal // Assert var page = Assert.Single(buffer.Pages); Assert.Equal(1, page.Count); - Assert.Equal("Hello world", page.Buffer[0].Value); + Assert.IsAssignableFrom(page.Buffer[0].Value); } [Fact] - public void Append_AddsHtmlContentRazorValue() + public void AppendHtml_AddsHtmlContentRazorValue() { // Arrange - var buffer = new ViewBuffer(new TestViewBufferScope(), "some-name"); + var buffer = new ViewBuffer(new TestViewBufferScope(), "some-name", pageSize: 32); var content = new HtmlString("hello-world"); // Act @@ -47,10 +47,10 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal } [Fact] - public void AppendHtml_AddsHtmlStringValues() + public void AppendHtml_AddsString() { // Arrange - var buffer = new ViewBuffer(new TestViewBufferScope(), "some-name"); + var buffer = new ViewBuffer(new TestViewBufferScope(), "some-name", pageSize: 32); var value = "Hello world"; // Act @@ -59,24 +59,23 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal // Assert var page = Assert.Single(buffer.Pages); Assert.Equal(1, page.Count); - var htmlString = Assert.IsType(page.Buffer[0].Value); - Assert.Equal("Hello world", htmlString.ToString()); + Assert.Equal("Hello world", Assert.IsType(page.Buffer[0].Value)); } [Fact] public void Append_CreatesNewPages_WhenCurrentPageIsFull() { // Arrange - var buffer = new ViewBuffer(new TestViewBufferScope(), "some-name"); - var expected = Enumerable.Range(0, TestViewBufferScope.DefaultBufferSize).Select(i => i.ToString()); + var buffer = new ViewBuffer(new TestViewBufferScope(), "some-name", pageSize: 32); + var expected = Enumerable.Range(0, 32).Select(i => i.ToString()); // Act foreach (var item in expected) { - buffer.Append(item); + buffer.AppendHtml(item); } - buffer.Append("Hello"); - buffer.Append("world"); + buffer.AppendHtml("Hello"); + buffer.AppendHtml("world"); // Assert Assert.Equal(2, buffer.Pages.Count); @@ -92,19 +91,19 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal [Theory] [InlineData(1)] - [InlineData(TestViewBufferScope.DefaultBufferSize + 3)] + [InlineData(35)] public void Clear_ResetsBackingBufferAndIndex(int valuesToWrite) { // Arrange - var buffer = new ViewBuffer(new TestViewBufferScope(), "some-name"); + var buffer = new ViewBuffer(new TestViewBufferScope(), "some-name", pageSize: 32); // Act for (var i = 0; i < valuesToWrite; i++) { - buffer.Append("Hello"); + buffer.AppendHtml("Hello"); } buffer.Clear(); - buffer.Append("world"); + buffer.AppendHtml("world"); // Assert var page = Assert.Single(buffer.Pages); @@ -112,27 +111,11 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal Assert.Equal("world", page.Buffer[0].Value); } - [Fact] - public void WriteTo_WritesSelf_WhenWriterIsHtmlTextWriter() - { - // Arrange - var buffer = new ViewBuffer(new TestViewBufferScope(), "some-name"); - var htmlWriter = new Mock(); - htmlWriter.Setup(w => w.Write(buffer)).Verifiable(); - - // Act - buffer.Append("Hello world"); - buffer.WriteTo(htmlWriter.Object, new HtmlTestEncoder()); - - // Assert - htmlWriter.Verify(); - } - [Fact] public void WriteTo_WritesRazorValues_ToTextWriter() { // Arrange - var buffer = new ViewBuffer(new TestViewBufferScope(), "some-name"); + var buffer = new ViewBuffer(new TestViewBufferScope(), "some-name", pageSize: 32); var writer = new StringWriter(); // Act @@ -142,7 +125,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal buffer.WriteTo(writer, new HtmlTestEncoder()); // Assert - Assert.Equal("Hello world 123", writer.ToString()); + Assert.Equal("HtmlEncode[[Hello]] world 123", writer.ToString()); } [Theory] @@ -153,7 +136,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal public void WriteTo_WritesRazorValuesFromAllBuffers(int valuesToWrite) { // Arrange - var buffer = new ViewBuffer(new TestViewBufferScope(4), "some-name"); + var buffer = new ViewBuffer(new TestViewBufferScope(), "some-name", pageSize: 4); var writer = new StringWriter(); var expected = string.Join("", Enumerable.Range(0, valuesToWrite).Select(_ => "abc")); @@ -168,27 +151,11 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal Assert.Equal(expected, writer.ToString()); } - [Fact] - public async Task WriteToAsync_WritesSelf_WhenWriterIsHtmlTextWriter() - { - // Arrange - var buffer = new ViewBuffer(new TestViewBufferScope(), "some-name"); - var htmlWriter = new Mock(); - htmlWriter.Setup(w => w.Write(buffer)).Verifiable(); - - // Act - buffer.Append("Hello world"); - await buffer.WriteToAsync(htmlWriter.Object, new HtmlTestEncoder()); - - // Assert - htmlWriter.Verify(); - } - [Fact] public async Task WriteToAsync_WritesRazorValues_ToTextWriter() { // Arrange - var buffer = new ViewBuffer(new TestViewBufferScope(), "some-name"); + var buffer = new ViewBuffer(new TestViewBufferScope(), "some-name", pageSize: 128); var writer = new StringWriter(); // Act @@ -199,7 +166,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal await buffer.WriteToAsync(writer, new HtmlTestEncoder()); // Assert - Assert.Equal("Hello world 123", writer.ToString()); + Assert.Equal("HtmlEncode[[Hello]] world 123", writer.ToString()); } [Theory] @@ -210,7 +177,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal public async Task WriteToAsync_WritesRazorValuesFromAllBuffers(int valuesToWrite) { // Arrange - var buffer = new ViewBuffer(new TestViewBufferScope(4), "some-name"); + var buffer = new ViewBuffer(new TestViewBufferScope(), "some-name", pageSize: 4); var writer = new StringWriter(); var expected = string.Join("", Enumerable.Range(0, valuesToWrite).Select(_ => "abc")); @@ -227,20 +194,66 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal } [Fact] - public void AppendHtml_ViewBuffer_TakesPage_IfOriginalIsEmpty() + public void CopyTo_Flattens() { // Arrange - var scope = new TestViewBufferScope(4); + var buffer = new ViewBuffer(new TestViewBufferScope(), "some-name", pageSize: 4); - var original = new ViewBuffer(scope, "original"); - var other = new ViewBuffer(scope, "other"); + var nestedItems = new List(); + var nested = new HtmlContentBuilder(nestedItems); + nested.AppendHtml("Hello"); + buffer.AppendHtml(nested); - other.Append("Hi"); + var destinationItems = new List(); + var destination = new HtmlContentBuilder(destinationItems); + + // Act + buffer.CopyTo(destination); + + // Assert + Assert.Same(nested, buffer.Pages[0].Buffer[0].Value); + Assert.Equal("Hello", Assert.IsType(nestedItems[0]).Value); + Assert.Equal("Hello", Assert.IsType(destinationItems[0]).Value); + } + + [Fact] + public void MoveTo_FlattensAndClears() + { + // Arrange + var buffer = new ViewBuffer(new TestViewBufferScope(), "some-name", pageSize: 4); + + var nestedItems = new List(); + var nested = new HtmlContentBuilder(nestedItems); + nested.AppendHtml("Hello"); + buffer.AppendHtml(nested); + + var destinationItems = new List(); + var destination = new HtmlContentBuilder(destinationItems); + + // Act + buffer.MoveTo(destination); + + // Assert + Assert.Empty(nestedItems); + Assert.Empty(buffer.Pages); + Assert.Equal("Hello", Assert.IsType(destinationItems[0]).Value); + } + + [Fact] + public void MoveTo_ViewBuffer_TakesPage_IfOriginalIsEmpty() + { + // Arrange + var scope = new TestViewBufferScope(); + + var original = new ViewBuffer(scope, "original", pageSize: 4); + var other = new ViewBuffer(scope, "other", pageSize: 4); + + other.AppendHtml("Hi"); var page = other.Pages[0]; // Act - original.AppendHtml(other); + other.MoveTo(original); // Assert Assert.Empty(other.Pages); // Page was taken @@ -248,25 +261,25 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal } [Fact] - public void AppendHtml_ViewBuffer_TakesPage_IfCurrentPageInOriginalIsFull() + public void MoveTo_ViewBuffer_TakesPage_IfCurrentPageInOriginalIsFull() { // Arrange - var scope = new TestViewBufferScope(4); + var scope = new TestViewBufferScope(); - var original = new ViewBuffer(scope, "original"); - var other = new ViewBuffer(scope, "other"); + var original = new ViewBuffer(scope, "original", pageSize: 4); + var other = new ViewBuffer(scope, "other", pageSize: 4); for (var i = 0; i < 4; i++) { - original.Append($"original-{i}"); + original.AppendHtml($"original-{i}"); } - other.Append("Hi"); + other.AppendHtml("Hi"); var page = other.Pages[0]; // Act - original.AppendHtml(other); + other.MoveTo(original); // Assert Assert.Empty(other.Pages); // Page was taken @@ -275,30 +288,30 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal } [Fact] - public void AppendHtml_ViewBuffer_TakesPage_IfCurrentPageDoesNotHaveCapacity() + public void MoveTo_ViewBuffer_TakesPage_IfCurrentPageDoesNotHaveCapacity() { // Arrange - var scope = new TestViewBufferScope(4); + var scope = new TestViewBufferScope(); - var original = new ViewBuffer(scope, "original"); - var other = new ViewBuffer(scope, "other"); + var original = new ViewBuffer(scope, "original", pageSize: 4); + var other = new ViewBuffer(scope, "other", pageSize: 4); for (var i = 0; i < 3; i++) { - original.Append($"original-{i}"); + original.AppendHtml($"original-{i}"); } // With two items, we'd try to copy the items, but there's no room in the current page. // So we just take over the page. for (var i = 0; i < 2; i++) { - other.Append($"other-{i}"); + other.AppendHtml($"other-{i}"); } var page = other.Pages[0]; // Act - original.AppendHtml(other); + other.MoveTo(original); // Assert Assert.Empty(other.Pages); // Page was taken @@ -307,29 +320,29 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal } [Fact] - public void AppendHtml_ViewBuffer_CopiesItems_IfCurrentPageHasRoom() + public void MoveTo_ViewBuffer_CopiesItems_IfCurrentPageHasRoom() { // Arrange - var scope = new TestViewBufferScope(4); + var scope = new TestViewBufferScope(); - var original = new ViewBuffer(scope, "original"); - var other = new ViewBuffer(scope, "other"); + var original = new ViewBuffer(scope, "original", pageSize: 4); + var other = new ViewBuffer(scope, "other", pageSize: 4); for (var i = 0; i < 2; i++) { - original.Append($"original-{i}"); + original.AppendHtml($"original-{i}"); } // With two items, this is half full so we try to copy the items. for (var i = 0; i < 2; i++) { - other.Append($"other-{i}"); + other.AppendHtml($"other-{i}"); } var page = other.Pages[0]; // Act - original.AppendHtml(other); + other.MoveTo(original); // Assert Assert.Empty(other.Pages); // Other is cleared @@ -344,30 +357,30 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal } [Fact] - public void AppendHtml_ViewBuffer_CanAddToTakenPage() + public void MoveTo_ViewBuffer_CanAddToTakenPage() { // Arrange - var scope = new TestViewBufferScope(4); + var scope = new TestViewBufferScope(); - var original = new ViewBuffer(scope, "original"); - var other = new ViewBuffer(scope, "other"); + var original = new ViewBuffer(scope, "original", pageSize: 4); + var other = new ViewBuffer(scope, "other", pageSize: 4); for (var i = 0; i < 3; i++) { - original.Append($"original-{i}"); + original.AppendHtml($"original-{i}"); } // More than half full, so we take the page for (var i = 0; i < 3; i++) { - other.Append($"other-{i}"); + other.AppendHtml($"other-{i}"); } var page = other.Pages[0]; - original.AppendHtml(other); + other.MoveTo(original); // Act - original.Append("after-merge"); + original.AppendHtml("after-merge"); // Assert Assert.Empty(other.Pages); // Other is cleared @@ -388,28 +401,28 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal } [Fact] - public void AppendHtml_ViewBuffer_MultiplePages() + public void MoveTo_ViewBuffer_MultiplePages() { // Arrange - var scope = new TestViewBufferScope(4); + var scope = new TestViewBufferScope(); - var original = new ViewBuffer(scope, "original"); - var other = new ViewBuffer(scope, "other"); + var original = new ViewBuffer(scope, "original", pageSize: 4); + var other = new ViewBuffer(scope, "other", pageSize: 4); for (var i = 0; i < 2; i++) { - original.Append($"original-{i}"); + original.AppendHtml($"original-{i}"); } for (var i = 0; i < 9; i++) { - other.Append($"other-{i}"); + other.AppendHtml($"other-{i}"); } var pages = new List(other.Pages); // Act - original.AppendHtml(other); + other.MoveTo(original); // Assert Assert.Empty(other.Pages); // Other is cleared diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorTextWriterTest.cs b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/ViewBufferTextWriterTest.cs similarity index 66% rename from test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorTextWriterTest.cs rename to test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/ViewBufferTextWriterTest.cs index d5b7e69537..ec9dc1ae72 100644 --- a/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorTextWriterTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/ViewBufferTextWriterTest.cs @@ -8,15 +8,14 @@ using System.Linq; using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc.Rendering; -using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal; using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.WebEncoders.Testing; using Moq; using Xunit; -namespace Microsoft.AspNetCore.Mvc.Razor.Test +namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal { - public class RazorTextWriterTest + public class ViewBufferTextWriterTest { [Fact] [ReplaceCulture] @@ -24,8 +23,8 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test { // Arrange var expected = new object[] { "True", "3", "18446744073709551615", "Hello world", "3.14", "2.718", "m" }; - var buffer = new ViewBuffer(new TestViewBufferScope(), "some-name"); - var writer = new RazorTextWriter(TextWriter.Null, buffer, new HtmlTestEncoder()); + var buffer = new ViewBuffer(new TestViewBufferScope(), "some-name", pageSize: 4); + var writer = new ViewBufferTextWriter(buffer, Encoding.UTF8); // Act writer.Write(true); @@ -46,10 +45,9 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test { // Arrange var expected = new[] { "True", "3", "18446744073709551615", "Hello world", "3.14", "2.718" }; - var unbufferedWriter = new Mock(); - unbufferedWriter.SetupGet(w => w.Encoding).Returns(Encoding.UTF8); - var buffer = new ViewBuffer(new TestViewBufferScope(), "some-name"); - var writer = new RazorTextWriter(unbufferedWriter.Object, buffer, new HtmlTestEncoder()); + var inner = new Mock(); + var buffer = new ViewBuffer(new TestViewBufferScope(), "some-name", pageSize: 4); + var writer = new ViewBufferTextWriter(buffer, Encoding.UTF8, new HtmlTestEncoder(), inner.Object); var testClass = new TestClass(); // Act @@ -65,7 +63,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test Assert.Empty(buffer.Pages); foreach (var item in expected) { - unbufferedWriter.Verify(v => v.Write(item), Times.Once()); + inner.Verify(v => v.Write(item), Times.Once()); } } @@ -74,10 +72,9 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test public async Task Write_WritesCharValues_ToUnderlyingStream_WhenNotBuffering() { // Arrange - var unbufferedWriter = new Mock { CallBase = true }; - unbufferedWriter.SetupGet(w => w.Encoding).Returns(Encoding.UTF8); - var buffer = new ViewBuffer(new TestViewBufferScope(), "some-name"); - var writer = new RazorTextWriter(unbufferedWriter.Object, buffer, new HtmlTestEncoder()); + var inner = new Mock { CallBase = true }; + var buffer = new ViewBuffer(new TestViewBufferScope(), "some-name", pageSize: 4); + var writer = new ViewBufferTextWriter(buffer, Encoding.UTF8, new HtmlTestEncoder(), inner.Object); var buffer1 = new[] { 'a', 'b', 'c', 'd' }; var buffer2 = new[] { 'd', 'e', 'f' }; @@ -90,12 +87,12 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test await writer.WriteLineAsync(buffer1); // Assert - unbufferedWriter.Verify(v => v.Write('x'), Times.Once()); - unbufferedWriter.Verify(v => v.Write(buffer1, 1, 2), Times.Once()); - unbufferedWriter.Verify(v => v.Write(buffer1, 0, 4), Times.Once()); - unbufferedWriter.Verify(v => v.Write(buffer2, 0, 3), Times.Once()); - unbufferedWriter.Verify(v => v.WriteAsync(buffer2, 1, 1), Times.Once()); - unbufferedWriter.Verify(v => v.WriteLine(), Times.Once()); + inner.Verify(v => v.Write('x'), Times.Once()); + inner.Verify(v => v.Write(buffer1, 1, 2), Times.Once()); + inner.Verify(v => v.Write(buffer1, 0, 4), Times.Once()); + inner.Verify(v => v.Write(buffer2, 0, 3), Times.Once()); + inner.Verify(v => v.WriteAsync(buffer2, 1, 1), Times.Once()); + inner.Verify(v => v.WriteLine(), Times.Once()); } [Fact] @@ -103,10 +100,9 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test public async Task Write_WritesStringValues_ToUnbufferedStream_WhenNotBuffering() { // Arrange - var unbufferedWriter = new Mock(); - unbufferedWriter.SetupGet(w => w.Encoding).Returns(Encoding.UTF8); - var buffer = new ViewBuffer(new TestViewBufferScope(), "some-name"); - var writer = new RazorTextWriter(unbufferedWriter.Object, buffer, new HtmlTestEncoder()); + var inner = new Mock(); + var buffer = new ViewBuffer(new TestViewBufferScope(), "some-name", pageSize: 4); + var writer = new ViewBufferTextWriter(buffer, Encoding.UTF8, new HtmlTestEncoder(), inner.Object); // Act await writer.FlushAsync(); @@ -116,10 +112,10 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test await writer.WriteLineAsync("gh"); // Assert - unbufferedWriter.Verify(v => v.Write("a"), Times.Once()); - unbufferedWriter.Verify(v => v.WriteLine("ab"), Times.Once()); - unbufferedWriter.Verify(v => v.WriteAsync("ef"), Times.Once()); - unbufferedWriter.Verify(v => v.WriteLineAsync("gh"), Times.Once()); + inner.Verify(v => v.Write("a"), Times.Once()); + inner.Verify(v => v.WriteLine("ab"), Times.Once()); + inner.Verify(v => v.WriteAsync("ef"), Times.Once()); + inner.Verify(v => v.WriteLineAsync("gh"), Times.Once()); } [Fact] @@ -129,8 +125,8 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test // Arrange var newLine = Environment.NewLine; var expected = new List { "False", newLine, "1.1", newLine, "3", newLine }; - var buffer = new ViewBuffer(new TestViewBufferScope(), "some-name"); - var writer = new RazorTextWriter(TextWriter.Null, buffer, new HtmlTestEncoder()); + var buffer = new ViewBuffer(new TestViewBufferScope(), "some-name", pageSize: 4); + var writer = new ViewBufferTextWriter(buffer, Encoding.UTF8); // Act writer.WriteLine(false); @@ -146,10 +142,9 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test public void WriteLine_WritesDataTypes_ToUnbufferedStream_WhenNotBuffering() { // Arrange - var unbufferedWriter = new Mock(); - unbufferedWriter.SetupGet(w => w.Encoding).Returns(Encoding.UTF8); - var buffer = new ViewBuffer(new TestViewBufferScope(), "some-name"); - var writer = new RazorTextWriter(unbufferedWriter.Object, buffer, new HtmlTestEncoder()); + var inner = new Mock(); + var buffer = new ViewBuffer(new TestViewBufferScope(), "some-name", pageSize: 4); + var writer = new ViewBufferTextWriter(buffer, Encoding.UTF8, new HtmlTestEncoder(), inner.Object); // Act writer.Flush(); @@ -158,10 +153,10 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test writer.WriteLine(3L); // Assert - unbufferedWriter.Verify(v => v.Write("False"), Times.Once()); - unbufferedWriter.Verify(v => v.Write("1.1"), Times.Once()); - unbufferedWriter.Verify(v => v.Write("3"), Times.Once()); - unbufferedWriter.Verify(v => v.WriteLine(), Times.Exactly(3)); + inner.Verify(v => v.Write("False"), Times.Once()); + inner.Verify(v => v.Write("1.1"), Times.Once()); + inner.Verify(v => v.Write("3"), Times.Once()); + inner.Verify(v => v.WriteLine(), Times.Exactly(3)); } [Fact] @@ -169,8 +164,8 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test { // Arrange var newLine = Environment.NewLine; - var buffer = new ViewBuffer(new TestViewBufferScope(), "some-name"); - var writer = new RazorTextWriter(TextWriter.Null, buffer, new HtmlTestEncoder()); + var buffer = new ViewBuffer(new TestViewBufferScope(), "some-name", pageSize: 4); + var writer = new ViewBufferTextWriter(buffer, Encoding.UTF8); // Act writer.WriteLine(); @@ -190,8 +185,8 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test var input2 = "from"; var input3 = "ASP"; var input4 = ".Net"; - var buffer = new ViewBuffer(new TestViewBufferScope(), "some-name"); - var writer = new RazorTextWriter(TextWriter.Null, buffer, new HtmlTestEncoder()); + var buffer = new ViewBuffer(new TestViewBufferScope(), "some-name", pageSize: 4); + var writer = new ViewBufferTextWriter(buffer, Encoding.UTF8); // Act writer.Write(input1); @@ -208,9 +203,10 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test public void Write_HtmlContent_AfterFlush_GoesToStream() { // Arrange - var stringWriter = new StringWriter(); - var buffer = new ViewBuffer(new TestViewBufferScope(), "some-name"); - var writer = new RazorTextWriter(stringWriter, buffer, new HtmlTestEncoder()); + var inner = new StringWriter(); + var buffer = new ViewBuffer(new TestViewBufferScope(), "some-name", pageSize: 4); + var writer = new ViewBufferTextWriter(buffer, Encoding.UTF8, new HtmlTestEncoder(), inner); + writer.Flush(); var content = new HtmlString("Hello, world!"); @@ -219,7 +215,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test writer.Write(content); // Assert - Assert.Equal("Hello, world!", stringWriter.ToString()); + Assert.Equal("Hello, world!", inner.ToString()); } private static object[] GetValues(ViewBuffer buffer) diff --git a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/ViewContextTests.cs b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/ViewContextTests.cs index f7466f7e09..78a20795b0 100644 --- a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/ViewContextTests.cs +++ b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/ViewContextTests.cs @@ -60,7 +60,7 @@ namespace Microsoft.AspNetCore.Mvc.Rendering htmlHelperOptions: new HtmlHelperOptions()); var view = Mock.Of(); var viewData = new ViewDataDictionary(originalContext.ViewData); - var writer = new HtmlContentWrapperTextWriter(new HtmlContentBuilder(), Encoding.UTF8); + var writer = new StringWriter(); // Act var context = new ViewContext(originalContext, view, viewData, writer);