diff --git a/src/Microsoft.AspNet.Mvc.Razor/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Mvc.Razor/Properties/Resources.Designer.cs
index a76d917405..f6dfb487a3 100644
--- a/src/Microsoft.AspNet.Mvc.Razor/Properties/Resources.Designer.cs
+++ b/src/Microsoft.AspNet.Mvc.Razor/Properties/Resources.Designer.cs
@@ -446,6 +446,22 @@ namespace Microsoft.AspNet.Mvc.Razor
return string.Format(CultureInfo.CurrentCulture, GetString("CouldNotResolveApplicationRelativeUrl_TagHelper"), p0, p1, p2, p3, p4, p5);
}
+ ///
+ /// A circular layout reference was detected when rendering '{0}'. The layout page '{1}' has already been rendered.
+ ///
+ internal static string LayoutHasCircularReference
+ {
+ get { return GetString("LayoutHasCircularReference"); }
+ }
+
+ ///
+ /// A circular layout reference was detected when rendering '{0}'. The layout page '{1}' has already been rendered.
+ ///
+ internal static string FormatLayoutHasCircularReference(object p0, object p1)
+ {
+ return string.Format(CultureInfo.CurrentCulture, GetString("LayoutHasCircularReference"), p0, p1);
+ }
+
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);
diff --git a/src/Microsoft.AspNet.Mvc.Razor/RazorView.cs b/src/Microsoft.AspNet.Mvc.Razor/RazorView.cs
index 574f1026b4..c2a758371b 100644
--- a/src/Microsoft.AspNet.Mvc.Razor/RazorView.cs
+++ b/src/Microsoft.AspNet.Mvc.Razor/RazorView.cs
@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.IO;
+using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNet.Http.Features;
using Microsoft.AspNet.Mvc.Rendering;
@@ -35,7 +36,8 @@ namespace Microsoft.AspNet.Mvc.Razor
/// The HTML encoder.
/// Determines if the view is to be executed as a partial.
/// pages
- public RazorView(IRazorViewEngine viewEngine,
+ public RazorView(
+ IRazorViewEngine viewEngine,
IRazorPageActivator pageActivator,
IViewStartProvider viewStartProvider,
IRazorPage razorPage,
@@ -170,7 +172,8 @@ namespace Microsoft.AspNet.Mvc.Razor
RazorPage.Layout = layout;
}
- private async Task RenderLayoutAsync(ViewContext context,
+ private async Task RenderLayoutAsync(
+ ViewContext context,
IBufferedTextWriter bodyWriter)
{
// A layout page can specify another layout page. We'll need to continue
@@ -192,6 +195,15 @@ namespace Microsoft.AspNet.Mvc.Razor
var layoutPage = GetLayoutPage(context, previousPage.Layout);
+ if (renderedLayouts.Count > 0 &&
+ renderedLayouts.Any(l => string.Equals(l.Path, layoutPage.Path, StringComparison.Ordinal)))
+ {
+ // If the layout has been previously rendered as part of this view, we're potentially in a layout
+ // rendering cycle.
+ throw new InvalidOperationException(
+ Resources.FormatLayoutHasCircularReference(previousPage.Path, layoutPage.Path));
+ }
+
// Notify the previous page that any writes that are performed on it are part of sections being written
// in the layout.
previousPage.IsLayoutBeingRendered = true;
diff --git a/src/Microsoft.AspNet.Mvc.Razor/Resources.resx b/src/Microsoft.AspNet.Mvc.Razor/Resources.resx
index bd3dede0e4..4431f51965 100644
--- a/src/Microsoft.AspNet.Mvc.Razor/Resources.resx
+++ b/src/Microsoft.AspNet.Mvc.Razor/Resources.resx
@@ -200,4 +200,7 @@
@{3} "{4}, {5}"
+
+ A circular layout reference was detected when rendering '{0}'. The layout page '{1}' has already been rendered.
+
\ No newline at end of file
diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewTest.cs
index 915ab50456..3606c509bb 100644
--- a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewTest.cs
+++ b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewTest.cs
@@ -8,7 +8,6 @@ using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNet.Http.Features;
using Microsoft.AspNet.Http.Internal;
-using Microsoft.AspNet.Mvc.Actions;
using Microsoft.AspNet.Mvc.ModelBinding;
using Microsoft.AspNet.PageExecutionInstrumentation;
using Microsoft.Framework.WebEncoders.Testing;
@@ -568,11 +567,13 @@ namespace Microsoft.AspNet.Mvc.Razor
await v.RenderSectionAsync("sectionA");
});
});
+ nestedLayout.Path = "NestedLayout";
var baseLayout = new TestableRazorPage(v =>
{
v.HtmlEncoder = htmlEncoder;
v.RenderSection("sectionB");
});
+ baseLayout.Path = "Layout";
var viewEngine = new Mock();
viewEngine.Setup(p => p.FindPage(It.IsAny(), "NestedLayout"))
@@ -784,6 +785,8 @@ namespace Microsoft.AspNet.Mvc.Razor
v.RenderBodyPublic();
v.Layout = "~/Shared/Layout2.cshtml";
});
+ layout1.Path = "~/Shared/Layout1.cshtml";
+
var layout2 = new TestableRazorPage(v =>
{
v.HtmlEncoder = htmlEncoder;
@@ -791,6 +794,8 @@ namespace Microsoft.AspNet.Mvc.Razor
v.Write(v.RenderSection("bar"));
v.RenderBodyPublic();
});
+ layout2.Path = "~/Shared/Layout2.cshtml";
+
var viewEngine = new Mock();
viewEngine.Setup(p => p.FindPage(It.IsAny(), "~/Shared/Layout1.cshtml"))
.Returns(new RazorPageResult("~/Shared/Layout1.cshtml", layout1));
@@ -812,6 +817,87 @@ namespace Microsoft.AspNet.Mvc.Razor
Assert.Equal(expected, viewContext.Writer.ToString());
}
+ [Fact]
+ public async Task RenderAsync_Throws_IfLayoutPageReferencesSelf()
+ {
+ // Arrange
+ var expectedMessage = "A circular layout reference was detected when rendering " +
+ "'Shared/Layout.cshtml'. The layout page 'Shared/Layout.cshtml' has already been rendered.";
+ var page = new TestableRazorPage(v =>
+ {
+ v.Layout = "_Layout";
+ });
+ var layout = new TestableRazorPage(v =>
+ {
+ v.Layout = "_Layout";
+ v.RenderBodyPublic();
+ });
+ layout.Path = "Shared/Layout.cshtml";
+
+ var viewEngine = new Mock();
+ viewEngine.Setup(p => p.FindPage(It.IsAny(), "_Layout"))
+ .Returns(new RazorPageResult("_Layout", layout));
+
+ var view = new RazorView(viewEngine.Object,
+ Mock.Of(),
+ CreateViewStartProvider(),
+ page,
+ new CommonTestEncoder(),
+ isPartial: false);
+ var viewContext = CreateViewContext(view);
+
+ // Act and Assert
+ var exception = await Assert.ThrowsAsync(() => view.RenderAsync(viewContext));
+
+ // Assert
+ Assert.Equal(expectedMessage, exception.Message);
+ }
+
+ [Fact]
+ public async Task RenderAsync_Throws_IfNestedLayoutPagesResultInCyclicReferences()
+ {
+ // Arrange
+ var expectedMessage = "A circular layout reference was detected when rendering " +
+ "'/Shared/Layout2.cshtml'. The layout page 'Shared/_Layout.cshtml' has already been rendered.";
+ var page = new TestableRazorPage(v =>
+ {
+ v.Layout = "_Layout";
+ });
+ var layout1 = new TestableRazorPage(v =>
+ {
+ v.Layout = "_Layout2";
+ v.RenderBodyPublic();
+ });
+ layout1.Path = "Shared/_Layout.cshtml";
+
+ var layout2 = new TestableRazorPage(v =>
+ {
+ v.Layout = "_Layout";
+ v.RenderBodyPublic();
+ });
+ layout2.Path = "/Shared/Layout2.cshtml";
+
+ var viewEngine = new Mock();
+ viewEngine.Setup(p => p.FindPage(It.IsAny(), "_Layout"))
+ .Returns(new RazorPageResult("_Layout", layout1));
+ viewEngine.Setup(p => p.FindPage(It.IsAny(), "_Layout2"))
+ .Returns(new RazorPageResult("_Layout2", layout2));
+
+ var view = new RazorView(viewEngine.Object,
+ Mock.Of(),
+ CreateViewStartProvider(),
+ page,
+ new CommonTestEncoder(),
+ isPartial: false);
+ var viewContext = CreateViewContext(view);
+
+ // Act and Assert
+ var exception = await Assert.ThrowsAsync(() => view.RenderAsync(viewContext));
+
+ // Assert
+ Assert.Equal(expectedMessage, exception.Message);
+ }
+
[Fact]
public async Task RenderAsync_ExecutesNestedLayoutsWithNestedSections()
{
@@ -848,6 +934,8 @@ namespace Microsoft.AspNet.Mvc.Razor
await writer.WriteLineAsync(htmlEncoder.HtmlEncode(v.RenderSection("foo").ToString()));
});
});
+ nestedLayout.Path = "~/Shared/Layout2.cshtml";
+
var baseLayout = new TestableRazorPage(v =>
{
v.HtmlEncoder = htmlEncoder;
@@ -855,6 +943,8 @@ namespace Microsoft.AspNet.Mvc.Razor
v.RenderBodyPublic();
v.Write(v.RenderSection("foo"));
});
+ baseLayout.Path = "~/Shared/Layout1.cshtml";
+
var viewEngine = new Mock();
viewEngine.Setup(p => p.FindPage(It.IsAny(), "~/Shared/Layout1.cshtml"))
.Returns(new RazorPageResult("~/Shared/Layout1.cshtml", nestedLayout));