diff --git a/src/Microsoft.AspNet.Mvc.Razor/BufferEntryCollection.cs b/src/Microsoft.AspNet.Mvc.Razor/BufferEntryCollection.cs new file mode 100644 index 0000000000..a4e5d28bdd --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Razor/BufferEntryCollection.cs @@ -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 +{ + /// + /// Represents a hierarchy of strings and provides an enumerator that iterates over it as a sequence. + /// + public class BufferEntryCollection : IEnumerable + { + // Specifies the maximum size we'll allow for direct conversion from + // char arrays to string. + private const int MaxCharToStringLength = 1024; + private readonly List _buffer = new List(); + + public IReadOnlyList BufferEntries + { + get { return _buffer; } + } + + /// + /// Adds a string value to the buffer. + /// + /// The value to add. + public void Add(string value) + { + _buffer.Add(value); + } + + /// + /// Adds a subarray of characters to the buffer. + /// + /// The array to add. + /// The character position in the array at which to start copying data. + /// The number of characters to copy. + 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; + } + } + + /// + /// Adds an instance of to the buffer. + /// + /// The buffer collection to add. + public void Add([NotNull] BufferEntryCollection buffer) + { + _buffer.Add(buffer.BufferEntries); + } + + /// + public IEnumerator GetEnumerator() + { + return new BufferEntryEnumerator(_buffer); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + private sealed class BufferEntryEnumerator : IEnumerator + { + private readonly Stack> _enumerators = new Stack>(); + private readonly List _initialBuffer; + + public BufferEntryEnumerator(List buffer) + { + _initialBuffer = buffer; + Reset(); + } + + public IEnumerator 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; + 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(); + } + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Razor/IRazorPage.cs b/src/Microsoft.AspNet.Mvc.Razor/IRazorPage.cs index 1b39030d2a..057f066d2a 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/IRazorPage.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/IRazorPage.cs @@ -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 /// ViewContext ViewContext { get; set; } + /// + /// Gets or sets the action invoked to render the body. + /// + // TODO: https://github.com/aspnet/Mvc/issues/845 tracks making this async + Action RenderBodyDelegate { get; set; } + /// /// Gets the path to the page. /// string Path { get; set; } - string BodyContent { get; set; } - /// /// Gets or sets the path of a layout page. /// diff --git a/src/Microsoft.AspNet.Mvc.Razor/Microsoft.AspNet.Mvc.Razor.kproj b/src/Microsoft.AspNet.Mvc.Razor/Microsoft.AspNet.Mvc.Razor.kproj index e895509e69..68240d76e0 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/Microsoft.AspNet.Mvc.Razor.kproj +++ b/src/Microsoft.AspNet.Mvc.Razor/Microsoft.AspNet.Mvc.Razor.kproj @@ -22,6 +22,7 @@ + @@ -30,6 +31,8 @@ + + diff --git a/src/Microsoft.AspNet.Mvc.Razor/Properties/AssemblyInfo.cs b/src/Microsoft.AspNet.Mvc.Razor/Properties/AssemblyInfo.cs index 6d457fde04..d7eeff62c3 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/Properties/AssemblyInfo.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/Properties/AssemblyInfo.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")] \ No newline at end of file +[assembly: InternalsVisibleTo("Microsoft.AspNet.Mvc.Razor.Test")] diff --git a/src/Microsoft.AspNet.Mvc.Razor/RazorPage.cs b/src/Microsoft.AspNet.Mvc.Razor/RazorPage.cs index 6aab1aca64..a18a792994 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/RazorPage.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/RazorPage.cs @@ -88,7 +88,8 @@ namespace Microsoft.AspNet.Mvc.Razor } } - public string BodyContent { get; set; } + /// + public Action RenderBodyDelegate { get; set; } /// public Dictionary 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")); diff --git a/src/Microsoft.AspNet.Mvc.Razor/RazorTextWriter.cs b/src/Microsoft.AspNet.Mvc.Razor/RazorTextWriter.cs new file mode 100644 index 0000000000..5159e48a19 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Razor/RazorTextWriter.cs @@ -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 +{ + /// + /// A that represents individual write operations as a sequence of strings. + /// + /// + /// This is primarily designed to avoid creating large in-memory strings. + /// Refer to https://aspnetwebstack.codeplex.com/workitem/585 for more details. + /// + 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; } + + /// + public override void Write(char value) + { + Buffer.Add(value.ToString()); + } + + /// + 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); + } + + /// + public override void Write(string value) + { + if (!string.IsNullOrEmpty(value)) + { + Buffer.Add(value); + } + } + + /// + public override Task WriteAsync(char value) + { + Write(value); + return _completedTask; + } + + /// + public override Task WriteAsync([NotNull] char[] buffer, int index, int count) + { + Write(buffer, index, count); + return _completedTask; + } + + /// + public override Task WriteAsync(string value) + { + Write(value); + return _completedTask; + } + + /// + public override void WriteLine() + { + Buffer.Add(Environment.NewLine); + } + + /// + public override void WriteLine(string value) + { + Write(value); + WriteLine(); + } + + /// + public override Task WriteLineAsync(char value) + { + WriteLine(value); + return _completedTask; + } + + /// + public override Task WriteLineAsync(char[] value, int start, int offset) + { + WriteLine(value, start, offset); + return _completedTask; + } + + /// + public override Task WriteLineAsync(string value) + { + WriteLine(value); + return _completedTask; + } + + /// + public override Task WriteLineAsync() + { + WriteLine(); + return _completedTask; + } + + /// + /// Copies the content of the to the instance. + /// + /// The writer to copy contents to. + public void CopyTo(TextWriter writer) + { + var targetRazorTextWriter = writer as RazorTextWriter; + if (targetRazorTextWriter != null) + { + targetRazorTextWriter.Buffer.Add(Buffer); + } + else + { + WriteList(writer, Buffer); + } + } + + /// + /// Copies the content of the to the specified instance. + /// + /// The writer to copy contents to. + /// A task that represents the asynchronous copy operation. + 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); + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Razor/RazorView.cs b/src/Microsoft.AspNet.Mvc.Razor/RazorView.cs index 07efd3fbcc..31bdd04ea0 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/RazorView.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/RazorView.cs @@ -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 RenderPageAsync(IRazorPage page, - ViewContext context, - bool executeViewStart) + private async Task 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); } } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Microsoft.AspNet.Mvc.Core.Test.kproj b/test/Microsoft.AspNet.Mvc.Core.Test/Microsoft.AspNet.Mvc.Core.Test.kproj index 448c9a3636..9cc9c1569c 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/Microsoft.AspNet.Mvc.Core.Test.kproj +++ b/test/Microsoft.AspNet.Mvc.Core.Test/Microsoft.AspNet.Mvc.Core.Test.kproj @@ -104,6 +104,7 @@ + diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/BufferEntryCollectionTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/BufferEntryCollectionTest.cs new file mode 100644 index 0000000000..cd6999435b --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/BufferEntryCollectionTest.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( + () => 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( + () => collection.Add(new[] { 'a', 'b', 'c' }, 1, 3)); + Assert.Equal("count", ex.ParamName); + } + + public static IEnumerable 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 expected) + { + // Arrange + var collection = new BufferEntryCollection(); + + // Act + collection.Add(value, index, count); + + // Assert + Assert.Equal(expected, collection.BufferEntries); + } + + public static IEnumerable 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); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/Microsoft.AspNet.Mvc.Razor.Test.kproj b/test/Microsoft.AspNet.Mvc.Razor.Test/Microsoft.AspNet.Mvc.Razor.Test.kproj index 75641c822d..f6d31ead62 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Test/Microsoft.AspNet.Mvc.Razor.Test.kproj +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/Microsoft.AspNet.Mvc.Razor.Test.kproj @@ -21,9 +21,11 @@ + + diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorPageTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorPageTest.cs index b59cd388b3..081df63fe2 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorPageTest.cs +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorPageTest.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 { { @@ -289,9 +289,14 @@ Layout end new StringWriter()); } + private static Action CreateBodyAction(string value) + { + return writer => writer.Write(value); + } + public abstract class TestableRazorPage : RazorPage { - public HtmlString RenderBodyPublic() + public HelperResult RenderBodyPublic() { return base.RenderBody(); } diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorTextWriterTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorTextWriterTest.cs new file mode 100644 index 0000000000..ee3a49ae37 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorTextWriterTest.cs @@ -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(expected, writer.Buffer.BufferEntries); + } + + [Fact] + [ReplaceCulture] + public void WriteLine_WritesDataTypes_ToBuffer() + { + // Arrange + var newLine = Environment.NewLine; + var expected = new List { "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(new char[] { 'a', 'b', 'c', 'd' }, 1, 3); + var input2 = new ArraySegment(new char[] { 'e', 'f' }, 0, 2); + var input3 = new ArraySegment(new char[] { 'g', 'h', 'i', 'j' }, 3, 1); + var writer = new RazorTextWriter(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(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(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"; + } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewTest.cs index 686d9affee..acc9b39ed2 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewTest.cs +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewTest.cs @@ -143,7 +143,7 @@ namespace Microsoft.AspNet.Mvc.Razor await view.RenderAsync(viewContext); // Assert - Assert.IsType(actual); + Assert.IsType(actual); Assert.NotSame(original, actual); }