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
This commit is contained in:
Pranav K 2014-10-10 10:53:12 -07:00
parent 20eadb94ee
commit 18e11f546d
16 changed files with 648 additions and 236 deletions

View File

@ -6,7 +6,7 @@ using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
namespace Microsoft.AspNet.Mvc.Razor
namespace Microsoft.AspNet.Mvc.Rendering
{
/// <summary>
/// Represents a hierarchy of strings and provides an enumerator that iterates over it as a sequence.

View File

@ -330,11 +330,11 @@ namespace Microsoft.AspNet.Mvc.Rendering
public async Task<HtmlString> 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);
}
}

View File

@ -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;
}
/// <summary>
/// Initializes a new instance of <see cref="HtmlString"/> that is backed by <paramref name="writer"/>.
/// </summary>
/// <param name="writer"></param>
public HtmlString([NotNull] StringCollectionTextWriter writer)
{
_writer = writer;
}
public static HtmlString Empty
{
get
@ -22,8 +34,30 @@ namespace Microsoft.AspNet.Mvc.Rendering
}
}
/// <summary>
/// Writes the value in this instance of <see cref="HtmlString"/> to the target <paramref name="targetWriter"/>.
/// </summary>
/// <param name="targetWriter">The <see cref="TextWriter"/> to write contents to.</param>
public void WriteTo(TextWriter targetWriter)
{
if (_writer != null)
{
_writer.CopyTo(targetWriter);
}
else
{
targetWriter.Write(_input);
}
}
/// <inheritdoc />
public override string ToString()
{
if (_writer != null)
{
return _writer.ToString();
}
return _input;
}
}

View File

@ -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
{
/// <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 StringCollectionTextWriter : TextWriter
{
private static readonly Task _completedTask = Task.FromResult(0);
private readonly Encoding _encoding;
/// <summary>
/// Creates a new instance of <see cref="StringCollectionTextWriter"/>.
/// </summary>
/// <param name="encoding">The character <see cref="Encoding"/> in which the output is written.</param>
public StringCollectionTextWriter(Encoding encoding)
{
_encoding = encoding;
Buffer = new BufferEntryCollection();
}
/// <inheritdoc />
public override Encoding Encoding
{
get { return _encoding; }
}
/// <summary>
/// A collection of entries buffered by this instance of <see cref="StringCollectionTextWriter"/>.
/// </summary>
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(nameof(index));
}
if (count < 0 || (buffer.Length - index < count))
{
throw new ArgumentOutOfRangeException(nameof(count));
}
Buffer.Add(buffer, index, count);
}
/// <inheritdoc />
public override void Write(string value)
{
if (string.IsNullOrEmpty(value))
{
return;
}
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;
}
/// <inheritdoc />
public void CopyTo(TextWriter writer)
{
var targetStringCollectionWriter = writer as StringCollectionTextWriter;
if (targetStringCollectionWriter != null)
{
targetStringCollectionWriter.Buffer.Add(Buffer);
}
else
{
WriteList(writer, Buffer);
}
}
/// <inheritdoc />
public Task CopyToAsync(TextWriter writer)
{
var targetStringCollectionWriter = writer as StringCollectionTextWriter;
if (targetStringCollectionWriter != null)
{
targetStringCollectionWriter.Buffer.Add(Buffer);
}
else
{
return WriteListAsync(writer, Buffer);
}
return _completedTask;
}
/// <inheritdoc />
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);
}
}
}
}

View File

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

View File

