From 833b5db4d85fba51942773af7e78ada201be41a2 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Mon, 15 Apr 2019 12:45:21 -0700 Subject: [PATCH 1/2] Prevent synchronous writes when using Razor * 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 --- src/Mvc/Mvc.Razor/src/RazorPageBase.cs | 11 +- src/Mvc/Mvc.Razor/src/RazorView.cs | 33 ++-- .../src/Buffers/ViewBufferTextWriter.cs | 181 ++++-------------- .../src/ViewComponentResultExecutor.cs | 47 ++++- .../test/Buffers/ViewBufferTextWriterTest.cs | 109 ++--------- .../test/ViewComponentResultTest.cs | 44 ++++- .../Mvc.FunctionalTests/FlushPointTest.cs | 27 +++ .../Components/ComponentWithFlush.cs | 16 ++ .../RazorWebSite/Controllers/FlushPoint.cs | 4 + .../FlushFollowedByLargeContent.cshtml | 7 + .../FlushPoint/FlushInvokedInComponent.cshtml | 1 + .../ComponentWithFlush/Default.cshtml | 8 + 12 files changed, 221 insertions(+), 267 deletions(-) create mode 100644 src/Mvc/test/WebSites/RazorWebSite/Components/ComponentWithFlush.cs create mode 100644 src/Mvc/test/WebSites/RazorWebSite/Views/FlushPoint/FlushFollowedByLargeContent.cshtml create mode 100644 src/Mvc/test/WebSites/RazorWebSite/Views/FlushPoint/FlushInvokedInComponent.cshtml create mode 100644 src/Mvc/test/WebSites/RazorWebSite/Views/Shared/Components/ComponentWithFlush/Default.cshtml diff --git a/src/Mvc/Mvc.Razor/src/RazorPageBase.cs b/src/Mvc/Mvc.Razor/src/RazorPageBase.cs index 392455db59a9..c8af0141b96e 100644 --- a/src/Mvc/Mvc.Razor/src/RazorPageBase.cs +++ b/src/Mvc/Mvc.Razor/src/RazorPageBase.cs @@ -370,12 +370,7 @@ public virtual void Write(object value) 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 @@ public virtual void Write(object value) bufferedWriter.Buffer.AppendHtml(htmlContent); } } + else + { + htmlContent.WriteTo(writer, encoder); + } return; } diff --git a/src/Mvc/Mvc.Razor/src/RazorView.cs b/src/Mvc/Mvc.Razor/src/RazorView.cs index 8b5b3ebe901c..f4246d7fef0e 100644 --- a/src/Mvc/Mvc.Razor/src/RazorView.cs +++ b/src/Mvc/Mvc.Razor/src/RazorView.cs @@ -233,7 +233,7 @@ private async Task RenderLayoutAsync( // (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 @@ private async Task RenderLayoutAsync( 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 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); + } + 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 another buffer. Use MoveTo to combine them. - bodyWriter.Buffer.MoveTo(viewBufferTextWriter.Buffer); + await bodyWriter.Buffer.WriteToAsync(writer, _htmlEncoder); + await writer.FlushAsync(); } } } diff --git a/src/Mvc/Mvc.ViewFeatures/src/Buffers/ViewBufferTextWriter.cs b/src/Mvc/Mvc.ViewFeatures/src/Buffers/ViewBufferTextWriter.cs index 84f3a993a238..6eca8ddcb40e 100644 --- a/src/Mvc/Mvc.ViewFeatures/src/Buffers/ViewBufferTextWriter.cs +++ b/src/Mvc/Mvc.ViewFeatures/src/Buffers/ViewBufferTextWriter.cs @@ -86,25 +86,20 @@ public ViewBufferTextWriter(ViewBuffer buffer, Encoding encoding, HtmlEncoder ht /// public override Encoding Encoding { get; } - /// - public bool IsBuffering { get; private set; } = true; - /// /// Gets the . /// public ViewBuffer Buffer { get; } + /// + /// Gets a value that indiciates if or was invoked. + /// + public bool Flushed { get; private set; } + /// public override void Write(char value) { - if (IsBuffering) - { - Buffer.AppendHtml(value.ToString()); - } - else - { - _inner.Write(value); - } + Buffer.AppendHtml(value.ToString()); } /// @@ -125,14 +120,7 @@ public override void Write(char[] buffer, int index, int count) 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)); } /// @@ -143,14 +131,7 @@ public override void Write(string value) return; } - if (IsBuffering) - { - Buffer.AppendHtml(value); - } - else - { - _inner.Write(value); - } + Buffer.AppendHtml(value); } /// @@ -186,14 +167,7 @@ public void Write(IHtmlContent value) return; } - if (IsBuffering) - { - Buffer.AppendHtml(value); - } - else - { - value.WriteTo(_inner, _htmlEncoder); - } + Buffer.AppendHtml(value); } /// @@ -207,14 +181,7 @@ public void Write(IHtmlContentContainer value) return; } - if (IsBuffering) - { - value.MoveTo(Buffer); - } - else - { - value.WriteTo(_inner, _htmlEncoder); - } + value.MoveTo(Buffer); } /// @@ -245,15 +212,8 @@ public override void WriteLine(object value) /// 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; } /// @@ -273,121 +233,64 @@ public override Task WriteAsync(char[] buffer, int index, int count) 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; } /// 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; } /// public override void WriteLine() { - if (IsBuffering) - { - Buffer.AppendHtml(NewLine); - } - else - { - _inner.WriteLine(); - } + Buffer.AppendHtml(NewLine); } /// public override void WriteLine(string value) { - if (IsBuffering) - { - Buffer.AppendHtml(value); - Buffer.AppendHtml(NewLine); - } - else - { - _inner.WriteLine(value); - } + Buffer.AppendHtml(value); + Buffer.AppendHtml(NewLine); } /// 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; } /// 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; + } /// 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; } /// public override Task WriteLineAsync() { - if (IsBuffering) - { - Buffer.AppendHtml(NewLine); - return Task.CompletedTask; - } - else - { - return _inner.WriteLineAsync(); - } + Buffer.AppendHtml(NewLine); + return Task.CompletedTask; } /// /// 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. /// public override void Flush() { @@ -396,20 +299,16 @@ public override void Flush() return; } - if (IsBuffering) - { - IsBuffering = false; - Buffer.WriteTo(_inner, _htmlEncoder); - Buffer.Clear(); - } + Flushed = true; + + Buffer.WriteTo(_inner, _htmlEncoder); + Buffer.Clear(); _inner.Flush(); } /// /// 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. /// /// A that represents the asynchronous copy and flush operations. public override async Task FlushAsync() @@ -419,12 +318,10 @@ public override async Task FlushAsync() return; } - if (IsBuffering) - { - IsBuffering = false; - await Buffer.WriteToAsync(_inner, _htmlEncoder); - Buffer.Clear(); - } + Flushed = true; + + await Buffer.WriteToAsync(_inner, _htmlEncoder); + Buffer.Clear(); await _inner.FlushAsync(); } diff --git a/src/Mvc/Mvc.ViewFeatures/src/ViewComponentResultExecutor.cs b/src/Mvc/Mvc.ViewFeatures/src/ViewComponentResultExecutor.cs index 0d839e7b6840..ed480263c9f1 100644 --- a/src/Mvc/Mvc.ViewFeatures/src/ViewComponentResultExecutor.cs +++ b/src/Mvc/Mvc.ViewFeatures/src/ViewComponentResultExecutor.cs @@ -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.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 @@ public class ViewComponentResultExecutor : IActionResultExecutor _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 mvcHelperOptions, ILoggerFactory loggerFactory, HtmlEncoder htmlEncoder, IModelMetadataProvider modelMetadataProvider, ITempDataDictionaryFactory tempDataDictionaryFactory) + : this(mvcHelperOptions, loggerFactory, htmlEncoder, modelMetadataProvider, tempDataDictionaryFactory, null) + { + } + + public ViewComponentResultExecutor( + IOptions mvcHelperOptions, + ILoggerFactory loggerFactory, + HtmlEncoder htmlEncoder, + IModelMetadataProvider modelMetadataProvider, + ITempDataDictionaryFactory tempDataDictionaryFactory, + IHttpResponseStreamWriterFactory writerFactory) { if (mvcHelperOptions == null) { @@ -62,6 +76,7 @@ public ViewComponentResultExecutor( _htmlEncoder = htmlEncoder; _modelMetadataProvider = modelMetadataProvider; _tempDataDictionaryFactory = tempDataDictionaryFactory; + _writerFactory = writerFactory; } /// @@ -105,14 +120,9 @@ public virtual async Task ExecuteAsync(ActionContext context, ViewComponentResul 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(); - if (syncIOFeature != null) - { - syncIOFeature.AllowSynchronousIO = true; - } + _writerFactory ??= context.HttpContext.RequestServices.GetRequiredService(); - 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 @@ public virtual async Task ExecuteAsync(ActionContext context, ViewComponentResul // IViewComponentHelper is stateful, we want to make sure to retrieve it every time we need it. var viewComponentHelper = context.HttpContext.RequestServices.GetRequiredService(); (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); + } } } diff --git a/src/Mvc/Mvc.ViewFeatures/test/Buffers/ViewBufferTextWriterTest.cs b/src/Mvc/Mvc.ViewFeatures/test/Buffers/ViewBufferTextWriterTest.cs index 8b0619ad775c..7a0c5bb46c2e 100644 --- a/src/Mvc/Mvc.ViewFeatures/test/Buffers/ViewBufferTextWriterTest.cs +++ b/src/Mvc/Mvc.ViewFeatures/test/Buffers/ViewBufferTextWriterTest.cs @@ -7,7 +7,6 @@ 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 @@ public class ViewBufferTextWriterTest { [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 @@ public void Write_WritesDataTypes_ToBuffer() [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(); + 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(); + var writer = new ViewBufferTextWriter(buffer, Encoding.UTF8); // Act - writer.Flush(); + await writer.FlushAsync(); + writer.Write(true); writer.Write(3); writer.Write(ulong.MaxValue); - writer.Write(testClass); + writer.Write(new TestClass()); writer.Write(3.14); writer.Write(2.718m); + writer.Write('m'); // 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 { 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(); - var buffer = new ViewBuffer(new TestViewBufferScope(), "some-name", pageSize: 4); - var writer = new ViewBufferTextWriter(buffer, Encoding.UTF8, new HtmlTestEncoder(), inner.Object); - - // Act - await writer.FlushAsync(); - writer.Write("a"); - writer.WriteLine("ab"); - await writer.WriteAsync("ef"); - await writer.WriteLineAsync("gh"); - - // 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 @@ public void WriteLine_WritesDataTypes_ToBuffer() [Fact] [ReplaceCulture] - public void WriteLine_WritesDataTypes_ToUnbufferedStream_WhenNotBuffering() + public void WriteLine_WritesDataType_AfterFlush() { // Arrange + var newLine = Environment.NewLine; + var expected = new List { "False", newLine, "1.1", newLine, "3", newLine }; var inner = new Mock(); 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 @@ public void WriteLine_WritesDataTypes_ToUnbufferedStream_WhenNotBuffering() 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 @@ public async Task Write_WritesStringBuffer() Assert.Equal(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(); diff --git a/src/Mvc/Mvc.ViewFeatures/test/ViewComponentResultTest.cs b/src/Mvc/Mvc.ViewFeatures/test/ViewComponentResultTest.cs index 2c65acccb2ce..b217c0867956 100644 --- a/src/Mvc/Mvc.ViewFeatures/test/ViewComponentResultTest.cs +++ b/src/Mvc/Mvc.ViewFeatures/test/ViewComponentResultTest.cs @@ -397,6 +397,48 @@ public async Task ExecuteResultAsync_ExecutesViewComponent_ByType() 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(new HtmlContentBuilder().AppendHtml(expected)); + + var helper = Mock.Of(h => h.InvokeAsync(It.IsAny(), It.IsAny()) == result); + + var httpContext = new DefaultHttpContext(); + var services = CreateServices(diagnosticListener: null, httpContext, new[] { descriptor }); + services.AddSingleton(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 @@ private IServiceCollection CreateServices( services.AddSingleton(); services.AddSingleton(); services.AddSingleton, ViewComponentResultExecutor>(); + services.AddSingleton(); return services; } @@ -625,7 +668,6 @@ private ActionContext CreateActionContext(params ViewComponentDescriptor[] descr return CreateActionContext(null, descriptors); } - private class FixedSetViewComponentDescriptorProvider : IViewComponentDescriptorProvider { private readonly ViewComponentDescriptor[] _descriptors; diff --git a/src/Mvc/test/Mvc.FunctionalTests/FlushPointTest.cs b/src/Mvc/test/Mvc.FunctionalTests/FlushPointTest.cs index ebbba5d9e6c0..f98f327c8ede 100644 --- a/src/Mvc/test/Mvc.FunctionalTests/FlushPointTest.cs +++ b/src/Mvc/test/Mvc.FunctionalTests/FlushPointTest.cs @@ -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() { diff --git a/src/Mvc/test/WebSites/RazorWebSite/Components/ComponentWithFlush.cs b/src/Mvc/test/WebSites/RazorWebSite/Components/ComponentWithFlush.cs new file mode 100644 index 000000000000..7a3045dbad2b --- /dev/null +++ b/src/Mvc/test/WebSites/RazorWebSite/Components/ComponentWithFlush.cs @@ -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(); + } + } +} \ No newline at end of file diff --git a/src/Mvc/test/WebSites/RazorWebSite/Controllers/FlushPoint.cs b/src/Mvc/test/WebSites/RazorWebSite/Controllers/FlushPoint.cs index d55b831bb8d9..82c2b625ac75 100644 --- a/src/Mvc/test/WebSites/RazorWebSite/Controllers/FlushPoint.cs +++ b/src/Mvc/test/WebSites/RazorWebSite/Controllers/FlushPoint.cs @@ -12,6 +12,10 @@ public IActionResult PageWithLayout() return View(); } + public IActionResult FlushFollowedByLargeContent() => View(); + + public IActionResult FlushInvokedInComponent() => View(); + public IActionResult PageWithoutLayout() { return View(); diff --git a/src/Mvc/test/WebSites/RazorWebSite/Views/FlushPoint/FlushFollowedByLargeContent.cshtml b/src/Mvc/test/WebSites/RazorWebSite/Views/FlushPoint/FlushFollowedByLargeContent.cshtml new file mode 100644 index 000000000000..a437fd50f627 --- /dev/null +++ b/src/Mvc/test/WebSites/RazorWebSite/Views/FlushPoint/FlushFollowedByLargeContent.cshtml @@ -0,0 +1,7 @@ +Header content +@{ + await FlushAsync(); + var largeContent = new string('a', 1024 * 1024); +} + +@largeContent diff --git a/src/Mvc/test/WebSites/RazorWebSite/Views/FlushPoint/FlushInvokedInComponent.cshtml b/src/Mvc/test/WebSites/RazorWebSite/Views/FlushPoint/FlushInvokedInComponent.cshtml new file mode 100644 index 000000000000..2d084cd2616f --- /dev/null +++ b/src/Mvc/test/WebSites/RazorWebSite/Views/FlushPoint/FlushInvokedInComponent.cshtml @@ -0,0 +1 @@ +@await Component.InvokeAsync("ComponentWithFlush") \ No newline at end of file diff --git a/src/Mvc/test/WebSites/RazorWebSite/Views/Shared/Components/ComponentWithFlush/Default.cshtml b/src/Mvc/test/WebSites/RazorWebSite/Views/Shared/Components/ComponentWithFlush/Default.cshtml new file mode 100644 index 000000000000..b7256a326e20 --- /dev/null +++ b/src/Mvc/test/WebSites/RazorWebSite/Views/Shared/Components/ComponentWithFlush/Default.cshtml @@ -0,0 +1,8 @@ +Hello from component +@{ + await FlushAsync(); + var largeContent = new string('a', 1024 * 1024); +} + +@largeContent + From 4ea68b97acf39d3bd3806697ae8971c694daebfb Mon Sep 17 00:00:00 2001 From: Pranav K Date: Tue, 16 Apr 2019 11:33:06 -0700 Subject: [PATCH 2/2] Always refs --- .../ref/Microsoft.AspNetCore.Mvc.ViewFeatures.netcoreapp3.0.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Mvc/Mvc.ViewFeatures/ref/Microsoft.AspNetCore.Mvc.ViewFeatures.netcoreapp3.0.cs b/src/Mvc/Mvc.ViewFeatures/ref/Microsoft.AspNetCore.Mvc.ViewFeatures.netcoreapp3.0.cs index f6a32d05542e..8e358a95051e 100644 --- a/src/Mvc/Mvc.ViewFeatures/ref/Microsoft.AspNetCore.Mvc.ViewFeatures.netcoreapp3.0.cs +++ b/src/Mvc/Mvc.ViewFeatures/ref/Microsoft.AspNetCore.Mvc.ViewFeatures.netcoreapp3.0.cs @@ -1204,7 +1204,9 @@ public virtual void AddAndTrackValidationAttributes(Microsoft.AspNetCore.Mvc.Ren } public partial class ViewComponentResultExecutor : Microsoft.AspNetCore.Mvc.Infrastructure.IActionResultExecutor { + [System.ObsoleteAttribute("This constructor is obsolete and will be removed in a future version.")] public ViewComponentResultExecutor(Microsoft.Extensions.Options.IOptions 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 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; } }