To render a partial view with a strongly-typed model in ASP.NET Core MVC, use the <partial> tag helper and pass the model through its model attribute: <partial name="_ProductCard" model="item" />. The partial receives that object as its @model, so you get compile-time checking and IntelliSense inside the fragment. This article shows the modern tag-helper syntax on .NET 9, how it compares to Html.PartialAsync, how to return a partial from a controller action for AJAX/HTMX, and when to reach for a view component instead.
What partial views are for
A partial view is a reusable .cshtml fragment that renders a piece of markup — a product card, a comment block, a form section — without a layout of its own. Partials keep views DRY: instead of copying the same 20 lines of card markup everywhere a product appears, you define _ProductCard.cshtml once and render it wherever you need it. By convention partial file names start with an underscore, and shared partials live in Views/Shared/ so any controller's views can find them.
Partials render inside the request they are called from. They share the parent view's ViewData and ModelState, but they do not trigger a controller action or run independent business logic — that distinction matters when we compare them to view components later.
How partial names are resolved
When you write <partial name="_ProductCard" ... /> without a path, the Razor view engine searches a predictable set of locations in order:
- The current controller's view folder, for example
Views/Product/_ProductCard.cshtml. Views/Shared/_ProductCard.cshtml.
The first match wins, which lets a specific controller override a shared partial simply by placing a same-named file in its own folder. You can also pass an explicit path — <partial name="~/Views/Catalog/_ProductCard.cshtml" ... /> — when you want to bypass the search. For a card reused across many controllers, Views/Shared/ is the right home; that is where our example keeps _ProductCard.cshtml.
Render a partial with the tag helper (preferred)
Since ASP.NET Core 2.1 the <partial> tag helper has been the recommended way to render a partial. It is declarative, reads like normal HTML, and evaluates asynchronously under the hood. Here is a parent view that loops over a product list and renders a card for each item:
@model IEnumerable<PartialViewWithModel.Models.Product>
<h1>Products</h1>
<div class="product-grid">
@foreach (var item in Model)
{
<partial name="_ProductCard" model="item" />
}
</div>
The model attribute passes a single Product to the partial. The tag helper resolves _ProductCard by searching the current controller's view folder and then Views/Shared/, so no path is needed for shared partials.
The partial itself declares the type it expects with @model:
@model PartialViewWithModel.Models.Product
<article class="card">
<h2>@Model.Name</h2>
<p class="price">@Model.Price.ToString("C")</p>
<p>@Model.Description</p>
@if (!Model.InStock)
{
<span class="badge">Out of stock</span>
}
</article>
Because the model is strongly typed, @Model.Price is a real decimal and the compiler catches a mistyped property name before the page ever runs.