@ -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
{
/// <summary>
/// A <see cref="TextWriter"/> that represents individual write operations as a sequence of strings when buffering.
/// The writer is backed by an unbuffered writer. When <c>Flush</c> or <c>FlushAsync</c> 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 <see cref="TextWriter"/> that is backed by a unbuffered writer (over the Response stream) and a buffered
/// <see cref="StringCollectionTextWriter"/>. When <c>Flush</c> or <c>FlushAsync</c> is invoked, the writer
/// copies all content from the buffered writer to the unbuffered one and switches to writing to the unbuffered
/// writer for all further write operations.
/// </summary>
/// <remarks>
/// This 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
/// <see cref="RazorPage.FlushAsync"/> expects.
/// </remarks>
public class RazorTextWriter : TextWriter, IBufferedTextWriter
{
private static readonly Task _completedTask = Task.FromResult(0);
private readonly TextWriter _unbufferedWriter;
private readonly Encoding _encoding;
/// <summary>
/// Creates a new instance of <see cref="RazorTextWriter"/>.
/// </summary>
@ -32,36 +29,44 @@ namespace Microsoft.AspNet.Mvc.Razor
/// <param name="encoding">The character <see cref="Encoding"/> in which the output is written.</param>
public RazorTextWriter(TextWriter unbufferedWriter, Encoding encoding)
{
_unbufferedWriter = unbufferedWriter;
_encoding = encoding;
Buffer = new BufferEntryCollection();
UnbufferedWriter = unbufferedWriter;
BufferedWriter = new StringCollectionTextWriter(encoding);
TargetWriter = BufferedWriter;
}
/// <inheritdoc />
public override Encoding Encoding
{
get { return _encoding; }
get { return BufferedWriter.Encoding; }
}
/// <inheritdoc />
public bool IsBuffering { get; private set; } = true;
/// <summary>
/// A collection of entries buffered by this instance of <see cref="RazorTextWriter"/>.
/// </summary>
public BufferEntryCollection Buffer { get; private set; }
// Internal for unit testing
internal StringCollectionTextWriter BufferedWriter { get; }
private TextWriter UnbufferedWriter { get; }
private TextWriter TargetWriter { get; set; }
/// <inheritdoc />
public override void Write(char value)
{
if (IsBuffering)
TargetWriter.Write(value);
}
/// <inheritdoc />
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);
}
/// <inheritdoc />
@ -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);
}
/// <inheritdoc />
@ -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);
}
}
/// <inheritdoc />
public override Task WriteAsync(char value)
{
if (IsBuffering)
{
Write(value);
return _completedTask;
}
else
{
return _unbufferedWriter.WriteAsync(value);
}
return TargetWriter.WriteAsync(value);
}
/// <inheritdoc />
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);
}
/// <inheritdoc />
public override Task WriteAsync(string value)
{
if (IsBuffering)
{
Write(value);
return _completedTask;
}
else
{
return _unbufferedWriter.WriteAsync(value);
}
return TargetWriter.WriteAsync(value);
}
/// <inheritdoc />
public override void WriteLine()
{
if (IsBuffering)
{
Buffer.Add(Environment.NewLine);
}
else
{
_unbufferedWriter.WriteLine();
}
TargetWriter.WriteLine();
}
/// <inheritdoc />
public override void WriteLine(string value)
{
if (IsBuffering)
{
Write(value);
WriteLine();
}
else
{
_unbufferedWriter.WriteLine(value);
}
TargetWriter.WriteLine(value);
}
/// <inheritdoc />
public override Task WriteLineAsync(char value)
{
if (IsBuffering)
{
WriteLine(value);
return _completedTask;
}
else
{
return _unbufferedWriter.WriteLineAsync(value);
}
return TargetWriter.WriteLineAsync(value);
}
/// <inheritdoc />
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);
}
/// <inheritdoc />
public override Task WriteLineAsync(string value)
{
if (IsBuffering)
{
WriteLine(value);
return _completedTask;
}
else
{
return _unbufferedWriter.WriteLineAsync(value);
}
return TargetWriter.WriteLineAsync(value);
}
/// <inheritdoc />
public override Task WriteLineAsync()
{
if (IsBuffering)
{
WriteLine();
return _completedTask;
}
else
{
return _unbufferedWriter.WriteLineAsync();
}
return TargetWriter.WriteLineAsync();
}
/// <summary>
@ -242,10 +166,11 @@ namespace Microsoft.AspNet.Mvc.Razor
if (IsBuffering)
{
IsBuffering = false;
CopyTo(_unbufferedWriter);
TargetWriter = UnbufferedWriter;
CopyTo(UnbufferedWriter);
}
_unbufferedWriter.Flush();
UnbufferedWriter.Flush();
}
/// <summary>
@ -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();
}
/// <inheritdoc />
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);
}
/// <inheritdoc />
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;
}
}
}

View File

@ -61,13 +61,30 @@ namespace Microsoft.AspNet.Mvc.Razor
_pageExecutionFeature = context.HttpContext.GetFeature<IPageExecutionListenerFeature>();
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);
}
}

View File

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

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.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<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 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<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 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<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 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<object>(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";
}
}
}
}

View File

@ -17,44 +17,87 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
{
private readonly IServiceProvider _services = TestHelper.CreateServices("RazorInstrumentationWebsite");
private readonly Action<IApplicationBuilder> _app = new Startup().Configure;
private readonly string _expected = string.Join(Environment.NewLine,
@"<div>",
@"2147483647",
"",
@"viewstart-content",
@"<p class=""Hello world"">",
@"page-content",
@"</p>",
@"</div>");
private readonly IEnumerable<Tuple<int, int, bool>> _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<object[]> ActionNames
public static IEnumerable<object[]> InstrumentationData
{
get
{
yield return new[] { "FullPath" };
yield return new[] { "ViewDiscoveryPath" };
var expected = string.Join(Environment.NewLine,
@"<div>",
@"2147483647",
"",
@"viewstart-content",
@"<p class=""Hello world"">",
@"page-content",
@"</p>",
@"</div>");
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,
"<div>",
"2147483647",
"",
"viewstart-content",
"view-with-partial-content",
"",
@"<p class=""class"">partial-content</p>",
"",
@"<p class=""class"">partial-content</p>",
"</div>");
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<Tuple<int, int, bool>> 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<Tuple<int, int, bool>> 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<Tuple<int, int, bool>> 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)

View File

@ -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<TestableRazorPage> executeAction,
ViewContext context = null)
{

View File

@ -32,7 +32,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
writer.Write('m');
// Assert
Assert.Equal<object>(expected, writer.Buffer.BufferEntries);
Assert.Equal<object>(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<object>(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<object>(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());

View File

@ -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<TextWriter>();
var writer = new StringWriter();
var executed = false;
var feature = new Mock<IPageExecutionListenerFeature>(MockBehavior.Strict);
var pageContext = Mock.Of<IPageExecutionContext>();
feature.Setup(f => f.GetContext("/MyPartialPage.cshtml", writer))
feature.Setup(f => f.GetContext("/MyPartialPage.cshtml", It.IsAny<RazorTextWriter>()))
.Returns(pageContext)
.Verifiable();
feature.Setup(f => f.DecorateWriter(It.IsAny<RazorTextWriter>()))
.Returns((RazorTextWriter r) => r)
.Verifiable();
var page = new TestableRazorPage(v =>
{
Assert.Same(writer, v.Output);
Assert.IsType<RazorTextWriter>(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]

View File

@ -17,5 +17,10 @@ namespace RazorInstrumentationWebSite
{
return View();
}
public ActionResult ViewWithPartial()
{
return View();
}
}
}

View File

@ -0,0 +1,5 @@
view-with-partial-content
@await Html.PartialAsync("_PartialView")
@{
await Html.RenderPartialAsync("_PartialView");
}

View File

@ -0,0 +1,4 @@
@{
var cls = "class";
}
<p class="@cls">partial-content</p>