From 18e11f546d76c949350b2c796a16f8abcfbae1cc Mon Sep 17 00:00:00 2001 From: Pranav K Date: Fri, 10 Oct 2014 10:53:12 -0700 Subject: [PATCH] DecorateWriter does not get called for partial views rendered via Html.PartialAsync * Introducing StringCollectionTextWriter to buffer the contents of PartialAsync * Ensure DecorateWriter is called for partial views Fixes #1266 --- .../Rendering}/BufferEntryCollection.cs | 2 +- .../Rendering/Html/HtmlHelper.cs | 4 +- .../Rendering/HtmlString.cs | 34 +++ .../Rendering/StringCollectionTextWriter.cs | 190 +++++++++++++++ src/Microsoft.AspNet.Mvc.Razor/RazorPage.cs | 2 +- .../RazorTextWriter.cs | 228 +++++------------- src/Microsoft.AspNet.Mvc.Razor/RazorView.cs | 19 +- .../Rendering}/BufferEntryCollectionTest.cs | 2 +- .../StringCollectionTextWriterTest.cs | 160 ++++++++++++ .../RazorInstrumentationTests.cs | 147 +++++++---- .../RazorPageTest.cs | 27 +++ .../RazorTextWriterTest.cs | 38 +-- .../RazorViewTest.cs | 17 +- .../HomeController.cs | 5 + .../Views/Home/VIewWithPartial.cshtml | 5 + .../Views/Home/_PartialView.cshtml | 4 + 16 files changed, 648 insertions(+), 236 deletions(-) rename src/{Microsoft.AspNet.Mvc.Razor => Microsoft.AspNet.Mvc.Core/Rendering}/BufferEntryCollection.cs (99%) create mode 100644 src/Microsoft.AspNet.Mvc.Core/Rendering/StringCollectionTextWriter.cs rename test/{Microsoft.AspNet.Mvc.Razor.Test => Microsoft.AspNet.Mvc.Core.Test/Rendering}/BufferEntryCollectionTest.cs (99%) create mode 100644 test/Microsoft.AspNet.Mvc.Core.Test/Rendering/StringCollectionTextWriterTest.cs create mode 100644 test/WebSites/RazorInstrumentationWebsite/Views/Home/VIewWithPartial.cshtml create mode 100644 test/WebSites/RazorInstrumentationWebsite/Views/Home/_PartialView.cshtml diff --git a/src/Microsoft.AspNet.Mvc.Razor/BufferEntryCollection.cs b/src/Microsoft.AspNet.Mvc.Core/Rendering/BufferEntryCollection.cs similarity index 99% rename from src/Microsoft.AspNet.Mvc.Razor/BufferEntryCollection.cs rename to src/Microsoft.AspNet.Mvc.Core/Rendering/BufferEntryCollection.cs index a4e5d28bdd..0a97d95169 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/BufferEntryCollection.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Rendering/BufferEntryCollection.cs @@ -6,7 +6,7 @@ using System.Collections; using System.Collections.Generic; using System.Diagnostics; -namespace Microsoft.AspNet.Mvc.Razor +namespace Microsoft.AspNet.Mvc.Rendering { /// /// Represents a hierarchy of strings and provides an enumerator that iterates over it as a sequence. diff --git a/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/HtmlHelper.cs b/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/HtmlHelper.cs index ae2858ba32..8063dfeeb1 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/HtmlHelper.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/HtmlHelper.cs @@ -330,11 +330,11 @@ namespace Microsoft.AspNet.Mvc.Rendering public async Task PartialAsync([NotNull] string partialViewName, object model, ViewDataDictionary viewData) { - using (var writer = new StringWriter(CultureInfo.CurrentCulture)) + using (var writer = new StringCollectionTextWriter(Encoding.UTF8)) { await RenderPartialCoreAsync(partialViewName, model, viewData, writer); - return new HtmlString(writer.ToString()); + return new HtmlString(writer); } } diff --git a/src/Microsoft.AspNet.Mvc.Core/Rendering/HtmlString.cs b/src/Microsoft.AspNet.Mvc.Core/Rendering/HtmlString.cs index a51265511e..c791e66719 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Rendering/HtmlString.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Rendering/HtmlString.cs @@ -1,12 +1,15 @@ // 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.IO; + namespace Microsoft.AspNet.Mvc.Rendering { public class HtmlString { private static readonly HtmlString _empty = new HtmlString(string.Empty); + private readonly StringCollectionTextWriter _writer; private readonly string _input; public HtmlString(string input) @@ -14,6 +17,15 @@ namespace Microsoft.AspNet.Mvc.Rendering _input = input; } + /// + /// Initializes a new instance of that is backed by . + /// + /// + public HtmlString([NotNull] StringCollectionTextWriter writer) + { + _writer = writer; + } + public static HtmlString Empty { get @@ -22,8 +34,30 @@ namespace Microsoft.AspNet.Mvc.Rendering } } + /// + /// Writes the value in this instance of to the target . + /// + /// The to write contents to. + public void WriteTo(TextWriter targetWriter) + { + if (_writer != null) + { + _writer.CopyTo(targetWriter); + } + else + { + targetWriter.Write(_input); + } + } + + /// public override string ToString() { + if (_writer != null) + { + return _writer.ToString(); + } + return _input; } } diff --git a/src/Microsoft.AspNet.Mvc.Core/Rendering/StringCollectionTextWriter.cs b/src/Microsoft.AspNet.Mvc.Core/Rendering/StringCollectionTextWriter.cs new file mode 100644 index 0000000000..b8475e273c --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/Rendering/StringCollectionTextWriter.cs @@ -0,0 +1,190 @@ +// 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.Rendering +{ + /// + /// 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 StringCollectionTextWriter : TextWriter + { + private static readonly Task _completedTask = Task.FromResult(0); + private readonly Encoding _encoding; + + /// + /// Creates a new instance of . + /// + /// The character in which the output is written. + public StringCollectionTextWriter(Encoding encoding) + { + _encoding = encoding; + Buffer = new BufferEntryCollection(); + } + + /// + public override Encoding Encoding + { + get { return _encoding; } + } + + /// + /// A collection of entries buffered by this instance of . + /// + 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(nameof(index)); + } + if (count < 0 || (buffer.Length - index < count)) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + + Buffer.Add(buffer, index, count); + } + + /// + public override void Write(string value) + { + if (string.IsNullOrEmpty(value)) + { + return; + } + + 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; + } + + /// + public void CopyTo(TextWriter writer) + { + var targetStringCollectionWriter = writer as StringCollectionTextWriter; + if (targetStringCollectionWriter != null) + { + targetStringCollectionWriter.Buffer.Add(Buffer); + } + else + { + WriteList(writer, Buffer); + } + } + + /// + public Task CopyToAsync(TextWriter writer) + { + var targetStringCollectionWriter = writer as StringCollectionTextWriter; + if (targetStringCollectionWriter != null) + { + targetStringCollectionWriter.Buffer.Add(Buffer); + } + else + { + return WriteListAsync(writer, Buffer); + } + + return _completedTask; + } + + /// + public override string ToString() + { + return string.Join(string.Empty, Buffer); + } + + 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/RazorPage.cs b/src/Microsoft.AspNet.Mvc.Razor/RazorPage.cs index 710cfb611b..3b01f1e05a 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/RazorPage.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/RazorPage.cs @@ -227,7 +227,7 @@ namespace Microsoft.AspNet.Mvc.Razor var htmlString = value as HtmlString; if (htmlString != null) { - writer.Write(htmlString.ToString()); + writer.Write(htmlString); } else { diff --git a/src/Microsoft.AspNet.Mvc.Razor/RazorTextWriter.cs b/src/Microsoft.AspNet.Mvc.Razor/RazorTextWriter.cs index 9ea894453c..32fdecf300 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/RazorTextWriter.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/RazorTextWriter.cs @@ -5,25 +5,22 @@ using System; using System.IO; using System.Text; using System.Threading.Tasks; +using Microsoft.AspNet.Mvc.Rendering; namespace Microsoft.AspNet.Mvc.Razor { /// - /// A that represents individual write operations as a sequence of strings when buffering. - /// The writer is backed by an unbuffered writer. When Flush or FlushAsync is invoked, the writer - /// copies all content to the unbuffered writier and switches to writing to the unbuffered writer for all further - /// write operations. + /// A that is backed by a unbuffered writer (over the Response stream) and a buffered + /// . When Flush or FlushAsync is invoked, the writer + /// copies all content from the buffered writer to the unbuffered one and switches to writing to the unbuffered + /// writer for all further write operations. /// /// - /// This is primarily designed to avoid creating large in-memory strings. - /// Refer to https://aspnetwebstack.codeplex.com/workitem/585 for more details. + /// This type is designed to avoid creating large in-memory strings when buffering and supporting the contract that + /// expects. /// public class RazorTextWriter : TextWriter, IBufferedTextWriter { - private static readonly Task _completedTask = Task.FromResult(0); - private readonly TextWriter _unbufferedWriter; - private readonly Encoding _encoding; - /// /// Creates a new instance of . /// @@ -32,36 +29,44 @@ namespace Microsoft.AspNet.Mvc.Razor /// The character in which the output is written. public RazorTextWriter(TextWriter unbufferedWriter, Encoding encoding) { - _unbufferedWriter = unbufferedWriter; - _encoding = encoding; - Buffer = new BufferEntryCollection(); + UnbufferedWriter = unbufferedWriter; + BufferedWriter = new StringCollectionTextWriter(encoding); + TargetWriter = BufferedWriter; } /// public override Encoding Encoding { - get { return _encoding; } + get { return BufferedWriter.Encoding; } } /// public bool IsBuffering { get; private set; } = true; - /// - /// A collection of entries buffered by this instance of . - /// - public BufferEntryCollection Buffer { get; private set; } + // Internal for unit testing + internal StringCollectionTextWriter BufferedWriter { get; } + + private TextWriter UnbufferedWriter { get; } + + private TextWriter TargetWriter { get; set; } /// public override void Write(char value) { - if (IsBuffering) + TargetWriter.Write(value); + } + + /// + public override void Write(object value) + { + var htmlString = value as HtmlString; + if (htmlString != null) { - Buffer.Add(value.ToString()); - } - else - { - _unbufferedWriter.Write(value); + htmlString.WriteTo(TargetWriter); + return; } + + base.Write(value); } /// @@ -69,25 +74,14 @@ namespace Microsoft.AspNet.Mvc.Razor { if (index < 0) { - throw new ArgumentOutOfRangeException("index"); + throw new ArgumentOutOfRangeException(nameof(index)); } - if (count < 0) + if (count < 0 || (buffer.Length - index < count)) { - throw new ArgumentOutOfRangeException("count"); - } - if (buffer.Length - index < count) - { - throw new ArgumentOutOfRangeException("count"); + throw new ArgumentOutOfRangeException(nameof(count)); } - if (IsBuffering) - { - Buffer.Add(buffer, index, count); - } - else - { - _unbufferedWriter.Write(buffer, index, count); - } + TargetWriter.Write(buffer, index, count); } /// @@ -95,141 +89,71 @@ namespace Microsoft.AspNet.Mvc.Razor { if (!string.IsNullOrEmpty(value)) { - if (IsBuffering) - { - Buffer.Add(value); - } - else - { - _unbufferedWriter.Write(value); - } + TargetWriter.Write(value); } } /// public override Task WriteAsync(char value) { - if (IsBuffering) - { - Write(value); - return _completedTask; - } - else - { - return _unbufferedWriter.WriteAsync(value); - } + return TargetWriter.WriteAsync(value); } /// public override Task WriteAsync([NotNull] char[] buffer, int index, int count) { - if (IsBuffering) + if (index < 0) { - Write(buffer, index, count); - return _completedTask; + throw new ArgumentOutOfRangeException(nameof(index)); } - else + if (count < 0 || (buffer.Length - index < count)) { - return _unbufferedWriter.WriteAsync(buffer, index, count); + throw new ArgumentOutOfRangeException(nameof(count)); } + return TargetWriter.WriteAsync(buffer, index, count); } /// public override Task WriteAsync(string value) { - if (IsBuffering) - { - Write(value); - return _completedTask; - } - else - { - return _unbufferedWriter.WriteAsync(value); - } + return TargetWriter.WriteAsync(value); } /// public override void WriteLine() { - if (IsBuffering) - { - Buffer.Add(Environment.NewLine); - } - else - { - _unbufferedWriter.WriteLine(); - } + TargetWriter.WriteLine(); } /// public override void WriteLine(string value) { - if (IsBuffering) - { - Write(value); - WriteLine(); - } - else - { - _unbufferedWriter.WriteLine(value); - } + TargetWriter.WriteLine(value); } /// public override Task WriteLineAsync(char value) { - if (IsBuffering) - { - WriteLine(value); - return _completedTask; - } - else - { - return _unbufferedWriter.WriteLineAsync(value); - } + return TargetWriter.WriteLineAsync(value); } /// public override Task WriteLineAsync(char[] value, int start, int offset) { - if (IsBuffering) - { - WriteLine(value, start, offset); - return _completedTask; - } - else - { - return _unbufferedWriter.WriteLineAsync(value, start, offset); - } + return TargetWriter.WriteLineAsync(value, start, offset); } /// public override Task WriteLineAsync(string value) { - if (IsBuffering) - { - WriteLine(value); - return _completedTask; - } - else - { - return _unbufferedWriter.WriteLineAsync(value); - } + return TargetWriter.WriteLineAsync(value); } /// public override Task WriteLineAsync() { - if (IsBuffering) - { - WriteLine(); - return _completedTask; - } - else - { - return _unbufferedWriter.WriteLineAsync(); - } + return TargetWriter.WriteLineAsync(); } /// @@ -242,10 +166,11 @@ namespace Microsoft.AspNet.Mvc.Razor if (IsBuffering) { IsBuffering = false; - CopyTo(_unbufferedWriter); + TargetWriter = UnbufferedWriter; + CopyTo(UnbufferedWriter); } - _unbufferedWriter.Flush(); + UnbufferedWriter.Flush(); } /// @@ -259,60 +184,37 @@ namespace Microsoft.AspNet.Mvc.Razor if (IsBuffering) { IsBuffering = false; - await CopyToAsync(_unbufferedWriter); + TargetWriter = UnbufferedWriter; + await CopyToAsync(UnbufferedWriter); } - await _unbufferedWriter.FlushAsync(); + await UnbufferedWriter.FlushAsync(); } /// public void CopyTo(TextWriter writer) { - var targetRazorTextWriter = writer as RazorTextWriter; - if (targetRazorTextWriter != null && targetRazorTextWriter.IsBuffering) - { - targetRazorTextWriter.Buffer.Add(Buffer); - } - else - { - // If the target writer is not buffering, we can directly copy to it's unbuffered writer - var targetWriter = targetRazorTextWriter != null ? targetRazorTextWriter._unbufferedWriter : writer; - WriteList(targetWriter, Buffer); - } + writer = UnWrapRazorTextWriter(writer); + BufferedWriter.CopyTo(writer); } /// public Task CopyToAsync(TextWriter writer) + { + writer = UnWrapRazorTextWriter(writer); + return BufferedWriter.CopyToAsync(writer); + } + + private static TextWriter UnWrapRazorTextWriter(TextWriter writer) { var targetRazorTextWriter = writer as RazorTextWriter; - if (targetRazorTextWriter != null && targetRazorTextWriter.IsBuffering) + if (targetRazorTextWriter != null) { - targetRazorTextWriter.Buffer.Add(Buffer); - } - else - { - // If the target writer is not buffering, we can directly copy to it's unbuffered writer - var targetWriter = targetRazorTextWriter != null ? targetRazorTextWriter._unbufferedWriter : writer; - return WriteListAsync(targetWriter, Buffer); + writer = targetRazorTextWriter.IsBuffering ? targetRazorTextWriter.BufferedWriter : + targetRazorTextWriter.UnbufferedWriter; } - 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); - } + return writer; } } } \ 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 6064bea6fd..2ea9e674f6 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/RazorView.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/RazorView.cs @@ -61,13 +61,30 @@ namespace Microsoft.AspNet.Mvc.Razor _pageExecutionFeature = context.HttpContext.GetFeature(); - if (!_isPartial) + if (_isPartial) + { + await RenderPartialAsync(context); + } + else { var bodyWriter = await RenderPageAsync(_razorPage, context, executeViewStart: true); await RenderLayoutAsync(context, bodyWriter); } + } + + private async Task RenderPartialAsync(ViewContext context) + { + if (EnableInstrumentation) + { + // When instrmenting, we need to Decorate the output in an instrumented writer which + // RenderPageAsync does. + var bodyWriter = await RenderPageAsync(_razorPage, context, executeViewStart: false); + await bodyWriter.CopyToAsync(context.Writer); + } else { + // For the non-instrumented case, we don't need to buffer contents. For Html.Partial, the writer is + // an in memory writer and for Partial views, we directly write to the Response. await RenderPageCoreAsync(_razorPage, context); } } diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/BufferEntryCollectionTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/Rendering/BufferEntryCollectionTest.cs similarity index 99% rename from test/Microsoft.AspNet.Mvc.Razor.Test/BufferEntryCollectionTest.cs rename to test/Microsoft.AspNet.Mvc.Core.Test/Rendering/BufferEntryCollectionTest.cs index c0d7b2aedc..251634f016 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Test/BufferEntryCollectionTest.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/Rendering/BufferEntryCollectionTest.cs @@ -6,7 +6,7 @@ using System.Collections.Generic; using System.Linq; using Xunit; -namespace Microsoft.AspNet.Mvc.Razor +namespace Microsoft.AspNet.Mvc.Rendering { public class BufferEntryCollectionTest { diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Rendering/StringCollectionTextWriterTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/Rendering/StringCollectionTextWriterTest.cs new file mode 100644 index 0000000000..7497cf8c7b --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Core.Test/Rendering/StringCollectionTextWriterTest.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.IO; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNet.Testing; +using Xunit; + +namespace Microsoft.AspNet.Mvc.Rendering +{ + public class StringCollectionTextWriterTest + { + [Fact] + [ReplaceCulture] + public void Write_WritesDataTypes_ToBuffer() + { + // Arrange + var expected = new[] { "True", "3", "18446744073709551615", "Hello world", "3.14", "2.718", "m" }; + var writer = new StringCollectionTextWriter(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 StringCollectionTextWriter(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 StringCollectionTextWriter(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 StringCollectionTextWriter(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 StringCollectionTextWriter(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_IfTargetTextWriterIsAStringCollectionTextWriter() + { + // Arrange + var source = new StringCollectionTextWriter(Encoding.UTF8); + var target = new StringCollectionTextWriter(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_IfTargetTextWriterIsNotAStringCollectionTextWriter() + { + // Arrange + var source = new StringCollectionTextWriter(Encoding.UTF8); + var target = new StringWriter(); + var expected = @"Hello world" + Environment.NewLine + "abc"; + + // Act + source.WriteLine("Hello world"); + source.Write(new[] { 'x', 'a', 'b', 'c' }, 1, 3); + source.CopyTo(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.FunctionalTests/RazorInstrumentationTests.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/RazorInstrumentationTests.cs index bc0e0a88bf..d3763ea973 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/RazorInstrumentationTests.cs +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/RazorInstrumentationTests.cs @@ -17,44 +17,87 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests { private readonly IServiceProvider _services = TestHelper.CreateServices("RazorInstrumentationWebsite"); private readonly Action _app = new Startup().Configure; - private readonly string _expected = string.Join(Environment.NewLine, - @"
", - @"2147483647", - "", - @"viewstart-content", - @"

