Implement a backing-buffer for Razor using pooled memory

Fixes #3532
This commit is contained in:
Pranav K 2015-12-02 15:27:31 -08:00
parent 1f76b3c7b7
commit ff34c5404a
17 changed files with 1061 additions and 145 deletions

View File

@ -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
{
/// <summary>
/// Creates and manages the lifetime of <see cref="RazorBufferSegment"/> instances.
/// </summary>
public interface IRazorBufferScope
{
/// <summary>
/// Gets a <see cref="RazorBufferSegment"/>.
/// </summary>
/// <returns>The <see cref="RazorBufferSegment"/>.</returns>
RazorBufferSegment GetSegment();
}
}

View File

@ -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
{
/// <summary>
/// A <see cref="IRazorBufferScope"/> that uses pooled memory.
/// </summary>
public class MemoryPoolRazorBufferScope : IRazorBufferScope, IDisposable
{
private const int SegmentSize = 1024;
private readonly IArraySegmentPool<RazorValue> _pool;
private List<LeasedArraySegment<RazorValue>> _leased;
private bool _disposed;
/// <summary>
/// Initializes a new instance of <see cref="MemoryPoolRazorBufferScope"/>.
/// </summary>
/// <param name="pool">The <see cref="IArraySegmentPool{RazorValue}"/> for creating
/// <see cref="RazorValue"/> instances.</param>
public MemoryPoolRazorBufferScope(IArraySegmentPool<RazorValue> pool)
{
_pool = pool;
}
/// <inheritdoc />
public RazorBufferSegment GetSegment()
{
if (_disposed)
{
throw new ObjectDisposedException(typeof(MemoryPoolRazorBufferScope).FullName);
}
if (_leased == null)
{
_leased = new List<LeasedArraySegment<RazorValue>>(1);
}
LeasedArraySegment<RazorValue> segment = null;
try
{
segment = _pool.Lease(SegmentSize);
_leased.Add(segment);
}
catch when (segment != null)
{
segment.Owner.Return(segment);
throw;
}
return new RazorBufferSegment(segment.Data);
}
/// <inheritdoc />
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();
}
}
}
}

View File

@ -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
{
/// <summary>
/// An <see cref="IHtmlContentBuilder"/> that is backed by a buffer provided by <see cref="IRazorBufferScope"/>.
/// </summary>
[DebuggerDisplay("{DebuggerToString()}")]
public class RazorBuffer : IHtmlContentBuilder
{
private readonly IRazorBufferScope _bufferScope;
private readonly string _name;
/// <summary>
/// Initializes a new instance of <see cref="RazorBuffer"/>.
/// </summary>
/// <param name="bufferScope">The <see cref="IRazorBufferScope"/>.</param>
/// <param name="name">A name to identify this instance.</param>
public RazorBuffer(IRazorBufferScope bufferScope, string name)
{
if (bufferScope == null)
{
throw new ArgumentNullException(nameof(bufferScope));
}
_bufferScope = bufferScope;
_name = name;
}
/// <summary>
/// Gets the backing buffer.
/// </summary>
public IList<RazorBufferSegment> BufferSegments { get; private set; }
/// <summary>
/// Gets the count of entries in the last element of <see cref="BufferSegments"/>.
/// </summary>
public int CurrentCount { get; private set; }
/// <inheritdoc />
public IHtmlContentBuilder Append(string unencoded)
{
if (unencoded == null)
{
return this;
}
AppendValue(new RazorValue(unencoded));
return this;
}
/// <inheritdoc />
public IHtmlContentBuilder Append(IHtmlContent content)
{
if (content == null)
{
return this;
}
AppendValue(new RazorValue(content));
return this;
}
/// <inheritdoc />
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<RazorBufferSegment>(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++;
}
/// <inheritdoc />
public IHtmlContentBuilder Clear()
{
if (BufferSegments != null)
{
CurrentCount = 0;
BufferSegments = null;
}
return this;
}
/// <inheritdoc />
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;
}
}

View File

@ -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
{
/// <summary>
/// Encapsulates a <see cref="ArraySegment{RazorValue}"/>.
/// </summary>
public struct RazorBufferSegment
{
/// <summary>
/// Initializes a new instance of <see cref="RazorBufferSegment"/>.
/// </summary>
/// <param name="data">The <see cref="ArraySegment{RazorValue}"/> to encapsulate.</param>
public RazorBufferSegment(ArraySegment<RazorValue> data)
{
Data = data;
}
/// <summary>
/// Gets the <see cref="ArraySegment{RazorValue}"/>.
/// </summary>
public ArraySegment<RazorValue> Data { get; }
}
}

View File

