diff --git a/src/Microsoft.AspNet.Mvc.Razor/Buffer/IRazorBufferScope.cs b/src/Microsoft.AspNet.Mvc.Razor/Buffer/IRazorBufferScope.cs
new file mode 100644
index 0000000000..794ebfb00e
--- /dev/null
+++ b/src/Microsoft.AspNet.Mvc.Razor/Buffer/IRazorBufferScope.cs
@@ -0,0 +1,17 @@
+// 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.
+
+namespace Microsoft.AspNet.Mvc.Razor.Buffer
+{
+ ///
+ /// Creates and manages the lifetime of instances.
+ ///
+ public interface IRazorBufferScope
+ {
+ ///
+ /// Gets a .
+ ///
+ /// The .
+ RazorBufferSegment GetSegment();
+ }
+}
diff --git a/src/Microsoft.AspNet.Mvc.Razor/Buffer/MemoryPoolRazorBufferScope.cs b/src/Microsoft.AspNet.Mvc.Razor/Buffer/MemoryPoolRazorBufferScope.cs
new file mode 100644
index 0000000000..1c8cbe3239
--- /dev/null
+++ b/src/Microsoft.AspNet.Mvc.Razor/Buffer/MemoryPoolRazorBufferScope.cs
@@ -0,0 +1,82 @@
+// 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 Microsoft.Extensions.MemoryPool;
+
+namespace Microsoft.AspNet.Mvc.Razor.Buffer
+{
+ ///
+ /// A that uses pooled memory.
+ ///
+ public class MemoryPoolRazorBufferScope : IRazorBufferScope, IDisposable
+ {
+ private const int SegmentSize = 1024;
+ private readonly IArraySegmentPool _pool;
+ private List> _leased;
+ private bool _disposed;
+
+ ///
+ /// Initializes a new instance of .
+ ///
+ /// The for creating
+ /// instances.
+ public MemoryPoolRazorBufferScope(IArraySegmentPool pool)
+ {
+ _pool = pool;
+ }
+
+ ///
+ public RazorBufferSegment GetSegment()
+ {
+ if (_disposed)
+ {
+ throw new ObjectDisposedException(typeof(MemoryPoolRazorBufferScope).FullName);
+ }
+
+ if (_leased == null)
+ {
+ _leased = new List>(1);
+ }
+
+ LeasedArraySegment segment = null;
+
+ try
+ {
+ segment = _pool.Lease(SegmentSize);
+ _leased.Add(segment);
+ }
+ catch when (segment != null)
+ {
+ segment.Owner.Return(segment);
+ throw;
+ }
+
+ return new RazorBufferSegment(segment.Data);
+ }
+
+ ///
+ public void Dispose()
+ {
+ if (!_disposed)
+ {
+ _disposed = true;
+
+ if (_leased == null)
+ {
+ return;
+ }
+
+ for (var i = 0; i < _leased.Count; i++)
+ {
+ var segment = _leased[i];
+ Array.Clear(segment.Data.Array, segment.Data.Offset, segment.Data.Count);
+ segment.Owner.Return(segment);
+ }
+
+ _leased.Clear();
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.AspNet.Mvc.Razor/Buffer/RazorBuffer.cs b/src/Microsoft.AspNet.Mvc.Razor/Buffer/RazorBuffer.cs
new file mode 100644
index 0000000000..906479aeef
--- /dev/null
+++ b/src/Microsoft.AspNet.Mvc.Razor/Buffer/RazorBuffer.cs
@@ -0,0 +1,152 @@
+// 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.Diagnostics;
+using System.IO;
+using System.Text.Encodings.Web;
+using Microsoft.AspNet.Html;
+using Microsoft.AspNet.Mvc.Rendering;
+
+namespace Microsoft.AspNet.Mvc.Razor.Buffer
+{
+ ///
+ /// An that is backed by a buffer provided by .
+ ///
+ [DebuggerDisplay("{DebuggerToString()}")]
+ public class RazorBuffer : IHtmlContentBuilder
+ {
+ private readonly IRazorBufferScope _bufferScope;
+ private readonly string _name;
+
+ ///
+ /// Initializes a new instance of .
+ ///
+ /// The .
+ /// A name to identify this instance.
+ public RazorBuffer(IRazorBufferScope bufferScope, string name)
+ {
+ if (bufferScope == null)
+ {
+ throw new ArgumentNullException(nameof(bufferScope));
+ }
+
+ _bufferScope = bufferScope;
+ _name = name;
+ }
+
+ ///
+ /// Gets the backing buffer.
+ ///
+ public IList BufferSegments { get; private set; }
+
+ ///
+ /// Gets the count of entries in the last element of .
+ ///
+ public int CurrentCount { get; private set; }
+
+ ///
+ public IHtmlContentBuilder Append(string unencoded)
+ {
+ if (unencoded == null)
+ {
+ return this;
+ }
+
+ AppendValue(new RazorValue(unencoded));
+ return this;
+ }
+
+ ///
+ public IHtmlContentBuilder Append(IHtmlContent content)
+ {
+ if (content == null)
+ {
+ return this;
+ }
+
+ AppendValue(new RazorValue(content));
+ return this;
+ }
+
+ ///
+ public IHtmlContentBuilder AppendHtml(string encoded)
+ {
+ if (encoded == null)
+ {
+ return this;
+ }
+
+ var value = new HtmlString(encoded);
+ AppendValue(new RazorValue(value));
+ return this;
+ }
+
+ private void AppendValue(RazorValue value)
+ {
+ RazorBufferSegment segment;
+ if (BufferSegments == null)
+ {
+ BufferSegments = new List(1);
+ segment = _bufferScope.GetSegment();
+ BufferSegments.Add(segment);
+ }
+ else
+ {
+ segment = BufferSegments[BufferSegments.Count - 1];
+ if (CurrentCount == segment.Data.Count)
+ {
+ segment = _bufferScope.GetSegment();
+ BufferSegments.Add(segment);
+ CurrentCount = 0;
+ }
+ }
+
+ segment.Data.Array[segment.Data.Offset + CurrentCount] = value;
+ CurrentCount++;
+ }
+
+ ///
+ public IHtmlContentBuilder Clear()
+ {
+ if (BufferSegments != null)
+ {
+ CurrentCount = 0;
+ BufferSegments = null;
+ }
+
+ return this;
+ }
+
+ ///
+ public void WriteTo(TextWriter writer, HtmlEncoder encoder)
+ {
+ if (BufferSegments == null)
+ {
+ return;
+ }
+
+ var htmlTextWriter = writer as HtmlTextWriter;
+ if (htmlTextWriter != null)
+ {
+ htmlTextWriter.Write(this);
+ return;
+ }
+
+ for (var i = 0; i < BufferSegments.Count; i++)
+ {
+ var segment = BufferSegments[i];
+ var count = i == BufferSegments.Count - 1 ? CurrentCount : segment.Data.Count;
+
+ for (var j = 0; j < count; j++)
+ {
+ var value = segment.Data.Array[segment.Data.Offset + j];
+ value.WriteTo(writer, encoder);
+ }
+ }
+ }
+
+ private string DebuggerToString() => _name;
+ }
+}
diff --git a/src/Microsoft.AspNet.Mvc.Razor/Buffer/RazorBufferSegment.cs b/src/Microsoft.AspNet.Mvc.Razor/Buffer/RazorBufferSegment.cs
new file mode 100644
index 0000000000..2a61592124
--- /dev/null
+++ b/src/Microsoft.AspNet.Mvc.Razor/Buffer/RazorBufferSegment.cs
@@ -0,0 +1,27 @@
+// 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;
+
+namespace Microsoft.AspNet.Mvc.Razor.Buffer
+{
+ ///
+ /// Encapsulates a .
+ ///
+ public struct RazorBufferSegment
+ {
+ ///
+ /// Initializes a new instance of .
+ ///
+ /// The to encapsulate.
+ public RazorBufferSegment(ArraySegment data)
+ {
+ Data = data;
+ }
+
+ ///
+ /// Gets the .
+ ///
+ public ArraySegment Data { get; }
+ }
+}
diff --git a/src/Microsoft.AspNet.Mvc.Razor/Buffer/RazorValue.cs b/src/Microsoft.AspNet.Mvc.Razor/Buffer/RazorValue.cs
new file mode 100644
index 0000000000..0807575985
--- /dev/null
+++ b/src/Microsoft.AspNet.Mvc.Razor/Buffer/RazorValue.cs
@@ -0,0 +1,65 @@
+// 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.Diagnostics;
+using System.IO;
+using System.Text.Encodings.Web;
+using Microsoft.AspNet.Html;
+
+namespace Microsoft.AspNet.Mvc.Razor.Buffer
+{
+ ///
+ /// Encapsulates a string or value.
+ ///
+ public struct RazorValue
+ {
+ ///
+ /// Initializes a new instance of with a string value.
+ ///
+ /// The value.
+ public RazorValue(string value)
+ {
+ Value = value;
+ }
+
+ ///
+ /// Initializes a new instance of with a value.
+ ///
+ /// The .
+ public RazorValue(IHtmlContent content)
+ {
+ Value = content;
+ }
+
+ ///
+ /// Gets the value.
+ ///
+ public object Value { get; }
+
+ ///
+ /// Writes the by encoding it with the specified to the
+ /// specified .
+ ///
+ /// The to write the value to.
+ /// The which encodes the content to be written.
+ public void WriteTo(TextWriter writer, HtmlEncoder encoder)
+ {
+ if (Value == null)
+ {
+ return;
+ }
+
+ var stringValue = Value as string;
+ if (stringValue != null)
+ {
+ writer.Write(stringValue);
+ }
+ else
+ {
+ Debug.Assert(Value is IHtmlContent);
+ var htmlContentValue = (IHtmlContent)Value;
+ htmlContentValue.WriteTo(writer, encoder);
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.AspNet.Mvc.Razor/DependencyInjection/MvcRazorMvcCoreBuilderExtensions.cs b/src/Microsoft.AspNet.Mvc.Razor/DependencyInjection/MvcRazorMvcCoreBuilderExtensions.cs
index 897a6d9e65..fbefa3faa5 100644
--- a/src/Microsoft.AspNet.Mvc.Razor/DependencyInjection/MvcRazorMvcCoreBuilderExtensions.cs
+++ b/src/Microsoft.AspNet.Mvc.Razor/DependencyInjection/MvcRazorMvcCoreBuilderExtensions.cs
@@ -6,6 +6,7 @@ using System.Linq;
using System.Reflection;
using Microsoft.AspNet.Mvc;
using Microsoft.AspNet.Mvc.Razor;
+using Microsoft.AspNet.Mvc.Razor.Buffer;
using Microsoft.AspNet.Mvc.Razor.Compilation;
using Microsoft.AspNet.Mvc.Razor.Directives;
using Microsoft.AspNet.Mvc.Razor.Internal;
@@ -14,6 +15,7 @@ using Microsoft.AspNet.Razor.TagHelpers;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.CompilationAbstractions;
using Microsoft.Extensions.DependencyInjection.Extensions;
+using Microsoft.Extensions.MemoryPool;
using Microsoft.Extensions.OptionsModel;
namespace Microsoft.Extensions.DependencyInjection
@@ -157,6 +159,9 @@ namespace Microsoft.Extensions.DependencyInjection
// Consumed by the Cache tag helper to cache results across the lifetime of the application.
services.TryAddSingleton();
+
+ services.TryAddSingleton, DefaultArraySegmentPool>();
+ services.TryAddScoped();
}
}
}
diff --git a/src/Microsoft.AspNet.Mvc.Razor/HtmlContentWrapperTextWriter.cs b/src/Microsoft.AspNet.Mvc.Razor/HtmlContentWrapperTextWriter.cs
new file mode 100644
index 0000000000..6a53c013a1
--- /dev/null
+++ b/src/Microsoft.AspNet.Mvc.Razor/HtmlContentWrapperTextWriter.cs
@@ -0,0 +1,176 @@
+// 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.AspNet.Html;
+using Microsoft.AspNet.Mvc.Internal;
+
+namespace Microsoft.AspNet.Mvc.Razor
+{
+ ///
+ /// 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.Append(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.AspNet.Mvc.Razor/RazorTextWriter.cs b/src/Microsoft.AspNet.Mvc.Razor/RazorTextWriter.cs
index 40b98c729a..bbb40f4011 100644
--- a/src/Microsoft.AspNet.Mvc.Razor/RazorTextWriter.cs
+++ b/src/Microsoft.AspNet.Mvc.Razor/RazorTextWriter.cs
@@ -7,20 +7,15 @@ using System.Text;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Microsoft.AspNet.Html;
-using Microsoft.AspNet.Mvc.ViewFeatures;
namespace Microsoft.AspNet.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
+ /// . 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.
///
- ///
- /// This type is designed to avoid creating large in-memory strings when buffering and supporting the contract that
- /// expects.
- ///
public class RazorTextWriter : HtmlTextWriter
{
///
@@ -28,14 +23,14 @@ namespace Microsoft.AspNet.Mvc.Razor
///
/// The to write output to when this instance
/// is no longer buffering.
- /// The character in which the output is written.
+ /// The to buffer output to.
/// The HTML encoder.
- public RazorTextWriter(TextWriter unbufferedWriter, Encoding encoding, HtmlEncoder encoder)
+ public RazorTextWriter(TextWriter unbufferedWriter, IHtmlContentBuilder buffer, HtmlEncoder encoder)
{
UnbufferedWriter = unbufferedWriter;
HtmlEncoder = encoder;
- BufferedWriter = new StringCollectionTextWriter(encoding);
+ BufferedWriter = new HtmlContentWrapperTextWriter(buffer, unbufferedWriter.Encoding);
TargetWriter = BufferedWriter;
}
@@ -51,10 +46,10 @@ namespace Microsoft.AspNet.Mvc.Razor
///
/// Gets the buffered content.
///
- public IHtmlContent Buffer => BufferedWriter.Content;
+ public IHtmlContent Buffer => BufferedWriter.ContentBuilder;
// Internal for unit testing
- internal StringCollectionTextWriter BufferedWriter { get; }
+ internal HtmlContentWrapperTextWriter BufferedWriter { get; }
private TextWriter UnbufferedWriter { get; }
@@ -80,7 +75,8 @@ namespace Microsoft.AspNet.Mvc.Razor
{
throw new ArgumentOutOfRangeException(nameof(index));
}
- if (count < 0 || (buffer.Length - index < count))
+
+ if (count < 0 || (index + count > buffer.Length))
{
throw new ArgumentOutOfRangeException(nameof(count));
}
diff --git a/src/Microsoft.AspNet.Mvc.Razor/RazorView.cs b/src/Microsoft.AspNet.Mvc.Razor/RazorView.cs
index 6975a45b83..715977de63 100644
--- a/src/Microsoft.AspNet.Mvc.Razor/RazorView.cs
+++ b/src/Microsoft.AspNet.Mvc.Razor/RazorView.cs
@@ -3,11 +3,14 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics;
using System.Linq;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
+using Microsoft.AspNet.Mvc.Razor.Buffer;
using Microsoft.AspNet.Mvc.Rendering;
using Microsoft.AspNet.Mvc.ViewEngines;
+using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.AspNet.Mvc.Razor
{
@@ -20,6 +23,7 @@ namespace Microsoft.AspNet.Mvc.Razor
private readonly IRazorViewEngine _viewEngine;
private readonly IRazorPageActivator _pageActivator;
private readonly HtmlEncoder _htmlEncoder;
+ private IRazorBufferScope _bufferScope;
///
/// Initializes a new instance of
@@ -93,6 +97,7 @@ namespace Microsoft.AspNet.Mvc.Razor
throw new ArgumentNullException(nameof(context));
}
+ _bufferScope = context.HttpContext.RequestServices.GetRequiredService();
var bodyWriter = await RenderPageAsync(RazorPage, context, ViewStartPages);
await RenderLayoutAsync(context, bodyWriter);
}
@@ -102,7 +107,9 @@ namespace Microsoft.AspNet.Mvc.Razor
ViewContext context,
IReadOnlyList viewStartPages)
{
- var razorTextWriter = new RazorTextWriter(context.Writer, context.Writer.Encoding, _htmlEncoder);
+ Debug.Assert(_bufferScope != null);
+ var buffer = new RazorBuffer(_bufferScope, page.Path);
+ var razorTextWriter = new RazorTextWriter(context.Writer, buffer, _htmlEncoder);
// The writer for the body is passed through the ViewContext, allowing things like HtmlHelpers
// and ViewComponents to reference it.
diff --git a/src/Microsoft.AspNet.Mvc.TagHelpers/CacheTagHelper.cs b/src/Microsoft.AspNet.Mvc.TagHelpers/CacheTagHelper.cs
index c692ed32bf..f13a082afc 100644
--- a/src/Microsoft.AspNet.Mvc.TagHelpers/CacheTagHelper.cs
+++ b/src/Microsoft.AspNet.Mvc.TagHelpers/CacheTagHelper.cs
@@ -3,9 +3,12 @@
using System;
using System.Collections.Generic;
+using System.IO;
using System.Security.Cryptography;
using System.Text;
+using System.Text.Encodings.Web;
using System.Threading.Tasks;
+using Microsoft.AspNet.Html;
using Microsoft.AspNet.Mvc.Rendering;
using Microsoft.AspNet.Mvc.ViewFeatures;
using Microsoft.AspNet.Razor.TagHelpers;
@@ -41,9 +44,10 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
/// Creates a new .
///
/// The .
- public CacheTagHelper(IMemoryCache memoryCache)
+ public CacheTagHelper(IMemoryCache memoryCache, HtmlEncoder htmlEncoder)
{
MemoryCache = memoryCache;
+ HtmlEncoder = htmlEncoder;
}
///
@@ -60,6 +64,11 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
///
protected IMemoryCache MemoryCache { get; }
+ ///
+ /// Gets the which encodes the content to be cached.
+ ///
+ protected HtmlEncoder HtmlEncoder { get; }
+
///
/// Gets or sets the for the current executing View.
///
@@ -147,7 +156,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
throw new ArgumentNullException(nameof(output));
}
- TagHelperContent result = null;
+ IHtmlContent result = null;
if (Enabled)
{
var key = GenerateKey(context);
@@ -157,8 +166,15 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
// created within this scope get copied to this scope.
using (var link = MemoryCache.CreateLinkingScope())
{
- result = await output.GetChildContentAsync();
+ var content = await output.GetChildContentAsync();
+ var stringBuilder = new StringBuilder();
+ using (var writer = new StringWriter(stringBuilder))
+ {
+ content.WriteTo(writer, HtmlEncoder);
+ }
+
+ result = new StringBuilderHtmlContent(stringBuilder);
MemoryCache.Set(key, result, GetMemoryCacheEntryOptions(link));
}
}
@@ -371,5 +387,30 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
return trimmedValues;
}
+
+ private class StringBuilderHtmlContent : IHtmlContent
+ {
+ private readonly StringBuilder _builder;
+
+ public StringBuilderHtmlContent(StringBuilder builder)
+ {
+ _builder = builder;
+ }
+
+ 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]);
+ }
+ }
+ }
}
}
\ No newline at end of file
diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/Buffer/RazorBufferTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/Buffer/RazorBufferTest.cs
new file mode 100644
index 0000000000..bb5e0a13bf
--- /dev/null
+++ b/test/Microsoft.AspNet.Mvc.Razor.Test/Buffer/RazorBufferTest.cs
@@ -0,0 +1,198 @@
+// 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.IO;
+using System.Linq;
+using Microsoft.AspNet.Html;
+using Microsoft.AspNet.Mvc.Rendering;
+using Microsoft.Extensions.WebEncoders.Testing;
+using Moq;
+using Xunit;
+
+namespace Microsoft.AspNet.Mvc.Razor.Buffer
+{
+ public class RazorBufferTest
+ {
+ [Fact]
+ public void Append_AddsStringRazorValue()
+ {
+ // Arrange
+ var buffer = new RazorBuffer(new TestRazorBufferScope(), "some-name");
+
+ // Act
+ buffer.Append("Hello world");
+
+ // Assert
+ var segment = Assert.Single(buffer.BufferSegments);
+ Assert.Equal(1, buffer.CurrentCount);
+ Assert.Equal("Hello world", segment.Data.Array[0].Value);
+ }
+
+ [Fact]
+ public void Append_AddsHtmlContentRazorValue()
+ {
+ // Arrange
+ var buffer = new RazorBuffer(new TestRazorBufferScope(), "some-name");
+ var content = new HtmlString("hello-world");
+
+ // Act
+ buffer.Append(content);
+
+ // Assert
+ var segment = Assert.Single(buffer.BufferSegments);
+ Assert.Equal(1, buffer.CurrentCount);
+ Assert.Same(content, segment.Data.Array[0].Value);
+ }
+
+ [Fact]
+ public void AppendHtml_AddsHtmlStringValues()
+ {
+ // Arrange
+ var buffer = new RazorBuffer(new TestRazorBufferScope(), "some-name");
+ var value = "Hello world";
+
+ // Act
+ buffer.AppendHtml(value);
+
+ // Assert
+ var segment = Assert.Single(buffer.BufferSegments);
+ Assert.Equal(1, buffer.CurrentCount);
+ var htmlString = Assert.IsType(segment.Data.Array[0].Value);
+ Assert.Equal("Hello world", htmlString.ToString());
+ }
+
+ [Fact]
+ public void Append_CreatesNewSegments_WhenCurrentSegmentIsFull()
+ {
+ // Arrange
+ var buffer = new RazorBuffer(new TestRazorBufferScope(), "some-name");
+ var expected = Enumerable.Range(0, TestRazorBufferScope.BufferSize).Select(i => i.ToString());
+
+ // Act
+ foreach (var item in expected)
+ {
+ buffer.Append(item);
+ }
+ buffer.Append("Hello");
+ buffer.Append("world");
+
+ // Assert
+ Assert.Equal(2, buffer.CurrentCount);
+ Assert.Collection(buffer.BufferSegments,
+ segment => Assert.Equal(expected, segment.Data.Array.Select(v => v.Value)),
+ segment =>
+ {
+ var array = segment.Data.Array;
+ Assert.Equal("Hello", array[0].Value);
+ Assert.Equal("world", array[1].Value);
+ });
+ }
+
+ [Fact]
+ public void Append_CreatesNewSegments_WhenCurrentSegmentIsFull_ForBuffersWithNonZeroOffsets()
+ {
+ // Arrange
+ var buffer = new RazorBuffer(new TestRazorBufferScope(3, 2), "some-name");
+
+ // Act
+ buffer.Append("1");
+ buffer.Append("2");
+ buffer.Append("3");
+ buffer.Append("4");
+
+ // Assert
+ Assert.Equal(2, buffer.CurrentCount);
+ Assert.Collection(buffer.BufferSegments,
+ segment =>
+ {
+ var array = segment.Data.Array;
+ Assert.Equal("1", array[3].Value);
+ Assert.Equal("2", array[4].Value);
+ },
+ segment =>
+ {
+ var array = segment.Data.Array;
+ Assert.Equal("3", array[3].Value);
+ Assert.Equal("4", array[4].Value);
+ });
+ }
+
+ [Theory]
+ [InlineData(1)]
+ [InlineData(TestRazorBufferScope.BufferSize + 3)]
+ public void Clear_ResetsBackingBufferAndIndex(int valuesToWrite)
+ {
+ // Arrange
+ var buffer = new RazorBuffer(new TestRazorBufferScope(), "some-name");
+
+ // Act
+ for (var i = 0; i < valuesToWrite; i++)
+ {
+ buffer.Append("Hello");
+ }
+ buffer.Clear();
+ buffer.Append("world");
+
+ // Assert
+ var segment = Assert.Single(buffer.BufferSegments);
+ Assert.Equal(1, buffer.CurrentCount);
+ Assert.Equal("world", segment.Data.Array[0].Value);
+ }
+
+ [Fact]
+ public void WriteTo_WritesSelf_WhenWriterIsHtmlTextWriter()
+ {
+ // Arrange
+ var buffer = new RazorBuffer(new TestRazorBufferScope(), "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 RazorBuffer(new TestRazorBufferScope(), "some-name");
+ var writer = new StringWriter();
+
+ // Act
+ buffer.Append("Hello");
+ buffer.Append(new HtmlString(" world"));
+ buffer.AppendHtml(" 123");
+ buffer.WriteTo(writer, new HtmlTestEncoder());
+
+ // Assert
+ Assert.Equal("Hello world 123", writer.ToString());
+ }
+
+ [Theory]
+ [InlineData(9)]
+ [InlineData(10)]
+ [InlineData(11)]
+ [InlineData(23)]
+ public void WriteTo_WritesRazorValuesFromAllBuffers(int valuesToWrite)
+ {
+ // Arrange
+ var buffer = new RazorBuffer(new TestRazorBufferScope(1, 5), "some-name");
+ var writer = new StringWriter();
+ var expected = string.Join("", Enumerable.Range(0, valuesToWrite).Select(_ => "abc"));
+
+ // Act
+ for (var i = 0; i < valuesToWrite; i++)
+ {
+ buffer.AppendHtml("abc");
+ }
+ buffer.WriteTo(writer, new HtmlTestEncoder());
+
+ // Assert
+ Assert.Equal(expected, writer.ToString());
+ }
+ }
+}
diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/HtmlContentWrapperTextWriterTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/HtmlContentWrapperTextWriterTest.cs
new file mode 100644
index 0000000000..78bfc7746e
--- /dev/null
+++ b/test/Microsoft.AspNet.Mvc.Razor.Test/HtmlContentWrapperTextWriterTest.cs
@@ -0,0 +1,159 @@
+// 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.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.AspNet.Mvc.Razor.Buffer;
+using Microsoft.AspNet.Mvc.Rendering;
+using Xunit;
+
+namespace Microsoft.AspNet.Mvc.Razor
+{
+ 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 RazorBuffer(new TestRazorBufferScope(), "some-name");
+ 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
+ var bufferValues = GetValues(buffer);
+ Assert.Equal(4, bufferValues.Length);
+ Assert.Equal("bcd", bufferValues[0]);
+ Assert.Equal("ef", bufferValues[1]);
+ Assert.Equal("j", bufferValues[2]);
+ Assert.Equal(Environment.NewLine, bufferValues[3]);
+ }
+
+ [Fact]
+ public void Write_SplitsCharBuffer_Into1kbStrings()
+ {
+ // Arrange
+ var charArray = Enumerable.Range(0, 2050).Select(_ => 'a').ToArray();
+ var buffer = new RazorBuffer(new TestRazorBufferScope(), "some-name");
+ var writer = new HtmlContentWrapperTextWriter(buffer, Encoding.UTF8);
+
+ // Act
+ writer.Write(charArray);
+
+ // Assert
+ Assert.Collection(GetValues(buffer),
+ 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 RazorBuffer(new TestRazorBufferScope(), "some-name");
+ var writer = new HtmlContentWrapperTextWriter(buffer, Encoding.UTF8);
+ var content = new HtmlString("Hello, world!");
+
+ // Act
+ writer.Write(content);
+
+ // Assert
+ Assert.Collection(
+ GetValues(buffer),
+ item => Assert.Same(content, item));
+ }
+
+ [Fact]
+ public void Write_Object_HtmlContent_AddsToEntries()
+ {
+ // Arrange
+ var buffer = new RazorBuffer(new TestRazorBufferScope(), "some-name");
+ var writer = new HtmlContentWrapperTextWriter(buffer, Encoding.UTF8);
+ var content = new HtmlString("Hello, world!");
+
+ // Act
+ writer.Write((object)content);
+
+ // Assert
+ Assert.Collection(
+ GetValues(buffer),
+ item => Assert.Same(content, item));
+ }
+
+ [Fact]
+ public void WriteLine_Object_HtmlContent_AddsToEntries()
+ {
+ // Arrange
+ var buffer = new RazorBuffer(new TestRazorBufferScope(), "some-name");
+ var writer = new HtmlContentWrapperTextWriter(buffer, Encoding.UTF8);
+ var content = new HtmlString("Hello, world!");
+
+ // Act
+ writer.WriteLine(content);
+
+ // Assert
+ Assert.Collection(
+ GetValues(buffer),
+ 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 RazorBuffer(new TestRazorBufferScope(), "some-name");
+ var writer = new HtmlContentWrapperTextWriter(buffer, Encoding.UTF8);
+
+ // Act
+ writer.Write(input1);
+ writer.WriteLine(input2);
+ await writer.WriteAsync(input3);
+ await writer.WriteLineAsync(input4);
+
+ // Assert
+ var actual = GetValues(buffer);
+ Assert.Equal(new object[] { input1, input2, newLine, input3, input4, newLine }, actual);
+ }
+
+ [Fact]
+ public void Write_HtmlContent_WritesToBuffer()
+ {
+ // Arrange
+ var buffer = new RazorBuffer(new TestRazorBufferScope(), "some-name");
+ var writer = new HtmlContentWrapperTextWriter(buffer, Encoding.UTF8);
+ var content = new HtmlString("Hello, world!");
+
+ // Act
+ writer.Write(content);
+
+ // Assert
+ Assert.Collection(
+ GetValues(buffer),
+ item => Assert.Same(content, item));
+ }
+
+ private static object[] GetValues(RazorBuffer buffer)
+ {
+ return buffer.BufferSegments
+ .SelectMany(c => c.Data)
+ .Select(d => d.Value)
+ .TakeWhile(d => d != null)
+ .ToArray();
+ }
+ }
+}
diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorPageTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorPageTest.cs
index cf35631d21..1bde6c983e 100644
--- a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorPageTest.cs
+++ b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorPageTest.cs
@@ -11,6 +11,7 @@ using Microsoft.AspNet.Html;
using Microsoft.AspNet.Http.Internal;
using Microsoft.AspNet.Mvc.Abstractions;
using Microsoft.AspNet.Mvc.ModelBinding;
+using Microsoft.AspNet.Mvc.Razor.Buffer;
using Microsoft.AspNet.Mvc.Rendering;
using Microsoft.AspNet.Mvc.Routing;
using Microsoft.AspNet.Mvc.TestCommon;
@@ -184,7 +185,8 @@ namespace Microsoft.AspNet.Mvc.Razor
var page = CreatePage(v =>
{
v.HtmlEncoder = new HtmlTestEncoder();
- v.StartTagHelperWritingScope(new RazorTextWriter(TextWriter.Null, Encoding.UTF8, v.HtmlEncoder));
+ var buffer = new RazorBuffer(new TestRazorBufferScope(), v.Path);
+ v.StartTagHelperWritingScope(new RazorTextWriter(TextWriter.Null, buffer, v.HtmlEncoder));
v.Write("Hello ");
v.Write("World!");
var returnValue = v.EndTagHelperWritingScope();
@@ -1125,7 +1127,8 @@ namespace Microsoft.AspNet.Mvc.Razor
public async Task Write_WithHtmlString_WritesValueWithoutEncoding()
{
// Arrange
- var writer = new RazorTextWriter(TextWriter.Null, Encoding.UTF8, new HtmlTestEncoder());
+ var buffer = new RazorBuffer(new TestRazorBufferScope(), string.Empty);
+ var writer = new RazorTextWriter(TextWriter.Null, buffer, new HtmlTestEncoder());
var page = CreatePage(p =>
{
@@ -1137,9 +1140,7 @@ namespace Microsoft.AspNet.Mvc.Razor
await page.ExecuteAsync();
// Assert
- var buffer = writer.BufferedWriter.Entries;
- Assert.Equal(1, buffer.Count);
- Assert.Equal("Hello world", HtmlContentUtilities.HtmlContentToString(((IHtmlContent)buffer[0])));
+ Assert.Equal("Hello world", HtmlContentUtilities.HtmlContentToString(writer.Buffer));
}
private static TestableRazorPage CreatePage(
diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorTextWriterTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorTextWriterTest.cs
index 21c9be6ff2..e23766384b 100644
--- a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorTextWriterTest.cs
+++ b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorTextWriterTest.cs
@@ -4,8 +4,10 @@
using System;
using System.Collections.Generic;
using System.IO;
+using System.Linq;
using System.Text;
using System.Threading.Tasks;
+using Microsoft.AspNet.Mvc.Razor.Buffer;
using Microsoft.AspNet.Mvc.Rendering;
using Microsoft.AspNet.Testing;
using Microsoft.Extensions.WebEncoders.Testing;
@@ -21,8 +23,9 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
public void Write_WritesDataTypes_ToBuffer()
{
// Arrange
- var expected = new[] { "True", "3", "18446744073709551615", "Hello world", "3.14", "2.718", "m" };
- var writer = new RazorTextWriter(TextWriter.Null, Encoding.UTF8, new HtmlTestEncoder());
+ var expected = new object[] { "True", "3", "18446744073709551615", "Hello world", "3.14", "2.718", "m" };
+ var buffer = new RazorBuffer(new TestRazorBufferScope(), "some-name");
+ var writer = new RazorTextWriter(TextWriter.Null, buffer, new HtmlTestEncoder());
// Act
writer.Write(true);
@@ -34,7 +37,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
writer.Write('m');
// Assert
- Assert.Equal(expected, writer.BufferedWriter.Entries);
+ Assert.Equal(expected, GetValues(buffer));
}
[Fact]
@@ -44,7 +47,9 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
// Arrange
var expected = new[] { "True", "3", "18446744073709551615", "Hello world", "3.14", "2.718" };
var unbufferedWriter = new Mock();
- var writer = new RazorTextWriter(unbufferedWriter.Object, Encoding.UTF8, new HtmlTestEncoder());
+ unbufferedWriter.SetupGet(w => w.Encoding).Returns(Encoding.UTF8);
+ var buffer = new RazorBuffer(new TestRazorBufferScope(), "some-name");
+ var writer = new RazorTextWriter(unbufferedWriter.Object, buffer, new HtmlTestEncoder());
var testClass = new TestClass();
// Act
@@ -57,7 +62,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
writer.Write(2.718m);
// Assert
- Assert.Empty(writer.BufferedWriter.Entries);
+ Assert.Null(buffer.BufferSegments);
foreach (var item in expected)
{
unbufferedWriter.Verify(v => v.Write(item), Times.Once());
@@ -70,7 +75,9 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
{
// Arrange
var unbufferedWriter = new Mock { CallBase = true };
- var writer = new RazorTextWriter(unbufferedWriter.Object, Encoding.UTF8, new HtmlTestEncoder());
+ unbufferedWriter.SetupGet(w => w.Encoding).Returns(Encoding.UTF8);
+ var buffer = new RazorBuffer(new TestRazorBufferScope(), "some-name");
+ var writer = new RazorTextWriter(unbufferedWriter.Object, buffer, new HtmlTestEncoder());
var buffer1 = new[] { 'a', 'b', 'c', 'd' };
var buffer2 = new[] { 'd', 'e', 'f' };
@@ -83,7 +90,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
await writer.WriteLineAsync(buffer1);
// Assert
- Assert.Empty(writer.BufferedWriter.Entries);
+ Assert.Null(buffer.BufferSegments);
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());
@@ -98,7 +105,9 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
{
// Arrange
var unbufferedWriter = new Mock();
- var writer = new RazorTextWriter(unbufferedWriter.Object, Encoding.UTF8, new HtmlTestEncoder());
+ unbufferedWriter.SetupGet(w => w.Encoding).Returns(Encoding.UTF8);
+ var buffer = new RazorBuffer(new TestRazorBufferScope(), "some-name");
+ var writer = new RazorTextWriter(unbufferedWriter.Object, buffer, new HtmlTestEncoder());
// Act
await writer.FlushAsync();
@@ -108,7 +117,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
await writer.WriteLineAsync("gh");
// Assert
- Assert.Empty(writer.BufferedWriter.Entries);
+ Assert.Null(buffer.BufferSegments);
unbufferedWriter.Verify(v => v.Write("a"), Times.Once());
unbufferedWriter.Verify(v => v.WriteLine("ab"), Times.Once());
unbufferedWriter.Verify(v => v.WriteAsync("ef"), Times.Once());
@@ -122,7 +131,8 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
// Arrange
var newLine = Environment.NewLine;
var expected = new List