Introducing RazorTextWriter
RazorTextWriter represents the result of rendering a page as a sequence of strings rather than a concatenated string. This avoids building up large strings in memory.
This commit is contained in:
parent
dc58eb297d
commit
5168808906
|
|
@ -0,0 +1,176 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. 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;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.Razor
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a hierarchy of strings and provides an enumerator that iterates over it as a sequence.
|
||||
/// </summary>
|
||||
public class BufferEntryCollection : IEnumerable<string>
|
||||
{
|
||||
// Specifies the maximum size we'll allow for direct conversion from
|
||||
// char arrays to string.
|
||||
private const int MaxCharToStringLength = 1024;
|
||||
private readonly List<object> _buffer = new List<object>();
|
||||
|
||||
public IReadOnlyList<object> BufferEntries
|
||||
{
|
||||
get { return _buffer; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a string value to the buffer.
|
||||
/// </summary>
|
||||
/// <param name="value">The value to add.</param>
|
||||
public void Add(string value)
|
||||
{
|
||||
_buffer.Add(value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a subarray of characters to the buffer.
|
||||
/// </summary>
|
||||
/// <param name="value">The array to add.</param>
|
||||
/// <param name="index">The character position in the array at which to start copying data.</param>
|
||||
/// <param name="count">The number of characters to copy.</param>
|
||||
public void Add([NotNull] char[] value, int index, int count)
|
||||
{
|
||||
if (index < 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException("index");
|
||||
}
|
||||
if (count < 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException("count");
|
||||
}
|
||||
if (value.Length - index < count)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException("count");
|
||||
}
|
||||
|
||||
while (count > 0)
|
||||
{
|
||||
// Split large char arrays into 1KB strings.
|
||||
var currentCount = Math.Min(count, MaxCharToStringLength);
|
||||
Add(new string(value, index, currentCount));
|
||||
index += currentCount;
|
||||
count -= currentCount;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds an instance of <see cref="BufferEntryCollection"/> to the buffer.
|
||||
/// </summary>
|
||||
/// <param name="buffer">The buffer collection to add.</param>
|
||||
public void Add([NotNull] BufferEntryCollection buffer)
|
||||
{
|
||||
_buffer.Add(buffer.BufferEntries);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerator<string> GetEnumerator()
|
||||
{
|
||||
return new BufferEntryEnumerator(_buffer);
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
{
|
||||
return GetEnumerator();
|
||||
}
|
||||
|
||||
private sealed class BufferEntryEnumerator : IEnumerator<string>
|
||||
{
|
||||
private readonly Stack<IEnumerator<object>> _enumerators = new Stack<IEnumerator<object>>();
|
||||
private readonly List<object> _initialBuffer;
|
||||
|
||||
public BufferEntryEnumerator(List<object> buffer)
|
||||
{
|
||||
_initialBuffer = buffer;
|
||||
Reset();
|
||||
}
|
||||
|
||||
public IEnumerator<object> CurrentEnumerator
|
||||
{
|
||||
get
|
||||
{
|
||||
return _enumerators.Peek();
|
||||
}
|
||||
}
|
||||
|
||||
public string Current
|
||||
{
|
||||
get
|
||||
{
|
||||
var currentEnumerator = CurrentEnumerator;
|
||||
Debug.Assert(currentEnumerator != null);
|
||||
|
||||
return (string)currentEnumerator.Current;
|
||||
}
|
||||
}
|
||||
|
||||
object IEnumerator.Current
|
||||
{
|
||||
get
|
||||
{
|
||||
return Current;
|
||||
}
|
||||
}
|
||||
|
||||
public bool MoveNext()
|
||||
{
|
||||
var currentEnumerator = CurrentEnumerator;
|
||||
if (currentEnumerator.MoveNext())
|
||||
{
|
||||
var current = currentEnumerator.Current;
|
||||
var buffer = current as List<object>;
|
||||
if (buffer != null)
|
||||
{
|
||||
// If the next item is a collection, recursively call in to it.
|
||||
var enumerator = buffer.GetEnumerator();
|
||||
_enumerators.Push(enumerator);
|
||||
return MoveNext();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
else if (_enumerators.Count > 1)
|
||||
{
|
||||
// The current enumerator is exhausted and we have a parent.
|
||||
// Pop the current enumerator out and continue with it's parent.
|
||||
var enumerator = _enumerators.Pop();
|
||||
enumerator.Dispose();
|
||||
|
||||
return MoveNext();
|
||||
}
|
||||
|
||||
// We've exactly one element in our stack which cannot move next.
|
||||
return false;
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
DisposeEnumerators();
|
||||
|
||||
_enumerators.Push(_initialBuffer.GetEnumerator());
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
DisposeEnumerators();
|
||||
}
|
||||
|
||||
private void DisposeEnumerators()
|
||||
{
|
||||
while (_enumerators.Count > 0)
|
||||
{
|
||||
_enumerators.Pop().Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,9 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.Razor
|
||||
|
|
@ -16,13 +18,17 @@ namespace Microsoft.AspNet.Mvc.Razor
|
|||
/// </summary>
|
||||
ViewContext ViewContext { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the action invoked to render the body.
|
||||
/// </summary>
|
||||
// TODO: https://github.com/aspnet/Mvc/issues/845 tracks making this async
|
||||
Action<TextWriter> RenderBodyDelegate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the path to the page.
|
||||
/// </summary>
|
||||
string Path { get; set; }
|
||||
|
||||
string BodyContent { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the path of a layout page.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@
|
|||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="AttributeValue.cs" />
|
||||
<Compile Include="BufferEntryCollection.cs" />
|
||||
<Compile Include="Compilation\CompilationFailedException.cs" />
|
||||
<Compile Include="Compilation\CompilationMessage.cs" />
|
||||
<Compile Include="Compilation\CompilationResult.cs" />
|
||||
|
|
@ -30,6 +31,8 @@
|
|||
<Compile Include="Compilation\RoslynCompilationService.cs" />
|
||||
<Compile Include="Extensions\DictionaryExtensions.cs" />
|
||||
<Compile Include="IRazorPage.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="RazorTextWriter.cs" />
|
||||
<Compile Include="IViewStartProvider.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="ViewStartProvider.cs" />
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
|
||||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("Microsoft.AspNet.Mvc.Razor.Test")]
|
||||
[assembly: InternalsVisibleTo("Microsoft.AspNet.Mvc.Razor.Test")]
|
||||
|
|
|
|||
|
|
@ -88,7 +88,8 @@ namespace Microsoft.AspNet.Mvc.Razor
|
|||
}
|
||||
}
|
||||
|
||||
public string BodyContent { get; set; }
|
||||
/// <inheritdoc />
|
||||
public Action<TextWriter> RenderBodyDelegate { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Dictionary<string, HelperResult> PreviousSectionWriters { get; set; }
|
||||
|
|
@ -243,14 +244,15 @@ namespace Microsoft.AspNet.Mvc.Razor
|
|||
WritePositionTaggedLiteral(writer, value.Value, value.Position);
|
||||
}
|
||||
|
||||
protected virtual HtmlString RenderBody()
|
||||
protected virtual HelperResult RenderBody()
|
||||
{
|
||||
if (BodyContent == null)
|
||||
if (RenderBodyDelegate == null)
|
||||
{
|
||||
throw new InvalidOperationException(Resources.FormatRenderBodyCannotBeCalled("RenderBody"));
|
||||
}
|
||||
|
||||
_renderedBody = true;
|
||||
return new HtmlString(BodyContent);
|
||||
return new HelperResult(RenderBodyDelegate);
|
||||
}
|
||||
|
||||
public void DefineSection(string name, HelperResult action)
|
||||
|
|
@ -315,7 +317,7 @@ namespace Microsoft.AspNet.Mvc.Razor
|
|||
}
|
||||
|
||||
// If BodyContent is set, ensure it was rendered.
|
||||
if (BodyContent != null && !_renderedBody)
|
||||
if (RenderBodyDelegate != null && !_renderedBody)
|
||||
{
|
||||
// If a body was defined, then RenderBody should have been called.
|
||||
throw new InvalidOperationException(Resources.FormatRenderBodyNotCalled("RenderBody"));
|
||||
|
|
|
|||
|
|
@ -0,0 +1,185 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.Razor
|
||||
{
|
||||
/// <summary>
|
||||
/// A <see cref="TextWriter"/> that represents individual write operations as a sequence of strings.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is primarily designed to avoid creating large in-memory strings.
|
||||
/// Refer to https://aspnetwebstack.codeplex.com/workitem/585 for more details.
|
||||
/// </remarks>
|
||||
public class RazorTextWriter : TextWriter
|
||||
{
|
||||
private static readonly Task _completedTask = Task.FromResult(0);
|
||||
private readonly Encoding _encoding;
|
||||
|
||||
public RazorTextWriter(Encoding encoding)
|
||||
{
|
||||
_encoding = encoding;
|
||||
Buffer = new BufferEntryCollection();
|
||||
}
|
||||
|
||||
public override Encoding Encoding
|
||||
{
|
||||
get { return _encoding; }
|
||||
}
|
||||
|
||||
public BufferEntryCollection Buffer { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Write(char value)
|
||||
{
|
||||
Buffer.Add(value.ToString());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Write([NotNull] char[] buffer, int index, int count)
|
||||
{
|
||||
if (index < 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException("index");
|
||||
}
|
||||
if (count < 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException("count");
|
||||
}
|
||||
if (buffer.Length - index < count)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException("count");
|
||||
}
|
||||
|
||||
Buffer.Add(buffer, index, count);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Write(string value)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(value))
|
||||
{
|
||||
Buffer.Add(value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task WriteAsync(char value)
|
||||
{
|
||||
Write(value);
|
||||
return _completedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task WriteAsync([NotNull] char[] buffer, int index, int count)
|
||||
{
|
||||
Write(buffer, index, count);
|
||||
return _completedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task WriteAsync(string value)
|
||||
{
|
||||
Write(value);
|
||||
return _completedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void WriteLine()
|
||||
{
|
||||
Buffer.Add(Environment.NewLine);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void WriteLine(string value)
|
||||
{
|
||||
Write(value);
|
||||
WriteLine();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task WriteLineAsync(char value)
|
||||
{
|
||||
WriteLine(value);
|
||||
return _completedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task WriteLineAsync(char[] value, int start, int offset)
|
||||
{
|
||||
WriteLine(value, start, offset);
|
||||
return _completedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task WriteLineAsync(string value)
|
||||
{
|
||||
WriteLine(value);
|
||||
return _completedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task WriteLineAsync()
|
||||
{
|
||||
WriteLine();
|
||||
return _completedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copies the content of the <see cref="RazorTextWriter"/> to the <see cref="TextWriter"/> instance.
|
||||
/// </summary>
|
||||
/// <param name="writer">The writer to copy contents to.</param>
|
||||
public void CopyTo(TextWriter writer)
|
||||
{
|
||||
var targetRazorTextWriter = writer as RazorTextWriter;
|
||||
if (targetRazorTextWriter != null)
|
||||
{
|
||||
targetRazorTextWriter.Buffer.Add(Buffer);
|
||||
}
|
||||
else
|
||||
{
|
||||
WriteList(writer, Buffer);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copies the content of the <see cref="RazorTextWriter"/> to the specified <see cref="TextWriter"/> instance.
|
||||
/// </summary>
|
||||
/// <param name="writer">The writer to copy contents to.</param>
|
||||
/// <returns>A task that represents the asynchronous copy operation.</returns>
|
||||
public Task CopyToAsync(TextWriter writer)
|
||||
{
|
||||
var targetRazorTextWriter = writer as RazorTextWriter;
|
||||
if (targetRazorTextWriter != null)
|
||||
{
|
||||
targetRazorTextWriter.Buffer.Add(Buffer);
|
||||
}
|
||||
else
|
||||
{
|
||||
return WriteListAsync(writer, Buffer);
|
||||
}
|
||||
|
||||
return _completedTask;
|
||||
}
|
||||
|
||||
private static void WriteList(TextWriter writer, BufferEntryCollection values)
|
||||
{
|
||||
foreach (var value in values)
|
||||
{
|
||||
writer.Write(value);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task WriteListAsync(TextWriter writer, BufferEntryCollection values)
|
||||
{
|
||||
foreach (var value in values)
|
||||
{
|
||||
await writer.WriteAsync(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,9 +2,6 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNet.Mvc.Rendering;
|
||||
|
||||
|
|
@ -48,8 +45,8 @@ namespace Microsoft.AspNet.Mvc.Razor
|
|||
{
|
||||
if (_executeViewHierarchy)
|
||||
{
|
||||
var bodyContent = await RenderPageAsync(_page, context, executeViewStart: true);
|
||||
await RenderLayoutAsync(context, bodyContent);
|
||||
var bodyWriter = await RenderPageAsync(_page, context, executeViewStart: true);
|
||||
await RenderLayoutAsync(context, bodyWriter);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -57,17 +54,16 @@ namespace Microsoft.AspNet.Mvc.Razor
|
|||
}
|
||||
}
|
||||
|
||||
private async Task<string> RenderPageAsync(IRazorPage page,
|
||||
ViewContext context,
|
||||
bool executeViewStart)
|
||||
private async Task<RazorTextWriter> RenderPageAsync(IRazorPage page,
|
||||
ViewContext context,
|
||||
bool executeViewStart)
|
||||
{
|
||||
var contentBuilder = new StringBuilder(1024);
|
||||
using (var bodyWriter = new StringWriter(contentBuilder))
|
||||
using (var bufferedWriter = new RazorTextWriter(context.Writer.Encoding))
|
||||
{
|
||||
// The writer for the body is passed through the ViewContext, allowing things like HtmlHelpers
|
||||
// and ViewComponents to reference it.
|
||||
var oldWriter = context.Writer;
|
||||
context.Writer = bodyWriter;
|
||||
context.Writer = bufferedWriter;
|
||||
try
|
||||
{
|
||||
if (executeViewStart)
|
||||
|
|
@ -75,15 +71,15 @@ namespace Microsoft.AspNet.Mvc.Razor
|
|||
// Execute view starts using the same context + writer as the page to render.
|
||||
await RenderViewStartAsync(context);
|
||||
}
|
||||
|
||||
await RenderPageCoreAsync(page, context);
|
||||
return bufferedWriter;
|
||||
}
|
||||
finally
|
||||
{
|
||||
context.Writer = oldWriter;
|
||||
}
|
||||
}
|
||||
|
||||
return contentBuilder.ToString();
|
||||
}
|
||||
|
||||
private async Task RenderPageCoreAsync(IRazorPage page, ViewContext context)
|
||||
|
|
@ -107,7 +103,7 @@ namespace Microsoft.AspNet.Mvc.Razor
|
|||
}
|
||||
|
||||
private async Task RenderLayoutAsync(ViewContext context,
|
||||
string bodyContent)
|
||||
RazorTextWriter bodyWriter)
|
||||
{
|
||||
// A layout page can specify another layout page. We'll need to continue
|
||||
// looking for layout pages until they're no longer specified.
|
||||
|
|
@ -122,9 +118,8 @@ namespace Microsoft.AspNet.Mvc.Razor
|
|||
}
|
||||
|
||||
layoutPage.PreviousSectionWriters = previousPage.SectionWriters;
|
||||
layoutPage.BodyContent = bodyContent;
|
||||
|
||||
bodyContent = await RenderPageAsync(layoutPage, context, executeViewStart: false);
|
||||
layoutPage.RenderBodyDelegate = bodyWriter.CopyTo;
|
||||
bodyWriter = await RenderPageAsync(layoutPage, context, executeViewStart: false);
|
||||
|
||||
// Verify that RenderBody is called, or that RenderSection is called for all sections
|
||||
layoutPage.EnsureBodyAndSectionsWereRendered();
|
||||
|
|
@ -132,7 +127,7 @@ namespace Microsoft.AspNet.Mvc.Razor
|
|||
previousPage = layoutPage;
|
||||
}
|
||||
|
||||
await context.Writer.WriteAsync(bodyContent);
|
||||
await bodyWriter.CopyToAsync(context.Writer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -104,6 +104,7 @@
|
|||
<Compile Include="Routing\AttributeRoutePrecedenceTests.cs" />
|
||||
<Compile Include="Routing\AttributeRouteTemplateTests.cs" />
|
||||
<Compile Include="Routing\AttributeRoutingTest.cs" />
|
||||
<Compile Include="StaticActionDiscoveryConventions.cs" />
|
||||
<Compile Include="StaticControllerAssemblyProvider.cs" />
|
||||
<Compile Include="Routing\AttributeRouteTests.cs" />
|
||||
<Compile Include="TestController.cs" />
|
||||
|
|
|
|||
|
|
@ -0,0 +1,160 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. 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.Linq;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.Razor
|
||||
{
|
||||
public class BufferEntryCollectionTest
|
||||
{
|
||||
[Fact]
|
||||
public void Add_AddsBufferEntries()
|
||||
{
|
||||
// Arrange
|
||||
var collection = new BufferEntryCollection();
|
||||
var inner = new BufferEntryCollection();
|
||||
|
||||
// Act
|
||||
collection.Add("Hello");
|
||||
collection.Add(new[] { 'a', 'b', 'c' }, 1, 2);
|
||||
collection.Add(inner);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("Hello", collection.BufferEntries[0]);
|
||||
Assert.Equal("bc", collection.BufferEntries[1]);
|
||||
Assert.Same(inner.BufferEntries, collection.BufferEntries[2]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddChar_ThrowsIfIndexIsOutOfBounds()
|
||||
{
|
||||
// Arrange
|
||||
var collection = new BufferEntryCollection();
|
||||
|
||||
// Act and Assert
|
||||
var ex = Assert.Throws<ArgumentOutOfRangeException>(
|
||||
() => collection.Add(new[] { 'a', 'b', 'c' }, -1, 2));
|
||||
Assert.Equal("index", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddChar_ThrowsIfCountWouldCauseOutOfBoundReads()
|
||||
{
|
||||
// Arrange
|
||||
var collection = new BufferEntryCollection();
|
||||
|
||||
// Act and Assert
|
||||
var ex = Assert.Throws<ArgumentOutOfRangeException>(
|
||||
() => collection.Add(new[] { 'a', 'b', 'c' }, 1, 3));
|
||||
Assert.Equal("count", ex.ParamName);
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> AddWithChar_RepresentsStringsAsChunkedEntriesData
|
||||
{
|
||||
get
|
||||
{
|
||||
var charArray1 = new[] { 'a' };
|
||||
var expected1 = new[] { "a" };
|
||||
yield return new object[] { charArray1, 0, 1, expected1 };
|
||||
|
||||
var charArray2 = Enumerable.Repeat('a', 10).ToArray();
|
||||
var expected2 = new[] { new string(charArray2) };
|
||||
yield return new object[] { charArray2, 0, 10, expected2 };
|
||||
|
||||
var charArray3 = Enumerable.Repeat('b', 1024).ToArray();
|
||||
var expected3 = new[] { new string('b', 1023) };
|
||||
yield return new object[] { charArray3, 1, 1023, expected3 };
|
||||
|
||||
var charArray4 = Enumerable.Repeat('c', 1027).ToArray();
|
||||
var expected4 = new[] { new string('c', 1024), "cc" };
|
||||
yield return new object[] { charArray4, 1, 1026, expected4 };
|
||||
|
||||
var charArray5 = Enumerable.Repeat('d', 4099).ToArray();
|
||||
var expected5 = new[] { new string('d', 1024), new string('d', 1024), new string('d', 1024), new string('d', 1024), "d" };
|
||||
yield return new object[] { charArray5, 2, 4097, expected5 };
|
||||
|
||||
var charArray6 = Enumerable.Repeat('e', 1025).ToArray();
|
||||
var expected6 = new[] { "ee" };
|
||||
yield return new object[] { charArray6, 1023, 2, expected6 };
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData("AddWithChar_RepresentsStringsAsChunkedEntriesData")]
|
||||
public void AddWithChar_RepresentsStringsAsChunkedEntries(char[] value, int index, int count, IList<object> expected)
|
||||
{
|
||||
// Arrange
|
||||
var collection = new BufferEntryCollection();
|
||||
|
||||
// Act
|
||||
collection.Add(value, index, count);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, collection.BufferEntries);
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> Enumerator_TraversesThroughBufferData
|
||||
{
|
||||
get
|
||||
{
|
||||
var collection1 = new BufferEntryCollection();
|
||||
collection1.Add("foo");
|
||||
collection1.Add("bar");
|
||||
|
||||
var expected1 = new[]
|
||||
{
|
||||
"foo",
|
||||
"bar"
|
||||
};
|
||||
yield return new object[] { collection1, expected1 };
|
||||
|
||||
// Nested collection
|
||||
var nestedCollection2 = new BufferEntryCollection();
|
||||
nestedCollection2.Add("level 1");
|
||||
var nestedCollection2SecondLevel = new BufferEntryCollection();
|
||||
nestedCollection2SecondLevel.Add("level 2");
|
||||
nestedCollection2.Add(nestedCollection2SecondLevel);
|
||||
var collection2 = new BufferEntryCollection();
|
||||
collection2.Add("foo");
|
||||
collection2.Add(nestedCollection2);
|
||||
collection2.Add("qux");
|
||||
|
||||
var expected2 = new[]
|
||||
{
|
||||
"foo",
|
||||
"level 1",
|
||||
"level 2",
|
||||
"qux"
|
||||
};
|
||||
yield return new object[] { collection2, expected2 };
|
||||
|
||||
// Nested collection
|
||||
var collection3 = new BufferEntryCollection();
|
||||
collection3.Add("Hello");
|
||||
var emptyNestedCollection = new BufferEntryCollection();
|
||||
emptyNestedCollection.Add(new BufferEntryCollection());
|
||||
collection3.Add(emptyNestedCollection);
|
||||
collection3.Add("world");
|
||||
|
||||
var expected3 = new[]
|
||||
{
|
||||
"Hello",
|
||||
"world"
|
||||
};
|
||||
yield return new object[] { collection3, expected3 };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[Theory]
|
||||
[MemberData("Enumerator_TraversesThroughBufferData")]
|
||||
public void Enumerator_TraversesThroughBuffer(BufferEntryCollection buffer, string[] expected)
|
||||
{
|
||||
// Act and Assert
|
||||
Assert.Equal(expected, buffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -21,9 +21,11 @@
|
|||
<Content Include="project.json" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="BufferEntryCollectionTest.cs" />
|
||||
<Compile Include="MvcRazorCodeParserTest.cs" />
|
||||
<Compile Include="RazorCompilationServiceTest.cs" />
|
||||
<Compile Include="RazorPageActivatorTest.cs" />
|
||||
<Compile Include="RazorTextWriterTest.cs" />
|
||||
<Compile Include="RazorViewEngineTest.cs" />
|
||||
<Compile Include="RazorPageTest.cs" />
|
||||
<Compile Include="RazorViewTest.cs" />
|
||||
|
|
|
|||
|
|
@ -122,7 +122,7 @@ namespace Microsoft.AspNet.Mvc.Razor
|
|||
{
|
||||
{ "baz", new HelperResult(writer => { }) }
|
||||
};
|
||||
page.BodyContent = "body-content";
|
||||
page.RenderBodyDelegate = CreateBodyAction("body-content");
|
||||
|
||||
// Act
|
||||
await page.ExecuteAsync();
|
||||
|
|
@ -146,7 +146,7 @@ namespace Microsoft.AspNet.Mvc.Razor
|
|||
{
|
||||
{ "baz", new HelperResult(writer => { }) }
|
||||
};
|
||||
page.BodyContent = "body-content";
|
||||
page.RenderBodyDelegate = CreateBodyAction("body-content");
|
||||
|
||||
// Act
|
||||
await page.ExecuteAsync();
|
||||
|
|
@ -211,7 +211,7 @@ namespace Microsoft.AspNet.Mvc.Razor
|
|||
var page = CreatePage(v =>
|
||||
{
|
||||
});
|
||||
page.BodyContent = "some content";
|
||||
page.RenderBodyDelegate = CreateBodyAction("some content");
|
||||
|
||||
// Act
|
||||
await page.ExecuteAsync();
|
||||
|
|
@ -240,7 +240,7 @@ Layout end
|
|||
v.WriteLiteral("Layout end" + Environment.NewLine);
|
||||
|
||||
});
|
||||
page.BodyContent = "body content" + Environment.NewLine;
|
||||
page.RenderBodyDelegate = CreateBodyAction("body content" + Environment.NewLine);
|
||||
page.PreviousSectionWriters = new Dictionary<string, HelperResult>
|
||||
{
|
||||
{
|
||||
|
|
@ -289,9 +289,14 @@ Layout end
|
|||
new StringWriter());
|
||||
}
|
||||
|
||||
private static Action<TextWriter> CreateBodyAction(string value)
|
||||
{
|
||||
return writer => writer.Write(value);
|
||||
}
|
||||
|
||||
public abstract class TestableRazorPage : RazorPage
|
||||
{
|
||||
public HtmlString RenderBodyPublic()
|
||||
public HelperResult RenderBodyPublic()
|
||||
{
|
||||
return base.RenderBody();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,197 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNet.Testing;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.Razor.Test
|
||||
{
|
||||
public class RazorTextWriterTest
|
||||
{
|
||||
[Fact]
|
||||
[ReplaceCulture]
|
||||
public void Write_WritesDataTypes_ToBuffer()
|
||||
{
|
||||
// Arrange
|
||||
var expected = new[] { "True", "3", "18446744073709551615", "Hello world", "3.14", "2.718", "m" };
|
||||
var writer = new RazorTextWriter(Encoding.UTF8);
|
||||
|
||||
// Act
|
||||
writer.Write(true);
|
||||
writer.Write(3);
|
||||
writer.Write(ulong.MaxValue);
|
||||
writer.Write(new TestClass());
|
||||
writer.Write(3.14);
|
||||
writer.Write(2.718m);
|
||||
writer.Write('m');
|
||||
|
||||
// Assert
|
||||
Assert.Equal<object>(expected, writer.Buffer.BufferEntries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[ReplaceCulture]
|
||||
public void WriteLine_WritesDataTypes_ToBuffer()
|
||||
{
|
||||
// Arrange
|
||||
var newLine = Environment.NewLine;
|
||||
var expected = new List<object> { "False", newLine, "1.1", newLine, "3", newLine };
|
||||
var writer = new RazorTextWriter(Encoding.UTF8);
|
||||
|
||||
// Act
|
||||
writer.WriteLine(false);
|
||||
writer.WriteLine(1.1f);
|
||||
writer.WriteLine(3L);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, writer.Buffer.BufferEntries);
|
||||
}
|
||||
|
||||
[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(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 buffer = writer.Buffer.BufferEntries;
|
||||
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(Encoding.UTF8);
|
||||
|
||||
// Act
|
||||
writer.WriteLine();
|
||||
await writer.WriteLineAsync();
|
||||
|
||||
// Assert
|
||||
var actual = writer.Buffer.BufferEntries;
|
||||
Assert.Equal<object>(new[] { newLine, newLine }, actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Write_WritesStringBuffer()
|
||||
{
|
||||
// Arrange
|
||||
var newLine = Environment.NewLine;
|
||||
var input1 = "Hello";
|
||||
var input2 = "from";
|
||||
var input3 = "ASP";
|
||||
var input4 = ".Net";
|
||||
var writer = new RazorTextWriter(Encoding.UTF8);
|
||||
|
||||
// Act
|
||||
writer.Write(input1);
|
||||
writer.WriteLine(input2);
|
||||
await writer.WriteAsync(input3);
|
||||
await writer.WriteLineAsync(input4);
|
||||
|
||||
// Assert
|
||||
var actual = writer.Buffer.BufferEntries;
|
||||
Assert.Equal<object>(new[] { input1, input2, newLine, input3, input4, newLine }, actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Copy_CopiesContent_IfTargetTextWriterIsARazorTextWriter()
|
||||
{
|
||||
// Arrange
|
||||
var source = new RazorTextWriter(Encoding.UTF8);
|
||||
var target = new RazorTextWriter(Encoding.UTF8);
|
||||
|
||||
// Act
|
||||
source.Write("Hello world");
|
||||
source.Write(new char[1], 0, 1);
|
||||
source.CopyTo(target);
|
||||
|
||||
// Assert
|
||||
// Make sure content was written to the source.
|
||||
Assert.Equal(2, source.Buffer.BufferEntries.Count);
|
||||
Assert.Equal(1, target.Buffer.BufferEntries.Count);
|
||||
Assert.Same(source.Buffer.BufferEntries, target.Buffer.BufferEntries[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Copy_WritesContent_IfTargetTextWriterIsNotARazorTextWriter()
|
||||
{
|
||||
// Arrange
|
||||
var source = new RazorTextWriter(Encoding.UTF8);
|
||||
var target = new StringWriter();
|
||||
var expected = @"Hello world
|
||||
abc";
|
||||
|
||||
// Act
|
||||
source.WriteLine("Hello world");
|
||||
source.Write(new[] { 'x', 'a', 'b', 'c' }, 1, 3);
|
||||
source.CopyTo(target);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, target.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CopyAsync_WritesContent_IfTargetTextWriterIsARazorTextWriter()
|
||||
{
|
||||
// Arrange
|
||||
var source = new RazorTextWriter(Encoding.UTF8);
|
||||
var target = new RazorTextWriter(Encoding.UTF8);
|
||||
|
||||
// Act
|
||||
source.WriteLine("Hello world");
|
||||
source.Write(new[] { 'x', 'a', 'b', 'c' }, 1, 3);
|
||||
await source.CopyToAsync(target);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(3, source.Buffer.BufferEntries.Count);
|
||||
Assert.Equal(1, target.Buffer.BufferEntries.Count);
|
||||
Assert.Same(source.Buffer.BufferEntries, target.Buffer.BufferEntries[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CopyAsync_WritesContent_IfTargetTextWriterIsNotARazorTextWriter()
|
||||
{
|
||||
// Arrange
|
||||
var source = new RazorTextWriter(Encoding.UTF8);
|
||||
var target = new StringWriter();
|
||||
var expected = @"Hello world
|
||||
";
|
||||
|
||||
// Act
|
||||
source.Write("Hello ");
|
||||
await source.WriteLineAsync(new[] { 'w', 'o', 'r', 'l', 'd' });
|
||||
await source.CopyToAsync(target);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, target.ToString());
|
||||
}
|
||||
|
||||
private class TestClass
|
||||
{
|
||||
public override string ToString()
|
||||
{
|
||||
return "Hello world";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -143,7 +143,7 @@ namespace Microsoft.AspNet.Mvc.Razor
|
|||
await view.RenderAsync(viewContext);
|
||||
|
||||
// Assert
|
||||
Assert.IsType<StringWriter>(actual);
|
||||
Assert.IsType<RazorTextWriter>(actual);
|
||||
Assert.NotSame(original, actual);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue