Adding support for flush points in Razor pages

Fixes #1042
This commit is contained in:
Pranav K 2014-08-20 18:18:58 -07:00
parent 88eb29b5d0
commit a931e21456
21 changed files with 1012 additions and 52 deletions

View File

@ -165,6 +165,11 @@ namespace MvcSample.Web
return View(CreateUser());
}
public ActionResult FlushPoint()
{
return View();
}
private static IEnumerable<SelectListItem> CreateAddresses()
{
var addresses = new[]

View File

@ -0,0 +1,62 @@
@{
Layout = "/Views/Shared/_FlushPointLayout.cshtml";
ViewBag.Title = "Home Page";
}
@section content
{
<div class="jumbotron">
<h1>ASP.NET</h1>
<p class="lead">ASP.NET is a free web framework for building great Web sites and Web applications using HTML, CSS and JavaScript.</p>
<p><a href="http://asp.net" class="btn btn-primary btn-lg">Learn more &raquo;</a></p>
</div>
<div class="row">
<div class="col-md-4">
<h2>Getting started</h2>
<p>
ASP.NET MVC gives you a powerful, patterns-based way to build dynamic websites that
enables a clean separation of concerns and gives you full control over markup
for enjoyable, agile development.
</p>
<p><a class="btn btn-default" href="http://go.microsoft.com/fwlink/?LinkId=301865">Learn more &raquo;</a></p>
</div>
<div class="col-md-4">
<h2>Get more libraries</h2>
<p>NuGet is a free Visual Studio extension that makes it easy to add, remove, and update libraries and tools in Visual Studio projects.</p>
<p><a class="btn btn-default" href="http://go.microsoft.com/fwlink/?LinkId=301866">Learn more &raquo;</a></p>
</div>
<div class="col-md-4">
<h2>Web Hosting</h2>
<p>You can easily find a web hosting company that offers the right mix of features and price for your applications.</p>
<p><a class="btn btn-default" href="http://go.microsoft.com/fwlink/?LinkId=301867">Learn more &raquo;</a></p>
</div>
</div>
<address>
One Microsoft Way<br />
Redmond, WA 98052-6399<br />
<abbr title="Phone">P:</abbr>
425.555.0100
</address>
<address>
<strong>Support:</strong> <a href="mailto:Support@example.com">Support@example.com</a><br />
<strong>Marketing:</strong> <a href="mailto:Marketing@example.com">Marketing@example.com</a>
</address>
@* Remove the Wait() calls once we add support for async sections *@
@{
FlushAsync().Wait();
Task.Delay(TimeSpan.FromSeconds(1)).Wait();
}
<div class="row">
<div class="col-md-6">
Integer pharetra dignissim tortor, quis facilisis tellus faucibus in. Phasellus fringilla pellentesque justo eu tempor. Sed eget viverra lacus, eget gravida turpis. Donec dapibus sodales leo, non pharetra lacus volutpat quis. Quisque et auctor nulla. Nunc a orci ut libero luctus ornare ut eget erat. Integer eu risus tempor, scelerisque lacus nec, dignissim nisl. Ut ac leo nec velit tempus fringilla. Phasellus id nisi tortor. Aliquam magna nunc, congue eget sem quis, porta accumsan magna. Sed sit amet dapibus sem, placerat malesuada felis. Quisque vel dui ut est luctus congue.
</div>
<div class="col-md-6">
Aliquam nec elementum orci, ut interdum mi. Sed lorem lacus, malesuada in nisi a, auctor lobortis ipsum. Ut id erat suscipit, pharetra dui eu, sodales erat. Duis nibh turpis, vestibulum sit amet leo vitae, faucibus iaculis lectus. Nam lacinia purus fringilla dolor dictum posuere nec in libero. Integer id tempor elit. Mauris adipiscing ut sem id tincidunt. Cras turpis elit, dignissim vitae adipiscing vitae, ullamcorper in felis. Nullam et adipiscing neque. Integer ullamcorper eget tortor in sodales. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Suspendisse pretium tristique libero, ut gravida est placerat quis. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Sed sed lorem cursus, porttitor nibh eu, bibendum turpis.
</div>
</div>
}

View File

@ -0,0 +1,36 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>@ViewBag.Title - My ASP.NET Application</title>
<link rel="stylesheet" href="~/content/bootstrap.min.css" />
</head>
<body>
<div class="navbar navbar-inverse navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li><a href="/">Home</a></li>
</ul>
</div>
</div>
</div>
<div class="container body-content">
@{ await FlushAsync(); }
@RenderBody()
@RenderSection("content")
<hr />
<footer>
<p>&copy; @DateTime.Now.Year - My ASP.NET Application</p>
</footer>
</div>
</body>
</html>

View File

@ -6,16 +6,35 @@ using System.IO;
namespace Microsoft.AspNet.Mvc.Razor
{
/// <summary>
/// Represents a deferred write operation in a <see cref="RazorPage"/>.
/// </summary>
public class HelperResult
{
private readonly Action<TextWriter> _action;
/// <summary>
/// Creates a new instance of <see cref="HelperResult"/>.
/// </summary>
/// <param name="action">The delegate to invoke when <see cref="WriteTo(TextWriter)"/> is called.</param>
public HelperResult([NotNull] Action<TextWriter> action)
{
_action = action;
}
public void WriteTo([NotNull] TextWriter writer)
/// <summary>
/// Gets the delegate to invoke when <see cref="WriteTo(TextWriter)"/> is called.
/// </summary>
public Action<TextWriter> WriteAction
{
get { return _action; }
}
/// <summary>
/// Method invoked to produce content from the <see cref="HelperResult"/>.
/// </summary>
/// <param name="writer">The <see cref="TextWriter"/> instance to write to.</param>
public virtual void WriteTo([NotNull] TextWriter writer)
{
_action(writer);
}

View File

@ -24,6 +24,16 @@ namespace Microsoft.AspNet.Mvc.Razor
// TODO: https://github.com/aspnet/Mvc/issues/845 tracks making this async
Action<TextWriter> RenderBodyDelegate { get; set; }
/// <summary>
/// Gets or sets a flag that determines if the layout of this page is being rendered.
/// </summary>
/// <remarks>
/// Sections defined in a page are deferred and executed as part of the layout page.
/// When this flag is set, all write operations performed by the page are part of a
/// section being rendered.
/// </remarks>
bool IsLayoutBeingRendered { get; set; }
/// <summary>
/// Gets the application base relative path to the page.
/// </summary>

View File

@ -42,6 +42,22 @@ namespace Microsoft.AspNet.Mvc.Razor
return string.Format(CultureInfo.CurrentCulture, GetString("CompilationFailed"), p0);
}
/// <summary>
/// '{0}' cannot be invoked when a Layout page is set to be executed.
/// </summary>
internal static string FlushPointCannotBeInvoked
{
get { return GetString("FlushPointCannotBeInvoked"); }
}
/// <summary>
/// '{0}' cannot be invoked when a Layout page is set to be executed.
/// </summary>
internal static string FormatFlushPointCannotBeInvoked(object p0)
{
return string.Format(CultureInfo.CurrentCulture, GetString("FlushPointCannotBeInvoked"), p0);
}
/// <summary>
/// The layout view '{0}' could not be located.
/// </summary>
@ -58,6 +74,22 @@ namespace Microsoft.AspNet.Mvc.Razor
return string.Format(CultureInfo.CurrentCulture, GetString("LayoutCannotBeLocated"), p0);
}
/// <summary>
/// A layout page cannot be rendered after '{0}' has been invoked.
/// </summary>
internal static string LayoutCannotBeRendered
{
get { return GetString("LayoutCannotBeRendered"); }
}
/// <summary>
/// A layout page cannot be rendered after '{0}' has been invoked.
/// </summary>
internal static string FormatLayoutCannotBeRendered(object p0)
{
return string.Format(CultureInfo.CurrentCulture, GetString("LayoutCannotBeRendered"), p0);
}
/// <summary>
/// The 'inherits' keyword is not allowed when a '{0}' keyword is used.
/// </summary>

View File

@ -19,8 +19,8 @@ namespace Microsoft.AspNet.Mvc.Razor
/// </summary>
public abstract class RazorPage : IRazorPage
{
private IUrlHelper _urlHelper;
private readonly HashSet<string> _renderedSections = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
private IUrlHelper _urlHelper;
private bool _renderedBody;
public RazorPage()
@ -90,6 +90,9 @@ namespace Microsoft.AspNet.Mvc.Razor
/// <inheritdoc />
public Action<TextWriter> RenderBodyDelegate { get; set; }
/// <inheritdoc />
public bool IsLayoutBeingRendered { get; set; }
/// <inheritdoc />
public Dictionary<string, HelperResult> PreviousSectionWriters { get; set; }
@ -283,13 +286,19 @@ namespace Microsoft.AspNet.Mvc.Razor
return new HelperResult(RenderBodyDelegate);
}
public void DefineSection(string name, HelperResult action)
/// <summary>
/// Creates a named content section in the page that can be invoked in a Layout page using
/// <see cref="RenderSection(string)"/> or <see cref="RenderSection(string, bool)"/>.
/// </summary>
/// <param name="name">The name of the section to create.</param>
/// <param name="section">The <see cref="HelperResult"/> to execute when rendering the section.</param>
public void DefineSection(string name, HelperResult section)
{
if (SectionWriters.ContainsKey(name))
{
throw new InvalidOperationException(Resources.FormatSectionAlreadyDefined(name));
}
SectionWriters[name] = action;
SectionWriters[name] = section;
}
public bool IsSectionDefined([NotNull] string name)
@ -329,6 +338,24 @@ namespace Microsoft.AspNet.Mvc.Razor
}
}
/// <summary>
/// Invokes <see cref="TextWriter.FlushAsync"/> on <see cref="Output"/> writing out any buffered
/// content to the <see cref="HttpResponse.Body"/>.
/// </summary>
/// <returns>A <see cref="Task"/> that represents the asynchronous flush operation.</returns>
public Task FlushAsync()
{
// Calls to Flush are allowed if the page does not specify a Layout or if it is executing a section in the
// Layout.
if (!IsLayoutBeingRendered && !string.IsNullOrEmpty(Layout))
{
var message = Resources.FormatLayoutCannotBeRendered(nameof(FlushAsync));
throw new InvalidOperationException(message);
}
return Output.FlushAsync();
}
/// <inheritdoc />
public void EnsureBodyAndSectionsWereRendered()
{

View File

@ -9,7 +9,10 @@ using System.Threading.Tasks;
namespace Microsoft.AspNet.Mvc.Razor
{
/// <summary>
/// A <see cref="TextWriter"/> that represents individual write operations as a sequence of strings.
/// 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.
/// </summary>
/// <remarks>
/// This is primarily designed to avoid creating large in-memory strings.
@ -18,25 +21,49 @@ namespace Microsoft.AspNet.Mvc.Razor
public class RazorTextWriter : TextWriter
{
private static readonly Task _completedTask = Task.FromResult(0);
private readonly TextWriter _unbufferedWriter;
private readonly Encoding _encoding;
public RazorTextWriter(Encoding encoding)
/// <summary>
/// Creates a new instance of <see cref="RazorTextWriter"/>.
/// </summary>
/// <param name="unbufferedWriter">The <see cref="TextWriter"/> to write output to when this instance
/// is no longer buffering.</param>
/// <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();
}
/// <inheritdoc />
public override Encoding Encoding
{
get { return _encoding; }
}
/// <summary>
/// Gets a flag that determines if this instance of <see cref="RazorTextWriter"/> is buffering content.
/// </summary>
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; }
/// <inheritdoc />
public override void Write(char value)
{
Buffer.Add(value.ToString());
if (IsBuffering)
{
Buffer.Add(value.ToString());
}
else
{
_unbufferedWriter.Write(value);
}
}
/// <inheritdoc />
@ -55,7 +82,14 @@ namespace Microsoft.AspNet.Mvc.Razor
throw new ArgumentOutOfRangeException("count");
}
Buffer.Add(buffer, index, count);
if (IsBuffering)
{
Buffer.Add(buffer, index, count);
}
else
{
_unbufferedWriter.Write(buffer, index, count);
}
}
/// <inheritdoc />
@ -63,70 +97,174 @@ namespace Microsoft.AspNet.Mvc.Razor
{
if (!string.IsNullOrEmpty(value))
{
Buffer.Add(value);
if (IsBuffering)
{
Buffer.Add(value);
}
else
{
_unbufferedWriter.Write(value);
}
}
}
/// <inheritdoc />
public override Task WriteAsync(char value)
{
Write(value);
return _completedTask;
if (IsBuffering)
{
Write(value);
return _completedTask;
}
else
{
return _unbufferedWriter.WriteAsync(value);
}
}
/// <inheritdoc />
public override Task WriteAsync([NotNull] char[] buffer, int index, int count)
{
Write(buffer, index, count);
return _completedTask;
if (IsBuffering)
{
Write(buffer, index, count);
return _completedTask;
}
else
{
return _unbufferedWriter.WriteAsync(buffer, index, count);
}
}
/// <inheritdoc />
public override Task WriteAsync(string value)
{
Write(value);
return _completedTask;
if (IsBuffering)
{
Write(value);
return _completedTask;
}
else
{
return _unbufferedWriter.WriteAsync(value);
}
}
/// <inheritdoc />
public override void WriteLine()
{
Buffer.Add(Environment.NewLine);
if (IsBuffering)
{
Buffer.Add(Environment.NewLine);
}
else
{
_unbufferedWriter.WriteLine();
}
}
/// <inheritdoc />
public override void WriteLine(string value)
{
Write(value);
WriteLine();
if (IsBuffering)
{
Write(value);
WriteLine();
}
else
{
_unbufferedWriter.WriteLine(value);
}
}
/// <inheritdoc />
public override Task WriteLineAsync(char value)
{
WriteLine(value);
return _completedTask;
if (IsBuffering)
{
WriteLine(value);
return _completedTask;
}
else
{
return _unbufferedWriter.WriteLineAsync(value);
}
}
/// <inheritdoc />
public override Task WriteLineAsync(char[] value, int start, int offset)
{
WriteLine(value, start, offset);
return _completedTask;
if (IsBuffering)
{
WriteLine(value, start, offset);
return _completedTask;
}
else
{
return _unbufferedWriter.WriteLineAsync(value, start, offset);
}
}
/// <inheritdoc />
public override Task WriteLineAsync(string value)
{
WriteLine(value);
return _completedTask;
if (IsBuffering)
{
WriteLine(value);
return _completedTask;
}
else
{
return _unbufferedWriter.WriteLineAsync(value);
}
}
/// <inheritdoc />
public override Task WriteLineAsync()
{
WriteLine();
return _completedTask;
if (IsBuffering)
{
WriteLine();
return _completedTask;
}
else
{
return _unbufferedWriter.WriteLineAsync();
}
}
/// <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()
{
if (IsBuffering)
{
IsBuffering = false;
CopyTo(_unbufferedWriter);
}
_unbufferedWriter.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()
{
if (IsBuffering)
{
IsBuffering = false;
await CopyToAsync(_unbufferedWriter);
}
await _unbufferedWriter.FlushAsync();
}
/// <summary>
@ -136,13 +274,15 @@ namespace Microsoft.AspNet.Mvc.Razor
public void CopyTo(TextWriter writer)
{
var targetRazorTextWriter = writer as RazorTextWriter;
if (targetRazorTextWriter != null)
if (targetRazorTextWriter != null && targetRazorTextWriter.IsBuffering)
{
targetRazorTextWriter.Buffer.Add(Buffer);
}
else
{
WriteList(writer, Buffer);
// 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);
}
}
@ -154,13 +294,15 @@ namespace Microsoft.AspNet.Mvc.Razor
public Task CopyToAsync(TextWriter writer)
{
var targetRazorTextWriter = writer as RazorTextWriter;
if (targetRazorTextWriter != null)
if (targetRazorTextWriter != null && targetRazorTextWriter.IsBuffering)
{
targetRazorTextWriter.Buffer.Add(Buffer);
}
else
{
return WriteListAsync(writer, Buffer);
// 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);
}
return _completedTask;

View File

@ -65,12 +65,13 @@ namespace Microsoft.AspNet.Mvc.Razor
ViewContext context,
bool executeViewStart)
{
using (var bufferedWriter = new RazorTextWriter(context.Writer.Encoding))
using (var bufferedWriter = new RazorTextWriter(context.Writer, context.Writer.Encoding))
{
// The writer for the body is passed through the ViewContext, allowing things like HtmlHelpers
// and ViewComponents to reference it.
var oldWriter = context.Writer;
context.Writer = bufferedWriter;
try
{
if (executeViewStart)
@ -117,6 +118,17 @@ namespace Microsoft.AspNet.Mvc.Razor
var previousPage = _razorPage;
while (!string.IsNullOrEmpty(previousPage.Layout))
{
if (!bodyWriter.IsBuffering)
{
// 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
// the body content. Throwing this exception wouldn't return a 500 (since content has already been
// written), but a diagnostic component should be able to capture it.
var message = Resources.FormatLayoutCannotBeRendered("FlushAsync");
throw new InvalidOperationException(message);
}
var layoutPage = _pageFactory.CreateInstance(previousPage.Layout);
if (layoutPage == null)
{
@ -124,6 +136,9 @@ namespace Microsoft.AspNet.Mvc.Razor
throw new InvalidOperationException(message);
}
// Notify the previous page that any writes that are performed on it are part of sections being written
// in the layout.
previousPage.IsLayoutBeingRendered = true;
layoutPage.PreviousSectionWriters = previousPage.SectionWriters;
layoutPage.RenderBodyDelegate = bodyWriter.CopyTo;
bodyWriter = await RenderPageAsync(layoutPage, context, executeViewStart: false);
@ -134,7 +149,11 @@ namespace Microsoft.AspNet.Mvc.Razor
previousPage = layoutPage;
}
await bodyWriter.CopyToAsync(context.Writer);
if (bodyWriter.IsBuffering)
{
// Only copy buffered content to the Output if we're currently buffering.
await bodyWriter.CopyToAsync(context.Writer);
}
}
}
}

View File

@ -123,9 +123,15 @@
<data name="CompilationFailed" xml:space="preserve">
<value>Compilation for '{0}' failed:</value>
</data>
<data name="FlushPointCannotBeInvoked" xml:space="preserve">
<value>'{0}' cannot be invoked when a Layout page is set to be executed.</value>
</data>
<data name="LayoutCannotBeLocated" xml:space="preserve">
<value>The layout view '{0}' could not be located.</value>
</data>
<data name="LayoutCannotBeRendered" xml:space="preserve">
<value>A layout page cannot be rendered after '{0}' has been invoked.</value>
</data>
<data name="MvcRazorCodeParser_CannotHaveModelAndInheritsKeyword" xml:space="preserve">
<value>The 'inherits' keyword is not allowed when a '{0}' keyword is used.</value>
</data>

View File

@ -0,0 +1,113 @@
// 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;
using Microsoft.AspNet.Builder;
using Microsoft.AspNet.TestHost;
using Microsoft.Framework.DependencyInjection;
using Microsoft.Framework.DependencyInjection.Fallback;
using RazorWebSite;
using Xunit;
namespace Microsoft.AspNet.Mvc.FunctionalTests
{
public class FlushPointTest
{
private readonly IServiceProvider _provider = TestHelper.CreateServices("RazorWebSite");
private readonly Action<IBuilder> _app = new Startup().Configure;
[Fact]
public async Task FlushPointsAreExecutedForPagesWithLayouts()
{
var waitService = new WaitService();
var serviceProvider = GetServiceProvider(waitService);
var server = TestServer.Create(serviceProvider, _app);
var client = server.CreateClient();
// Act
var stream = await client.GetStreamAsync("http://localhost/FlushPoint/PageWithLayout");
// Assert - 1
Assert.Equal(@"<title>Page With Layout</title>", GetTrimmedString(stream));
waitService.WaitForServer();
// Assert - 2
Assert.Equal(@"RenderBody content", GetTrimmedString(stream));
waitService.WaitForServer();
// Assert - 3
Assert.Equal(@"<span>Content that takes time to produce</span>",
GetTrimmedString(stream));
}
[Fact]
public async Task FlushPointsAreExecutedForPagesWithoutLayouts()
{
var waitService = new WaitService();
var serviceProvider = GetServiceProvider(waitService);
var server = TestServer.Create(serviceProvider, _app);
var client = server.CreateClient();
// Act
var stream = await client.GetStreamAsync("http://localhost/FlushPoint/PageWithoutLayout");
// Assert - 1
Assert.Equal("Initial content", GetTrimmedString(stream));
waitService.WaitForServer();
// Assert - 2
Assert.Equal("Secondary content", GetTrimmedString(stream));
waitService.WaitForServer();
// Assert - 3
Assert.Equal("Final content", GetTrimmedString(stream));
}
[Fact]
public async Task FlushPointsAreExecutedForPagesWithComponentsAndPartials()
{
var waitService = new WaitService();
var serviceProvider = GetServiceProvider(waitService);
var server = TestServer.Create(serviceProvider, _app);
var client = server.CreateClient();
// Act
var stream = await client.GetStreamAsync("http://localhost/FlushPoint/PageWithPartialsAndViewComponents");
// Assert - 1
Assert.Equal(
@"<title>Page With Components and Partials</title>
RenderBody content", GetTrimmedString(stream));
waitService.WaitForServer();
// Assert - 2
Assert.Equal("partial-content", GetTrimmedString(stream));
waitService.WaitForServer();
// Assert - 3
Assert.Equal(
@"component-content
<span>Content that takes time to produce</span>", GetTrimmedString(stream));
}
private IServiceProvider GetServiceProvider(WaitService waitService)
{
var services = new ServiceCollection();
services.AddInstance(waitService);
return services.BuildServiceProvider(_provider);
}
private string GetTrimmedString(Stream stream)
{
var buffer = new byte[1024];
var count = stream.Read(buffer, 0, buffer.Length);
return Encoding.UTF8.GetString(buffer, 0, count).Trim();
}
}
}

View File

@ -7,6 +7,7 @@ using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.Mvc.Rendering;
using Microsoft.AspNet.PipelineCore;
using Microsoft.AspNet.Testing;
using Moq;
using Xunit;
@ -280,8 +281,7 @@ Layout end
var services = new Mock<IServiceProvider>();
services.Setup(s => s.GetService(typeof(IUrlHelper)))
.Returns(helper.Object);
Mock.Get(page.Context).Setup(c => c.RequestServices)
.Returns(services.Object);
page.Context.RequestServices = services.Object;
// Act
await page.ExecuteAsync();
@ -292,8 +292,69 @@ Layout end
helper.Verify();
}
private static TestableRazorPage CreatePage(Action<TestableRazorPage> executeAction)
[Fact]
public async Task FlushAsync_InvokesFlushOnWriter()
{
// Arrange
var writer = new Mock<TextWriter>();
var context = CreateViewContext(writer.Object);
var page = CreatePage(p =>
{
p.FlushAsync().Wait();
}, context);
// Act
await page.ExecuteAsync();
// Assert
writer.Verify(v => v.FlushAsync(), Times.Once());
}
[Fact]
public async Task FlushAsync_ThrowsIfTheLayoutHasBeenSet()
{
// Arrange
var expected = @"A layout page cannot be rendered after 'FlushAsync' has been invoked.";
var writer = new Mock<TextWriter>();
var context = CreateViewContext(writer.Object);
var page = CreatePage(p =>
{
p.Layout = "foo";
p.FlushAsync().Wait();
}, context);
// Act and Assert
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => page.ExecuteAsync());
Assert.Equal(expected, ex.Message);
}
[Fact]
public async Task FlushAsync_DoesNotThrowWhenIsRenderingLayoutIsSet()
{
// Arrange
var writer = new Mock<TextWriter>();
var context = CreateViewContext(writer.Object);
var page = CreatePage(p =>
{
p.Layout = "bar";
p.DefineSection("test-section", new HelperResult(_ =>
{
p.FlushAsync().Wait();
}));
}, context);
// Act
await page.ExecuteAsync();
page.IsLayoutBeingRendered = true;
// Assert
Assert.DoesNotThrow(() => page.SectionWriters["test-section"].WriteTo(TextWriter.Null));
}
private static TestableRazorPage CreatePage(Action<TestableRazorPage> executeAction,
ViewContext context = null)
{
context = context ?? CreateViewContext();
var view = new Mock<TestableRazorPage> { CallBase = true };
if (executeAction != null)
{
@ -301,19 +362,20 @@ Layout end
.Callback(() => executeAction(view.Object))
.Returns(Task.FromResult(0));
}
view.Object.ViewContext = CreateViewContext();
view.Object.ViewContext = context;
return view.Object;
}
private static ViewContext CreateViewContext()
private static ViewContext CreateViewContext(TextWriter writer = null)
{
var actionContext = new ActionContext(Mock.Of<HttpContext>(), routeData: null, actionDescriptor: null);
writer = writer ?? new StringWriter();
var actionContext = new ActionContext(new DefaultHttpContext(), routeData: null, actionDescriptor: null);
return new ViewContext(
actionContext,
Mock.Of<IView>(),
null,
new StringWriter());
writer);
}
private static Action<TextWriter> CreateBodyAction(string value)

