Any time you catch yourself copying the same chunk of Razor between views — a product card, an address block, a pagination bar — that chunk wants to be a partial view: a .cshtml file without a layout that renders inside other views. This guide covers creating partials, the <partial> tag helper, naming and folder resolution, and when to reach for a view component instead, on .NET 10 with verified code from the companion repository.

Creating a partial
A partial is just a Razor file, conventionally named with a leading underscore, placed where the views that use it can find it. Ours renders one product card:
@* Views/Shared/_ProductCard.cshtml *@
@model AspNetCoreMvcExamples.Models.Product
<div class="card mb-3">
<div class="card-body">
<h5 class="card-title">@Model.Name</h5>
<p class="card-text">₹@Model.Price.ToString("N2")</p>
<p class="card-text">
@if (Model.Stock > 0) { <span class="badge bg-success">@Model.Stock in stock</span> }
else { <span class="badge bg-danger">Out of stock</span> }
</p>
<a class="btn btn-sm btn-outline-primary" href="/products/@Model.Id">Details</a>
</div>
</div>
There is no Layout, no @section — a partial renders exactly its own markup into the parent's output stream.
Rendering with the partial tag helper
The modern way is the <partial> tag helper:
@* Views/Products/Index.cshtml *@
@model IReadOnlyList<AspNetCoreMvcExamples.Models.Product>
<div class="row">
@foreach (var product in Model)
{
<div class="col-md-4">
<partial name="_ProductCard" model="product" />
</div>
}
</div>
Each iteration passes a different Product to the same partial — one definition, many renderings. The detail page reuses it with its own model:
@* Views/Products/Detail.cshtml *@
@model AspNetCoreMvcExamples.Models.Product
<partial name="_ProductCard" model="Model" />
Change the card once, and the listing, the detail page, and anywhere else it appears update together — that is the entire point.
How partial names resolve
name="_ProductCard" (no path, no extension) makes Razor search, in order:
- The current controller's view folder —
Views/Products/ Views/Shared/
Put page-specific partials next to their page, and genuinely shared ones in Shared. You can also give an explicit application-relative path (name="~/Views/Shared/_ProductCard.cshtml"), which skips searching — useful when two folders contain a partial with the same name and you must disambiguate.
The underscore prefix is convention only; Razor does not treat underscored files specially in MVC. It survives as a signal to humans: "this file is not a full page."
Passing data: model and view-data
The model attribute is the primary channel, and the partial's @model type makes it compile-time safe. For small extras (a flag, a heading) there is view-data:
<partial name="_ProductCard" model="product" view-data='new ViewDataDictionary(ViewData) { ["Highlight"] = true }' />
Use it sparingly — once a partial needs three ViewData keys, wrap them in a tiny view model instead. A partial with a precise model type documents itself.
If you omit model entirely, the partial receives the parent's model — convenient for splitting a large page into sections that all share one model.
Html.PartialAsync and RenderPartialAsync
The tag helper covers nearly everything, but two method-style equivalents exist:
@await Html.PartialAsync("_ProductCard", product)
@{ await Html.RenderPartialAsync("_ProductCard", product); }
PartialAsync returns IHtmlContent; RenderPartialAsync writes directly to the output stream (marginally faster for very large fragments, awkward syntax). Prefer the tag helper for readability; never use the obsolete synchronous Html.Partial, which can deadlock — the analyzer MVC1000 flags it.
Partials in forms
A partial rendered inside a <form> participates in model binding as long as the input names line up with the parent model. The stock pattern is the validation scripts partial every MVC template ships with:
@section Scripts {
<partial name="_ValidationScriptsPartial" />
}
For editing collections (order lines, phone numbers), partials pair with for-loop indexing (Items[0].Name) so the binder reconstructs the list — a topic that deserves its own post.
Partial view vs. view component
A partial is markup reuse; it renders whatever model the caller already has. The moment the fragment needs to fetch its own data — a cart summary that queries the database, a tag cloud, a "related articles" box — you want a view component: a small class with an InvokeAsync method that can use dependency injection, plus its own view. Rule of thumb:
- Caller already has the data → partial view.
- Fragment must get its own data or contains real logic → view component.
- Fragment must update without a page reload → neither; that's an API endpoint plus fetch, or Blazor.
Summary
Partial views turn repeated Razor into a single, named, strongly-typed file: underscore-prefixed by convention, resolved from the controller's folder then Shared, rendered with <partial name="..." model="..." />, and combined with view components when the fragment needs its own data. The product-card example — used by both the listing grid and the detail page — runs as-is in the companion repository.
Comments (0)
No comments yet — be the first to share your thoughts.