Prevent synchronous writes when using Razor (#9395)

* Do not perform synchronous writes to the Response TextWriter after a Razor FlushAsync
* Use ViewBuffer to perform async writes to the response when using ViewComponentResult

Related to #6397 

Fixes https://github.com/aspnet/AspNetCore/issues/4885
This commit is contained in:
Pranav K 2019-04-16 16:12:37 -07:00 committed by GitHub
parent fb7e8a6895
commit 2be80522b2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 227 additions and 271 deletions

View File

@ -370,12 +370,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor
var encoder = HtmlEncoder; var encoder = HtmlEncoder;
if (value is IHtmlContent htmlContent) if (value is IHtmlContent htmlContent)
{ {
var bufferedWriter = writer as ViewBufferTextWriter; if (writer is ViewBufferTextWriter bufferedWriter)
if (bufferedWriter == null || !bufferedWriter.IsBuffering)
{
htmlContent.WriteTo(writer, encoder);
}
else
{ {
if (value is IHtmlContentContainer htmlContentContainer) if (value is IHtmlContentContainer htmlContentContainer)
{ {
@ -389,6 +384,10 @@ namespace Microsoft.AspNetCore.Mvc.Razor
bufferedWriter.Buffer.AppendHtml(htmlContent); bufferedWriter.Buffer.AppendHtml(htmlContent);
} }
} }
else
{
htmlContent.WriteTo(writer, encoder);
}
return; return;
} }

View File

@ -233,7 +233,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor
// (including the layout page we just rendered). // (including the layout page we just rendered).
while (!string.IsNullOrEmpty(previousPage.Layout)) while (!string.IsNullOrEmpty(previousPage.Layout))
{ {
if (!bodyWriter.IsBuffering) if (bodyWriter.Flushed)
{ {
// Once a call to RazorPage.FlushAsync is made, we can no longer render Layout pages - content has // Once a call to RazorPage.FlushAsync is made, we can no longer render Layout pages - content has
// already been written to the client and the layout content would be appended rather than surround // already been written to the client and the layout content would be appended rather than surround
@ -274,25 +274,22 @@ namespace Microsoft.AspNetCore.Mvc.Razor
layoutPage.EnsureRenderedBodyOrSections(); layoutPage.EnsureRenderedBodyOrSections();
} }
if (bodyWriter.IsBuffering) // We've got a bunch of content in the view buffer. How to best deal with it
// really depends on whether or not we're writing directly to the output or if we're writing to
// another buffer.
if (context.Writer is ViewBufferTextWriter viewBufferTextWriter)
{ {
// If IsBuffering - then we've got a bunch of content in the view buffer. How to best deal with it // This means we're writing to another buffer. Use MoveTo to combine them.
// really depends on whether or not we're writing directly to the output or if we're writing to bodyWriter.Buffer.MoveTo(viewBufferTextWriter.Buffer);
// another buffer. }
var viewBufferTextWriter = context.Writer as ViewBufferTextWriter; else
if (viewBufferTextWriter == null || !viewBufferTextWriter.IsBuffering) {
// This means we're writing to a 'real' writer, probably to the actual output stream.
// We're using PagedBufferedTextWriter here to 'smooth' synchronous writes of IHtmlContent values.
using (var writer = _bufferScope.CreateWriter(context.Writer))
{ {
// This means we're writing to a 'real' writer, probably to the actual output stream. await bodyWriter.Buffer.WriteToAsync(writer, _htmlEncoder);
// We're using PagedBufferedTextWriter here to 'smooth' synchronous writes of IHtmlContent values. await writer.FlushAsync();
using (var writer = _bufferScope.CreateWriter(context.Writer))
{
await bodyWriter.Buffer.WriteToAsync(writer, _htmlEncoder);
}
}
else
{
// This means we're writing to another buffer. Use MoveTo to combine them.
bodyWriter.Buffer.MoveTo(viewBufferTextWriter.Buffer);
} }
} }
} }

View File

@ -1204,7 +1204,9 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
} }
public partial class ViewComponentResultExecutor : Microsoft.AspNetCore.Mvc.Infrastructure.IActionResultExecutor<Microsoft.AspNetCore.Mvc.ViewComponentResult> public partial class ViewComponentResultExecutor : Microsoft.AspNetCore.Mvc.Infrastructure.IActionResultExecutor<Microsoft.AspNetCore.Mvc.ViewComponentResult>
{ {
[System.ObsoleteAttribute("This constructor is obsolete and will be removed in a future version.")]
public ViewComponentResultExecutor(Microsoft.Extensions.Options.IOptions<Microsoft.AspNetCore.Mvc.MvcViewOptions> mvcHelperOptions, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory, System.Text.Encodings.Web.HtmlEncoder htmlEncoder, Microsoft.AspNetCore.Mvc.ModelBinding.IModelMetadataProvider modelMetadataProvider, Microsoft.AspNetCore.Mvc.ViewFeatures.ITempDataDictionaryFactory tempDataDictionaryFactory) { } public ViewComponentResultExecutor(Microsoft.Extensions.Options.IOptions<Microsoft.AspNetCore.Mvc.MvcViewOptions> mvcHelperOptions, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory, System.Text.Encodings.Web.HtmlEncoder htmlEncoder, Microsoft.AspNetCore.Mvc.ModelBinding.IModelMetadataProvider modelMetadataProvider, Microsoft.AspNetCore.Mvc.ViewFeatures.ITempDataDictionaryFactory tempDataDictionaryFactory) { }
public ViewComponentResultExecutor(Microsoft.Extensions.Options.IOptions<Microsoft.AspNetCore.Mvc.MvcViewOptions> mvcHelperOptions, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory, System.Text.Encodings.Web.HtmlEncoder htmlEncoder, Microsoft.AspNetCore.Mvc.ModelBinding.IModelMetadataProvider modelMetadataProvider, Microsoft.AspNetCore.Mvc.ViewFeatures.ITempDataDictionaryFactory tempDataDictionaryFactory, Microsoft.AspNetCore.Mvc.Infrastructure.IHttpResponseStreamWriterFactory writerFactory) { }
[System.Diagnostics.DebuggerStepThroughAttribute] [System.Diagnostics.DebuggerStepThroughAttribute]
public virtual System.Threading.Tasks.Task ExecuteAsync(Microsoft.AspNetCore.Mvc.ActionContext context, Microsoft.AspNetCore.Mvc.ViewComponentResult result) { throw null; } public virtual System.Threading.Tasks.Task ExecuteAsync(Microsoft.AspNetCore.Mvc.ActionContext context, Microsoft.AspNetCore.Mvc.ViewComponentResult result) { throw null; }
} }