@ -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
{
/// <summary>
/// Encapsulates a string or <see cref="IHtmlContent"/> value.
/// </summary>
public struct RazorValue
{
/// <summary>
/// Initializes a new instance of <see cref="RazorValue"/> with a <c>string</c> value.
/// </summary>
/// <param name="value">The value.</param>
public RazorValue(string value)
{
Value = value;
}
/// <summary>
/// Initializes a new instance of <see cref="RazorValue"/> with a <see cref="IHtmlContent"/> value.
/// </summary>
/// <param name="value">The <see cref="IHtmlContent"/>.</param>
public RazorValue(IHtmlContent content)
{
Value = content;
}
/// <summary>
/// Gets the value.
/// </summary>
public object Value { get; }
/// <summary>
/// Writes the <see cref="Value"/> by encoding it with the specified <paramref name="encoder"/> to the
/// specified <paramref name="writer"/>.
/// </summary>
/// <param name="writer">The <see cref="TextWriter"/> to write the value to.</param>
/// <param name="encoder">The <see cref="HtmlEncoder"/> which encodes the content to be written.</param>
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);
}
}
}
}

View File

@ -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<IMemoryCache, MemoryCache>();
services.TryAddSingleton<IArraySegmentPool<RazorValue>, DefaultArraySegmentPool<RazorValue>>();
services.TryAddScoped<IRazorBufferScope, MemoryPoolRazorBufferScope>();
}
}
}

View File

@ -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
{
/// <summary>
/// <see cref="HtmlTextWriter"/> implementation which writes to an <see cref="IHtmlContentBuilder"/> instance.
/// </summary>
public class HtmlContentWrapperTextWriter : HtmlTextWriter
{
private const int MaxCharToStringLength = 1024;
/// <summary>
/// Initializes a new instance of the <see cref="HtmlContentWrapperTextWriter"/> class.
/// </summary>
/// <param name="contentBuilder">The <see cref="IHtmlContentBuilder"/> to write to.</param>
/// <param name="encoding">The <see cref="Encoding"/> in which output is written.</param>
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;
}
/// <summary>
/// The <see cref="IHtmlContentBuilder"/> this <see cref="HtmlContentWrapperTextWriter"/> writes to.
/// </summary>
public IHtmlContentBuilder ContentBuilder { get; }
/// <inheritdoc />
public override Encoding Encoding { get; }
/// <inheritdoc />
public override void Write(char value)
{
Write(value.ToString());
}
/// <inheritdoc />
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;
}
}
/// <inheritdoc />
public override void Write(string value)
{
if (string.IsNullOrEmpty(value))
{
return;
}
ContentBuilder.Append(value);
}
/// <inheritdoc />
public override void Write(IHtmlContent value)
{
if (value == null)
{
throw new ArgumentNullException(nameof(value));
}
ContentBuilder.Append(value);
}
/// <inheritdoc />
public override Task WriteAsync(char value)
{
Write(value.ToString());
return TaskCache.CompletedTask;
}
/// <inheritdoc />
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;
}
/// <inheritdoc />
public override Task WriteAsync(string value)
{
Write(value);
return TaskCache.CompletedTask;
}
/// <inheritdoc />
public override void WriteLine()
{
Write(Environment.NewLine);
}
/// <inheritdoc />
public override void WriteLine(string value)
{
Write(value);
WriteLine();
}
/// <inheritdoc />
public override Task WriteLineAsync(char value)
{
WriteLine(value);
return TaskCache.CompletedTask;
}
/// <inheritdoc />
public override Task WriteLineAsync(char[] value, int start, int offset)
{
WriteLine(value, start, offset);
return TaskCache.CompletedTask;
}
/// <inheritdoc />
public override Task WriteLineAsync(string value)
{
WriteLine(value);
return TaskCache.CompletedTask;
}
/// <inheritdoc />
public override Task WriteLineAsync()
{
WriteLine();
return TaskCache.CompletedTask;
}
}
}

View File

@ -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
{
/// <summary>
/// An <see cref="HtmlTextWriter"/> that is backed by a unbuffered writer (over the Response stream) and a buffered
/// <see cref="StringCollectionTextWriter"/>. When <c>Flush</c> or <c>FlushAsync</c> is invoked, the writer
/// <see cref="IHtmlContentBuilder"/>. When <c>Flush</c> or <c>FlushAsync</c> 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.
/// </summary>
/// <remarks>
/// This type is designed to avoid creating large in-memory strings when buffering and supporting the contract that
/// <see cref="RazorPage.FlushAsync"/> expects.
/// </remarks>
public class RazorTextWriter : HtmlTextWriter
{
/// <summary>
@ -28,14 +23,14 @@ namespace Microsoft.AspNet.Mvc.Razor
/// </summary>
/// <param name="unbufferedWriter">The <see cref="TextWriter"/> to write output to when this instance
/// is no longer buffering.</param>
/// <param name="encoding">The character <see cref="Encoding"/> in which the output is written.</param>
/// <param name="buffer">The <see cref="IHtmlContentBuilder"/> to buffer output to.</param>
/// <param name="encoder">The HTML encoder.</param>
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
/// <summary>
/// Gets the buffered content.
/// </summary>
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));
}