", - @"page-content", - @"

", - @"
"); - private readonly IEnumerable> _expectedLineMappings = new[] - { - Tuple.Create(93, 2, true), - Tuple.Create(96, 16, false), - Tuple.Create(112, 2, true), - Tuple.Create(0, 2, true), - Tuple.Create(2, 8, true), - Tuple.Create(10, 16, false), - Tuple.Create(26, 1, true), - Tuple.Create(27, 21, true), - Tuple.Create(0, 7, true), - Tuple.Create(8, 12, false), - Tuple.Create(20, 2, true), - Tuple.Create(23, 12, false), - Tuple.Create(35, 8, true), - }; - public static IEnumerable ActionNames + public static IEnumerable InstrumentationData { get { - yield return new[] { "FullPath" }; - yield return new[] { "ViewDiscoveryPath" }; + var expected = string.Join(Environment.NewLine, + @"
", + @"2147483647", + "", + @"viewstart-content", + @"

", + @"page-content", + @"

", + @"
"); + + var expectedLineMappings = new[] + { + Tuple.Create(93, 2, true), + Tuple.Create(96, 16, false), + Tuple.Create(112, 2, true), + Tuple.Create(0, 2, true), + Tuple.Create(2, 8, true), + Tuple.Create(10, 16, false), + Tuple.Create(26, 1, true), + Tuple.Create(27, 21, true), + Tuple.Create(0, 7, true), + Tuple.Create(8, 12, false), + Tuple.Create(20, 2, true), + Tuple.Create(23, 12, false), + Tuple.Create(35, 8, true), + }; + + yield return new object[] { "FullPath", expected, expectedLineMappings }; + yield return new object[] { "ViewDiscoveryPath", expected, expectedLineMappings }; + + var expected2 = string.Join(Environment.NewLine, + "
", + "2147483647", + "", + "viewstart-content", + "view-with-partial-content", + "", + @"

