Advanced View Components — Parameters, Caching and Testing

Building on the View Component introduction in Chapter 27, this lesson covers the advanced patterns used in production: typed parameters, multiple view variants, error resilience, caching, and testing. The notification bell with unread count, a popular posts sidebar with configurable item count, and a cart summary header are the canonical View Component examples — self-contained UI widgets that fetch their own data and render independently of the page they appear on.

Typed Parameters and Multiple Views

// ── View Component with typed parameters ──────────────────────────────────
public class PostListViewComponent : ViewComponent
{
    private readonly IPostService _service;
    public PostListViewComponent(IPostService service) => _service = service;

    // InvokeAsync can take any number of named parameters
    // Parameters come from the Tag Helper invocation or Component.InvokeAsync
    public async Task<IViewComponentResult> InvokeAsync(
        int    count      = 5,
        string category   = "",
        string displayMode = "full")   // "full", "compact", "minimal"
    {
        var posts = string.IsNullOrEmpty(category)
            ? await _service.GetRecentAsync(count)
            : await _service.GetRecentByCategoryAsync(category, count);

        var vm = new PostListViewModel(posts, displayMode);

        // Return different views based on display mode
        return displayMode switch
        {
            "compact"  => View("Compact",  vm),
            "minimal"  => View("Minimal",  vm),
            _          => View(vm),            // default view (Default.cshtml)
        };
    }
}

// ── View files:
// Views/Shared/Components/PostList/Default.cshtml  — full card layout
// Views/Shared/Components/PostList/Compact.cshtml  — condensed list
// Views/Shared/Components/PostList/Minimal.cshtml  — just titles

// ── Invocation with parameters ─────────────────────────────────────────────
<vc:post-list count="10" category="technology" display-mode="compact"></vc:post-list>

@* Or the method syntax: *@
@await Component.InvokeAsync("PostList",
    new { count = 10, category = "technology", displayMode = "compact" })
Note: Parameters to InvokeAsync match from the Tag Helper invocation by name, case-insensitively. The Tag Helper attribute display-mode="compact" maps to the C# parameter displayMode. The parameter matching is by name, not position — unlike method calls, you can pass parameters in any order. Optional parameters (with default values) do not need to be specified in the invocation. This makes View Components very flexible to use.
Tip: Cache View Component output to avoid repeated database calls for data that changes infrequently. Use IMemoryCache inside the View Component’s InvokeAsync: check the cache first, return cached result if hit, fetch from database and cache if miss. For the sidebar recent posts (changes every few minutes at most), a 5-minute cache means the database is queried once every 5 minutes regardless of how many users are loading the page simultaneously. At scale this is a significant reduction in database load.
Warning: View Components run during the view rendering phase — after the controller action has already completed. If a View Component throws an exception (database timeout, service unavailable), the exception occurs during rendering and propagates to the global exception handler. This means a failing navigation View Component can produce a 500 error for every page of the application. Always wrap View Component service calls in try/catch; return an empty or fallback view on error rather than letting exceptions propagate.

Notification Bell with Error Resilience

public class NotificationBellViewComponent(
    INotificationService notificationService,
    ILogger<NotificationBellViewComponent> logger) : ViewComponent
{
    public async Task<IViewComponentResult> InvokeAsync()
    {
        try
        {
            var userId = HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier);
            if (userId is null) return View(new NotificationBellViewModel(0));

            var unreadCount = await notificationService.GetUnreadCountAsync(userId);
            return View(new NotificationBellViewModel(unreadCount));
        }
        catch (Exception ex)
        {
            // Log but don't crash — sidebar should never take down the whole page
            logger.LogError(ex, "Failed to load notification count for user.");
            return View(new NotificationBellViewModel(0));  // graceful fallback
        }
    }
}

// Unit test using TestableViewComponent pattern
[Fact]
public async Task InvokeAsync_ReturnsUnreadCount_ForAuthenticatedUser()
{
    // Arrange
    var mockService = new Mock<INotificationService>();
    mockService.Setup(s => s.GetUnreadCountAsync("user-1")).ReturnsAsync(5);

    var component = new NotificationBellViewComponent(
        mockService.Object,
        Mock.Of<ILogger<NotificationBellViewComponent>>());

    // Set up ViewComponentContext with authenticated user
    component.ViewComponentContext = new ViewComponentContext
    {
        ViewContext = TestHelper.CreateViewContext("user-1"),
    };

    // Act
    var result = await component.InvokeAsync() as ViewViewComponentResult;

    // Assert
    var vm = result?.ViewData?.Model as NotificationBellViewModel;
    Assert.Equal(5, vm?.UnreadCount);
}

Common Mistakes

Mistake 1 — Not handling exceptions in InvokeAsync (sidebar crashes entire page)

❌ Wrong — database timeout in View Component propagates as 500 for every page of the application.

✅ Correct — wrap InvokeAsync in try/catch; return a fallback view on error.

Mistake 2 — Wrong view file path (views not found exception)

❌ Wrong — view at Views/Shared/PostList/Default.cshtml (missing Components folder).

✅ Correct — path must be Views/Shared/Components/PostList/Default.cshtml.

🧠 Test Yourself

A View Component’s InvokeAsync needs to return different HTML based on a parameter. How do you render different view files?