diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/IRazorViewEngine.cs b/src/Microsoft.AspNetCore.Mvc.Razor/IRazorViewEngine.cs
index 76e146d3a4..6af2cafef8 100644
--- a/src/Microsoft.AspNetCore.Mvc.Razor/IRazorViewEngine.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Razor/IRazorViewEngine.cs
@@ -15,9 +15,13 @@ namespace Microsoft.AspNetCore.Mvc.Razor
/// .
///
/// The .
- /// The name of the page.
+ /// The name or path of the page.
/// The of locating the page.
- /// .
+ ///
+ /// Use when the absolute or relative
+ /// path of the page is known.
+ /// .
+ ///
RazorPageResult FindPage(ActionContext context, string pageName);
///
diff --git a/src/Microsoft.AspNetCore.Mvc.TagHelpers/PartialTagHelper.cs b/src/Microsoft.AspNetCore.Mvc.TagHelpers/PartialTagHelper.cs
new file mode 100644
index 0000000000..8d26b511ff
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Mvc.TagHelpers/PartialTagHelper.cs
@@ -0,0 +1,119 @@
+// 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 System;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Mvc.Rendering;
+using Microsoft.AspNetCore.Mvc.ViewEngines;
+using Microsoft.AspNetCore.Mvc.ViewFeatures;
+using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal;
+using Microsoft.AspNetCore.Razor.TagHelpers;
+
+namespace Microsoft.AspNetCore.Mvc.TagHelpers
+{
+ ///
+ /// Renders a partial view.
+ ///
+ [HtmlTargetElement("partial", Attributes = "name", TagStructure = TagStructure.WithoutEndTag)]
+ public class PartialTagHelper : TagHelper
+ {
+ private const string ForAttributeName = "asp-for";
+ private readonly ICompositeViewEngine _viewEngine;
+ private readonly IViewBufferScope _viewBufferScope;
+
+ public PartialTagHelper(
+ ICompositeViewEngine viewEngine,
+ IViewBufferScope viewBufferScope)
+ {
+ _viewEngine = viewEngine ?? throw new ArgumentNullException(nameof(viewEngine));
+ _viewBufferScope = viewBufferScope ?? throw new ArgumentNullException(nameof(viewBufferScope));
+ }
+
+ ///
+ /// The name or path of the partial view that is rendered to the response.
+ ///
+ public string Name { get; set; }
+
+ ///
+ /// An expression to be evaluated against the current model.
+ ///
+ [HtmlAttributeName(ForAttributeName)]
+ public ModelExpression For { get; set; }
+
+ ///
+ /// A to pass into the partial view.
+ ///
+ public ViewDataDictionary ViewData { get; set; }
+
+ [HtmlAttributeNotBound]
+ [ViewContext]
+ public ViewContext ViewContext { get; set; }
+
+ ///
+ public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
+ {
+ if (context == null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ if (output == null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ var viewBuffer = new ViewBuffer(_viewBufferScope, Name, ViewBuffer.PartialViewPageSize);
+ using (var writer = new ViewBufferTextWriter(viewBuffer, Encoding.UTF8))
+ {
+ await RenderPartialViewAsync(writer);
+
+ // Reset the TagName. We don't want `partial` to render.
+ output.TagName = null;
+ output.Content.SetHtmlContent(viewBuffer);
+ }
+ }
+
+ private async Task RenderPartialViewAsync(TextWriter writer)
+ {
+ var viewEngineResult = _viewEngine.GetView(ViewContext.ExecutingFilePath, Name, isMainPage: false);
+ var getViewLocations = viewEngineResult.SearchedLocations;
+ if (!viewEngineResult.Success)
+ {
+ viewEngineResult = _viewEngine.FindView(ViewContext, Name, isMainPage: false);
+ }
+
+ if (!viewEngineResult.Success)
+ {
+ var searchedLocations = Enumerable.Concat(getViewLocations, viewEngineResult.SearchedLocations);
+ var locations = string.Empty;
+ if (searchedLocations.Any())
+ {
+ locations += Environment.NewLine + string.Join(Environment.NewLine, searchedLocations);
+ }
+
+ throw new InvalidOperationException(
+ Resources.FormatViewEngine_PartialViewNotFound(Name, locations));
+ }
+
+ var view = viewEngineResult.View;
+ // Determine which ViewData we should use to construct a new ViewData
+ var baseViewData = ViewData ?? ViewContext.ViewData;
+ var model = For?.Model ?? ViewContext.ViewData.Model;
+ var newViewData = new ViewDataDictionary
- /// The name of the view that is rendered to the response.
+ /// The name or path of the view that is rendered to the response.
/// The model that is rendered by the view.
/// The created object for the response.
[NonAction]
@@ -161,7 +161,7 @@ namespace Microsoft.AspNetCore.Mvc
///
/// Creates a object by specifying a .
///
- /// The name of the view that is rendered to the response.
+ /// The name or path of the partial view that is rendered to the response.
/// The created object for the response.
[NonAction]
public virtual PartialViewResult PartialView(string viewName)
@@ -185,7 +185,7 @@ namespace Microsoft.AspNetCore.Mvc
/// Creates a object by specifying a
/// and the to be rendered by the partial view.
///
- /// The name of the partial view that is rendered to the response.
+ /// The name or path of the partial view that is rendered to the response.
/// The model that is rendered by the partial view.
/// The created object for the response.
[NonAction]
diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/PartialViewResult.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/PartialViewResult.cs
index 64a8c5fcbb..e9f2763057 100644
--- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/PartialViewResult.cs
+++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/PartialViewResult.cs
@@ -7,7 +7,6 @@ using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.ViewEngines;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
-using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal;
using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.AspNetCore.Mvc
@@ -23,7 +22,7 @@ namespace Microsoft.AspNetCore.Mvc
public int? StatusCode { get; set; }
///
- /// Gets or sets the name of the partial view to render.
+ /// Gets or sets the name or path of the partial view that is rendered to the response.
///
///
/// When null, defaults to .
diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Rendering/HtmlHelperPartialExtensions.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Rendering/HtmlHelperPartialExtensions.cs
index 1cd317bc60..8d9bdac59a 100644
--- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Rendering/HtmlHelperPartialExtensions.cs
+++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Rendering/HtmlHelperPartialExtensions.cs
@@ -18,7 +18,7 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
///
/// The instance this method extends.
///
- /// The name of the partial view used to create the HTML markup. Must not be null.
+ /// The name or path of the partial view used to create the HTML markup. Must not be null.
///
///
/// A that on completion returns a new instance containing
@@ -46,7 +46,7 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
///
/// The instance this method extends.
///
- /// The name of the partial view used to create the HTML markup. Must not be null.
+ /// The name or path of the partial view used to create the HTML markup. Must not be null.
///
/// A to pass into the partial view.
///
@@ -76,7 +76,7 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
///
/// The instance this method extends.
///
- /// The name of the partial view used to create the HTML markup. Must not be null.
+ /// The name or path of the partial view used to create the HTML markup. Must not be null.
///
/// A model to pass into the partial view.
///
@@ -106,7 +106,7 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
///
/// The instance this method extends.
///
- /// The name of the partial view used to create the HTML markup. Must not be null.
+ /// The name or path of the partial view used to create the HTML markup. Must not be null.
///
///
/// Returns a new instance containing the created HTML.
@@ -137,7 +137,7 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
///
/// The instance this method extends.
///
- /// The name of the partial view used to create the HTML markup. Must not be null.
+ /// The name or path of the partial view used to create the HTML markup. Must not be null.
///
/// A to pass into the partial view.
///
@@ -170,7 +170,7 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
///
/// The instance this method extends.
///
- /// The name of the partial view used to create the HTML markup. Must not be null.
+ /// The name or path of the partial view used to create the HTML markup. Must not be null.
///
/// A model to pass into the partial view.
///
@@ -203,7 +203,7 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
///
/// The instance this method extends.
///
- /// The name of the partial view used to create the HTML markup. Must not be null.
+ /// The name or path of the partial view used to create the HTML markup. Must not be null.
///
/// A model to pass into the partial view.
/// A to pass into the partial view.
@@ -239,7 +239,7 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
///
/// The instance this method extends.
///
- /// The name of the partial view used to create the HTML markup. Must not be null.
+ /// The name or path of the partial view used to create the HTML markup. Must not be null.
///
///
/// In this context, "renders" means the method writes its output using .
@@ -267,7 +267,7 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
///
/// The instance this method extends.
///
- /// The name of the partial view used to create the HTML markup. Must not be null.
+ /// The name or path of the partial view used to create the HTML markup. Must not be null.
///
/// A to pass into the partial view.
///
@@ -297,7 +297,7 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
///
/// The instance this method extends.
///
- /// The name of the partial view used to create the HTML markup. Must not be null.
+ /// The name or path of the partial view used to create the HTML markup. Must not be null.
///
/// A model to pass into the partial view.
///
@@ -327,7 +327,7 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
///
/// The instance this method extends.
///
- /// The name of the partial view used to create the HTML markup. Must not be null.
+ /// The name or path of the partial view used to create the HTML markup. Must not be null.
///
/// A that renders the created HTML when it executes.
///
@@ -355,7 +355,7 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
///
/// The instance this method extends.
///
- /// The name of the partial view used to create the HTML markup. Must not be null.
+ /// The name or path of the partial view used to create the HTML markup. Must not be null.
///
/// A to pass into the partial view.
/// A that renders the created HTML when it executes.
@@ -385,7 +385,7 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
///
/// The instance this method extends.
///
- /// The name of the partial view used to create the HTML markup. Must not be null.
+ /// The name or path of the partial view used to create the HTML markup. Must not be null.
///
/// A model to pass into the partial view.
/// A that renders the created HTML when it executes.
diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Rendering/IHtmlHelper.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Rendering/IHtmlHelper.cs
index 0a31ca723a..357d429c94 100644
--- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Rendering/IHtmlHelper.cs
+++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Rendering/IHtmlHelper.cs
@@ -570,7 +570,7 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
/// Renders HTML markup for the specified partial view.
///
///
- /// The name of the partial view used to create the HTML markup. Must not be null.
+ /// The name or path of the partial view used to create the HTML markup. Must not be null.
///
/// A model to pass into the partial view.
/// A to pass into the partial view.
diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewEngines/IViewEngine.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewEngines/IViewEngine.cs
index f08850bc21..f6f5c93cc7 100644
--- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewEngines/IViewEngine.cs
+++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewEngines/IViewEngine.cs
@@ -13,9 +13,11 @@ namespace Microsoft.AspNetCore.Mvc.ViewEngines
/// .
///
/// The .
- /// The name of the view.
+ /// The name or path of the view that is rendered to the response.
/// Determines if the page being found is the main page for an action.
/// The of locating the view.
+ /// Use when the absolute or relative
+ /// path of the view is known.
ViewEngineResult FindView(ActionContext context, string viewName, bool isMainPage);
///
diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewResult.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewResult.cs
index b169604d14..c98562fc9e 100644
--- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewResult.cs
+++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewResult.cs
@@ -7,7 +7,6 @@ using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.ViewEngines;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
-using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal;
using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.AspNetCore.Mvc
@@ -23,7 +22,7 @@ namespace Microsoft.AspNetCore.Mvc
public int? StatusCode { get; set; }
///
- /// Gets or sets the name of the view to render.
+ /// Gets or sets the name or path of the view that is rendered to the response.
///
///
/// When null, defaults to .
diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/HtmlGenerationTest.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/HtmlGenerationTest.cs
index 1579544e7c..db7411fff9 100644
--- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/HtmlGenerationTest.cs
+++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/HtmlGenerationTest.cs
@@ -61,8 +61,12 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
// Only attribute order should differ.
{ "Order", "/HtmlGeneration_Order/Submit" },
{ "OrderUsingHtmlHelpers", "/HtmlGeneration_Order/Submit" },
+ // Testing PartialTagHelper
+ { "PartialTagHelperWithoutModel", null },
+ { "Warehouse", null },
// Testing InputTagHelpers invoked in the partial views
{ "ProductList", "/HtmlGeneration_Product" },
+ { "ProductListUsingTagHelpers", "/HtmlGeneration_Product" },
// Testing the ScriptTagHelper
{ "Script", null },
};
diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.PartialTagHelperWithoutModel.html b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.PartialTagHelperWithoutModel.html
new file mode 100644
index 0000000000..81eed3cee9
--- /dev/null
+++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.PartialTagHelperWithoutModel.html
@@ -0,0 +1 @@
+PartialTagHelperWithoutModel: Hello from partial
\ No newline at end of file
diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.ProductListUsingTagHelpers.html b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.ProductListUsingTagHelpers.html
new file mode 100644
index 0000000000..861d7b33a4
--- /dev/null
+++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.ProductListUsingTagHelpers.html
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Warehouse.html b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Warehouse.html
new file mode 100644
index 0000000000..d86cdb95db
--- /dev/null
+++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Warehouse.html
@@ -0,0 +1,15 @@
+
City_1
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/Microsoft.AspNetCore.Mvc.TagHelpers.Test/PartialTagHelperTest.cs b/test/Microsoft.AspNetCore.Mvc.TagHelpers.Test/PartialTagHelperTest.cs
new file mode 100644
index 0000000000..937e95ad44
--- /dev/null
+++ b/test/Microsoft.AspNetCore.Mvc.TagHelpers.Test/PartialTagHelperTest.cs
@@ -0,0 +1,473 @@
+// 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 System;
+using System.Collections.Generic;
+using System.IO;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc.Abstractions;
+using Microsoft.AspNetCore.Mvc.ModelBinding;
+using Microsoft.AspNetCore.Mvc.Rendering;
+using Microsoft.AspNetCore.Mvc.TestCommon;
+using Microsoft.AspNetCore.Mvc.ViewEngines;
+using Microsoft.AspNetCore.Mvc.ViewFeatures;
+using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal;
+using Microsoft.AspNetCore.Razor.TagHelpers;
+using Microsoft.AspNetCore.Routing;
+using Microsoft.Extensions.WebEncoders.Testing;
+using Moq;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Mvc.TagHelpers
+{
+ public class PartialTagHelperTest
+ {
+ [Fact]
+ public async Task ProcessAsync_RendersPartialView_IfGetViewReturnsView()
+ {
+ // Arrange
+ var expected = "Hello world!";
+ var bufferScope = new TestViewBufferScope();
+ var partialName = "_Partial";
+ var model = new object();
+ var viewContext = GetViewContext();
+
+ var view = new Mock();
+ view.Setup(v => v.RenderAsync(It.IsAny()))
+ .Callback((ViewContext v) =>
+ {
+ v.Writer.Write(expected);
+ })
+ .Returns(Task.CompletedTask);
+
+ var viewEngine = new Mock();
+ viewEngine.Setup(v => v.GetView(It.IsAny(), partialName, false))
+ .Returns(ViewEngineResult.Found(partialName, view.Object));
+
+ var tagHelper = new PartialTagHelper(viewEngine.Object, bufferScope)
+ {
+ Name = partialName,
+ ViewContext = viewContext,
+ };
+ var tagHelperContext = GetTagHelperContext();
+ var output = GetTagHelperOutput();
+
+ // Act
+ await tagHelper.ProcessAsync(tagHelperContext, output);
+
+ // Assert
+ var content = HtmlContentUtilities.HtmlContentToString(output.Content, new HtmlTestEncoder());
+ Assert.Equal(expected, content);
+ }
+
+ [Fact]
+ public async Task ProcessAsync_RendersPartialView_IfFindViewReturnsView()
+ {
+ // Arrange
+ var expected = "Hello world!";
+ var bufferScope = new TestViewBufferScope();
+ var partialName = "_Partial";
+ var model = new object();
+ var viewContext = GetViewContext();
+
+ var view = new Mock();
+ view.Setup(v => v.RenderAsync(It.IsAny()))
+ .Callback((ViewContext v) =>
+ {
+ v.Writer.Write(expected);
+ })
+ .Returns(Task.CompletedTask);
+
+ var viewEngine = new Mock();
+ viewEngine.Setup(v => v.GetView(It.IsAny(), partialName, false))
+ .Returns(ViewEngineResult.NotFound(partialName, new[] { partialName }));
+
+ viewEngine.Setup(v => v.FindView(viewContext, partialName, false))
+ .Returns(ViewEngineResult.Found(partialName, view.Object));
+
+ var tagHelper = new PartialTagHelper(viewEngine.Object, bufferScope)
+ {
+ Name = partialName,
+ ViewContext = viewContext,
+ };
+ var tagHelperContext = GetTagHelperContext();
+ var output = GetTagHelperOutput();
+
+ // Act
+ await tagHelper.ProcessAsync(tagHelperContext, output);
+
+ // Assert
+ var content = HtmlContentUtilities.HtmlContentToString(output.Content, new HtmlTestEncoder());
+ Assert.Equal(expected, content);
+ }
+
+ [Fact]
+ public async Task ProcessAsync_UsesViewDataFromContext()
+ {
+ // Arrange
+ var expected = "Implicit";
+ var bufferScope = new TestViewBufferScope();
+ var partialName = "_Partial";
+ var viewContext = GetViewContext();
+ viewContext.ViewData["key"] = expected;
+
+ var view = new Mock();
+ view.Setup(v => v.RenderAsync(It.IsAny()))
+ .Callback((ViewContext v) =>
+ {
+ v.Writer.Write(v.ViewData["key"]);
+ })
+ .Returns(Task.CompletedTask);
+
+ var viewEngine = new Mock();
+ viewEngine.Setup(v => v.GetView(It.IsAny(), partialName, false))
+ .Returns(ViewEngineResult.NotFound(partialName, new[] { partialName }));
+
+ viewEngine.Setup(v => v.FindView(viewContext, partialName, false))
+ .Returns(ViewEngineResult.Found(partialName, view.Object));
+
+ var tagHelper = new PartialTagHelper(viewEngine.Object, bufferScope)
+ {
+ Name = partialName,
+ ViewContext = viewContext,
+ };
+ var tagHelperContext = GetTagHelperContext();
+ var output = GetTagHelperOutput();
+
+ // Act
+ await tagHelper.ProcessAsync(tagHelperContext, output);
+
+ // Assert
+ var content = HtmlContentUtilities.HtmlContentToString(output.Content, new HtmlTestEncoder());
+ Assert.Equal(expected, content);
+ }
+
+ [Fact]
+ public async Task ProcessAsync_UsesPassedInViewData_WhenNotNull()
+ {
+ // Arrange
+ var expected = "Explicit";
+ var bufferScope = new TestViewBufferScope();
+ var partialName = "_Partial";
+ var model = new object();
+ var viewData = new ViewDataDictionary(new TestModelMetadataProvider(), new ModelStateDictionary());
+ viewData["key"] = expected;
+ var viewContext = GetViewContext();
+ viewContext.ViewData["key"] = "ViewContext";
+
+ var view = new Mock();
+ view.Setup(v => v.RenderAsync(It.IsAny()))
+ .Callback((ViewContext v) =>
+ {
+ v.Writer.Write(v.ViewData["key"]);
+ })
+ .Returns(Task.CompletedTask);
+
+ var viewEngine = new Mock();
+ viewEngine.Setup(v => v.GetView(It.IsAny(), partialName, false))
+ .Returns(ViewEngineResult.Found(partialName, view.Object));
+
+ var tagHelper = new PartialTagHelper(viewEngine.Object, bufferScope)
+ {
+ Name = partialName,
+ ViewContext = viewContext,
+ ViewData = viewData,
+ };
+ var tagHelperContext = GetTagHelperContext();
+ var output = GetTagHelperOutput();
+
+ // Act
+ await tagHelper.ProcessAsync(tagHelperContext, output);
+
+ // Assert
+ var content = HtmlContentUtilities.HtmlContentToString(output.Content, new HtmlTestEncoder());
+ Assert.Equal(expected, content);
+ }
+
+ [Fact]
+ public async Task ProcessAsync_UsesModelExpression_ToDetermineModel()
+ {
+ // Arrange
+ var expected = new PropertyModel();
+ var bufferScope = new TestViewBufferScope();
+ var partialName = "_Partial";
+ var modelMetadataProvider = new TestModelMetadataProvider();
+ var containerModel = new TestModel { Property = expected };
+ var containerModelExplorer = modelMetadataProvider.GetModelExplorerForType(
+ typeof(TestModel),
+ containerModel);
+ var propertyModelExplorer = containerModelExplorer.GetExplorerForProperty(nameof(TestModel.Property));
+
+ var modelExpression = new ModelExpression("Property", propertyModelExplorer);
+ var viewContext = GetViewContext();
+ viewContext.ViewData.Model = new object();
+
+ var view = new Mock();
+ view.Setup(v => v.RenderAsync(It.IsAny()))
+ .Callback((ViewContext v) =>
+ {
+ var actual = Assert.IsType(v.ViewData.Model);
+ Assert.Same(expected, actual);
+ })
+ .Returns(Task.CompletedTask)
+ .Verifiable();
+
+ var viewEngine = new Mock();
+ viewEngine.Setup(v => v.GetView(It.IsAny(), partialName, false))
+ .Returns(ViewEngineResult.Found(partialName, view.Object));
+
+ var tagHelper = new PartialTagHelper(viewEngine.Object, bufferScope)
+ {
+ Name = partialName,
+ ViewContext = viewContext,
+ For = modelExpression,
+ };
+ var tagHelperContext = GetTagHelperContext();
+ var output = GetTagHelperOutput();
+
+ // Act
+ await tagHelper.ProcessAsync(tagHelperContext, output);
+
+ // Assert
+ view.Verify();
+ }
+
+ [Fact]
+ public async Task ProcessAsync_SetsHtmlFieldPrefix_UsingModelExpression()
+ {
+ // Arrange
+ var expected = "order.items[0].Property";
+ var bufferScope = new TestViewBufferScope();
+ var partialName = "_Partial";
+ var modelMetadataProvider = new TestModelMetadataProvider();
+ var containerModel = new TestModel { Property = new PropertyModel() };
+ var containerModelExplorer = modelMetadataProvider.GetModelExplorerForType(
+ typeof(TestModel),
+ containerModel);
+ var propertyModelExplorer = containerModelExplorer.GetExplorerForProperty(nameof(TestModel.Property));
+
+ var modelExpression = new ModelExpression("Property", propertyModelExplorer);
+ var viewContext = GetViewContext();
+ viewContext.ViewData.TemplateInfo.HtmlFieldPrefix = "order.items[0]";
+
+ var view = new Mock();
+ view.Setup(v => v.RenderAsync(It.IsAny()))
+ .Callback((ViewContext v) =>
+ {
+ Assert.Equal(expected, v.ViewData.TemplateInfo.HtmlFieldPrefix);
+ })
+ .Returns(Task.CompletedTask)
+ .Verifiable();
+
+ var viewEngine = new Mock();
+ viewEngine.Setup(v => v.GetView(It.IsAny(), partialName, false))
+ .Returns(ViewEngineResult.Found(partialName, view.Object));
+
+ var tagHelper = new PartialTagHelper(viewEngine.Object, bufferScope)
+ {
+ Name = partialName,
+ ViewContext = viewContext,
+ For = modelExpression,
+ };
+ var tagHelperContext = GetTagHelperContext();
+ var output = GetTagHelperOutput();
+
+ // Act
+ await tagHelper.ProcessAsync(tagHelperContext, output);
+
+ // Assert
+ view.Verify();
+ Assert.Equal("order.items[0]", viewContext.ViewData.TemplateInfo.HtmlFieldPrefix);
+ }
+
+ [Fact]
+ public async Task ProcessAsync_UsesModelOnViewContextViewData_WhenModelExpresionIsNull()
+ {
+ // Arrange
+ var bufferScope = new TestViewBufferScope();
+ var partialName = "_Partial";
+ var model = new object();
+ var viewContext = GetViewContext();
+ viewContext.ViewData.Model = model;
+
+ var view = new Mock();
+ view.Setup(v => v.RenderAsync(It.IsAny()))
+ .Callback((ViewContext v) =>
+ {
+ Assert.Same(model, v.ViewData.Model);
+ })
+ .Returns(Task.CompletedTask)
+ .Verifiable();
+
+ var viewEngine = new Mock();
+ viewEngine.Setup(v => v.GetView(It.IsAny(), partialName, false))
+ .Returns(ViewEngineResult.Found(partialName, view.Object));
+
+ var tagHelper = new PartialTagHelper(viewEngine.Object, bufferScope)
+ {
+ Name = partialName,
+ ViewContext = viewContext,
+ };
+ var tagHelperContext = GetTagHelperContext();
+ var output = GetTagHelperOutput();
+
+ // Act
+ await tagHelper.ProcessAsync(tagHelperContext, output);
+
+ // Assert
+ view.Verify();
+ }
+
+ [Fact]
+ public async Task ProcessAsync_DoesNotModifyHtmlFieldPrefix_WhenModelExpressionIsNull()
+ {
+ // Arrange
+ var expected = "original";
+ var bufferScope = new TestViewBufferScope();
+ var partialName = "_Partial";
+ var model = new object();
+ var viewContext = GetViewContext();
+ viewContext.ViewData.Model = model;
+ viewContext.ViewData.TemplateInfo.HtmlFieldPrefix = expected;
+
+ var view = new Mock();
+ view.Setup(v => v.RenderAsync(It.IsAny()))
+ .Callback((ViewContext v) =>
+ {
+ Assert.Equal(expected, v.ViewData.TemplateInfo.HtmlFieldPrefix);
+ })
+ .Returns(Task.CompletedTask)
+ .Verifiable();
+
+ var viewEngine = new Mock();
+ viewEngine.Setup(v => v.GetView(It.IsAny(), partialName, false))
+ .Returns(ViewEngineResult.Found(partialName, view.Object));
+
+ var tagHelper = new PartialTagHelper(viewEngine.Object, bufferScope)
+ {
+ Name = partialName,
+ ViewContext = viewContext,
+ };
+ var tagHelperContext = GetTagHelperContext();
+ var output = GetTagHelperOutput();
+
+ // Act
+ await tagHelper.ProcessAsync(tagHelperContext, output);
+
+ // Assert
+ view.Verify();
+ }
+
+ [Fact]
+ public async Task ProcessAsync_DisposesViewInstance()
+ {
+ // Arrange
+ var bufferScope = new TestViewBufferScope();
+ var partialName = "_Partial";
+ var viewContext = GetViewContext();
+
+ var disposable = new Mock();
+ disposable.Setup(d => d.Dispose()).Verifiable();
+ var view = disposable.As();
+
+ view.Setup(v => v.RenderAsync(It.IsAny()))
+ .Returns(Task.CompletedTask)
+ .Verifiable();
+
+ var viewEngine = new Mock();
+ viewEngine.Setup(v => v.GetView(It.IsAny(), partialName, false))
+ .Returns(ViewEngineResult.Found(partialName, view.Object));
+
+ var tagHelper = new PartialTagHelper(viewEngine.Object, bufferScope)
+ {
+ Name = partialName,
+ ViewContext = viewContext,
+ };
+ var tagHelperContext = GetTagHelperContext();
+ var output = GetTagHelperOutput();
+
+ // Act
+ await tagHelper.ProcessAsync(tagHelperContext, output);
+
+ // Assert
+ disposable.Verify();
+ view.Verify();
+ }
+
+ [Fact]
+ public async Task ProcessAsync_Throws_IfGetViewAndFindReturnNotFoundResults()
+ {
+ // Arrange
+ var bufferScope = new TestViewBufferScope();
+ var partialName = "_Partial";
+ var expected = string.Join(Environment.NewLine,
+ $"The partial view '{partialName}' was not found. The following locations were searched:",
+ "NotFound1",
+ "NotFound2",
+ "NotFound3",
+ "NotFound4");
+ var viewData = new ViewDataDictionary(new TestModelMetadataProvider(), new ModelStateDictionary());
+ var viewContext = GetViewContext();
+
+ var view = Mock.Of();
+ var viewEngine = new Mock();
+ viewEngine.Setup(v => v.GetView(It.IsAny(), partialName, false))
+ .Returns(ViewEngineResult.NotFound(partialName, new[] { "NotFound1", "NotFound2" }));
+
+ viewEngine.Setup(v => v.FindView(viewContext, partialName, false))
+ .Returns(ViewEngineResult.NotFound(partialName, new[] { $"NotFound3", $"NotFound4" }));
+
+ var tagHelper = new PartialTagHelper(viewEngine.Object, bufferScope)
+ {
+ Name = partialName,
+ ViewContext = viewContext,
+ ViewData = viewData,
+ };
+ var tagHelperContext = GetTagHelperContext();
+ var output = GetTagHelperOutput();
+
+ // Act & Assert
+ var exception = await Assert.ThrowsAsync(
+ () => tagHelper.ProcessAsync(tagHelperContext, output));
+ Assert.Equal(expected, exception.Message);
+ }
+
+ private static ViewContext GetViewContext()
+ {
+ return new ViewContext(
+ new ActionContext(new DefaultHttpContext(), new RouteData(), new ActionDescriptor()),
+ NullView.Instance,
+ new ViewDataDictionary(new TestModelMetadataProvider(), new ModelStateDictionary()),
+ Mock.Of(),
+ TextWriter.Null,
+ new HtmlHelperOptions());
+ }
+
+ private static TagHelperContext GetTagHelperContext()
+ {
+ return new TagHelperContext(
+ "partial",
+ new TagHelperAttributeList(),
+ new Dictionary