partial-content

", + "", + @"

partial-content

", + "
"); + var expectedLineMappings2 = new[] + { + Tuple.Create(93, 2, true), + Tuple.Create(96, 16, false), + Tuple.Create(112, 2, true), + Tuple.Create(0, 27, true), + Tuple.Create(28, 39, false), + // Html.PartialAsync() + Tuple.Create(29, 4, true), + Tuple.Create(33, 8, true), + Tuple.Create(41, 4, false), + Tuple.Create(45, 1, true), + Tuple.Create(46, 20, true), + Tuple.Create(67, 2, true), + // Html.RenderPartial() + Tuple.Create(29, 4, true), + Tuple.Create(33, 8, true), + Tuple.Create(41, 4, false), + Tuple.Create(45, 1, true), + Tuple.Create(46, 20, true), + Tuple.Create(0, 7, true), + Tuple.Create(8, 12, false), + Tuple.Create(20, 2, true), + Tuple.Create(23, 12, false), + Tuple.Create(35, 8, true) + }; + yield return new object[] { "ViewWithPartial", expected2, expectedLineMappings2 }; } } [Theory] - [MemberData(nameof(ActionNames))] - public async Task ViewsAreServedWithoutInstrumentationByDefault(string actionName) + [MemberData(nameof(InstrumentationData))] + public async Task ViewsAreServedWithoutInstrumentationByDefault(string actionName, + string expected, + IEnumerable> expectedLineMappings) { // Arrange var context = new TestPageExecutionContext(); @@ -66,13 +109,15 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests var body = await client.GetStringAsync("http://localhost/Home/" + actionName); // Assert - Assert.Equal(_expected, body.Trim()); + Assert.Equal(expected, body.Trim()); Assert.Empty(context.Values); } [Theory] - [MemberData(nameof(ActionNames))] - public async Task ViewsAreInstrumentedWhenPageExecutionListenerFeatureIsEnabled(string actionName) + [MemberData(nameof(InstrumentationData))] + public async Task ViewsAreInstrumentedWhenPageExecutionListenerFeatureIsEnabled(string actionName, + string expected, + IEnumerable> expectedLineMappings) { // Arrange var context = new TestPageExecutionContext(); @@ -85,13 +130,15 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests var body = await client.GetStringAsync("http://localhost/Home/" + actionName); // Assert - Assert.Equal(_expected, body.Trim()); - Assert.Equal(_expectedLineMappings, context.Values); + Assert.Equal(expected, body.Trim()); + Assert.Equal(expectedLineMappings, context.Values); } [Theory] - [MemberData(nameof(ActionNames))] - public async Task ViewsCanSwitchFromRegularToInstrumented(string actionName) + [MemberData(nameof(InstrumentationData))] + public async Task ViewsCanSwitchFromRegularToInstrumented(string actionName, + string expected, + IEnumerable> expectedLineMappings) { // Arrange - 1 var context = new TestPageExecutionContext(); @@ -103,7 +150,7 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests var body = await client.GetStringAsync("http://localhost/Home/" + actionName); // Assert - 1 - Assert.Equal(_expected, body.Trim()); + Assert.Equal(expected, body.Trim()); Assert.Empty(context.Values); // Arrange - 2 @@ -113,14 +160,30 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests body = await client.GetStringAsync("http://localhost/Home/" + actionName); // Assert - 2 - Assert.Equal(_expected, body.Trim()); - Assert.Equal(_expectedLineMappings, context.Values); + Assert.Equal(expected, body.Trim()); + Assert.Equal(expectedLineMappings, context.Values); } [Fact] public async Task SwitchingFromNonInstrumentedToInstrumentedWorksForLayoutAndViewStarts() { // Arrange - 1 + var expectedLineMappings = new[] + { + Tuple.Create(93, 2, true), + Tuple.Create(96, 16, false), + Tuple.Create(112, 2, true), + Tuple.Create(0, 2, true), + Tuple.Create(2, 8, true), + Tuple.Create(10, 16, false), + Tuple.Create(26, 1, true), + Tuple.Create(27, 21, true), + Tuple.Create(0, 7, true), + Tuple.Create(8, 12, false), + Tuple.Create(20, 2, true), + Tuple.Create(23, 12, false), + Tuple.Create(35, 8, true), + }; var context = new TestPageExecutionContext(); var services = GetServiceProvider(context); var server = TestServer.Create(services, _app); @@ -130,7 +193,6 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests var body = await client.GetStringAsync("http://localhost/Home/FullPath"); // Assert - 1 - Assert.Equal(_expected, body.Trim()); Assert.Empty(context.Values); // Arrange - 2 @@ -140,8 +202,7 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests body = await client.GetStringAsync("http://localhost/Home/ViewDiscoveryPath"); // Assert - 2 - Assert.Equal(_expected, body.Trim()); - Assert.Equal(_expectedLineMappings, context.Values); + Assert.Equal(expectedLineMappings, context.Values); } private IServiceProvider GetServiceProvider(TestPageExecutionContext pageExecutionContext) diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorPageTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorPageTest.cs index 1b44b1098a..7fa4921b1c 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorPageTest.cs +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorPageTest.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Text; using System.Threading.Tasks; using Microsoft.AspNet.Mvc.Rendering; using Microsoft.AspNet.PageExecutionInstrumentation; @@ -524,6 +525,32 @@ Layout end context.Verify(); } + [Fact] + public async Task Write_WithHtmlString_WritesValueWithoutEncoding() + { + // Arrange + var writer = new RazorTextWriter(TextWriter.Null, Encoding.UTF8); + var stringCollectionWriter = new StringCollectionTextWriter(Encoding.UTF8); + stringCollectionWriter.Write("text1"); + stringCollectionWriter.Write("text2"); + + var page = CreatePage(p => + { + p.Write(new HtmlString("Hello world")); + p.Write(new HtmlString(stringCollectionWriter)); + }); + page.ViewContext.Writer = writer; + + // Act + await page.ExecuteAsync(); + + // Assert + var buffer = writer.BufferedWriter.Buffer; + Assert.Equal(2, buffer.BufferEntries.Count); + Assert.Equal("Hello world", buffer.BufferEntries[0]); + Assert.Same(stringCollectionWriter.Buffer.BufferEntries, buffer.BufferEntries[1]); + } + private static TestableRazorPage CreatePage(Action executeAction, ViewContext context = null) { diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorTextWriterTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorTextWriterTest.cs index bc845188d7..22df755649 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorTextWriterTest.cs +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorTextWriterTest.cs @@ -32,7 +32,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Test writer.Write('m'); // Assert - Assert.Equal(expected, writer.Buffer.BufferEntries); + Assert.Equal(expected, writer.BufferedWriter.Buffer.BufferEntries); } [Fact] @@ -55,7 +55,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Test writer.Write(2.718m); // Assert - Assert.Empty(writer.Buffer.BufferEntries); + Assert.Empty(writer.BufferedWriter.Buffer.BufferEntries); foreach (var item in expected) { unbufferedWriter.Verify(v => v.Write(item), Times.Once()); @@ -81,7 +81,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Test await writer.WriteLineAsync(buffer1); // Assert - Assert.Empty(writer.Buffer.BufferEntries); + Assert.Empty(writer.BufferedWriter.Buffer.BufferEntries); 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()); @@ -106,7 +106,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Test await writer.WriteLineAsync("gh"); // Assert - Assert.Empty(writer.Buffer.BufferEntries); + Assert.Empty(writer.BufferedWriter.Buffer.BufferEntries); unbufferedWriter.Verify(v => v.Write("a"), Times.Once()); unbufferedWriter.Verify(v => v.WriteLine("ab"), Times.Once()); unbufferedWriter.Verify(v => v.WriteAsync("ef"), Times.Once()); @@ -128,7 +128,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Test writer.WriteLine(3L); // Assert - Assert.Equal(expected, writer.Buffer.BufferEntries); + Assert.Equal(expected, writer.BufferedWriter.Buffer.BufferEntries); } [Fact] @@ -146,7 +146,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Test writer.WriteLine(3L); // Assert - Assert.Empty(writer.Buffer.BufferEntries); + Assert.Empty(writer.BufferedWriter.Buffer.BufferEntries); 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()); @@ -168,7 +168,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Test await writer.WriteLineAsync(input3.Array, input3.Offset, input3.Count); // Assert - var buffer = writer.Buffer.BufferEntries; + var buffer = writer.BufferedWriter.Buffer.BufferEntries; Assert.Equal(4, buffer.Count); Assert.Equal("bcd", buffer[0]); Assert.Equal("ef", buffer[1]); @@ -188,7 +188,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Test await writer.WriteLineAsync(); // Assert - var actual = writer.Buffer.BufferEntries; + var actual = writer.BufferedWriter.Buffer.BufferEntries; Assert.Equal(new[] { newLine, newLine }, actual); } @@ -210,7 +210,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Test await writer.WriteLineAsync(input4); // Assert - var actual = writer.Buffer.BufferEntries; + var actual = writer.BufferedWriter.Buffer.BufferEntries; Assert.Equal(new[] { input1, input2, newLine, input3, input4, newLine }, actual); } @@ -228,9 +228,9 @@ namespace Microsoft.AspNet.Mvc.Razor.Test // 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]); + Assert.Equal(2, source.BufferedWriter.Buffer.BufferEntries.Count); + Assert.Equal(1, target.BufferedWriter.Buffer.BufferEntries.Count); + Assert.Same(source.BufferedWriter.Buffer.BufferEntries, target.BufferedWriter.Buffer.BufferEntries[0]); } [Fact] @@ -249,8 +249,8 @@ namespace Microsoft.AspNet.Mvc.Razor.Test // Assert // Make sure content was written to the source. - Assert.Equal(2, source.Buffer.BufferEntries.Count); - Assert.Empty(target.Buffer.BufferEntries); + Assert.Equal(2, source.BufferedWriter.Buffer.BufferEntries.Count); + Assert.Empty(target.BufferedWriter.Buffer.BufferEntries); unbufferedWriter.Verify(v => v.Write("Hello world"), Times.Once()); unbufferedWriter.Verify(v => v.Write("bc"), Times.Once()); } @@ -286,9 +286,9 @@ abc"; 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]); + Assert.Equal(3, source.BufferedWriter.Buffer.BufferEntries.Count); + Assert.Equal(1, target.BufferedWriter.Buffer.BufferEntries.Count); + Assert.Same(source.BufferedWriter.Buffer.BufferEntries, target.BufferedWriter.Buffer.BufferEntries[0]); } [Fact] @@ -307,8 +307,8 @@ abc"; // Assert // Make sure content was written to the source. - Assert.Equal(3, source.Buffer.BufferEntries.Count); - Assert.Empty(target.Buffer.BufferEntries); + Assert.Equal(3, source.BufferedWriter.Buffer.BufferEntries.Count); + Assert.Empty(target.BufferedWriter.Buffer.BufferEntries); unbufferedWriter.Verify(v => v.WriteAsync("Hello from Asp.Net"), Times.Once()); unbufferedWriter.Verify(v => v.WriteAsync(Environment.NewLine), Times.Once()); unbufferedWriter.Verify(v => v.WriteAsync("xyz"), Times.Once()); diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewTest.cs index 5c4ac4bfc7..5af5c8c47e 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewTest.cs +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewTest.cs @@ -35,7 +35,7 @@ namespace Microsoft.AspNet.Mvc.Razor } [Fact] - public async Task RenderAsync_AsPartial_DoesNotCreateOutputBuffer() + public async Task RenderAsync_AsPartial_DoesNotBufferOutput() { // Arrange TextWriter actual = null; @@ -56,7 +56,7 @@ namespace Microsoft.AspNet.Mvc.Razor // Assert Assert.Same(expected, actual); - Assert.Equal("Hello world", expected.ToString()); + Assert.Equal("Hello world", viewContext.Writer.ToString()); } [Fact] @@ -653,20 +653,26 @@ section-content-2"; public async Task RenderAsync_UsesPageExecutionFeatureFromRequest_ToGetExecutionContext() { // Arrange - var writer = Mock.Of(); + var writer = new StringWriter(); var executed = false; var feature = new Mock(MockBehavior.Strict); var pageContext = Mock.Of(); - feature.Setup(f => f.GetContext("/MyPartialPage.cshtml", writer)) + feature.Setup(f => f.GetContext("/MyPartialPage.cshtml", It.IsAny())) .Returns(pageContext) .Verifiable(); + feature.Setup(f => f.DecorateWriter(It.IsAny())) + .Returns((RazorTextWriter r) => r) + .Verifiable(); + var page = new TestableRazorPage(v => { - Assert.Same(writer, v.Output); + Assert.IsType(v.Output); Assert.Same(pageContext, v.PageExecutionContext); executed = true; + + v.Write("Hello world"); }); page.Path = "/MyPartialPage.cshtml"; @@ -684,6 +690,7 @@ section-content-2"; // Assert feature.Verify(); Assert.True(executed); + Assert.Equal("Hello world", viewContext.Writer.ToString()); } [Theory] diff --git a/test/WebSites/RazorInstrumentationWebsite/HomeController.cs b/test/WebSites/RazorInstrumentationWebsite/HomeController.cs index 14a87a4c6e..a945842683 100644 --- a/test/WebSites/RazorInstrumentationWebsite/HomeController.cs +++ b/test/WebSites/RazorInstrumentationWebsite/HomeController.cs @@ -17,5 +17,10 @@ namespace RazorInstrumentationWebSite { return View(); } + + public ActionResult ViewWithPartial() + { + return View(); + } } } \ No newline at end of file diff --git a/test/WebSites/RazorInstrumentationWebsite/Views/Home/VIewWithPartial.cshtml b/test/WebSites/RazorInstrumentationWebsite/Views/Home/VIewWithPartial.cshtml new file mode 100644 index 0000000000..8f42baf024 --- /dev/null +++ b/test/WebSites/RazorInstrumentationWebsite/Views/Home/VIewWithPartial.cshtml @@ -0,0 +1,5 @@ +view-with-partial-content +@await Html.PartialAsync("_PartialView") +@{ +await Html.RenderPartialAsync("_PartialView"); +} \ No newline at end of file diff --git a/test/WebSites/RazorInstrumentationWebsite/Views/Home/_PartialView.cshtml b/test/WebSites/RazorInstrumentationWebsite/Views/Home/_PartialView.cshtml new file mode 100644 index 0000000000..3b945a4613 --- /dev/null +++ b/test/WebSites/RazorInstrumentationWebsite/Views/Home/_PartialView.cshtml @@ -0,0 +1,4 @@ +@{ + var cls = "class"; +} +

partial-content

\ No newline at end of file