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:
Pranav K 2014-07-20 20:00:30 -07:00
parent dc58eb297d
commit 5168808906
13 changed files with 765 additions and 33 deletions

View File

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

View File

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

View File

@ -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" />

View File

@ -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")]

View File

@ -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"));

View File

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

View File

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

View File

@ -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" />

View File

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

View File

@ -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" />

View File

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

View File

@ -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";
}
}
}
}

View File

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