View File

@ -7,6 +7,7 @@ using System.IO;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNet.Testing;
using Moq;
using Xunit;
namespace Microsoft.AspNet.Mvc.Razor.Test
@ -19,7 +20,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
{
// Arrange
var expected = new[] { "True", "3", "18446744073709551615", "Hello world", "3.14", "2.718", "m" };
var writer = new RazorTextWriter(Encoding.UTF8);
var writer = new RazorTextWriter(TextWriter.Null, Encoding.UTF8);
// Act
writer.Write(true);
@ -34,6 +35,84 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
Assert.Equal<object>(expected, writer.Buffer.BufferEntries);
}
[Fact]
[ReplaceCulture]
public void Write_WritesDataTypes_ToUnderlyingStream_WhenNotBuffering()
{
// Arrange
var expected = new[] { "True", "3", "18446744073709551615", "Hello world", "3.14", "2.718" };
var unbufferedWriter = new Mock<TextWriter>();
var writer = new RazorTextWriter(unbufferedWriter.Object, 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.Empty(writer.Buffer.BufferEntries);
foreach (var item in expected)
{
unbufferedWriter.Verify(v => v.Write(item), Times.Once());
}
}
[Fact]
[ReplaceCulture]
public async Task Write_WritesCharValues_ToUnderlyingStream_WhenNotBuffering()
{
// Arrange
var unbufferedWriter = new Mock<TextWriter> { CallBase = true };
var writer = new RazorTextWriter(unbufferedWriter.Object, Encoding.UTF8);
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
Assert.Empty(writer.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());
unbufferedWriter.Verify(v => v.Write(buffer2, 0, 3), Times.Once());
unbufferedWriter.Verify(v => v.WriteAsync(buffer2, 1, 1), Times.Once());
unbufferedWriter.Verify(v => v.WriteLine(), Times.Once());
}
[Fact]
[ReplaceCulture]
public async Task Write_WritesStringValues_ToUnbufferedStream_WhenNotBuffering()
{
// Arrange
var unbufferedWriter = new Mock<TextWriter>();
var writer = new RazorTextWriter(unbufferedWriter.Object, Encoding.UTF8);
// Act
await writer.FlushAsync();
writer.Write("a");
writer.WriteLine("ab");
await writer.WriteAsync("ef");
await writer.WriteLineAsync("gh");
// Assert
Assert.Empty(writer.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());
unbufferedWriter.Verify(v => v.WriteLineAsync("gh"), Times.Once());
}
[Fact]
[ReplaceCulture]
public void WriteLine_WritesDataTypes_ToBuffer()
@ -41,7 +120,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
// Arrange
var newLine = Environment.NewLine;
var expected = new List<object> { "False", newLine, "1.1", newLine, "3", newLine };
var writer = new RazorTextWriter(Encoding.UTF8);
var writer = new RazorTextWriter(TextWriter.Null, Encoding.UTF8);
// Act
writer.WriteLine(false);
@ -52,6 +131,28 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
Assert.Equal(expected, writer.Buffer.BufferEntries);
}
[Fact]
[ReplaceCulture]
public void WriteLine_WritesDataTypes_ToUnbufferedStream_WhenNotBuffering()
{
// Arrange
var unbufferedWriter = new Mock<TextWriter>();
var writer = new RazorTextWriter(unbufferedWriter.Object, Encoding.UTF8);
// Act
writer.Flush();
writer.WriteLine(false);
writer.WriteLine(1.1f);
writer.WriteLine(3L);
// Assert
Assert.Empty(writer.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());
unbufferedWriter.Verify(v => v.WriteLine(), Times.Exactly(3));
}
[Fact]
public async Task Write_WritesCharBuffer()
{
@ -59,7 +160,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
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 RazorTextWriter(Encoding.UTF8);
var writer = new RazorTextWriter(TextWriter.Null, Encoding.UTF8);
// Act
writer.Write(input1.Array, input1.Offset, input1.Count);
@ -80,7 +181,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
{
// Arrange
var newLine = Environment.NewLine;
var writer = new RazorTextWriter(Encoding.UTF8);
var writer = new RazorTextWriter(TextWriter.Null, Encoding.UTF8);
// Act
writer.WriteLine();
@ -100,7 +201,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
var input2 = "from";
var input3 = "ASP";
var input4 = ".Net";
var writer = new RazorTextWriter(Encoding.UTF8);
var writer = new RazorTextWriter(TextWriter.Null, Encoding.UTF8);
// Act
writer.Write(input1);
@ -114,11 +215,11 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
}
[Fact]
public void Copy_CopiesContent_IfTargetTextWriterIsARazorTextWriter()
public void Copy_CopiesContent_IfTargetTextWriterIsARazorTextWriterAndBuffering()
{
// Arrange
var source = new RazorTextWriter(Encoding.UTF8);
var target = new RazorTextWriter(Encoding.UTF8);
var source = new RazorTextWriter(TextWriter.Null, Encoding.UTF8);
var target = new RazorTextWriter(TextWriter.Null, Encoding.UTF8);
// Act
source.Write("Hello world");
@ -132,11 +233,33 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
Assert.Same(source.Buffer.BufferEntries, target.Buffer.BufferEntries[0]);
}
[Fact]
public void Copy_CopiesContent_IfTargetTextWriterIsARazorTextWriterAndNotBuffering()
{
// Arrange
var unbufferedWriter = new Mock<TextWriter>();
var source = new RazorTextWriter(TextWriter.Null, Encoding.UTF8);
var target = new RazorTextWriter(unbufferedWriter.Object, Encoding.UTF8);
// Act
target.Flush();
source.Write("Hello world");
source.Write(new [] { 'a', 'b', 'c', 'd' }, 1, 2);
source.CopyTo(target);
// Assert
// Make sure content was written to the source.
Assert.Equal(2, source.Buffer.BufferEntries.Count);
Assert.Empty(target.Buffer.BufferEntries);
unbufferedWriter.Verify(v => v.Write("Hello world"), Times.Once());
unbufferedWriter.Verify(v => v.Write("bc"), Times.Once());
}
[Fact]
public void Copy_WritesContent_IfTargetTextWriterIsNotARazorTextWriter()
{
// Arrange
var source = new RazorTextWriter(Encoding.UTF8);
var source = new RazorTextWriter(TextWriter.Null, Encoding.UTF8);
var target = new StringWriter();
var expected = @"Hello world
abc";
@ -151,11 +274,11 @@ abc";
}
[Fact]
public async Task CopyAsync_WritesContent_IfTargetTextWriterIsARazorTextWriter()
public async Task CopyAsync_WritesContent_IfTargetTextWriterIsARazorTextWriterAndBuffering()
{
// Arrange
var source = new RazorTextWriter(Encoding.UTF8);
var target = new RazorTextWriter(Encoding.UTF8);
var source = new RazorTextWriter(TextWriter.Null,Encoding.UTF8);
var target = new RazorTextWriter(TextWriter.Null, Encoding.UTF8);
// Act
source.WriteLine("Hello world");
@ -168,11 +291,34 @@ abc";
Assert.Same(source.Buffer.BufferEntries, target.Buffer.BufferEntries[0]);
}
[Fact]
public async Task CopyAsync_WritesContent_IfTargetTextWriterIsARazorTextWriterAndNotBuffering()
{
// Arrange
var unbufferedWriter = new Mock<TextWriter>();
var source = new RazorTextWriter(TextWriter.Null, Encoding.UTF8);
var target = new RazorTextWriter(unbufferedWriter.Object, Encoding.UTF8);
// Act
await target.FlushAsync();
source.WriteLine("Hello from Asp.Net");
await source.WriteAsync(new [] { 'x', 'y', 'z', 'u' }, 0, 3);
await source.CopyToAsync(target);
// Assert
// Make sure content was written to the source.
Assert.Equal(3, source.Buffer.BufferEntries.Count);
Assert.Empty(target.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());
}
[Fact]
public async Task CopyAsync_WritesContent_IfTargetTextWriterIsNotARazorTextWriter()
{
// Arrange
var source = new RazorTextWriter(Encoding.UTF8);
var source = new RazorTextWriter(TextWriter.Null, Encoding.UTF8);
var target = new StringWriter();
var expected = @"Hello world
";

View File

@ -414,6 +414,161 @@ body-content";
Assert.Equal(expected, viewContext.Writer.ToString());
}
[Fact]
public async Task RenderAsync_DoesNotCopyContentOnceRazorTextWriterIsNoLongerBuffering()
{
// Arrange
var expected =
@"layout-1
body content
section-content-1
section-content-2";
var page = new TestableRazorPage(v =>
{
v.Layout = "layout-1";
v.WriteLiteral("body content" + Environment.NewLine);
v.DefineSection("foo", new HelperResult(_ =>
{
v.WriteLiteral("section-content-1" + Environment.NewLine);
v.FlushAsync().Wait();
v.WriteLiteral("section-content-2");
}));
});
var layout1 = new TestableRazorPage(v =>
{
v.Write("layout-1" + Environment.NewLine);
v.RenderBodyPublic();
v.Write(v.RenderSection("foo"));
});
var pageFactory = new Mock<IRazorPageFactory>();
pageFactory.Setup(p => p.CreateInstance("layout-1"))
.Returns(layout1);
var view = new RazorView(pageFactory.Object,
Mock.Of<IRazorPageActivator>(),
CreateViewStartProvider());
view.Contextualize(page, isPartial: false);
var viewContext = CreateViewContext(view);
// Act
await view.RenderAsync(viewContext);
// Assert
Assert.Equal(expected, viewContext.Writer.ToString());
}
[Fact]
public async Task FlushAsync_DoesNotThrowWhenInvokedInsideOfASection()
{
// Arrange
var expected =
@"layout-1
section-content-1
section-content-2";
var page = new TestableRazorPage(v =>
{
v.Layout = "layout-1";
v.DefineSection("foo", new HelperResult(_ =>
{
v.WriteLiteral("section-content-1" + Environment.NewLine);
v.FlushAsync().Wait();
v.WriteLiteral("section-content-2");
}));
});
var layout1 = new TestableRazorPage(v =>
{
v.Write("layout-1" + Environment.NewLine);
v.RenderBodyPublic();
v.Write(v.RenderSection("foo"));
});
var pageFactory = new Mock<IRazorPageFactory>();
pageFactory.Setup(p => p.CreateInstance("layout-1"))
.Returns(layout1);
var view = new RazorView(pageFactory.Object,
Mock.Of<IRazorPageActivator>(),
CreateViewStartProvider());
view.Contextualize(page, isPartial: false);
var viewContext = CreateViewContext(view);
// Act
await view.RenderAsync(viewContext);
// Assert
Assert.Equal(expected, viewContext.Writer.ToString());
}
[Fact]
public async Task RenderAsync_ThrowsIfLayoutIsSpecifiedWhenNotBuffered()
{
// Arrange
var expected = @"A layout page cannot be rendered after 'FlushAsync' has been invoked.";
var page = new TestableRazorPage(v =>
{
v.WriteLiteral("before-flush" + Environment.NewLine);
v.FlushAsync().Wait();
v.Layout = "test-layout";
v.WriteLiteral("after-flush");
});
var view = new RazorView(Mock.Of<IRazorPageFactory>(),
Mock.Of<IRazorPageActivator>(),
CreateViewStartProvider());
view.Contextualize(page, isPartial: false);
var viewContext = CreateViewContext(view);
// Act and Assert
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => view.RenderAsync(viewContext));
Assert.Equal(expected, ex.Message);
}
[Fact]
public async Task RenderAsync_ThrowsIfFlushWasInvokedInsideRenderedSectionAndLayoutWasSet()
{
// Arrange
var expected = @"A layout page cannot be rendered after 'FlushAsync' has been invoked.";
var page = new TestableRazorPage(v =>
{
v.DefineSection("foo", new HelperResult(writer =>
{
writer.WriteLine("foo-content");
v.FlushAsync().Wait();
}));
v.Layout = "~/Shared/Layout1.cshtml";
v.WriteLiteral("body-content");
});
var layout1 = new TestableRazorPage(v =>
{
v.Write("layout-1" + Environment.NewLine);
v.Write(v.RenderSection("foo"));
v.DefineSection("bar", new HelperResult(writer =>
{
writer.WriteLine("bar-content");
}));
v.RenderBodyPublic();
v.Layout = "~/Shared/Layout2.cshtml";
});
var pageFactory = new Mock<IRazorPageFactory>();
pageFactory.Setup(p => p.CreateInstance("~/Shared/Layout1.cshtml"))
.Returns(layout1);
var view = new RazorView(pageFactory.Object,
Mock.Of<IRazorPageActivator>(),
CreateViewStartProvider());
view.Contextualize(page, isPartial: false);
var viewContext = CreateViewContext(view);
// Act and Assert
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => view.RenderAsync(viewContext));
Assert.Equal(expected, ex.Message);
}
private static ViewContext CreateViewContext(RazorView view)
{
var httpContext = new DefaultHttpContext();

View File

@ -0,0 +1,25 @@
// 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 Microsoft.AspNet.Mvc;
namespace RazorWebSite
{
public class FlushPoint : Controller
{
public ViewResult PageWithLayout()
{
return View();
}
public ViewResult PageWithoutLayout()
{
return View();
}
public ViewResult PageWithPartialsAndViewComponents()
{
return View();
}
}
}

View File

@ -0,0 +1,34 @@
// 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.Threading;
namespace RazorWebSite
{
public class WaitService
{
private static readonly TimeSpan _waitTime = TimeSpan.FromSeconds(10);
private readonly ManualResetEventSlim _serverResetEvent = new ManualResetEventSlim();
private readonly ManualResetEventSlim _clientResetEvent = new ManualResetEventSlim();
public void NotifyClient()
{
_clientResetEvent.Set();
}
public void WaitForClient()
{
_clientResetEvent.Set();
_serverResetEvent.Wait(_waitTime);
_serverResetEvent.Reset();
}
public void WaitForServer()
{
_serverResetEvent.Set();
_clientResetEvent.Wait(_waitTime);
_clientResetEvent.Reset();
}
}
}

View File

@ -0,0 +1,14 @@
@inject WaitService WaitService
@{
Layout = "/Views/Shared/_LayoutWithFlush.cshtml";
ViewBag.Title = "Page With Layout";
}
RenderBody content
@section content
{
@{
FlushAsync().Wait();
WaitService.WaitForClient();
}
<span>Content that takes time to produce</span>
}

View File

@ -0,0 +1,15 @@
@inject WaitService WaitService
@{
Layout = "/Views/Shared/_LayoutWithPartialAndFlush.cshtml";
ViewBag.Title = "Page With Components and Partials";
}
RenderBody content
@section content
{
@{
FlushAsync().Wait();
WaitService.WaitForClient();
}
@Component.InvokeAsync("ComponentThatSetsTitle").Result
<span>Content that takes time to produce</span>
}

View File

@ -0,0 +1,15 @@
@inject WaitService WaitService
Initial content
@{
await FlushAsync();
WaitService.WaitForClient();
}
Secondary content
@{
await FlushAsync();
WaitService.WaitForClient();
}
Final content
@{
WaitService.NotifyClient();
}

View File

@ -0,0 +1,11 @@
@inject WaitService WaitService
<title>@ViewBag.Title</title>
@{
await FlushAsync();
WaitService.WaitForClient();
}
@RenderBody()
@RenderSection("content")
@{
WaitService.NotifyClient();
}

View File

@ -0,0 +1,12 @@
@inject WaitService WaitService
<title>@ViewBag.Title</title>
@RenderBody()
@{
await FlushAsync();
WaitService.WaitForClient();
}
@await Html.PartialAsync("_PartialThatSetsTitle")
@RenderSection("content")
@{
WaitService.NotifyClient();
}