Static Assets & MapStaticAssets
Key Points
MapStaticAssets(.NET 9+) replacesUseStaticFilesfor modern apps. Pre-computes ETags, fingerprints filenames, automatically gzip+brotli, and integrates with build-time asset pipelines.- Outputs immutable, fingerprinted URLs (
site.6f8b2.css) withCache-Control: public, max-age=31536000, immutable. UseStaticFilesstill works and is still appropriate for non-fingerprinted dev/legacy scenarios.- Production hosting almost always puts a CDN in front of static assets —
MapStaticAssetsmakes the CDN edge configuration trivial because every URL is content-addressable.
Concepts (deep dive)
MapStaticAssets (.NET 9+)
var app = builder.Build();
app.MapStaticAssets(); // serves wwwroot with optimized headers
app.MapRazorPages();
app.MapControllers();
What it does at build time:
- Scans
wwwroot/. - Computes content hash for each file → fingerprinted filename in the URL space.
- Pre-compresses with gzip and brotli.
- Pre-computes ETag (strong; based on content hash).
- Generates a manifest (
appsettings.json-adjacent) mapping logical name → fingerprinted path.
What it does at runtime:
- Serves the appropriate compression based on
Accept-Encoding. - Sets
Cache-Control: public, max-age=31536000, immutablefor fingerprinted URLs. - Returns 304 on
If-None-Matchcache validation.
<link rel="stylesheet" href="..."> with fingerprints
The Assets tag helper / accessor resolves logical to fingerprinted path. In Blazor:
UseStaticFiles — the older approach
app.UseStaticFiles(); // serves wwwroot
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(Path.Combine(builder.Environment.ContentRootPath, "uploads")),
RequestPath = "/uploads",
OnPrepareResponse = ctx =>
{
ctx.Context.Response.Headers.CacheControl = "public, max-age=600";
}
});
UseStaticFiles doesn't fingerprint — combine with asp-append-version="true" tag helper for cache-busting.
Asset pipelines (Vite, esbuild, Webpack)
For complex frontends, the build pipeline produces fingerprinted assets that you serve via MapStaticAssets:
@Assets["dist/styles.css"] reads the manifest to find the actual fingerprinted name. .NET 9+'s MapStaticAssets integrates with this naturally.
Long cache + content addressing
The combination:
MapStaticAssetsproducessite.<hash>.css.- Browser caches it forever (
max-age=31536000, immutable). - New content → new hash → new URL → fresh fetch automatically.
Result: zero-cost client cache. The HTML page (which references the fingerprinted URLs) gets short-lived cache (e.g., 1 minute).
CDN integration
Put Azure Front Door / Cloudflare / CloudFront in front of wwwroot:
CDN serves cached responses; only fetches origin when cold. Combined with content-addressing, you can set hour-long edge cache without staleness concerns.
When to use UseStaticFiles instead
- Pre-.NET 9 apps without migration plan.
- Non-fingerprinted assets (user uploads, generated reports).
- Scenarios where fingerprinting isn't feasible (legacy paths, third-party tools expecting fixed URLs).
For new .NET 9+ apps: prefer MapStaticAssets.
Range requests
For audio/video streaming, both UseStaticFiles and MapStaticAssets handle Range requests for partial content (HTTP 206).
MIME types
var provider = new FileExtensionContentTypeProvider();
provider.Mappings[".geojson"] = "application/geo+json";
provider.Mappings[".webmanifest"] = "application/manifest+json";
app.UseStaticFiles(new StaticFileOptions
{
ContentTypeProvider = provider
});
For unusual file types, register the MIME mapping explicitly.
Default files
Order matters: UseDefaultFiles rewrites / to /index.html; then UseStaticFiles serves it.
Code: correct vs wrong
❌ Wrong: serving raw wwwroot without cache headers
✅ Correct: fingerprint + long cache
Or with manual cache:
app.UseStaticFiles(new StaticFileOptions
{
OnPrepareResponse = ctx =>
{
if (ctx.File.Name.Contains('.') && ctx.File.Name.Split('.').Length > 2) // crude fingerprint check
ctx.Context.Response.Headers.CacheControl = "public, max-age=31536000, immutable";
}
});
❌ Wrong: serving sensitive files
wwwroot/ is the public root. Sensitive files belong elsewhere (Storage/, Data/).
❌ Wrong: arbitrary file traversal
app.MapGet("/file/{path}", (string path) => Results.File($"./uploads/{path}"));
// "../etc/passwd" — directory traversal
✅ Correct: validate & sanitize
app.MapGet("/file/{path}", (string path, IWebHostEnvironment env) =>
{
var safe = Path.GetFileName(path); // strip ../
var full = Path.Combine(env.ContentRootPath, "uploads", safe);
if (!full.StartsWith(Path.Combine(env.ContentRootPath, "uploads")))
return Results.NotFound();
return File.Exists(full) ? Results.File(full) : Results.NotFound();
});
Design patterns for this topic
Pattern 1 — "MapStaticAssets for new apps"
- Intent: modern asset pipeline with fingerprinting + compression.
Pattern 2 — "CDN in front of wwwroot"
- Intent: edge caching for global users.
Pattern 3 — "Long cache + content addressing"
- Intent: zero-stale-risk forever caching.
Pattern 4 — "Custom file provider for user uploads"
- Intent: serve from non-default directories.
Pattern 5 — "Default files for SPA shell"
- Intent: serve
index.htmlfor/.
Pros & cons / trade-offs
| Approach | Pros | Cons |
|---|---|---|
MapStaticAssets | Modern; fingerprinting; compression | .NET 9+ only |
UseStaticFiles | Universal | No fingerprinting |
asp-append-version | Manual fingerprint | Per-file overhead |
| CDN | Edge cache | Setup |
When to use / when to avoid
- Use
MapStaticAssetsfor new .NET 9+ apps. - Use CDN in front of
wwwrootfor production. - Avoid serving sensitive data from
wwwroot. - Avoid path traversal when serving user-named files.
Interview Q&A
Q1. What's MapStaticAssets (.NET 9+)? Modern static file serving: fingerprints filenames, pre-computes compressed (gzip/brotli) variants, sets immutable cache headers automatically.
Q2. Why fingerprint static URLs? Cache forever; bust automatically when content changes.
Q3. MapStaticAssets vs UseStaticFiles? MapStaticAssets handles fingerprinting + compression at build time. UseStaticFiles is the older middleware without those features.
Q4. What cache headers does MapStaticAssets set? Cache-Control: public, max-age=31536000, immutable for fingerprinted URLs.
Q5. How do you serve files from outside wwwroot? Use UseStaticFiles with StaticFileOptions.FileProvider = new PhysicalFileProvider(...).
Q6. Why not put secrets in wwwroot? wwwroot is publicly exposed by static-file serving. Sensitive files must live elsewhere.
Q7. How does CDN integrate? CDN points origin at your Kestrel; users hit CDN; CDN caches based on Cache-Control. Fingerprinted URLs work perfectly.
Q8. What's a FileExtensionContentTypeProvider? Maps file extensions to MIME types. Add custom mappings for unusual types.
Q9. How do you handle large media (range requests)? Both static-file serving options support Range automatically. Browsers/players send Range: bytes=0-1023; server responds 206 Partial Content.
Q10. What's the default file convention? UseDefaultFiles rewrites / to /index.html (or default.htm, etc.). Run before UseStaticFiles/MapStaticAssets.
Gotchas / common mistakes
- ⚠️ Sensitive files in
wwwroot— exposed. - ⚠️ Path traversal when serving user-named files.
- ⚠️ Forgetting
UseDefaultFiles—/returns 404 instead ofindex.html. - ⚠️ No CDN for global apps — slow remote regions.
- ⚠️ Mixing fingerprinted and unfingerprinted — confusing cache strategy.