Environment, Cache and Script Tag Helpers

๐Ÿ“‹ Table of Contents โ–พ
  1. Environment Tag Helper
  2. Cache Tag Helper
  3. Common Mistakes

Beyond forms, Tag Helpers provide powerful tools for managing client-side resources and server-side rendering decisions. The environment Tag Helper conditionally renders content per environment. The script and link Tag Helpers add CDN-with-fallback, cache-busting, and file globbing. The cache Tag Helper stores rendered HTML fragments server-side, eliminating repeated database calls for content that changes infrequently. These features are used in every production ASP.NET Core layout file.

Environment Tag Helper

@* โ”€โ”€ Render content only in specific environments โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ *@

<environment include="Development">
    @* Only shown in Development โ€” full, unminified assets for debugging *@
    <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
    <script src="~/lib/jquery/dist/jquery.js"></script>
</environment>

<environment exclude="Development">
    @* Staging and Production โ€” minified CDN assets with local fallback *@
    <link rel="stylesheet"
          href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"
          asp-fallback-href="~/lib/bootstrap/dist/css/bootstrap.min.css"
          asp-fallback-test-class="sr-only"
          asp-fallback-test-property="position"
          asp-fallback-test-value="absolute"
          crossorigin="anonymous" />

    <script src="https://cdn.jsdelivr.net/npm/jquery@3.7.0/dist/jquery.min.js"
            asp-fallback-src="~/lib/jquery/dist/jquery.min.js"
            asp-fallback-test="window.jQuery"
            crossorigin="anonymous">
    </script>
</environment>
Note: The asp-fallback-src / asp-fallback-href mechanism works by injecting a small inline script after the CDN <script> tag that tests whether the CDN resource loaded successfully (asp-fallback-test="window.jQuery" checks if jQuery is defined). If the test fails (CDN unavailable), the script dynamically loads the fallback local resource. This gracefully handles CDN outages at the cost of a slightly larger HTML page due to the inline test script.
Tip: Always use asp-append-version="true" on local static file references in layouts. It appends a hash of the file’s content as a query string: /css/site.css?v=r3Jnzc7FHmMObFj4Z0. When you update the CSS file, the hash changes and browsers download the new version instead of serving the cached old version. Without this, a deployment that changes CSS or JS files may leave users with stale cached versions for hours or days, depending on their browser cache settings.
Warning: The cache Tag Helper (<cache expires-after="@TimeSpan.FromMinutes(10)">) stores rendered HTML in the server’s in-memory cache. On a multi-server deployment (multiple app instances), each server has its own independent in-memory cache โ€” users may see different cached content depending on which server handles their request. For consistent caching across multiple instances, use the distributed cache Tag Helper (<distributed-cache name="...">) backed by Redis or SQL Server, which stores the HTML in a shared external cache.

Cache Tag Helper

@* โ”€โ”€ Cache a rendered HTML fragment server-side โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ *@

@* Cache the popular posts sidebar for 10 minutes โ€” avoids database query per request *@
<cache expires-after="@TimeSpan.FromMinutes(10)">
    <aside class="sidebar">
        <h4>Popular Posts</h4>
        @await Component.InvokeAsync("PopularPosts")
    </aside>
</cache>

@* Cache with a vary-by key โ€” different cache entries per user *@
<cache varies-by-user="true" expires-after="@TimeSpan.FromMinutes(5)">
    <div class="user-notifications">
        @await Component.InvokeAsync("NotificationBell")
    </div>
</cache>

@* Cache with a named key โ€” invalidate programmatically via IMemoryCache *@
<cache name="homepage-hero" expires-sliding="@TimeSpan.FromHours(1)">
    <section class="hero-banner">
        @await Component.InvokeAsync("HeroBanner")
    </section>
</cache>

@* Distributed cache Tag Helper โ€” for multi-server consistent caching *@
@* Requires: builder.Services.AddStackExchangeRedisCache(...) *@
<distributed-cache name="global-stats"
                   expires-after="@TimeSpan.FromHours(1)">
    @await Component.InvokeAsync("SiteStatistics")
</distributed-cache>

Common Mistakes

Mistake 1 โ€” Using the cache Tag Helper without a backend on multi-server deployments

โŒ Wrong โ€” in-memory cache is per-server; different users on different servers see inconsistent cached content.

โœ… Correct โ€” use distributed-cache Tag Helper with Redis or SQL Server for multi-server deployments.

Mistake 2 โ€” Not using asp-append-version on static files (stale cache after deployment)

โŒ Wrong โ€” CSS/JS updates not picked up by users with cached old versions.

โœ… Correct โ€” always add asp-append-version="true" to all <link> and <script> references for local files.

🧠 Test Yourself

The cache Tag Helper caches a View Component rendering for 10 minutes. The underlying data changes 2 minutes into the cache lifetime. What does the next request see?