View File

@ -86,25 +86,20 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Buffers
/// <inheritdoc /> /// <inheritdoc />
public override Encoding Encoding { get; } public override Encoding Encoding { get; }
/// <inheritdoc />
public bool IsBuffering { get; private set; } = true;
/// <summary> /// <summary>
/// Gets the <see cref="ViewBuffer"/>. /// Gets the <see cref="ViewBuffer"/>.
/// </summary> /// </summary>
public ViewBuffer Buffer { get; } public ViewBuffer Buffer { get; }
/// <summary>
/// Gets a value that indiciates if <see cref="Flush"/> or <see cref="FlushAsync" /> was invoked.
/// </summary>
public bool Flushed { get; private set; }
/// <inheritdoc /> /// <inheritdoc />
public override void Write(char value) public override void Write(char value)
{ {
if (IsBuffering) Buffer.AppendHtml(value.ToString());
{
Buffer.AppendHtml(value.ToString());
}
else
{
_inner.Write(value);
}
} }
/// <inheritdoc /> /// <inheritdoc />
@ -125,14 +120,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Buffers
throw new ArgumentOutOfRangeException(nameof(count)); throw new ArgumentOutOfRangeException(nameof(count));
} }
if (IsBuffering) Buffer.AppendHtml(new string(buffer, index, count));
{
Buffer.AppendHtml(new string(buffer, index, count));
}
else
{
_inner.Write(buffer, index, count);
}
} }
/// <inheritdoc /> /// <inheritdoc />
@ -143,14 +131,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Buffers
return; return;
} }
if (IsBuffering) Buffer.AppendHtml(value);
{
Buffer.AppendHtml(value);
}
else
{
_inner.Write(value);
}
} }
/// <inheritdoc /> /// <inheritdoc />
@ -186,14 +167,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Buffers
return; return;
} }
if (IsBuffering) Buffer.AppendHtml(value);
{
Buffer.AppendHtml(value);
}
else
{
value.WriteTo(_inner, _htmlEncoder);
}
} }
/// <summary> /// <summary>
@ -207,14 +181,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Buffers
return; return;
} }
if (IsBuffering) value.MoveTo(Buffer);
{
value.MoveTo(Buffer);
}
else
{
value.WriteTo(_inner, _htmlEncoder);
}
} }
/// <inheritdoc /> /// <inheritdoc />
@ -245,15 +212,8 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Buffers
/// <inheritdoc /> /// <inheritdoc />
public override Task WriteAsync(char value) public override Task WriteAsync(char value)
{ {
if (IsBuffering) Buffer.AppendHtml(value.ToString());
{ return Task.CompletedTask;
Buffer.AppendHtml(value.ToString());
return Task.CompletedTask;
}
else
{
return _inner.WriteAsync(value);
}
} }
/// <inheritdoc /> /// <inheritdoc />
@ -273,121 +233,64 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Buffers
throw new ArgumentOutOfRangeException(nameof(count)); throw new ArgumentOutOfRangeException(nameof(count));
} }
if (IsBuffering) Buffer.AppendHtml(new string(buffer, index, count));
{ return Task.CompletedTask;
Buffer.AppendHtml(new string(buffer, index, count));
return Task.CompletedTask;
}
else
{
return _inner.WriteAsync(buffer, index, count);
}
} }
/// <inheritdoc /> /// <inheritdoc />
public override Task WriteAsync(string value) public override Task WriteAsync(string value)
{ {
if (IsBuffering) Buffer.AppendHtml(value);
{ return Task.CompletedTask;
Buffer.AppendHtml(value);
return Task.CompletedTask;
}
else
{
return _inner.WriteAsync(value);
}
} }
/// <inheritdoc /> /// <inheritdoc />
public override void WriteLine() public override void WriteLine()
{ {
if (IsBuffering) Buffer.AppendHtml(NewLine);
{
Buffer.AppendHtml(NewLine);
}
else
{
_inner.WriteLine();
}
} }
/// <inheritdoc /> /// <inheritdoc />
public override void WriteLine(string value) public override void WriteLine(string value)
{ {
if (IsBuffering) Buffer.AppendHtml(value);
{ Buffer.AppendHtml(NewLine);
Buffer.AppendHtml(value);
Buffer.AppendHtml(NewLine);
}
else
{
_inner.WriteLine(value);
}
} }
/// <inheritdoc /> /// <inheritdoc />
public override Task WriteLineAsync(char value) public override Task WriteLineAsync(char value)
{ {
if (IsBuffering) Buffer.AppendHtml(value.ToString());
{ Buffer.AppendHtml(NewLine);
Buffer.AppendHtml(value.ToString()); return Task.CompletedTask;
Buffer.AppendHtml(NewLine);
return Task.CompletedTask;
}
else
{
return _inner.WriteLineAsync(value);
}
} }
/// <inheritdoc /> /// <inheritdoc />
public override Task WriteLineAsync(char[] value, int start, int offset) public override Task WriteLineAsync(char[] value, int start, int offset)
{ {
if (IsBuffering) Buffer.AppendHtml(new string(value, start, offset));
{ Buffer.AppendHtml(NewLine);
Buffer.AppendHtml(new string(value, start, offset)); return Task.CompletedTask;
Buffer.AppendHtml(NewLine);
return Task.CompletedTask;
}
else
{
return _inner.WriteLineAsync(value, start, offset);
}
} }
/// <inheritdoc /> /// <inheritdoc />
public override Task WriteLineAsync(string value) public override Task WriteLineAsync(string value)
{ {
if (IsBuffering) Buffer.AppendHtml(value);
{ Buffer.AppendHtml(NewLine);
Buffer.AppendHtml(value); return Task.CompletedTask;
Buffer.AppendHtml(NewLine);
return Task.CompletedTask;
}
else
{
return _inner.WriteLineAsync(value);
}
} }
/// <inheritdoc /> /// <inheritdoc />
public override Task WriteLineAsync() public override Task WriteLineAsync()
{ {
if (IsBuffering) Buffer.AppendHtml(NewLine);
{ return Task.CompletedTask;
Buffer.AppendHtml(NewLine);
return Task.CompletedTask;
}
else
{
return _inner.WriteLineAsync();
}
} }
/// <summary> /// <summary>
/// Copies the buffered content to the unbuffered writer and invokes flush on it. /// Copies the buffered content to the unbuffered writer and invokes flush on it.
/// Additionally causes this instance to no longer buffer and direct all write operations
/// to the unbuffered writer.
/// </summary> /// </summary>
public override void Flush() public override void Flush()
{ {
@ -396,20 +299,16 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Buffers
return; return;
} }
if (IsBuffering) Flushed = true;
{
IsBuffering = false; Buffer.WriteTo(_inner, _htmlEncoder);
Buffer.WriteTo(_inner, _htmlEncoder); Buffer.Clear();
Buffer.Clear();
}
_inner.Flush(); _inner.Flush();
} }
/// <summary> /// <summary>
/// Copies the buffered content to the unbuffered writer and invokes flush on it. /// Copies the buffered content to the unbuffered writer and invokes flush on it.
/// Additionally causes this instance to no longer buffer and direct all write operations
/// to the unbuffered writer.
/// </summary> /// </summary>
/// <returns>A <see cref="Task"/> that represents the asynchronous copy and flush operations.</returns> /// <returns>A <see cref="Task"/> that represents the asynchronous copy and flush operations.</returns>
public override async Task FlushAsync() public override async Task FlushAsync()
@ -419,12 +318,10 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Buffers
return; return;
} }
if (IsBuffering) Flushed = true;
{
IsBuffering = false; await Buffer.WriteToAsync(_inner, _htmlEncoder);
await Buffer.WriteToAsync(_inner, _htmlEncoder); Buffer.Clear();
Buffer.Clear();
}
await _inner.FlushAsync(); await _inner.FlushAsync();
} }

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.IO;
using System.Text.Encodings.Web; using System.Text.Encodings.Web;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Html; using Microsoft.AspNetCore.Html;
@ -9,8 +10,8 @@ using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures.Buffers;
using Microsoft.AspNetCore.Mvc.ViewFeatures.Filters; using Microsoft.AspNetCore.Mvc.ViewFeatures.Filters;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
@ -24,13 +25,26 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
private readonly ILogger<ViewComponentResult> _logger; private readonly ILogger<ViewComponentResult> _logger;
private readonly IModelMetadataProvider _modelMetadataProvider; private readonly IModelMetadataProvider _modelMetadataProvider;
private readonly ITempDataDictionaryFactory _tempDataDictionaryFactory; private readonly ITempDataDictionaryFactory _tempDataDictionaryFactory;
private IHttpResponseStreamWriterFactory _writerFactory;
[Obsolete("This constructor is obsolete and will be removed in a future version.")]
public ViewComponentResultExecutor( public ViewComponentResultExecutor(
IOptions<MvcViewOptions> mvcHelperOptions, IOptions<MvcViewOptions> mvcHelperOptions,
ILoggerFactory loggerFactory, ILoggerFactory loggerFactory,
HtmlEncoder htmlEncoder, HtmlEncoder htmlEncoder,
IModelMetadataProvider modelMetadataProvider, IModelMetadataProvider modelMetadataProvider,
ITempDataDictionaryFactory tempDataDictionaryFactory) ITempDataDictionaryFactory tempDataDictionaryFactory)
: this(mvcHelperOptions, loggerFactory, htmlEncoder, modelMetadataProvider, tempDataDictionaryFactory, null)
{
}
public ViewComponentResultExecutor(
IOptions<MvcViewOptions> mvcHelperOptions,
ILoggerFactory loggerFactory,
HtmlEncoder htmlEncoder,
IModelMetadataProvider modelMetadataProvider,
ITempDataDictionaryFactory tempDataDictionaryFactory,
IHttpResponseStreamWriterFactory writerFactory)
{ {
if (mvcHelperOptions == null) if (mvcHelperOptions == null)
{ {
@ -62,6 +76,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
_htmlEncoder = htmlEncoder; _htmlEncoder = htmlEncoder;
_modelMetadataProvider = modelMetadataProvider; _modelMetadataProvider = modelMetadataProvider;
_tempDataDictionaryFactory = tempDataDictionaryFactory; _tempDataDictionaryFactory = tempDataDictionaryFactory;
_writerFactory = writerFactory;
} }
/// <inheritdoc /> /// <inheritdoc />
@ -105,14 +120,9 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
response.StatusCode = result.StatusCode.Value; response.StatusCode = result.StatusCode.Value;
} }
// Opt into sync IO support until we can work out an alternative https://github.com/aspnet/AspNetCore/issues/6397 _writerFactory ??= context.HttpContext.RequestServices.GetRequiredService<IHttpResponseStreamWriterFactory>();
var syncIOFeature = context.HttpContext.Features.Get<Http.Features.IHttpBodyControlFeature>();
if (syncIOFeature != null)
{
syncIOFeature.AllowSynchronousIO = true;
}
using (var writer = new HttpResponseStreamWriter(response.Body, resolvedContentTypeEncoding)) using (var writer = _writerFactory.CreateWriter(response.Body, resolvedContentTypeEncoding))
{ {
var viewContext = new ViewContext( var viewContext = new ViewContext(
context, context,
@ -127,9 +137,26 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
// IViewComponentHelper is stateful, we want to make sure to retrieve it every time we need it. // IViewComponentHelper is stateful, we want to make sure to retrieve it every time we need it.
var viewComponentHelper = context.HttpContext.RequestServices.GetRequiredService<IViewComponentHelper>(); var viewComponentHelper = context.HttpContext.RequestServices.GetRequiredService<IViewComponentHelper>();
(viewComponentHelper as IViewContextAware)?.Contextualize(viewContext); (viewComponentHelper as IViewContextAware)?.Contextualize(viewContext);
var viewComponentResult = await GetViewComponentResult(viewComponentHelper, _logger, result); var viewComponentResult = await GetViewComponentResult(viewComponentHelper, _logger, result);
viewComponentResult.WriteTo(writer, _htmlEncoder);
if (viewComponentResult is ViewBuffer viewBuffer)
{
// In the ordinary case, DefaultViewComponentHelper will return an instance of ViewBuffer. We can simply
// invoke WriteToAsync on it.
await viewBuffer.WriteToAsync(writer, _htmlEncoder);
await writer.FlushAsync();
}
else
{
using var memoryStream = new MemoryStream();
using (var intermediateWriter = _writerFactory.CreateWriter(response.Body, resolvedContentTypeEncoding))
{
viewComponentResult.WriteTo(intermediateWriter, _htmlEncoder);
}
memoryStream.Position = 0;
await memoryStream.CopyToAsync(response.Body);
}
} }
} }

