diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/Properties/Resources.Designer.cs b/src/Microsoft.AspNetCore.Mvc.Razor/Properties/Resources.Designer.cs
index 2d17448aec..5343888a29 100644
--- a/src/Microsoft.AspNetCore.Mvc.Razor/Properties/Resources.Designer.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Razor/Properties/Resources.Designer.cs
@@ -219,7 +219,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor
}
///
- /// {0} has not been called for the page at '{1}'.
+ /// {0} has not been called for the page at '{1}'. To ignore call {2}().
///
internal static string RenderBodyNotCalled
{
@@ -227,11 +227,11 @@ namespace Microsoft.AspNetCore.Mvc.Razor
}
///
- /// {0} has not been called for the page at '{1}'.
+ /// {0} has not been called for the page at '{1}'. To ignore call {2}().
///
- internal static string FormatRenderBodyNotCalled(object p0, object p1)
+ internal static string FormatRenderBodyNotCalled(object p0, object p1, object p2)
{
- return string.Format(CultureInfo.CurrentCulture, GetString("RenderBodyNotCalled"), p0, p1);
+ return string.Format(CultureInfo.CurrentCulture, GetString("RenderBodyNotCalled"), p0, p1, p2);
}
///
@@ -283,7 +283,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor
}
///
- /// The following sections have been defined but have not been rendered by the page at '{0}': '{1}'.
+ /// The following sections have been defined but have not been rendered by the page at '{0}': '{1}'. To ignore an unrendered section call {2}("sectionName").
///
internal static string SectionsNotRendered
{
@@ -291,11 +291,11 @@ namespace Microsoft.AspNetCore.Mvc.Razor
}
///
- /// The following sections have been defined but have not been rendered by the page at '{0}': '{1}'.
+ /// The following sections have been defined but have not been rendered by the page at '{0}': '{1}'. To ignore an unrendered section call {2}("sectionName").
///
- internal static string FormatSectionsNotRendered(object p0, object p1)
+ internal static string FormatSectionsNotRendered(object p0, object p1, object p2)
{
- return string.Format(CultureInfo.CurrentCulture, GetString("SectionsNotRendered"), p0, p1);
+ return string.Format(CultureInfo.CurrentCulture, GetString("SectionsNotRendered"), p0, p1, p2);
}
///
diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/RazorPage.cs b/src/Microsoft.AspNetCore.Mvc.Razor/RazorPage.cs
index c6ca34b420..2002ca9718 100644
--- a/src/Microsoft.AspNetCore.Mvc.Razor/RazorPage.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Razor/RazorPage.cs
@@ -12,7 +12,6 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Mvc.Razor.Internal;
using Microsoft.AspNetCore.Mvc.Rendering;
@@ -40,6 +39,8 @@ namespace Microsoft.AspNetCore.Mvc.Razor
private TagHelperAttributeInfo _tagHelperAttributeInfo;
private HtmlContentWrapperTextWriter _valueBuffer;
private IViewBufferScope _bufferScope;
+ private bool _ignoreBody;
+ private HashSet _ignoredSections;
public RazorPage()
{
@@ -655,6 +656,14 @@ namespace Microsoft.AspNetCore.Mvc.Razor
return BodyContent;
}
+ ///
+ /// In a Razor layout page, ignores rendering the portion of a content page that is not within a named section.
+ ///
+ public void IgnoreBody()
+ {
+ _ignoreBody = true;
+ }
+
///
/// Creates a named content section in the page that can be invoked in a Layout page using
/// or .
@@ -815,6 +824,34 @@ namespace Microsoft.AspNetCore.Mvc.Razor
}
}
+ ///
+ /// In layout pages, ignores rendering the content of the section named .
+ ///
+ /// The section to ignore.
+ public void IgnoreSection(string sectionName)
+ {
+ if (sectionName == null)
+ {
+ throw new ArgumentNullException(nameof(sectionName));
+ }
+
+ if (!PreviousSectionWriters.ContainsKey(sectionName))
+ {
+ // If the section is not defined, throw an error.
+ throw new InvalidOperationException(Resources.FormatSectionNotDefined(
+ ViewContext.ExecutingFilePath,
+ sectionName,
+ ViewContext.View.Path));
+ }
+
+ if (_ignoredSections == null)
+ {
+ _ignoredSections = new HashSet(StringComparer.OrdinalIgnoreCase);
+ }
+
+ _ignoredSections.Add(sectionName);
+ }
+
///
/// Invokes on and
/// on the response stream, writing out any buffered content to the .
@@ -860,17 +897,27 @@ namespace Microsoft.AspNetCore.Mvc.Razor
_renderedSections,
StringComparer.OrdinalIgnoreCase);
- if (sectionsNotRendered.Any())
+ string[] sectionsNotIgnored;
+ if (_ignoredSections != null)
{
- var sectionNames = string.Join(", ", sectionsNotRendered);
- throw new InvalidOperationException(Resources.FormatSectionsNotRendered(Path, sectionNames));
+ sectionsNotIgnored = sectionsNotRendered.Except(_ignoredSections, StringComparer.OrdinalIgnoreCase).ToArray();
+ }
+ else
+ {
+ sectionsNotIgnored = sectionsNotRendered.ToArray();
+ }
+
+ if (sectionsNotIgnored.Length > 0)
+ {
+ var sectionNames = string.Join(", ", sectionsNotIgnored);
+ throw new InvalidOperationException(Resources.FormatSectionsNotRendered(Path, sectionNames, nameof(IgnoreSection)));
}
}
- else if (BodyContent != null && !_renderedBody)
+ else if (BodyContent != null && !_renderedBody && !_ignoreBody)
{
// There are no sections defined, but RenderBody was NOT called.
- // If a body was defined, then RenderBody should have been called.
- var message = Resources.FormatRenderBodyNotCalled(nameof(RenderBody), Path);
+ // If a body was defined and the body not ignored, then RenderBody should have been called.
+ var message = Resources.FormatRenderBodyNotCalled(nameof(RenderBody), Path, nameof(IgnoreBody));
throw new InvalidOperationException(message);
}
}
diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/Resources.resx b/src/Microsoft.AspNetCore.Mvc.Razor/Resources.resx
index 6c36f0d352..f94c7316aa 100644
--- a/src/Microsoft.AspNetCore.Mvc.Razor/Resources.resx
+++ b/src/Microsoft.AspNetCore.Mvc.Razor/Resources.resx
@@ -157,7 +157,7 @@
{0} invocation in '{1}' is invalid. {0} can only be called from a layout page.
- {0} has not been called for the page at '{1}'.
+ {0} has not been called for the page at '{1}'. To ignore call {2}().
Section '{0}' is already defined.
@@ -169,7 +169,7 @@
The layout page '{0}' cannot find the section '{1}' in the content page '{2}'.
- The following sections have been defined but have not been rendered by the page at '{0}': '{1}'.
+ The following sections have been defined but have not been rendered by the page at '{0}': '{1}'. To ignore an unrendered section call {2}("sectionName").
View of type '{0}' cannot be activated by '{1}'.
diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorPageTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorPageTest.cs
index 5e4f3983dd..3779180b14 100644
--- a/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorPageTest.cs
+++ b/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorPageTest.cs
@@ -302,6 +302,29 @@ namespace Microsoft.AspNetCore.Mvc.Razor
Assert.Equal(message, ex.Message);
}
+ [Fact]
+ public async Task IgnoreSection_ThrowsIfSectionIsNotFound()
+ {
+ // Arrange
+ var context = CreateViewContext(viewPath: "/Views/TestPath/Test.cshtml");
+ context.ExecutingFilePath = "/Views/Shared/_Layout.cshtml";
+ var page = CreatePage(v =>
+ {
+ v.Path = "/Views/TestPath/Test.cshtml";
+ v.IgnoreSection("bar");
+ }, context);
+ page.PreviousSectionWriters = new Dictionary
+ {
+ { "baz", _nullRenderAsyncDelegate }
+ };
+
+ // Act & Assert
+ var ex = await Assert.ThrowsAsync(() => page.ExecuteAsync());
+ var message = $"The layout page '/Views/Shared/_Layout.cshtml' cannot find the section 'bar'" +
+ " in the content page '/Views/TestPath/Test.cshtml'.";
+ Assert.Equal(message, ex.Message);
+ }
+
[Fact]
public void IsSectionDefined_ThrowsIfPreviousSectionWritersIsNotRegistered()
{
@@ -471,7 +494,24 @@ namespace Microsoft.AspNetCore.Mvc.Razor
// Act & Assert
var ex = Assert.Throws(() => page.EnsureRenderedBodyOrSections());
- Assert.Equal($"RenderBody has not been called for the page at '{path}'.", ex.Message);
+ Assert.Equal($"RenderBody has not been called for the page at '{path}'. To ignore call IgnoreBody().", ex.Message);
+ }
+
+ [Fact]
+ public async Task EnsureRenderedBodyOrSections_SucceedsIfRenderBodyIsNotCalledFromPage_AndNoSectionsAreDefined_AndBodyIgnored()
+ {
+ // Arrange
+ var path = "page-path";
+ var page = CreatePage(v =>
+ {
+ });
+ page.Path = path;
+ page.BodyContent = new HtmlString("some content");
+ page.IgnoreBody();
+
+ // Act & Assert (does not throw)
+ await page.ExecuteAsync();
+ page.EnsureRenderedBodyOrSections();
}
[Fact]
@@ -495,10 +535,57 @@ namespace Microsoft.AspNetCore.Mvc.Razor
var ex = Assert.Throws(() => page.EnsureRenderedBodyOrSections());
Assert.Equal(
"The following sections have been defined but have not been rendered by the page at " +
- $"'{path}': '{sectionName}'.",
+ $"'{path}': '{sectionName}'. To ignore an unrendered section call IgnoreSection(\"sectionName\").",
ex.Message);
}
+ [Fact]
+ public async Task EnsureRenderedBodyOrSections_SucceedsIfDefinedSectionsAreNotRendered_AndIgnored()
+ {
+ // Arrange
+ var path = "page-path";
+ var sectionName = "sectionA";
+ var page = CreatePage(v =>
+ {
+ });
+ page.Path = path;
+ page.BodyContent = new HtmlString("some content");
+ page.PreviousSectionWriters = new Dictionary
+ {
+ { sectionName, _nullRenderAsyncDelegate }
+ };
+ page.IgnoreSection(sectionName);
+
+ // Act & Assert (does not throw)
+ await page.ExecuteAsync();
+ page.EnsureRenderedBodyOrSections();
+ }
+
+ [Fact]
+ public async Task ExecuteAsync_RendersSectionsThatAreNotIgnored()
+ {
+ // Arrange
+ var path = "page-path";
+ var page = CreatePage(async p =>
+ {
+ p.IgnoreSection("ignored");
+ p.Write(await p.RenderSectionAsync("not-ignored-section"));
+ });
+ page.Path = path;
+ page.BodyContent = new HtmlString("some content");
+ page.PreviousSectionWriters = new Dictionary
+ {
+ { "ignored", _nullRenderAsyncDelegate },
+ { "not-ignored-section", writer => writer.WriteAsync("not-ignored-section-content") }
+ };
+
+ // Act
+ await page.ExecuteAsync();
+
+ // Assert
+ Assert.Equal("not-ignored-section-content", page.RenderedContent);
+ }
+
[Fact]
public async Task EnsureRenderedBodyOrSections_SucceedsIfRenderBodyIsNotCalled_ButAllDefinedSectionsAreRendered()
{
diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorViewTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorViewTest.cs
index d0ba5c0fd6..08308b505d 100644
--- a/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorViewTest.cs
+++ b/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorViewTest.cs
@@ -684,7 +684,8 @@ namespace Microsoft.AspNetCore.Mvc.Razor
// Act and Assert
var ex = await Assert.ThrowsAsync(() => view.RenderAsync(viewContext));
Assert.Equal("The following sections have been defined but have not been rendered by the page "
- + $"at '{LayoutPath}': 'head, foot'.", ex.Message);
+ + $"at '{LayoutPath}': 'head, foot'. To ignore an unrendered section call IgnoreSection(\"sectionName\").",
+ ex.Message);
}
[Fact]
@@ -879,7 +880,8 @@ namespace Microsoft.AspNetCore.Mvc.Razor
// Act and Assert
var ex = await Assert.ThrowsAsync(() => view.RenderAsync(viewContext));
Assert.Equal("The following sections have been defined but have not been rendered by the page at "
- + "'/Shared/Layout1.cshtml': 'foo'.", ex.Message);
+ + "'/Shared/Layout1.cshtml': 'foo'. To ignore an unrendered section call IgnoreSection(\"sectionName\").",
+ ex.Message);
}
[Fact]
@@ -946,7 +948,8 @@ namespace Microsoft.AspNetCore.Mvc.Razor
// Act and Assert
var ex = await Assert.ThrowsAsync(() => view.RenderAsync(viewContext));
Assert.Equal("The following sections have been defined but have not been rendered by the page at " +
- "'/Shared/Layout1.cshtml': 'foo'.", ex.Message);
+ "'/Shared/Layout1.cshtml': 'foo'. To ignore an unrendered section call IgnoreSection(\"sectionName\").",
+ ex.Message);
}
[Fact]
@@ -978,7 +981,8 @@ namespace Microsoft.AspNetCore.Mvc.Razor
// Act and Assert
var ex = await Assert.ThrowsAsync(() => view.RenderAsync(viewContext));
- Assert.Equal($"RenderBody has not been called for the page at '{LayoutPath}'.", ex.Message);
+ Assert.Equal($"RenderBody has not been called for the page at '{LayoutPath}'. To ignore call IgnoreBody().",
+ ex.Message);
}
[Fact]