View File

@ -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;
/// <summary>
/// Initializes a new instance of <see cref="RazorView"/>
@ -93,6 +97,7 @@ namespace Microsoft.AspNet.Mvc.Razor
throw new ArgumentNullException(nameof(context));
}
_bufferScope = context.HttpContext.RequestServices.GetRequiredService<IRazorBufferScope>();
var bodyWriter = await RenderPageAsync(RazorPage, context, ViewStartPages);
await RenderLayoutAsync(context, bodyWriter);
}
@ -102,7 +107,9 @@ namespace Microsoft.AspNet.Mvc.Razor
ViewContext context,
IReadOnlyList<IRazorPage> 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.

View File

@ -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 <see cref="CacheTagHelper"/>.
/// </summary>
/// <param name="memoryCache">The <see cref="IMemoryCache"/>.</param>
public CacheTagHelper(IMemoryCache memoryCache)
public CacheTagHelper(IMemoryCache memoryCache, HtmlEncoder htmlEncoder)
{
MemoryCache = memoryCache;
HtmlEncoder = htmlEncoder;
}
/// <inheritdoc />
@ -60,6 +64,11 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
/// </summary>
protected IMemoryCache MemoryCache { get; }
/// <summary>
/// Gets the <see cref="System.Text.Encodings.Web.HtmlEncoder"/> which encodes the content to be cached.
/// </summary>
protected HtmlEncoder HtmlEncoder { get; }
/// <summary>
/// Gets or sets the <see cref="ViewContext"/> for the current executing View.
/// </summary>
@ -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]);
}
}
}
}
}

View File

@ -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<HtmlString>(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<HtmlTextWriter>();
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());
}
}
}

View File

@ -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<char>(new char[] { 'a', 'b', 'c', 'd' }, 1, 3);
var input2 = new ArraySegment<char>(new char[] { 'e', 'f' }, 0, 2);
var input3 = new ArraySegment<char>(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();
}
}
}

View File

@ -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(

View File

@ -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<TextWriter>();
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<TextWriter> { 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<TextWriter>();
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<object> { "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<TextWriter>();
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<char>(new char[] { 'a', 'b', 'c', 'd' }, 1, 3);
var input2 = new ArraySegment<char>(new char[] { 'e', 'f' }, 0, 2);
var input3 = new ArraySegment<char>(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<object>(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<object>(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()

View File

@ -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<IRazorBufferScope, TestRazorBufferScope>()
.BuildServiceProvider();
httpContext.RequestServices = serviceProvider;
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
return new ViewContext(
actionContext,

View File

@ -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<RazorValue>(razorValues, _offset, _count));
}
}
}

View File

@ -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<IMemoryCache>())
var cacheTagHelper = new CacheTagHelper(Mock.Of<IMemoryCache>(), new HtmlTestEncoder())
{
ViewContext = GetViewContext()
};
@ -55,7 +57,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
{
// Arrange
var tagHelperContext = GetTagHelperContext();
var cacheTagHelper = new CacheTagHelper(Mock.Of<IMemoryCache>())
var cacheTagHelper = new CacheTagHelper(Mock.Of<IMemoryCache>(), 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<IMemoryCache>())
var cacheTagHelper = new CacheTagHelper(Mock.Of<IMemoryCache>(), 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<IMemoryCache>())
var cacheTagHelper = new CacheTagHelper(Mock.Of<IMemoryCache>(), 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<IMemoryCache>())
var cacheTagHelper = new CacheTagHelper(Mock.Of<IMemoryCache>(), 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<IMemoryCache>())
var cacheTagHelper = new CacheTagHelper(Mock.Of<IMemoryCache>(), 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<IMemoryCache>())
var cacheTagHelper = new CacheTagHelper(Mock.Of<IMemoryCache>(), 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<IMemoryCache>())
var cacheTagHelper = new CacheTagHelper(Mock.Of<IMemoryCache>(), 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<IMemoryCache>())
var cacheTagHelper = new CacheTagHelper(Mock.Of<IMemoryCache>(), 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("<cache>");
tagHelperOutput1.PostContent.SetContent("</cache>");
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("<cache>");
tagHelperOutput2.PostContent.SetContent("</cache>");
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<IChangeToken>(), Mock.Of<IChangeToken>() };
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("<cache>");
tagHelperOutput1.PostContent.SetContent("</cache>");
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("<cache>");
tagHelperOutput2.PostContent.SetContent("</cache>");
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("<cache>");
tagHelperOutput1.PostContent.SetContent("</cache>");
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("<cache>");
tagHelperOutput2.PostContent.SetContent("</cache>");
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("<cache>");
tagHelperOutput1.PostContent.SetContent("</cache>");
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("<cache>");
tagHelperOutput2.PostContent.SetContent("</cache>");
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("<cache>");
tagHelperOutput.PostContent.SetContent("</cache>");
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>(tagHelperContent);
});
}