From ff34c5404a7f3cfdd5c9fb80af1d6463d3510f67 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Wed, 2 Dec 2015 15:27:31 -0800 Subject: [PATCH] Implement a backing-buffer for Razor using pooled memory Fixes #3532 --- .../Buffer/IRazorBufferScope.cs | 17 ++ .../Buffer/MemoryPoolRazorBufferScope.cs | 82 ++++++++ .../Buffer/RazorBuffer.cs | 152 ++++++++++++++ .../Buffer/RazorBufferSegment.cs | 27 +++ .../Buffer/RazorValue.cs | 65 ++++++ .../MvcRazorMvcCoreBuilderExtensions.cs | 5 + .../HtmlContentWrapperTextWriter.cs | 176 ++++++++++++++++ .../RazorTextWriter.cs | 20 +- src/Microsoft.AspNet.Mvc.Razor/RazorView.cs | 9 +- .../CacheTagHelper.cs | 47 ++++- .../Buffer/RazorBufferTest.cs | 198 ++++++++++++++++++ .../HtmlContentWrapperTextWriterTest.cs | 159 ++++++++++++++ .../RazorPageTest.cs | 11 +- .../RazorTextWriterTest.cs | 133 ++++-------- .../RazorViewTest.cs | 6 + .../TestRazorBufferScope.cs | 32 +++ .../CacheTagHelperTest.cs | 67 +++--- 17 files changed, 1061 insertions(+), 145 deletions(-) create mode 100644 src/Microsoft.AspNet.Mvc.Razor/Buffer/IRazorBufferScope.cs create mode 100644 src/Microsoft.AspNet.Mvc.Razor/Buffer/MemoryPoolRazorBufferScope.cs create mode 100644 src/Microsoft.AspNet.Mvc.Razor/Buffer/RazorBuffer.cs create mode 100644 src/Microsoft.AspNet.Mvc.Razor/Buffer/RazorBufferSegment.cs create mode 100644 src/Microsoft.AspNet.Mvc.Razor/Buffer/RazorValue.cs create mode 100644 src/Microsoft.AspNet.Mvc.Razor/HtmlContentWrapperTextWriter.cs create mode 100644 test/Microsoft.AspNet.Mvc.Razor.Test/Buffer/RazorBufferTest.cs create mode 100644 test/Microsoft.AspNet.Mvc.Razor.Test/HtmlContentWrapperTextWriterTest.cs create mode 100644 test/Microsoft.AspNet.Mvc.Razor.Test/TestRazorBufferScope.cs 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 { "False", newLine, "1.1", newLine, "3", newLine }; - var writer = new RazorTextWriter(TextWriter.Null, Encoding.UTF8, new HtmlTestEncoder()); + var buffer = new RazorBuffer(new TestRazorBufferScope(), "some-name"); + var writer = new RazorTextWriter(TextWriter.Null, buffer, new HtmlTestEncoder()); // Act writer.WriteLine(false); @@ -130,7 +140,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Test writer.WriteLine(3L); // Assert - Assert.Equal(expected, writer.BufferedWriter.Entries); + Assert.Equal(expected, GetValues(buffer)); } [Fact] @@ -139,7 +149,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 writer.Flush(); @@ -148,49 +160,27 @@ namespace Microsoft.AspNet.Mvc.Razor.Test writer.WriteLine(3L); // Assert - Assert.Empty(writer.BufferedWriter.Entries); + Assert.Null(buffer.BufferSegments); 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)); } - [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 writer = new RazorTextWriter(TextWriter.Null, Encoding.UTF8, new HtmlTestEncoder()); - - // 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 buffer = writer.BufferedWriter.Entries; - Assert.Equal(4, buffer.Count); - Assert.Equal("bcd", buffer[0]); - Assert.Equal("ef", buffer[1]); - Assert.Equal("j", buffer[2]); - Assert.Equal(Environment.NewLine, buffer[3]); - } - [Fact] public async Task WriteLines_WritesCharBuffer() { // Arrange var newLine = Environment.NewLine; - var writer = new RazorTextWriter(TextWriter.Null, Encoding.UTF8, new HtmlTestEncoder()); + var buffer = new RazorBuffer(new TestRazorBufferScope(), "some-name"); + var writer = new RazorTextWriter(TextWriter.Null, buffer, new HtmlTestEncoder()); // Act writer.WriteLine(); await writer.WriteLineAsync(); // Assert - var actual = writer.BufferedWriter.Entries; + var actual = GetValues(buffer); Assert.Equal(new[] { newLine, newLine }, actual); } @@ -203,7 +193,8 @@ namespace Microsoft.AspNet.Mvc.Razor.Test var input2 = "from"; var input3 = "ASP"; var input4 = ".Net"; - var writer = new RazorTextWriter(TextWriter.Null, Encoding.UTF8, new HtmlTestEncoder()); + var buffer = new RazorBuffer(new TestRazorBufferScope(), "some-name"); + var writer = new RazorTextWriter(TextWriter.Null, buffer, new HtmlTestEncoder()); // Act writer.Write(input1); @@ -212,66 +203,17 @@ namespace Microsoft.AspNet.Mvc.Razor.Test await writer.WriteLineAsync(input4); // Assert - var actual = writer.BufferedWriter.Entries; + var actual = GetValues(buffer); Assert.Equal(new[] { input1, input2, newLine, input3, input4, newLine }, actual); } - [Fact] - public void Write_HtmlContent_AddsToEntries() - { - // Arrange - var writer = new RazorTextWriter(TextWriter.Null, Encoding.UTF8, new HtmlTestEncoder()); - var content = new HtmlString("Hello, world!"); - - // Act - writer.Write(content); - - // Assert - Assert.Collection( - writer.BufferedWriter.Entries, - item => Assert.Same(content, item)); - } - - [Fact] - public void Write_Object_HtmlContent_AddsToEntries() - { - // Arrange - var writer = new RazorTextWriter(TextWriter.Null, Encoding.UTF8, new HtmlTestEncoder()); - var content = new HtmlString("Hello, world!"); - - // Act - writer.Write((object)content); - - // Assert - Assert.Collection( - writer.BufferedWriter.Entries, - item => Assert.Same(content, item)); - } - - [Fact] - public void WriteLine_Object_HtmlContent_AddsToEntries() - { - // Arrange - var writer = new RazorTextWriter(TextWriter.Null, Encoding.UTF8, new HtmlTestEncoder()); - var content = new HtmlString("Hello, world!"); - - // Act - writer.WriteLine(content); - - // Assert - Assert.Collection( - writer.BufferedWriter.Entries, - item => Assert.Same(content, item), - item => Assert.Equal(Environment.NewLine, item)); - } - [Fact] public void Write_HtmlContent_AfterFlush_GoesToStream() { // Arrange var stringWriter = new StringWriter(); - - var writer = new RazorTextWriter(stringWriter, Encoding.UTF8, new HtmlTestEncoder()); + var buffer = new RazorBuffer(new TestRazorBufferScope(), "some-name"); + var writer = new RazorTextWriter(stringWriter, buffer, new HtmlTestEncoder()); writer.Flush(); var content = new HtmlString("Hello, world!"); @@ -283,6 +225,15 @@ namespace Microsoft.AspNet.Mvc.Razor.Test Assert.Equal("Hello, world!", stringWriter.ToString()); } + private static object[] GetValues(RazorBuffer buffer) + { + return buffer.BufferSegments + .SelectMany(c => c.Data) + .Select(d => d.Value) + .TakeWhile(d => d != null) + .ToArray(); + } + private class TestClass { public override string ToString() diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewTest.cs index 93e1fa39c0..92d62b3ce2 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewTest.cs +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewTest.cs @@ -10,9 +10,11 @@ using Microsoft.AspNet.Http.Features; 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.ViewFeatures; using Microsoft.AspNet.Routing; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Primitives; using Microsoft.Extensions.WebEncoders.Testing; using Moq; @@ -1563,6 +1565,10 @@ namespace Microsoft.AspNet.Mvc.Razor private static ViewContext CreateViewContext(RazorView view) { var httpContext = new DefaultHttpContext(); + var serviceProvider = new ServiceCollection() + .AddScoped() + .BuildServiceProvider(); + httpContext.RequestServices = serviceProvider; var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); return new ViewContext( actionContext, diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/TestRazorBufferScope.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/TestRazorBufferScope.cs new file mode 100644 index 0000000000..ad78f0c0d0 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/TestRazorBufferScope.cs @@ -0,0 +1,32 @@ +// 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 +{ + public class TestRazorBufferScope : IRazorBufferScope + { + public const int BufferSize = 128; + private readonly int _offset; + private readonly int _count; + + public TestRazorBufferScope() + : this(0, BufferSize) + { + + } + + public TestRazorBufferScope(int offset, int count) + { + _offset = offset; + _count = count; + } + + public RazorBufferSegment GetSegment() + { + var razorValues = new RazorValue[BufferSize]; + return new RazorBufferSegment(new ArraySegment(razorValues, _offset, _count)); + } + } +} diff --git a/test/Microsoft.AspNet.Mvc.TagHelpers.Test/CacheTagHelperTest.cs b/test/Microsoft.AspNet.Mvc.TagHelpers.Test/CacheTagHelperTest.cs index 72885d68ca..6997003800 100644 --- a/test/Microsoft.AspNet.Mvc.TagHelpers.Test/CacheTagHelperTest.cs +++ b/test/Microsoft.AspNet.Mvc.TagHelpers.Test/CacheTagHelperTest.cs @@ -10,6 +10,7 @@ using System.Security.Cryptography; using System.Text; using System.Threading; using System.Threading.Tasks; +using Microsoft.AspNet.Html; using Microsoft.AspNet.Http.Internal; using Microsoft.AspNet.Mvc.Abstractions; using Microsoft.AspNet.Mvc.ModelBinding; @@ -21,6 +22,7 @@ using Microsoft.AspNet.Routing; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Internal; using Microsoft.Extensions.Primitives; +using Microsoft.Extensions.WebEncoders.Testing; using Moq; using Xunit; @@ -34,7 +36,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers // Arrange var id = Guid.NewGuid().ToString(); var tagHelperContext = GetTagHelperContext(id); - var cacheTagHelper = new CacheTagHelper(Mock.Of()) + var cacheTagHelper = new CacheTagHelper(Mock.Of(), new HtmlTestEncoder()) { ViewContext = GetViewContext() }; @@ -55,7 +57,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers { // Arrange var tagHelperContext = GetTagHelperContext(); - var cacheTagHelper = new CacheTagHelper(Mock.Of()) + var cacheTagHelper = new CacheTagHelper(Mock.Of(), new HtmlTestEncoder()) { ViewContext = GetViewContext(), VaryBy = varyBy @@ -83,7 +85,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers { // Arrange var tagHelperContext = GetTagHelperContext(); - var cacheTagHelper = new CacheTagHelper(Mock.Of()) + var cacheTagHelper = new CacheTagHelper(Mock.Of(), new HtmlTestEncoder()) { ViewContext = GetViewContext(), VaryByCookie = varyByCookie @@ -108,7 +110,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers { // Arrange var tagHelperContext = GetTagHelperContext(); - var cacheTagHelper = new CacheTagHelper(Mock.Of()) + var cacheTagHelper = new CacheTagHelper(Mock.Of(), new HtmlTestEncoder()) { ViewContext = GetViewContext(), VaryByHeader = varyByHeader @@ -135,7 +137,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers { // Arrange var tagHelperContext = GetTagHelperContext(); - var cacheTagHelper = new CacheTagHelper(Mock.Of()) + var cacheTagHelper = new CacheTagHelper(Mock.Of(), new HtmlTestEncoder()) { ViewContext = GetViewContext(), VaryByQuery = varyByQuery @@ -160,7 +162,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers { // Arrange var tagHelperContext = GetTagHelperContext(); - var cacheTagHelper = new CacheTagHelper(Mock.Of()) + var cacheTagHelper = new CacheTagHelper(Mock.Of(), new HtmlTestEncoder()) { ViewContext = GetViewContext(), VaryByRoute = varyByRoute @@ -181,7 +183,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers // Arrange var expected = "CacheTagHelper||testid||VaryByUser||"; var tagHelperContext = GetTagHelperContext(); - var cacheTagHelper = new CacheTagHelper(Mock.Of()) + var cacheTagHelper = new CacheTagHelper(Mock.Of(), new HtmlTestEncoder()) { ViewContext = GetViewContext(), VaryByUser = true @@ -200,7 +202,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers // Arrange var expected = "CacheTagHelper||testid||VaryByUser||test_name"; var tagHelperContext = GetTagHelperContext(); - var cacheTagHelper = new CacheTagHelper(Mock.Of()) + var cacheTagHelper = new CacheTagHelper(Mock.Of(), new HtmlTestEncoder()) { ViewContext = GetViewContext(), VaryByUser = true @@ -222,7 +224,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers var expected = GetHashedBytes("CacheTagHelper||testid||VaryBy||custom-value||" + "VaryByHeader(content-type||text/html)||VaryByUser||someuser"); var tagHelperContext = GetTagHelperContext(); - var cacheTagHelper = new CacheTagHelper(Mock.Of()) + var cacheTagHelper = new CacheTagHelper(Mock.Of(), new HtmlTestEncoder()) { ViewContext = GetViewContext(), VaryByUser = true, @@ -262,7 +264,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers var tagHelperOutput = GetTagHelperOutput( attributes: new TagHelperAttributeList(), childContent: childContent); - var cacheTagHelper = new CacheTagHelper(cache.Object) + var cacheTagHelper = new CacheTagHelper(cache.Object, new HtmlTestEncoder()) { ViewContext = GetViewContext(), Enabled = false @@ -303,7 +305,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers var tagHelperOutput = GetTagHelperOutput( attributes: new TagHelperAttributeList(), childContent: childContent); - var cacheTagHelper = new CacheTagHelper(cache.Object) + var cacheTagHelper = new CacheTagHelper(cache.Object, new HtmlTestEncoder()) { ViewContext = GetViewContext(), Enabled = true @@ -335,7 +337,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers var tagHelperOutput1 = GetTagHelperOutput( attributes: new TagHelperAttributeList(), childContent: childContent); - var cacheTagHelper1 = new CacheTagHelper(cache) + var cacheTagHelper1 = new CacheTagHelper(cache, new HtmlTestEncoder()) { VaryByQuery = "key1,key2", ViewContext = GetViewContext(), @@ -357,7 +359,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers var tagHelperOutput2 = GetTagHelperOutput( attributes: new TagHelperAttributeList(), childContent: "different-content"); - var cacheTagHelper2 = new CacheTagHelper(cache) + var cacheTagHelper2 = new CacheTagHelper(cache, new HtmlTestEncoder()) { VaryByQuery = "key1,key2", ViewContext = GetViewContext(), @@ -386,7 +388,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers var tagHelperOutput1 = GetTagHelperOutput(childContent: childContent1); tagHelperOutput1.PreContent.Append(""); tagHelperOutput1.PostContent.SetContent(""); - var cacheTagHelper1 = new CacheTagHelper(cache) + var cacheTagHelper1 = new CacheTagHelper(cache, new HtmlTestEncoder()) { VaryByCookie = "cookie1,cookie2", ViewContext = GetViewContext(), @@ -408,7 +410,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers var tagHelperOutput2 = GetTagHelperOutput(childContent: childContent2); tagHelperOutput2.PreContent.SetContent(""); tagHelperOutput2.PostContent.SetContent(""); - var cacheTagHelper2 = new CacheTagHelper(cache) + var cacheTagHelper2 = new CacheTagHelper(cache, new HtmlTestEncoder()) { VaryByCookie = "cookie1,cookie2", ViewContext = GetViewContext(), @@ -431,7 +433,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers // Arrange var expiresOn = DateTimeOffset.UtcNow.AddMinutes(4); var cache = new MemoryCache(new MemoryCacheOptions()); - var cacheTagHelper = new CacheTagHelper(cache) + var cacheTagHelper = new CacheTagHelper(cache, new HtmlTestEncoder()) { ExpiresOn = expiresOn }; @@ -449,7 +451,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers // Arrange var expiresOn = DateTimeOffset.UtcNow.AddMinutes(7); var cache = new MemoryCache(new MemoryCacheOptions()); - var cacheTagHelper = new CacheTagHelper(cache) + var cacheTagHelper = new CacheTagHelper(cache, new HtmlTestEncoder()) { }; @@ -470,7 +472,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers var expiresOn1 = DateTimeOffset.UtcNow.AddDays(12); var expiresOn2 = DateTimeOffset.UtcNow.AddMinutes(4); var cache = new MemoryCache(new MemoryCacheOptions()); - var cacheTagHelper = new CacheTagHelper(cache) + var cacheTagHelper = new CacheTagHelper(cache, new HtmlTestEncoder()) { ExpiresOn = expiresOn1 }; @@ -491,7 +493,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers // Arrange var expiresAfter = TimeSpan.FromSeconds(42); var cache = new MemoryCache(new MemoryCacheOptions()); - var cacheTagHelper = new CacheTagHelper(cache) + var cacheTagHelper = new CacheTagHelper(cache, new HtmlTestEncoder()) { ExpiresAfter = expiresAfter }; @@ -509,7 +511,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers // Arrange var expiresSliding = TimeSpan.FromSeconds(37); var cache = new MemoryCache(new MemoryCacheOptions()); - var cacheTagHelper = new CacheTagHelper(cache) + var cacheTagHelper = new CacheTagHelper(cache, new HtmlTestEncoder()) { ExpiresSliding = expiresSliding }; @@ -527,7 +529,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers // Arrange var priority = CacheItemPriority.High; var cache = new MemoryCache(new MemoryCacheOptions()); - var cacheTagHelper = new CacheTagHelper(cache) + var cacheTagHelper = new CacheTagHelper(cache, new HtmlTestEncoder()) { Priority = priority }; @@ -546,7 +548,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers var expiresSliding = TimeSpan.FromSeconds(30); var expected = new[] { Mock.Of(), Mock.Of() }; var cache = new MemoryCache(new MemoryCacheOptions()); - var cacheTagHelper = new CacheTagHelper(cache) + var cacheTagHelper = new CacheTagHelper(cache, new HtmlTestEncoder()) { ExpiresSliding = expiresSliding }; @@ -576,7 +578,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers var tagHelperOutput1 = GetTagHelperOutput(childContent: childContent1); tagHelperOutput1.PreContent.SetContent(""); tagHelperOutput1.PostContent.SetContent(""); - var cacheTagHelper1 = new CacheTagHelper(cache) + var cacheTagHelper1 = new CacheTagHelper(cache, new HtmlTestEncoder()) { ViewContext = GetViewContext(), ExpiresAfter = TimeSpan.FromMinutes(10) @@ -597,7 +599,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers var tagHelperOutput2 = GetTagHelperOutput(childContent: childContent2); tagHelperOutput2.PreContent.SetContent(""); tagHelperOutput2.PostContent.SetContent(""); - var cacheTagHelper2 = new CacheTagHelper(cache) + var cacheTagHelper2 = new CacheTagHelper(cache, new HtmlTestEncoder()) { ViewContext = GetViewContext(), ExpiresAfter = TimeSpan.FromMinutes(10) @@ -629,7 +631,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers var tagHelperOutput1 = GetTagHelperOutput(childContent: childContent1); tagHelperOutput1.PreContent.SetContent(""); tagHelperOutput1.PostContent.SetContent(""); - var cacheTagHelper1 = new CacheTagHelper(cache) + var cacheTagHelper1 = new CacheTagHelper(cache, new HtmlTestEncoder()) { ViewContext = GetViewContext(), ExpiresOn = currentTime.AddMinutes(5) @@ -651,7 +653,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers var tagHelperOutput2 = GetTagHelperOutput(childContent: childContent2); tagHelperOutput2.PreContent.SetContent(""); tagHelperOutput2.PostContent.SetContent(""); - var cacheTagHelper2 = new CacheTagHelper(cache) + var cacheTagHelper2 = new CacheTagHelper(cache, new HtmlTestEncoder()) { ViewContext = GetViewContext(), ExpiresOn = currentTime.AddMinutes(5) @@ -682,7 +684,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers var tagHelperOutput1 = GetTagHelperOutput(childContent: childContent1); tagHelperOutput1.PreContent.SetContent(""); tagHelperOutput1.PostContent.SetContent(""); - var cacheTagHelper1 = new CacheTagHelper(cache) + var cacheTagHelper1 = new CacheTagHelper(cache, new HtmlTestEncoder()) { ViewContext = GetViewContext(), ExpiresSliding = TimeSpan.FromSeconds(30) @@ -704,7 +706,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers var tagHelperOutput2 = GetTagHelperOutput(childContent: childContent2); tagHelperOutput2.PreContent.SetContent(""); tagHelperOutput2.PostContent.SetContent(""); - var cacheTagHelper2 = new CacheTagHelper(cache) + var cacheTagHelper2 = new CacheTagHelper(cache, new HtmlTestEncoder()) { ViewContext = GetViewContext(), ExpiresSliding = TimeSpan.FromSeconds(30) @@ -751,7 +753,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers }); tagHelperOutput.PreContent.SetContent(""); tagHelperOutput.PostContent.SetContent(""); - var cacheTagHelper = new CacheTagHelper(cache) + var cacheTagHelper = new CacheTagHelper(cache, new HtmlTestEncoder()) { ViewContext = GetViewContext(), }; @@ -759,13 +761,12 @@ namespace Microsoft.AspNet.Mvc.TagHelpers // Act - 1 await cacheTagHelper.ProcessAsync(tagHelperContext, tagHelperOutput); - TagHelperContent cachedValue; + IHtmlContent cachedValue; var result = cache.TryGetValue(key, out cachedValue); // Assert - 1 - Assert.Equal(expectedContent.GetContent(), tagHelperOutput.Content.GetContent()); + Assert.Equal("HtmlEncode[[some-content]]", tagHelperOutput.Content.GetContent()); Assert.True(result); - Assert.Equal(expectedContent, cachedValue); // Act - 2 tokenSource.Cancel(); @@ -808,7 +809,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers getChildContentAsync: useCachedResult => { var tagHelperContent = new DefaultTagHelperContent(); - tagHelperContent.SetContent(childContent); + tagHelperContent.SetHtmlContent(childContent); return Task.FromResult(tagHelperContent); }); }