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:
parent
fb7e8a6895
commit
2be80522b2
|
|
@ -370,12 +370,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor
|
|||
var encoder = HtmlEncoder;
|
||||
if (value is IHtmlContent htmlContent)
|
||||
{
|
||||
var bufferedWriter = writer as ViewBufferTextWriter;
|
||||
if (bufferedWriter == null || !bufferedWriter.IsBuffering)
|
||||
{
|
||||
htmlContent.WriteTo(writer, encoder);
|
||||
}
|
||||
else
|
||||
if (writer is ViewBufferTextWriter bufferedWriter)
|
||||
{
|
||||
if (value is IHtmlContentContainer htmlContentContainer)
|
||||
{
|
||||
|
|
@ -389,6 +384,10 @@ namespace Microsoft.AspNetCore.Mvc.Razor
|
|||
bufferedWriter.Buffer.AppendHtml(htmlContent);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
htmlContent.WriteTo(writer, encoder);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -233,7 +233,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor
|
|||
// (including the layout page we just rendered).
|
||||
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
|
||||
// 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();
|
||||
}
|
||||
|
||||
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
|
||||
// really depends on whether or not we're writing directly to the output or if we're writing to
|
||||
// another buffer.
|
||||
var viewBufferTextWriter = context.Writer as ViewBufferTextWriter;
|
||||
if (viewBufferTextWriter == null || !viewBufferTextWriter.IsBuffering)
|
||||
// This means we're writing to another buffer. Use MoveTo to combine them.
|
||||
bodyWriter.Buffer.MoveTo(viewBufferTextWriter.Buffer);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 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.
|
||||
// We're using PagedBufferedTextWriter here to 'smooth' synchronous writes of IHtmlContent values.
|
||||
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);
|
||||
await bodyWriter.Buffer.WriteToAsync(writer, _htmlEncoder);
|
||||
await writer.FlushAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1204,7 +1204,9 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
|||
}
|
||||
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, Microsoft.AspNetCore.Mvc.Infrastructure.IHttpResponseStreamWriterFactory writerFactory) { }
|
||||
[System.Diagnostics.DebuggerStepThroughAttribute]
|
||||
public virtual System.Threading.Tasks.Task ExecuteAsync(Microsoft.AspNetCore.Mvc.ActionContext context, Microsoft.AspNetCore.Mvc.ViewComponentResult result) { throw null; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -86,25 +86,20 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Buffers
|
|||
/// <inheritdoc />
|
||||
public override Encoding Encoding { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsBuffering { get; private set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="ViewBuffer"/>.
|
||||
/// </summary>
|
||||
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 />
|
||||
public override void Write(char value)
|
||||
{
|
||||
if (IsBuffering)
|
||||
{
|
||||
Buffer.AppendHtml(value.ToString());
|
||||
}
|
||||
else
|
||||
{
|
||||
_inner.Write(value);
|
||||
}
|
||||
Buffer.AppendHtml(value.ToString());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
|
@ -125,14 +120,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Buffers
|
|||
throw new ArgumentOutOfRangeException(nameof(count));
|
||||
}
|
||||
|
||||
if (IsBuffering)
|
||||
{
|
||||
Buffer.AppendHtml(new string(buffer, index, count));
|
||||
}
|
||||
else
|
||||
{
|
||||
_inner.Write(buffer, index, count);
|
||||
}
|
||||
Buffer.AppendHtml(new string(buffer, index, count));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
|
@ -143,14 +131,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Buffers
|
|||
return;
|
||||
}
|
||||
|
||||
if (IsBuffering)
|
||||
{
|
||||
Buffer.AppendHtml(value);
|
||||
}
|
||||
else
|
||||
{
|
||||
_inner.Write(value);
|
||||
}
|
||||
Buffer.AppendHtml(value);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
|
@ -186,14 +167,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Buffers
|
|||
return;
|
||||
}
|
||||
|
||||
if (IsBuffering)
|
||||
{
|
||||
Buffer.AppendHtml(value);
|
||||
}
|
||||
else
|
||||
{
|
||||
value.WriteTo(_inner, _htmlEncoder);
|
||||
}
|
||||
Buffer.AppendHtml(value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -207,14 +181,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Buffers
|
|||
return;
|
||||
}
|
||||
|
||||
if (IsBuffering)
|
||||
{
|
||||
value.MoveTo(Buffer);
|
||||
}
|
||||
else
|
||||
{
|
||||
value.WriteTo(_inner, _htmlEncoder);
|
||||
}
|
||||
value.MoveTo(Buffer);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
|
@ -245,15 +212,8 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Buffers
|
|||
/// <inheritdoc />
|
||||
public override Task WriteAsync(char value)
|
||||
{
|
||||
if (IsBuffering)
|
||||
{
|
||||
Buffer.AppendHtml(value.ToString());
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
else
|
||||
{
|
||||
return _inner.WriteAsync(value);
|
||||
}
|
||||
Buffer.AppendHtml(value.ToString());
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
|
@ -273,121 +233,64 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Buffers
|
|||
throw new ArgumentOutOfRangeException(nameof(count));
|
||||
}
|
||||
|
||||
if (IsBuffering)
|
||||
{
|
||||
Buffer.AppendHtml(new string(buffer, index, count));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
else
|
||||
{
|
||||
return _inner.WriteAsync(buffer, index, count);
|
||||
}
|
||||
Buffer.AppendHtml(new string(buffer, index, count));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task WriteAsync(string value)
|
||||
{
|
||||
if (IsBuffering)
|
||||
{
|
||||
Buffer.AppendHtml(value);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
else
|
||||
{
|
||||
return _inner.WriteAsync(value);
|
||||
}
|
||||
Buffer.AppendHtml(value);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void WriteLine()
|
||||
{
|
||||
if (IsBuffering)
|
||||
{
|
||||
Buffer.AppendHtml(NewLine);
|
||||
}
|
||||
else
|
||||
{
|
||||
_inner.WriteLine();
|
||||
}
|
||||
Buffer.AppendHtml(NewLine);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void WriteLine(string value)
|
||||
{
|
||||
if (IsBuffering)
|
||||
{
|
||||
Buffer.AppendHtml(value);
|
||||
Buffer.AppendHtml(NewLine);
|
||||
}
|
||||
else
|
||||
{
|
||||
_inner.WriteLine(value);
|
||||
}
|
||||
Buffer.AppendHtml(value);
|
||||
Buffer.AppendHtml(NewLine);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task WriteLineAsync(char value)
|
||||
{
|
||||
if (IsBuffering)
|
||||
{
|
||||
Buffer.AppendHtml(value.ToString());
|
||||
Buffer.AppendHtml(NewLine);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
else
|
||||
{
|
||||
return _inner.WriteLineAsync(value);
|
||||
}
|
||||
Buffer.AppendHtml(value.ToString());
|
||||
Buffer.AppendHtml(NewLine);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task WriteLineAsync(char[] value, int start, int offset)
|
||||
{
|
||||
if (IsBuffering)
|
||||
{
|
||||
Buffer.AppendHtml(new string(value, start, offset));
|
||||
Buffer.AppendHtml(NewLine);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
else
|
||||
{
|
||||
return _inner.WriteLineAsync(value, start, offset);
|
||||
}
|
||||
Buffer.AppendHtml(new string(value, start, offset));
|
||||
Buffer.AppendHtml(NewLine);
|
||||
return Task.CompletedTask;
|
||||
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task WriteLineAsync(string value)
|
||||
{
|
||||
if (IsBuffering)
|
||||
{
|
||||
Buffer.AppendHtml(value);
|
||||
Buffer.AppendHtml(NewLine);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
else
|
||||
{
|
||||
return _inner.WriteLineAsync(value);
|
||||
}
|
||||
Buffer.AppendHtml(value);
|
||||
Buffer.AppendHtml(NewLine);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task WriteLineAsync()
|
||||
{
|
||||
if (IsBuffering)
|
||||
{
|
||||
Buffer.AppendHtml(NewLine);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
else
|
||||
{
|
||||
return _inner.WriteLineAsync();
|
||||
}
|
||||
Buffer.AppendHtml(NewLine);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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>
|
||||
public override void Flush()
|
||||
{
|
||||
|
|
@ -396,20 +299,16 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Buffers
|
|||
return;
|
||||
}
|
||||
|
||||
if (IsBuffering)
|
||||
{
|
||||
IsBuffering = false;
|
||||
Buffer.WriteTo(_inner, _htmlEncoder);
|
||||
Buffer.Clear();
|
||||
}
|
||||
Flushed = true;
|
||||
|
||||
Buffer.WriteTo(_inner, _htmlEncoder);
|
||||
Buffer.Clear();
|
||||
|
||||
_inner.Flush();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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>
|
||||
/// <returns>A <see cref="Task"/> that represents the asynchronous copy and flush operations.</returns>
|
||||
public override async Task FlushAsync()
|
||||
|
|
@ -419,12 +318,10 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Buffers
|
|||
return;
|
||||
}
|
||||
|
||||
if (IsBuffering)
|
||||
{
|
||||
IsBuffering = false;
|
||||
await Buffer.WriteToAsync(_inner, _htmlEncoder);
|
||||
Buffer.Clear();
|
||||
}
|
||||
Flushed = true;
|
||||
|
||||
await Buffer.WriteToAsync(_inner, _htmlEncoder);
|
||||
Buffer.Clear();
|
||||
|
||||
await _inner.FlushAsync();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
// 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.Encodings.Web;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Html;
|
||||
|
|
@ -9,8 +10,8 @@ using Microsoft.AspNetCore.Mvc.Formatters;
|
|||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using Microsoft.AspNetCore.Mvc.ViewFeatures.Buffers;
|
||||
using Microsoft.AspNetCore.Mvc.ViewFeatures.Filters;
|
||||
using Microsoft.AspNetCore.WebUtilities;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
|
@ -24,13 +25,26 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
|||
private readonly ILogger<ViewComponentResult> _logger;
|
||||
private readonly IModelMetadataProvider _modelMetadataProvider;
|
||||
private readonly ITempDataDictionaryFactory _tempDataDictionaryFactory;
|
||||
private IHttpResponseStreamWriterFactory _writerFactory;
|
||||
|
||||
[Obsolete("This constructor is obsolete and will be removed in a future version.")]
|
||||
public ViewComponentResultExecutor(
|
||||
IOptions<MvcViewOptions> mvcHelperOptions,
|
||||
ILoggerFactory loggerFactory,
|
||||
HtmlEncoder htmlEncoder,
|
||||
IModelMetadataProvider modelMetadataProvider,
|
||||
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)
|
||||
{
|
||||
|
|
@ -62,6 +76,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
|||
_htmlEncoder = htmlEncoder;
|
||||
_modelMetadataProvider = modelMetadataProvider;
|
||||
_tempDataDictionaryFactory = tempDataDictionaryFactory;
|
||||
_writerFactory = writerFactory;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
|
@ -105,14 +120,9 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
|||
response.StatusCode = result.StatusCode.Value;
|
||||
}
|
||||
|
||||
// Opt into sync IO support until we can work out an alternative https://github.com/aspnet/AspNetCore/issues/6397
|
||||
var syncIOFeature = context.HttpContext.Features.Get<Http.Features.IHttpBodyControlFeature>();
|
||||
if (syncIOFeature != null)
|
||||
{
|
||||
syncIOFeature.AllowSynchronousIO = true;
|
||||
}
|
||||
_writerFactory ??= context.HttpContext.RequestServices.GetRequiredService<IHttpResponseStreamWriterFactory>();
|
||||
|
||||
using (var writer = new HttpResponseStreamWriter(response.Body, resolvedContentTypeEncoding))
|
||||
using (var writer = _writerFactory.CreateWriter(response.Body, resolvedContentTypeEncoding))
|
||||
{
|
||||
var viewContext = new ViewContext(
|
||||
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.
|
||||
var viewComponentHelper = context.HttpContext.RequestServices.GetRequiredService<IViewComponentHelper>();
|
||||
(viewComponentHelper as IViewContextAware)?.Contextualize(viewContext);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ using System.IO;
|
|||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Html;
|
||||
using Microsoft.AspNetCore.Testing;
|
||||
using Microsoft.Extensions.WebEncoders.Testing;
|
||||
using Moq;
|
||||
|
|
@ -19,7 +18,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Buffers
|
|||
{
|
||||
[Fact]
|
||||
[ReplaceCulture]
|
||||
public void Write_WritesDataTypes_ToBuffer()
|
||||
public void Write_WritesDataTypes()
|
||||
{
|
||||
// Arrange
|
||||
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]
|
||||
[ReplaceCulture]
|
||||
public void Write_WritesDataTypes_ToUnderlyingStream_WhenNotBuffering()
|
||||
public async Task Write_WritesDataTypes_AfterFlush()
|
||||
{
|
||||
// Arrange
|
||||
var expected = new[] { "True", "3", "18446744073709551615", "Hello world", "3.14", "2.718" };
|
||||
var inner = new Mock<TextWriter>();
|
||||
var expected = new object[] { "True", "3", "18446744073709551615", "Hello world", "3.14", "2.718", "m" };
|
||||
var buffer = new ViewBuffer(new TestViewBufferScope(), "some-name", pageSize: 4);
|
||||
var writer = new ViewBufferTextWriter(buffer, Encoding.UTF8, new HtmlTestEncoder(), inner.Object);
|
||||
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);
|
||||
var writer = new ViewBufferTextWriter(buffer, Encoding.UTF8);
|
||||
|
||||
// Act
|
||||
await writer.FlushAsync();
|
||||
writer.Write("a");
|
||||
writer.WriteLine("ab");
|
||||
await writer.WriteAsync("ef");
|
||||
await writer.WriteLineAsync("gh");
|
||||
|
||||
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
|
||||
inner.Verify(v => v.Write("a"), Times.Once());
|
||||
inner.Verify(v => v.WriteLine("ab"), Times.Once());
|
||||
inner.Verify(v => v.WriteAsync("ef"), Times.Once());
|
||||
inner.Verify(v => v.WriteLineAsync("gh"), Times.Once());
|
||||
Assert.Equal(expected, GetValues(buffer));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[ReplaceCulture]
|
||||
public void WriteLine_WritesDataTypes_ToBuffer()
|
||||
public void WriteLine_WritesDataTypes()
|
||||
{
|
||||
// Arrange
|
||||
var newLine = Environment.NewLine;
|
||||
|
|
@ -139,9 +83,11 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Buffers
|
|||
|
||||
[Fact]
|
||||
[ReplaceCulture]
|
||||
public void WriteLine_WritesDataTypes_ToUnbufferedStream_WhenNotBuffering()
|
||||
public void WriteLine_WritesDataType_AfterFlush()
|
||||
{
|
||||
// Arrange
|
||||
var newLine = Environment.NewLine;
|
||||
var expected = new List<object> { "False", newLine, "1.1", newLine, "3", newLine };
|
||||
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);
|
||||
|
|
@ -153,10 +99,12 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Buffers
|
|||
writer.WriteLine(3L);
|
||||
|
||||
// Assert
|
||||
inner.Verify(v => v.Write("False"), Times.Once());
|
||||
inner.Verify(v => v.Write("1.1"), Times.Once());
|
||||
inner.Verify(v => v.Write("3"), Times.Once());
|
||||
inner.Verify(v => v.WriteLine(), Times.Exactly(3));
|
||||
inner.Verify(v => v.Write("False"), Times.Never());
|
||||
inner.Verify(v => v.Write("1.1"), Times.Never());
|
||||
inner.Verify(v => v.Write("3"), Times.Never());
|
||||
inner.Verify(v => v.WriteLine(), Times.Never());
|
||||
|
||||
Assert.Equal(expected, GetValues(buffer));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -199,25 +147,6 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Buffers
|
|||
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)
|
||||
{
|
||||
var pages = new List<ViewBufferPage>();
|
||||
|
|
|
|||
|
|
@ -397,6 +397,48 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
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]
|
||||
public async Task ExecuteResultAsync_SetsStatusCode()
|
||||
{
|
||||
|
|
@ -600,6 +642,7 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
services.AddSingleton<HtmlEncoder, HtmlTestEncoder>();
|
||||
services.AddSingleton<IViewBufferScope, TestViewBufferScope>();
|
||||
services.AddSingleton<IActionResultExecutor<ViewComponentResult>, ViewComponentResultExecutor>();
|
||||
services.AddSingleton<IHttpResponseStreamWriterFactory, TestHttpResponseStreamWriterFactory>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
|
@ -625,7 +668,6 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
return CreateActionContext(null, descriptors);
|
||||
}
|
||||
|
||||
|
||||
private class FixedSetViewComponentDescriptorProvider : IViewComponentDescriptorProvider
|
||||
{
|
||||
private readonly ViewComponentDescriptor[] _descriptors;
|
||||
|
|
|
|||
|
|
@ -35,6 +35,33 @@ RenderBody content
|
|||
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]
|
||||
public async Task FlushPointsAreExecutedForPagesWithoutLayouts()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -12,6 +12,10 @@ namespace RazorWebSite
|
|||
return View();
|
||||
}
|
||||
|
||||
public IActionResult FlushFollowedByLargeContent() => View();
|
||||
|
||||
public IActionResult FlushInvokedInComponent() => View();
|
||||
|
||||
public IActionResult PageWithoutLayout()
|
||||
{
|
||||
return View();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
Header content
|
||||
@{
|
||||
await FlushAsync();
|
||||
var largeContent = new string('a', 1024 * 1024);
|
||||
}
|
||||
|
||||
<span id="large-content">@largeContent</span>
|
||||
|
|
@ -0,0 +1 @@
|
|||
@await Component.InvokeAsync("ComponentWithFlush")
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
Hello from component
|
||||
@{
|
||||
await FlushAsync();
|
||||
var largeContent = new string('a', 1024 * 1024);
|
||||
}
|
||||
|
||||
<span id="large-content">@largeContent</span>
|
||||
|
||||
Loading…
Reference in New Issue