View Components are self-contained UI widgets with their own controller-like logic and view. Unlike partial views (which only render data passed from the parent), View Components have their own InvokeAsync method that can inject services, query the database, and compute data independently. This makes them ideal for UI elements that appear on many pages but need their own data: a navigation menu showing category counts, a recent posts sidebar, a notification bell with unread count, a shopping cart header. View Components are the correct answer to “how do I show database-driven UI in my layout without every action method having to fetch that data?”
Creating a View Component
// ── 1. The View Component class ────────────────────────────────────────────
public class RecentPostsViewComponent : ViewComponent
{
private readonly IPostService _service;
// Constructor injection — ViewComponent supports DI
public RecentPostsViewComponent(IPostService service)
=> _service = service;
// InvokeAsync — called when the component is rendered
// Parameters can be passed from the calling view
public async Task<IViewComponentResult> InvokeAsync(int count = 5)
{
var posts = await _service.GetRecentAsync(count);
return View(posts); // renders Views/Shared/Components/RecentPosts/Default.cshtml
}
}
// ── 2. The View Component template ────────────────────────────────────────
// File: Views/Shared/Components/RecentPosts/Default.cshtml
@model IReadOnlyList<PostSummaryViewModel>
<div class="widget">
<h4>Recent Posts</h4>
<ul class="list-unstyled">
@foreach (var post in Model)
{
<li>
<a asp-controller="Posts" asp-action="Details"
asp-route-id="@post.Id">@post.Title</a>
<small class="text-muted">@post.PublishedAt.ToString("MMM d")</small>
</li>
}
</ul>
</div>
// ── 3. Invoke the View Component from a layout or view ────────────────────
// In _Layout.cshtml sidebar section:
@* Method 1: Component Tag Helper (preferred) *@
<vc:recent-posts count="5"></vc:recent-posts>
@* "recent-posts" = kebab-case of "RecentPosts" class name without "ViewComponent" suffix *@
@* Method 2: @await Component.InvokeAsync *@
@await Component.InvokeAsync("RecentPosts", new { count = 5 })
Views/Shared/Components/{ComponentName}/Default.cshtml. The Default.cshtml name is used when you call return View(model) without specifying a view name. You can return alternative views with return View("Alternative", model) — useful for rendering the same component differently based on context (compact vs expanded card layout). The Views/Shared/Components/ folder can also be placed inside a specific controller’s Views folder (Views/Posts/Components/) for controller-scoped components._ViewImports.cshtml for access to the <vc:...> Tag Helper syntax: @addTagHelper *, BlogApp.Web. Without this registration, <vc:recent-posts> is treated as an unknown HTML element and rendered as-is (not executed). The Tag Helper registration applies to custom Tag Helpers and View Component Tag Helpers in your own assembly. The built-in Tag Helpers (asp-for, etc.) are registered separately with @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers.HttpContext is accessible through ViewComponentContext, but ModelState and action-level filters do not apply. If a View Component’s service call fails (database timeout, service unavailable), it throws during view rendering — the exception propagates to the action’s exception handler. Always handle exceptions in View Component InvokeAsync to avoid a database timeout in a sidebar widget bringing down the entire page.Navigation Menu View Component Example
// ── NavigationMenuViewComponent.cs ────────────────────────────────────────
public class NavigationMenuViewComponent(ICategoryService categoryService) : ViewComponent
{
public async Task<IViewComponentResult> InvokeAsync()
{
var categories = await categoryService.GetWithPostCountsAsync();
return View(new NavigationMenuViewModel
{
Categories = categories,
CurrentPath = HttpContext.Request.Path, // highlight active nav item
});
}
}
// ── Views/Shared/Components/NavigationMenu/Default.cshtml ─────────────────
@model NavigationMenuViewModel
<ul class="navbar-nav">
<li class="nav-item @(Model.CurrentPath == "/" ? "active" : "")">
<a class="nav-link" asp-controller="Home" asp-action="Index">Home</a>
</li>
@foreach (var cat in Model.Categories)
{
<li class="nav-item">
<a class="nav-link" asp-controller="Posts"
asp-action="ByCategory" asp-route-slug="@cat.Slug">
@cat.Name <span class="badge">@cat.PostCount</span>
</a>
</li>
}
</ul>
@* In _Layout.cshtml: *@
@* <vc:navigation-menu></vc:navigation-menu> *@
Common Mistakes
Mistake 1 — Wrong view file location (view not found exception)
❌ Wrong — view at Views/Shared/RecentPosts/Default.cshtml (missing Components folder).
✅ Correct — path must be Views/Shared/Components/RecentPosts/Default.cshtml.
Mistake 2 — Not handling exceptions in InvokeAsync (sidebar error crashes entire page)
❌ Wrong — database timeout in navigation menu View Component throws, entire page returns 500.
✅ Correct — wrap InvokeAsync in try/catch; return an empty or fallback view on error.