Passing a model and using ViewData
The <partial> tag helper gives you two related attributes:
model— passes a single object that becomes the partial's@model.for— passes a model expression so the partial's inputs generate correctnameattributes for model binding (useful for editor partials).
You can also flow extra values through ViewData. Partials inherit the parent's ViewData by default, and you can add to or override it per render:
<partial name="_ProductCard" model="item"
view-data="new ViewDataDictionary(ViewData) { { \"Layout\", \"compact\" } }" />
Inside the partial you read ViewData["Layout"] as usual. Notice that we wrap the parent's ViewData in a new ViewDataDictionary rather than mutating it — that keeps the override local to this one render and avoids leaking the value into the rest of the page. Prefer the strongly-typed model for the primary data and reserve ViewData for small presentational flags such as a display mode, a heading level, or a CSS variant. If you find yourself passing several loosely related values through ViewData, that is usually a sign the partial deserves its own view model class passed via model instead.
The older Html.PartialAsync helper
Before the tag helper, you rendered partials with an HTML helper. It still works and is occasionally handy when you need a value in C# rather than markup:
@await Html.PartialAsync("_ProductCard", item)
There is also a synchronous Html.Partial(...) and a RenderPartialAsync variant that writes directly to the output stream. Avoid the synchronous Html.Partial — it can deadlock and the analyzer warns against it. For everyday rendering, use the <partial> tag helper; it is cleaner and async by default. Reach for Html.PartialAsync only when you must capture the rendered fragment as an IHtmlContent value in code.
Return a partial from a controller (AJAX and HTMX)
Partials shine in AJAX and HTMX scenarios where you want to swap a fragment of the page without a full reload. A controller action can return just the partial's HTML using PartialView:
public class ProductController : Controller
{
private readonly IProductService _service;
public ProductController(IProductService service) => _service = service;
public IActionResult Index() => View(_service.GetAll());
// GET /Product/Card/3 -> returns only the _ProductCard fragment
public IActionResult Card(int id)
{
var product = _service.Find(id);
if (product is null) return NotFound();
return PartialView("_ProductCard", product);
}
}
PartialView(...) renders the view without the surrounding _Layout, so the response is exactly the card markup. On the client, an HTMX attribute like hx-get="/Product/Card/3" can drop that fragment straight into the DOM:
<button hx-get="/Product/Card/3" hx-target="#detail" hx-swap="innerHTML">
Refresh card
</button>
<div id="detail"></div>
This pattern keeps rendering logic on the server in the same Razor partial you already use for the full page, which avoids duplicating markup in JavaScript. The full-page Index view and the AJAX Card action render the identical _ProductCard.cshtml, so a styling change is made in exactly one place. That single-source-of-truth property is the main reason partials remain central to server-rendered MVC even as HTMX and similar libraries make partial page updates popular again in .NET 9.
Partial views vs view components
Partials and view components look similar but solve different problems. A partial is passive: it renders whatever model the parent hands it and cannot fetch its own data. A view component is a small, self-contained unit with its own C# class and (optionally) dependency-injected services — it gathers its data and then renders a view.
Use a partial view when the parent already has the data and you just want to reuse markup: a product card, a form section, a table row. Use a view component when the fragment needs independent logic or data access that the parent should not know about: a shopping-cart summary, a navigation menu built from the database, or a "recently viewed" widget. You invoke a view component with <vc:cart-summary /> or @await Component.InvokeAsync("CartSummary"), and it can inject an IProductService of its own.
A quick rule of thumb: if the fragment needs a constructor and services, it is a view component; if it only needs a model, it is a partial. In practice most reusable UI in an MVC app starts as a partial and only graduates to a view component once it needs to do real work on its own. The example repository that accompanies this article, built on .NET 9 and ASP.NET Core MVC, uses a plain partial precisely because the parent controller already loads every product.
Key takeaways
- Use the
<partial name="_ProductCard" model="item" />tag helper as the default way to render partials in .NET 9 MVC. - Declare
@modelin the partial for compile-time checking and IntelliSense. - Name shared partials with a leading underscore and place them in
Views/Shared/for global reuse. - Return
PartialView("_Name", model)from an action to serve fragments for AJAX and HTMX without the layout. - Prefer
Html.PartialAsyncover the synchronousHtml.Partial, but favor the tag helper overall. - Choose a view component (not a partial) when the fragment must fetch its own data or inject services.
Frequently asked questions
What is the difference between a partial view and a view component in ASP.NET Core?
A partial view only renders a model the parent supplies and has no logic of its own, while a view component has a C# class that can inject services and fetch its own data before rendering. Use partials for pure markup reuse and view components when the fragment owns independent logic.
How do I pass a model to a partial view?
Use the tag helper's model attribute, <partial name="_ProductCard" model="item" />, and declare @model Product at the top of the partial. The object you pass becomes the partial's strongly-typed Model.
How do I return a partial view from a controller action?
Return PartialView("_ProductCard", product) instead of View(...). This renders the view without the layout, producing just the fragment's HTML — ideal for AJAX or HTMX requests that swap part of the page.
Should I use the partial tag helper or Html.PartialAsync?
Prefer the <partial> tag helper for rendering markup; it is declarative and asynchronous by default. Use Html.PartialAsync only when you need the rendered fragment as an IHtmlContent value in C# code, and avoid the synchronous Html.Partial, which can deadlock.
Comments (0)
No comments yet — be the first to share your thoughts.