View File

@ -7,7 +7,6 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Testing; using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.WebEncoders.Testing; using Microsoft.Extensions.WebEncoders.Testing;
using Moq; using Moq;
@ -19,7 +18,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Buffers
{ {
[Fact] [Fact]
[ReplaceCulture] [ReplaceCulture]
public void Write_WritesDataTypes_ToBuffer() public void Write_WritesDataTypes()
{ {
// Arrange // Arrange
var expected = new object[] { "True", "3", "18446744073709551615", "Hello world", "3.14", "2.718", "m" }; var expected = new object[] { "True", "3", "18446744073709551615", "Hello world", "3.14", "2.718", "m" };
@ -41,86 +40,31 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Buffers
[Fact] [Fact]
[ReplaceCulture] [ReplaceCulture]
public void Write_WritesDataTypes_ToUnderlyingStream_WhenNotBuffering() public async Task Write_WritesDataTypes_AfterFlush()
{ {
// Arrange // Arrange
var expected = new[] { "True", "3", "18446744073709551615", "Hello world", "3.14", "2.718" }; var expected = new object[] { "True", "3", "18446744073709551615", "Hello world", "3.14", "2.718", "m" };
var inner = new Mock<TextWriter>();
var buffer = new ViewBuffer(new TestViewBufferScope(), "some-name", pageSize: 4); var buffer = new ViewBuffer(new TestViewBufferScope(), "some-name", pageSize: 4);
var writer = new ViewBufferTextWriter(buffer, Encoding.UTF8, new HtmlTestEncoder(), inner.Object); var writer = new ViewBufferTextWriter(buffer, Encoding.UTF8);
var testClass = new TestClass();
// Act
writer.Flush();
writer.Write(true);
writer.Write(3);
writer.Write(ulong.MaxValue);
writer.Write(testClass);
writer.Write(3.14);
writer.Write(2.718m);
// Assert
Assert.Equal(0, buffer.Count);
foreach (var item in expected)
{
inner.Verify(v => v.Write(item), Times.Once());
}
}
[Fact]
[ReplaceCulture]
public async Task Write_WritesCharValues_ToUnderlyingStream_WhenNotBuffering()
{
// Arrange
var inner = new Mock<TextWriter> { CallBase = true };
var buffer = new ViewBuffer(new TestViewBufferScope(), "some-name", pageSize: 4);
var writer = new ViewBufferTextWriter(buffer, Encoding.UTF8, new HtmlTestEncoder(), inner.Object);
var buffer1 = new[] { 'a', 'b', 'c', 'd' };
var buffer2 = new[] { 'd', 'e', 'f' };
// Act
writer.Flush();
writer.Write('x');
writer.Write(buffer1, 1, 2);
writer.Write(buffer2);
await writer.WriteAsync(buffer2, 1, 1);
await writer.WriteLineAsync(buffer1);
// Assert
inner.Verify(v => v.Write('x'), Times.Once());
inner.Verify(v => v.Write(buffer1, 1, 2), Times.Once());
inner.Verify(v => v.Write(buffer1, 0, 4), Times.Once());
inner.Verify(v => v.Write(buffer2, 0, 3), Times.Once());
inner.Verify(v => v.WriteAsync(buffer2, 1, 1), Times.Once());
inner.Verify(v => v.WriteLine(), Times.Once());
}
[Fact]
[ReplaceCulture]
public async Task Write_WritesStringValues_ToUnbufferedStream_WhenNotBuffering()
{
// Arrange
var inner = new Mock<TextWriter>();
var buffer = new ViewBuffer(new TestViewBufferScope(), "some-name", pageSize: 4);
var writer = new ViewBufferTextWriter(buffer, Encoding.UTF8, new HtmlTestEncoder(), inner.Object);
// Act // Act
await writer.FlushAsync(); await writer.FlushAsync();
writer.Write("a");
writer.WriteLine("ab"); writer.Write(true);
await writer.WriteAsync("ef"); writer.Write(3);
await writer.WriteLineAsync("gh"); writer.Write(ulong.MaxValue);
writer.Write(new TestClass());
writer.Write(3.14);
writer.Write(2.718m);
writer.Write('m');
// Assert // Assert
inner.Verify(v => v.Write("a"), Times.Once()); Assert.Equal(expected, GetValues(buffer));
inner.Verify(v => v.WriteLine("ab"), Times.Once());
inner.Verify(v => v.WriteAsync("ef"), Times.Once());
inner.Verify(v => v.WriteLineAsync("gh"), Times.Once());
} }
[Fact] [Fact]
[ReplaceCulture] [ReplaceCulture]
public void WriteLine_WritesDataTypes_ToBuffer() public void WriteLine_WritesDataTypes()
{ {
// Arrange // Arrange
var newLine = Environment.NewLine; var newLine = Environment.NewLine;
@ -139,9 +83,11 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Buffers
[Fact] [Fact]
[ReplaceCulture] [ReplaceCulture]
public void WriteLine_WritesDataTypes_ToUnbufferedStream_WhenNotBuffering() public void WriteLine_WritesDataType_AfterFlush()
{ {
// Arrange // Arrange
var newLine = Environment.NewLine;
var expected = new List<object> { "False", newLine, "1.1", newLine, "3", newLine };
var inner = new Mock<TextWriter>(); var inner = new Mock<TextWriter>();
var buffer = new ViewBuffer(new TestViewBufferScope(), "some-name", pageSize: 4); var buffer = new ViewBuffer(new TestViewBufferScope(), "some-name", pageSize: 4);
var writer = new ViewBufferTextWriter(buffer, Encoding.UTF8, new HtmlTestEncoder(), inner.Object); var writer = new ViewBufferTextWriter(buffer, Encoding.UTF8, new HtmlTestEncoder(), inner.Object);
@ -153,10 +99,12 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Buffers
writer.WriteLine(3L); writer.WriteLine(3L);
// Assert // Assert
inner.Verify(v => v.Write("False"), Times.Once()); inner.Verify(v => v.Write("False"), Times.Never());
inner.Verify(v => v.Write("1.1"), Times.Once()); inner.Verify(v => v.Write("1.1"), Times.Never());
inner.Verify(v => v.Write("3"), Times.Once()); inner.Verify(v => v.Write("3"), Times.Never());
inner.Verify(v => v.WriteLine(), Times.Exactly(3)); inner.Verify(v => v.WriteLine(), Times.Never());
Assert.Equal(expected, GetValues(buffer));
} }
[Fact] [Fact]
@ -199,25 +147,6 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Buffers
Assert.Equal<object>(new[] { input1, input2, newLine, input3, input4, newLine }, actual); Assert.Equal<object>(new[] { input1, input2, newLine, input3, input4, newLine }, actual);
} }
[Fact]
public void Write_HtmlContent_AfterFlush_GoesToStream()
{
// Arrange
var inner = new StringWriter();
var buffer = new ViewBuffer(new TestViewBufferScope(), "some-name", pageSize: 4);
var writer = new ViewBufferTextWriter(buffer, Encoding.UTF8, new HtmlTestEncoder(), inner);
writer.Flush();
var content = new HtmlString("Hello, world!");
// Act
writer.Write(content);
// Assert
Assert.Equal("Hello, world!", inner.ToString());
}
private static object[] GetValues(ViewBuffer buffer) private static object[] GetValues(ViewBuffer buffer)
{ {
var pages = new List<ViewBufferPage>(); var pages = new List<ViewBufferPage>();

View File

@ -397,6 +397,48 @@ namespace Microsoft.AspNetCore.Mvc
Assert.Equal("Hello, World!", body); Assert.Equal("Hello, World!", body);
} }
[Fact]
public async Task ExecuteResultAsync_WithCustomViewComponentHelper()
{
// Arrange
var expected = "Hello from custom helper";
var methodInfo = typeof(TextViewComponent).GetMethod(nameof(TextViewComponent.Invoke));
var descriptor = new ViewComponentDescriptor()
{
FullName = "Full.Name.Text",
ShortName = "Text",
TypeInfo = typeof(TextViewComponent).GetTypeInfo(),
MethodInfo = methodInfo,
Parameters = methodInfo.GetParameters(),
};
var result = Task.FromResult<IHtmlContent>(new HtmlContentBuilder().AppendHtml(expected));
var helper = Mock.Of<IViewComponentHelper>(h => h.InvokeAsync(It.IsAny<Type>(), It.IsAny<object>()) == result);
var httpContext = new DefaultHttpContext();
var services = CreateServices(diagnosticListener: null, httpContext, new[] { descriptor });
services.AddSingleton<IViewComponentHelper>(helper);
httpContext.RequestServices = services.BuildServiceProvider();
httpContext.Response.Body = new MemoryStream();
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
var viewComponentResult = new ViewComponentResult()
{
Arguments = new { name = "World!" },
ViewComponentType = typeof(TextViewComponent),
TempData = _tempDataDictionary,
};
// Act
await viewComponentResult.ExecuteResultAsync(actionContext);
// Assert
var body = ReadBody(actionContext.HttpContext.Response);
Assert.Equal(expected, body);
}
[Fact] [Fact]
public async Task ExecuteResultAsync_SetsStatusCode() public async Task ExecuteResultAsync_SetsStatusCode()
{ {
@ -600,6 +642,7 @@ namespace Microsoft.AspNetCore.Mvc
services.AddSingleton<HtmlEncoder, HtmlTestEncoder>(); services.AddSingleton<HtmlEncoder, HtmlTestEncoder>();
services.AddSingleton<IViewBufferScope, TestViewBufferScope>(); services.AddSingleton<IViewBufferScope, TestViewBufferScope>();
services.AddSingleton<IActionResultExecutor<ViewComponentResult>, ViewComponentResultExecutor>(); services.AddSingleton<IActionResultExecutor<ViewComponentResult>, ViewComponentResultExecutor>();
services.AddSingleton<IHttpResponseStreamWriterFactory, TestHttpResponseStreamWriterFactory>();
return services; return services;
} }
@ -625,7 +668,6 @@ namespace Microsoft.AspNetCore.Mvc
return CreateActionContext(null, descriptors); return CreateActionContext(null, descriptors);
} }
private class FixedSetViewComponentDescriptorProvider : IViewComponentDescriptorProvider private class FixedSetViewComponentDescriptorProvider : IViewComponentDescriptorProvider
{ {
private readonly ViewComponentDescriptor[] _descriptors; private readonly ViewComponentDescriptor[] _descriptors;

View File

@ -35,6 +35,33 @@ RenderBody content
Assert.Equal(expected, body, ignoreLineEndingDifferences: true); Assert.Equal(expected, body, ignoreLineEndingDifferences: true);
} }
[Fact]
public async Task FlushFollowedByLargeContent()
{
// Arrange
var expected = new string('a', 1024 * 1024);
// Act
var document = await Client.GetHtmlDocumentAsync("http://localhost/FlushPoint/FlushFollowedByLargeContent");
// Assert
var largeContent = document.RequiredQuerySelector("#large-content");
Assert.StartsWith(expected, largeContent.TextContent);
}
[Fact]
public async Task FlushInvokedInComponent()
{
var expected = new string('a', 1024 * 1024);
// Act
var document = await Client.GetHtmlDocumentAsync("http://localhost/FlushPoint/FlushInvokedInComponent");
// Assert
var largeContent = document.RequiredQuerySelector("#large-content");
Assert.StartsWith(expected, largeContent.TextContent);
}
[Fact] [Fact]
public async Task FlushPointsAreExecutedForPagesWithoutLayouts() public async Task FlushPointsAreExecutedForPagesWithoutLayouts()
{ {

View File

@ -0,0 +1,16 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Mvc;
namespace MvcSample.Web.Components
{
[ViewComponent(Name = "ComponentWithFlush")]
public class ComponentWithFlush : ViewComponent
{
public IViewComponentResult Invoke()
{
return View();
}
}
}

View File

@ -12,6 +12,10 @@ namespace RazorWebSite
return View(); return View();
} }
public IActionResult FlushFollowedByLargeContent() => View();
public IActionResult FlushInvokedInComponent() => View();
public IActionResult PageWithoutLayout() public IActionResult PageWithoutLayout()
{ {
return View(); return View();

View File

@ -0,0 +1,7 @@
Header content
@{
await FlushAsync();
var largeContent = new string('a', 1024 * 1024);
}
<span id="large-content">@largeContent</span>

View File

@ -0,0 +1 @@
@await Component.InvokeAsync("ComponentWithFlush")

View File

@ -0,0 +1,8 @@
Hello from component
@{
await FlushAsync();
var largeContent = new string('a', 1024 * 1024);
}
<span id="large-content">@largeContent</span>