[{"data":1,"prerenderedAt":106},["ShallowReactive",2],{"qa-\u002Fdotnet\u002Fperformance-deployment\u002Fcaching":3},{"page":4,"siblings":94,"blog":103},{"id":5,"title":6,"body":7,"description":11,"difficulty":14,"extension":15,"framework":16,"frameworkSlug":17,"meta":18,"navigation":19,"order":20,"path":21,"questions":22,"questionsCount":85,"related":86,"seo":87,"seoDescription":88,"stem":89,"subtopic":6,"topic":90,"topicSlug":91,"updated":92,"__hash__":93},"qa\u002Fdotnet\u002Fperformance-deployment\u002Fcaching.md","Caching",{"type":8,"value":9,"toc":10},"minimark",[],{"title":11,"searchDepth":12,"depth":12,"links":13},"",2,[],"medium","md",".NET Core","dotnet",{},true,1,"\u002Fdotnet\u002Fperformance-deployment\u002Fcaching",[23,28,32,36,40,44,48,52,56,61,65,69,73,77,81],{"id":24,"difficulty":25,"q":26,"a":27},"why-cache","easy","Why is caching important in ASP.NET Core applications?","Caching stores the result of an expensive operation so subsequent requests can\nbe served from fast memory instead of re-computing or re-querying. It reduces\nlatency, cuts database load, and improves throughput under high concurrency.\n\n```csharp\n\u002F\u002F Without caching — every request hits the database:\npublic async Task\u003CIEnumerable\u003CProduct>> GetProductsAsync()\n    => await _db.Products.ToListAsync(); \u002F\u002F ~50 ms per call\n\n\u002F\u002F With IMemoryCache — first call populates, subsequent calls \u003C 1 ms:\npublic async Task\u003CIEnumerable\u003CProduct>> GetProductsAsync()\n{\n    if (_cache.TryGetValue(\"products:all\", out IEnumerable\u003CProduct>? cached))\n        return cached!;\n\n    var products = await _db.Products.ToListAsync();\n\n    _cache.Set(\"products:all\", products, TimeSpan.FromMinutes(5));\n    return products;\n}\n```\n\nThe three canonical reasons to cache:\n1. **Expensive queries** — database joins, aggregations, full-text search\n2. **External API calls** — rate-limited or slow third-party services\n3. **Computed results** — serialization, report generation, complex calculations\n\n**Rule of thumb:** Cache data that is read far more often than it changes and\nwhere a slightly stale value is acceptable. Never cache security-sensitive data\n(session tokens, permissions) without careful thought about invalidation.\n",{"id":29,"difficulty":25,"q":30,"a":31},"imemorycache","How do you use IMemoryCache in ASP.NET Core?","`IMemoryCache` is an in-process, per-server cache backed by `ConcurrentDictionary`.\nIt is registered by default in ASP.NET Core and injected like any service.\n\n```csharp\n\u002F\u002F Registration (already included in most default templates):\nbuilder.Services.AddMemoryCache();\n\n\u002F\u002F Injection and usage:\npublic class ProductService\n{\n    private readonly IMemoryCache   _cache;\n    private readonly AppDbContext   _db;\n\n    public ProductService(IMemoryCache cache, AppDbContext db)\n    {\n        _cache = cache;\n        _db    = db;\n    }\n\n    public async Task\u003CProduct?> GetByIdAsync(int id)\n    {\n        var key = $\"product:{id}\";\n\n        \u002F\u002F GetOrCreateAsync — atomic \"get or populate\" in one call:\n        return await _cache.GetOrCreateAsync(key, async entry =>\n        {\n            entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10);\n            entry.SlidingExpiration               = TimeSpan.FromMinutes(2);\n            entry.Priority                        = CacheItemPriority.Normal;\n\n            return await _db.Products.FindAsync(id);\n        });\n    }\n\n    public void Invalidate(int id) => _cache.Remove($\"product:{id}\");\n}\n```\n\nKey options:\n- `AbsoluteExpirationRelativeToNow` — evict after a fixed duration from insertion\n- `SlidingExpiration` — evict if not accessed for this duration (reset on each access)\n- `Priority` — controls eviction order under memory pressure (`Low` evicted first)\n\n**Rule of thumb:** `IMemoryCache` is ideal for single-server deployments and data\nthat fits in process memory. For multi-server deployments use `IDistributedCache`\n(Redis) so all instances share the same cache.\n",{"id":33,"difficulty":14,"q":34,"a":35},"idistributedcache","What is IDistributedCache and how does it differ from IMemoryCache?","`IDistributedCache` is an abstraction for a shared, out-of-process cache.\nMultiple application instances read and write the same cache store, so there\nis no inconsistency between servers. Redis and SQL Server are the most common\nimplementations.\n\n```csharp\n\u002F\u002F Registration — Redis:\nbuilder.Services.AddStackExchangeRedisCache(opts =>\n{\n    opts.Configuration = builder.Configuration.GetConnectionString(\"Redis\");\n    opts.InstanceName  = \"myapp:\";\n});\n\n\u002F\u002F Or in-memory (for dev\u002Ftest, single process only):\nbuilder.Services.AddDistributedMemoryCache();\n\n\u002F\u002F Usage — IDistributedCache works with byte[] or string:\npublic class ProductService\n{\n    private readonly IDistributedCache _cache;\n\n    public async Task\u003CProduct?> GetByIdAsync(int id)\n    {\n        var key  = $\"product:{id}\";\n        var json = await _cache.GetStringAsync(key);\n\n        if (json is not null)\n            return JsonSerializer.Deserialize\u003CProduct>(json);\n\n        var product = await _db.Products.FindAsync(id);\n        if (product is null) return null;\n\n        await _cache.SetStringAsync(key,\n            JsonSerializer.Serialize(product),\n            new DistributedCacheEntryOptions\n            {\n                AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10),\n                SlidingExpiration               = TimeSpan.FromMinutes(2),\n            });\n\n        return product;\n    }\n\n    public async Task InvalidateAsync(int id)\n        => await _cache.RemoveAsync($\"product:{id}\");\n}\n```\n\n| | IMemoryCache | IDistributedCache |\n|-|--------------|-------------------|\n| Scope | Single process | All instances |\n| Speed | \u003C 1 ms | ~1–5 ms (network) |\n| Capacity | Limited by RAM | Redis cluster capacity |\n| Consistency | Per-server | Shared |\n\n**Rule of thumb:** Use `IMemoryCache` on single-server deployments or for\ntransient per-request data. Use `IDistributedCache` (Redis) whenever you run\nmore than one application instance.\n",{"id":37,"difficulty":14,"q":38,"a":39},"cache-aside-pattern","What is the cache-aside pattern and how is it implemented in .NET?","**Cache-aside** (lazy loading) means the application manages the cache\nexplicitly: check the cache first, fetch from the source on a miss, write\nback to the cache, then return. The cache is a side structure that the\napplication populates on demand.\n\n```csharp\n\u002F\u002F Generic cache-aside helper — eliminates repeated boilerplate:\npublic static class CacheExtensions\n{\n    public static async Task\u003CT?> GetOrSetAsync\u003CT>(\n        this IDistributedCache cache,\n        string key,\n        Func\u003CTask\u003CT?>> factory,\n        TimeSpan expiry)\n    {\n        var json = await cache.GetStringAsync(key);\n        if (json is not null)\n            return JsonSerializer.Deserialize\u003CT>(json);\n\n        var value = await factory();     \u002F\u002F 1. MISS — call the source\n        if (value is not null)\n            await cache.SetStringAsync(  \u002F\u002F 2. POPULATE the cache\n                key,\n                JsonSerializer.Serialize(value),\n                new DistributedCacheEntryOptions\n                    { AbsoluteExpirationRelativeToNow = expiry });\n\n        return value;                    \u002F\u002F 3. RETURN to caller\n    }\n}\n\n\u002F\u002F Call site is now one line:\nvar product = await _cache.GetOrSetAsync(\n    $\"product:{id}\",\n    () => _db.Products.FindAsync(id).AsTask(),\n    TimeSpan.FromMinutes(10));\n\n\u002F\u002F On write — invalidate the cache entry so the next read is fresh:\nawait _db.SaveChangesAsync();\nawait _cache.RemoveAsync($\"product:{id}\");\n```\n\nThe alternative is **write-through**: update the cache at the same time as\nthe source. Write-through keeps the cache warm but adds write latency and\ncomplexity when the write fails.\n\n**Rule of thumb:** Use cache-aside when reads are much more frequent than\nwrites and a brief window of stale data is acceptable. Use write-through\nfor write-heavy workloads where staleness is not tolerable.\n",{"id":41,"difficulty":14,"q":42,"a":43},"response-caching","What is response caching in ASP.NET Core and how do you enable it?","**Response caching** caches the full HTTP response at the middleware level,\nkeyed by URL and `Vary` headers. It avoids running the controller and service\nlayer entirely on cache hits.\n\n```csharp\n\u002F\u002F Program.cs:\nbuilder.Services.AddResponseCaching();\n\u002F\u002F ...\napp.UseResponseCaching(); \u002F\u002F must come before routing \u002F endpoints\n\n\u002F\u002F Controller or action — [ResponseCache] sets the Cache-Control header:\n[HttpGet(\"products\")]\n[ResponseCache(Duration = 60, VaryByQueryKeys = new[] { \"page\", \"size\" })]\npublic async Task\u003CIActionResult> GetProducts(int page = 1, int size = 20)\n{\n    var products = await _productService.GetPageAsync(page, size);\n    return Ok(products);\n}\n\n\u002F\u002F Equivalent header set by the above:\n\u002F\u002F Cache-Control: public, max-age=60\n\u002F\u002F Vary: page, size\n\n\u002F\u002F Profile — reuse settings across multiple actions:\nbuilder.Services.AddControllersWithViews(opts =>\n{\n    opts.CacheProfiles.Add(\"5MinutePublic\", new CacheProfile\n    {\n        Duration = 300,\n        Location = ResponseCacheLocation.Any,\n    });\n});\n\n[ResponseCache(CacheProfileName = \"5MinutePublic\")]\npublic IActionResult StaticData() => Ok(_staticData);\n```\n\nResponse caching only works for GET\u002FHEAD requests, non-authenticated responses,\nand when no `Authorization` header is present. It relies on the client or a\nproxy honouring `Cache-Control`.\n\n**Rule of thumb:** Use response caching for public, read-only endpoints that\nreturn the same data for all users. For per-user or authenticated responses,\nuse `IMemoryCache` or `IDistributedCache` at the service layer instead.\n",{"id":45,"difficulty":14,"q":46,"a":47},"output-caching","What is output caching in .NET 7+ and how does it differ from response caching?","**Output caching** (introduced in .NET 7) caches the rendered endpoint output\non the server. Unlike response caching, it is server-controlled and works\nregardless of client `Cache-Control` headers or `Authorization` headers.\n\n```csharp\n\u002F\u002F Program.cs:\nbuilder.Services.AddOutputCache(opts =>\n{\n    \u002F\u002F Named policy — reuse across multiple endpoints:\n    opts.AddPolicy(\"5min\", policy => policy.Expire(TimeSpan.FromMinutes(5)));\n\n    \u002F\u002F Base policy — applies to all endpoints unless overridden:\n    opts.AddBasePolicy(policy => policy.Expire(TimeSpan.FromSeconds(60)));\n});\n\napp.UseOutputCache(); \u002F\u002F after UseRouting, before endpoints\n\n\u002F\u002F Apply to a minimal API endpoint:\napp.MapGet(\"\u002Fproducts\", async (AppDbContext db) => await db.Products.ToListAsync())\n   .CacheOutput(\"5min\");\n\n\u002F\u002F Apply to a controller action:\n[HttpGet]\n[OutputCache(PolicyName = \"5min\")]\npublic async Task\u003CIActionResult> GetProducts()\n    => Ok(await _service.GetAllAsync());\n\n\u002F\u002F Vary by query string parameter:\nopts.AddPolicy(\"ByPage\", policy =>\n    policy.SetVaryByQuery(\"page\", \"size\").Expire(TimeSpan.FromMinutes(5)));\n\n\u002F\u002F Tag-based invalidation — evict cache entries by tag:\n[HttpPost]\npublic async Task\u003CIActionResult> UpdateProduct(Product p)\n{\n    await _service.UpdateAsync(p);\n    await _outputCacheStore.EvictByTagAsync(\"products\", ct); \u002F\u002F invalidate\n    return NoContent();\n}\n```\n\nKey difference from response caching: output caching caches on the server\nand can cache authenticated responses and vary on any request property (headers,\nroute values, custom values). Response caching is HTTP cache semantics delegated\nto the client or proxy.\n\n**Rule of thumb:** Prefer output caching in .NET 7+ — it gives you server-side\ncontrol, tag-based invalidation, and works with authenticated endpoints. Use\nresponse caching only when you need standard HTTP cache semantics for CDN or\nproxy caching.\n",{"id":49,"difficulty":14,"q":50,"a":51},"cache-invalidation","What are the main strategies for cache invalidation in .NET?","Cache invalidation is famously hard because stale data causes correctness bugs\nwhile over-invalidation erases all performance benefit.\n\n```csharp\n\u002F\u002F Strategy 1: TTL (Time-To-Live) expiry — simplest, always stale briefly\n_cache.Set(\"products:all\", products,\n    new MemoryCacheEntryOptions\n    {\n        AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5),\n    });\n\n\u002F\u002F Strategy 2: Explicit removal on write — fresh immediately after mutation\npublic async Task UpdateProductAsync(Product product)\n{\n    await _db.SaveChangesAsync();\n    _cache.Remove($\"product:{product.Id}\");   \u002F\u002F evict specific entry\n    _cache.Remove(\"products:all\");              \u002F\u002F evict collection\n}\n\n\u002F\u002F Strategy 3: Cache tags (output cache) — evict by semantic group\napp.MapPut(\"\u002Fproducts\u002F{id}\", async (int id, Product p, IOutputCacheStore store, CancellationToken ct) =>\n{\n    await _db.SaveChangesAsync();\n    await store.EvictByTagAsync(\"products\", ct); \u002F\u002F evicts all \"products\"-tagged entries\n});\napp.MapGet(\"\u002Fproducts\", GetAll).CacheOutput(p => p.Tag(\"products\"));\napp.MapGet(\"\u002Fproducts\u002F{id}\", GetOne).CacheOutput(p => p.Tag(\"products\"));\n\n\u002F\u002F Strategy 4: CancellationTokenSource — invalidate a group via token\nprivate CancellationTokenSource _productsCts = new();\n\n_cache.Set(\"products:all\", data, new MemoryCacheEntryOptions()\n    .AddExpirationToken(new CancellationChangeToken(_productsCts.Token)));\n\npublic void InvalidateAll()\n{\n    _productsCts.Cancel();\n    _productsCts = new CancellationTokenSource(); \u002F\u002F reset for next group\n}\n```\n\n**Rule of thumb:** Use TTL for data that can tolerate brief staleness (catalogs,\nreference data). Use explicit removal for data where staleness causes bugs\n(user profiles, inventory counts). Use tag-based eviction with output cache\nfor groups of related entries.\n",{"id":53,"difficulty":14,"q":54,"a":55},"redis-in-dotnet","How do you connect to Redis in .NET and what is it used for beyond caching?","**StackExchange.Redis** is the standard Redis client for .NET. Beyond\n`IDistributedCache`, it supports pub\u002Fsub, sorted sets, Lua scripting, and\ndistributed locks — patterns that go well beyond simple key\u002Fvalue caching.\n\n```csharp\n\u002F\u002F Install:\n\u002F\u002F dotnet add package StackExchange.Redis\n\u002F\u002F dotnet add package Microsoft.Extensions.Caching.StackExchangeRedis\n\n\u002F\u002F Program.cs — register both the cache and the raw connection:\nbuilder.Services.AddStackExchangeRedisCache(opts =>\n    opts.Configuration = \"localhost:6379,password=secret,abortConnect=false\");\n\n\u002F\u002F Direct access via IConnectionMultiplexer (for pub\u002Fsub, sets, etc.):\nbuilder.Services.AddSingleton\u003CIConnectionMultiplexer>(\n    ConnectionMultiplexer.Connect(\"localhost:6379\"));\n\n\u002F\u002F Distributed lock (prevents cache stampede):\npublic class InventoryService\n{\n    private readonly IConnectionMultiplexer _redis;\n\n    public async Task\u003Cint> GetStockAsync(string sku)\n    {\n        var db  = _redis.GetDatabase();\n        var key = $\"stock:{sku}\";\n\n        \u002F\u002F Try to acquire a lock so only one thread recomputes on a miss:\n        var lockKey   = $\"lock:{key}\";\n        var lockValue = Guid.NewGuid().ToString();\n        var acquired  = await db.StringSetAsync(lockKey, lockValue,\n            TimeSpan.FromSeconds(5), When.NotExists);\n\n        if (!acquired)\n        {\n            await Task.Delay(50);        \u002F\u002F wait and retry\n            return await GetStockAsync(sku);\n        }\n\n        try\n        {\n            var cached = await db.StringGetAsync(key);\n            if (cached.HasValue) return (int)cached;\n\n            var stock = await _db.Inventory.Where(i => i.Sku == sku).SumAsync(i => i.Qty);\n            await db.StringSetAsync(key, stock, TimeSpan.FromMinutes(1));\n            return stock;\n        }\n        finally { await db.KeyDeleteAsync(lockKey); }\n    }\n}\n```\n\nRedis use cases beyond caching: rate limiting (sliding window counters),\nsession storage, real-time leaderboards (sorted sets), pub\u002Fsub message fan-out,\nand distributed work queues.\n\n**Rule of thumb:** Use `IDistributedCache` for standard caching and configuration-\nswappable behavior. Use `IConnectionMultiplexer` directly only when you need\nRedis-specific data structures or pub\u002Fsub.\n",{"id":57,"difficulty":58,"q":59,"a":60},"cache-stampede","hard","What is a cache stampede and how do you prevent it in .NET?","A **cache stampede** (also called \"thundering herd\") occurs when a popular cache\nentry expires and many concurrent requests all miss at the same time, all racing\nto rebuild it from the slow data source simultaneously.\n\n```csharp\n\u002F\u002F Problem — naive cache-aside under load:\n\u002F\u002F 1000 concurrent requests all miss → 1000 DB queries fire simultaneously\nif (!_cache.TryGetValue(key, out var value))\n{\n    value = await _db.Products.ToListAsync(); \u002F\u002F all 1000 threads hit here!\n    _cache.Set(key, value, TimeSpan.FromMinutes(5));\n}\n\n\u002F\u002F Solution 1: SemaphoreSlim — only one thread rebuilds, others wait:\nprivate static readonly SemaphoreSlim _lock = new(1, 1);\n\npublic async Task\u003CIEnumerable\u003CProduct>> GetProductsAsync()\n{\n    if (_cache.TryGetValue(\"products\", out IEnumerable\u003CProduct>? cached))\n        return cached!;\n\n    await _lock.WaitAsync();\n    try\n    {\n        \u002F\u002F Double-check after acquiring — another thread may have populated it:\n        if (_cache.TryGetValue(\"products\", out cached))\n            return cached!;\n\n        cached = await _db.Products.ToListAsync();\n        _cache.Set(\"products\", cached, TimeSpan.FromMinutes(5));\n        return cached;\n    }\n    finally { _lock.Release(); }\n}\n\n\u002F\u002F Solution 2: Lazy\u003CTask\u003CT>> per key — collapses concurrent misses into one op:\nprivate readonly ConcurrentDictionary\u003Cstring, Lazy\u003CTask\u003CIEnumerable\u003CProduct>>>> _locks = new();\n\npublic Task\u003CIEnumerable\u003CProduct>> GetProductsAsync()\n    => _locks.GetOrAdd(\"products\", _ => new Lazy\u003CTask\u003CIEnumerable\u003CProduct>>>(\n        () => _db.Products.ToListAsync())).Value;\n\n\u002F\u002F Solution 3: Probabilistic early refresh (background repopulation before expiry):\nentry.PostEvictionCallbacks.Add(new PostEvictionCallbackRegistration\n{\n    EvictionCallback = (k, v, r, s) =>\n    {\n        if (r == EvictionReason.Expired)\n            _ = Task.Run(() => RefreshCacheAsync(k.ToString()!));\n    }\n});\n```\n\n**Rule of thumb:** Use `SemaphoreSlim` with a double-check pattern for\nhigh-traffic endpoints. Background refresh is the most sophisticated approach\nbut adds complexity — reach for it only when measured latency spikes justify it.\n",{"id":62,"difficulty":14,"q":63,"a":64},"icacheentry-size","How do you limit memory usage in IMemoryCache?","By default, `IMemoryCache` has no size limit and can grow unbounded. You can\nset a size limit and assign a size cost to each entry to cap memory usage.\n\n```csharp\n\u002F\u002F Registration with size limit (units are arbitrary — just needs to be consistent):\nbuilder.Services.AddMemoryCache(opts =>\n{\n    opts.SizeLimit = 1024; \u002F\u002F max 1024 \"units\"\n});\n\n\u002F\u002F Each entry declares its size cost:\n_cache.Set(\"small-lookup\", value, new MemoryCacheEntryOptions\n{\n    Size     = 1,                                          \u002F\u002F costs 1 unit\n    Priority = CacheItemPriority.High,                     \u002F\u002F last to be evicted\n    AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1),\n});\n\n_cache.Set(\"large-dataset\", bigList, new MemoryCacheEntryOptions\n{\n    Size     = 50,                                         \u002F\u002F costs 50 units\n    Priority = CacheItemPriority.Low,                      \u002F\u002F evicted first under pressure\n    AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5),\n});\n\n\u002F\u002F Compact — manually free a percentage of cache:\nif (_cache is MemoryCache mc)\n    mc.Compact(0.25); \u002F\u002F evict 25% of items by priority\u002Fsize\u002Fexpiry\n\n\u002F\u002F Monitor cache stats:\nvar stats = mc.GetCurrentStatistics();\nConsole.WriteLine($\"Entries: {stats?.CurrentEntryCount}, \" +\n                  $\"Size: {stats?.CurrentEstimatedSize}\");\n```\n\nWithout `SizeLimit`, setting `Size` on entries has no effect. Both must be\nconfigured together.\n\n**Rule of thumb:** Set `SizeLimit` whenever cached entries vary significantly\nin size (e.g., large JSON blobs vs small integers). Without it, a burst of\nlarge entries can exhaust process memory silently.\n",{"id":66,"difficulty":14,"q":67,"a":68},"session-caching","How does ASP.NET Core session storage relate to caching, and how do you configure it?","ASP.NET Core **session** stores per-user state across requests using a session\ncookie. The session data is backed by `IDistributedCache`, so Redis or SQL\nServer can be used as the session store.\n\n```csharp\n\u002F\u002F Program.cs:\nbuilder.Services.AddDistributedMemoryCache(); \u002F\u002F or AddStackExchangeRedisCache\nbuilder.Services.AddSession(opts =>\n{\n    opts.IdleTimeout        = TimeSpan.FromMinutes(20); \u002F\u002F inactivity expiry\n    opts.Cookie.HttpOnly    = true;\n    opts.Cookie.IsEssential = true;   \u002F\u002F GDPR: always set the cookie\n    opts.Cookie.SecurePolicy = CookieSecurePolicy.Always;\n});\n\napp.UseSession(); \u002F\u002F after UseRouting, before endpoints\n\n\u002F\u002F Reading and writing session values:\napp.MapGet(\"\u002Fcart\u002Fcount\", (HttpContext ctx) =>\n{\n    var count = ctx.Session.GetInt32(\"CartCount\") ?? 0;\n    return Results.Ok(count);\n});\n\napp.MapPost(\"\u002Fcart\u002Fadd\", (HttpContext ctx) =>\n{\n    var count = (ctx.Session.GetInt32(\"CartCount\") ?? 0) + 1;\n    ctx.Session.SetInt32(\"CartCount\", count);\n    return Results.Ok(count);\n});\n\n\u002F\u002F Complex objects — serialize to JSON manually:\nctx.Session.SetString(\"Cart\", JsonSerializer.Serialize(cartItems));\nvar cart = JsonSerializer.Deserialize\u003CList\u003CCartItem>>(\n    ctx.Session.GetString(\"Cart\") ?? \"[]\");\n```\n\nSession is backed by `IDistributedCache` — switching from in-memory to Redis\nchanges only the `AddDistributedMemoryCache()` call, not the session code.\n\n**Rule of thumb:** Use session for lightweight per-user transient state (cart\nitem count, wizard step). Store larger or durable user data in a database\nkeyed by user ID, not in session.\n",{"id":70,"difficulty":58,"q":71,"a":72},"hybrid-cache","What is HybridCache in .NET 9 and why was it introduced?","**HybridCache** (introduced in .NET 9, available as a NuGet package for .NET 8)\ncombines a fast in-process L1 cache (`IMemoryCache`) with a shared L2 cache\n(`IDistributedCache` \u002F Redis). A cache hit serves from L1 in \u003C 1 ms; on L1\nmiss it checks L2 before falling back to the data source.\n\n```csharp\n\u002F\u002F Installation:\n\u002F\u002F dotnet add package Microsoft.Extensions.Caching.Hybrid\n\n\u002F\u002F Registration:\nbuilder.Services.AddHybridCache(opts =>\n{\n    opts.MaximumPayloadBytes         = 1024 * 1024; \u002F\u002F 1 MB max per entry\n    opts.DefaultEntryOptions = new HybridCacheEntryOptions\n    {\n        Expiration         = TimeSpan.FromMinutes(5),\n        LocalCacheExpiration = TimeSpan.FromMinutes(1), \u002F\u002F L1 evicts sooner\n    };\n});\n\n\u002F\u002F Usage — GetOrCreateAsync handles L1 hit, L2 hit, and source fetch:\npublic class ProductService\n{\n    private readonly HybridCache _cache;\n\n    public async ValueTask\u003CProduct?> GetByIdAsync(int id, CancellationToken ct)\n        => await _cache.GetOrCreateAsync(\n            $\"product:{id}\",\n            async cancel => await _db.Products.FindAsync(new object[] { id }, cancel),\n            cancellationToken: ct);\n\n    public async Task InvalidateAsync(int id)\n        => await _cache.RemoveAsync($\"product:{id}\");\n}\n```\n\nKey benefits over manual L1+L2 wiring:\n- **Stampede protection built in** — only one call to the factory per missing key\n- **Serialization handled** — no manual `JsonSerializer.Serialize` per call\n- **Tag-based invalidation** — `RemoveByTagAsync(\"products\")` evicts from both levels\n\n**Rule of thumb:** In .NET 9+, prefer `HybridCache` over manual `IMemoryCache` +\n`IDistributedCache` wiring. It solves stampede, serialization, and L1\u002FL2 sync\nin one abstraction.\n",{"id":74,"difficulty":58,"q":75,"a":76},"benchmark-cache","How do you measure the impact of caching on ASP.NET Core application performance?","Measuring cache effectiveness requires both micro-benchmarks (to measure the\ncache hit path) and load tests (to measure real-world throughput improvements).\n\n```csharp\n\u002F\u002F BenchmarkDotNet micro-benchmark — compare cached vs uncached:\n\u002F\u002F dotnet add package BenchmarkDotNet\n[MemoryDiagnoser]\npublic class ProductBenchmarks\n{\n    private readonly IMemoryCache  _cache = new MemoryCache(new MemoryCacheOptions());\n    private readonly FakeDbContext _db    = new();\n\n    [GlobalSetup]\n    public void Setup() => _cache.Set(\"products\", _db.Products.ToList());\n\n    [Benchmark(Baseline = true)]\n    public List\u003CProduct> NoCache()     => _db.Products.ToList();\n\n    [Benchmark]\n    public List\u003CProduct>? WithCache()  =>\n        _cache.TryGetValue(\"products\", out List\u003CProduct>? p) ? p : null;\n}\n\n\u002F\u002F Key cache metrics to track in production:\n\u002F\u002F - Hit rate (hits \u002F total requests): target > 90% for high-value entries\n\u002F\u002F - Miss latency: how long does a cache miss take vs a cache hit?\n\u002F\u002F - Eviction rate: high evictions → size limit too small or TTL too short\n\n\u002F\u002F Expose hit\u002Fmiss counters via metrics (System.Diagnostics.Metrics):\nprivate static readonly Counter\u003Clong> _hits   = Metrics.Meter.CreateCounter\u003Clong>(\"cache.hits\");\nprivate static readonly Counter\u003Clong> _misses = Metrics.Meter.CreateCounter\u003Clong>(\"cache.misses\");\n\npublic async Task\u003CProduct?> GetByIdAsync(int id)\n{\n    if (_cache.TryGetValue($\"product:{id}\", out Product? p))\n    {\n        _hits.Add(1, new TagList { { \"cache\", \"product\" } });\n        return p;\n    }\n    _misses.Add(1, new TagList { { \"cache\", \"product\" } });\n    \u002F\u002F ... fetch and populate\n    return p;\n}\n```\n\n**Rule of thumb:** Measure before and after. A cache that is never hit (low hit\nrate) wastes memory. A cache that is invalidated too aggressively offers no\nbenefit. Track hit rate per cache key in production dashboards.\n",{"id":78,"difficulty":14,"q":79,"a":80},"cache-warming","What is cache warming and how do you implement it in ASP.NET Core?","**Cache warming** (also called pre-population) loads frequently accessed data\ninto the cache at application startup, before the first real request arrives.\nThis avoids a burst of slow cache misses immediately after a deployment.\n\n```csharp\n\u002F\u002F IHostedService that runs once at startup to populate the cache:\npublic class CacheWarmupService : IHostedService\n{\n    private readonly IMemoryCache  _cache;\n    private readonly AppDbContext  _db;\n    private readonly ILogger\u003CCacheWarmupService> _logger;\n\n    public CacheWarmupService(\n        IMemoryCache cache,\n        AppDbContext db,\n        ILogger\u003CCacheWarmupService> logger)\n    {\n        _cache  = cache;\n        _db     = db;\n        _logger = logger;\n    }\n\n    public async Task StartAsync(CancellationToken ct)\n    {\n        _logger.LogInformation(\"Warming caches...\");\n\n        \u002F\u002F Load reference data that every request needs:\n        var categories = await _db.Categories\n            .AsNoTracking()\n            .ToListAsync(ct);\n\n        _cache.Set(\"categories:all\", categories, new MemoryCacheEntryOptions\n        {\n            AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1),\n            Priority = CacheItemPriority.NeverRemove, \u002F\u002F do not evict under pressure\n        });\n\n        var topProducts = await _db.Products\n            .OrderByDescending(p => p.Views)\n            .Take(100)\n            .AsNoTracking()\n            .ToListAsync(ct);\n\n        foreach (var product in topProducts)\n            _cache.Set($\"product:{product.Id}\", product, TimeSpan.FromMinutes(30));\n\n        _logger.LogInformation(\"Cache warm-up complete: {Count} products loaded\",\n            topProducts.Count);\n    }\n\n    public Task StopAsync(CancellationToken ct) => Task.CompletedTask;\n}\n\n\u002F\u002F Registration — runs before the app starts accepting traffic:\nbuilder.Services.AddHostedService\u003CCacheWarmupService>();\n```\n\nIn Kubernetes, keep the readiness probe returning `503` until warm-up completes\nby setting a flag that the readiness health check reads:\n\n```csharp\n\u002F\u002F Simple flag — readiness check returns Unhealthy until flag is set:\nbuilder.Services.AddSingleton\u003CCacheWarmupFlag>();\nbuilder.Services.AddHealthChecks()\n    .AddCheck\u003CCacheWarmupHealthCheck>(\"cache-warmup\", tags: [\"readiness\"]);\n```\n\n**Rule of thumb:** Warm only the data that is both expensive to compute and\naccessed on virtually every request. Warming too much delays startup and wastes\nmemory on data that may never be needed.\n",{"id":82,"difficulty":14,"q":83,"a":84},"etag-conditional-caching","What are ETags and how do you implement HTTP conditional caching in ASP.NET Core?","An **ETag** is a hash or version token attached to an HTTP response. On\nsubsequent requests the client sends the ETag back in `If-None-Match`; if\nthe resource has not changed the server returns `304 Not Modified` with no\nbody, saving bandwidth and processing.\n\n```csharp\n\u002F\u002F Manual ETag — compute a hash of the response data:\n[HttpGet(\"products\u002F{id}\")]\npublic async Task\u003CIActionResult> GetProduct(int id)\n{\n    var product = await _cache.GetOrCreateAsync($\"product:{id}\",\n        entry => { entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10);\n                   return _db.Products.FindAsync(id).AsTask(); });\n\n    if (product is null) return NotFound();\n\n    \u002F\u002F Compute a deterministic ETag from the entity's updated timestamp:\n    var etag = new EntityTagHeaderValue(\n        $\"\\\"{product.UpdatedAt.Ticks}\\\"\");\n\n    \u002F\u002F Check the request's If-None-Match header:\n    if (Request.Headers.IfNoneMatch.ToString() == etag.ToString())\n        return StatusCode(StatusCodes.Status304NotModified); \u002F\u002F no body sent\n\n    Response.Headers.ETag = etag.ToString();\n    Response.Headers.CacheControl = \"private, max-age=0, must-revalidate\";\n    return Ok(product);\n}\n\n\u002F\u002F Middleware approach — ResponseCaching honours ETags automatically\n\u002F\u002F when the response has Cache-Control: public, max-age=N\n\u002F\u002F and the client sends If-None-Match or If-Modified-Since.\n\n\u002F\u002F For collections — hash the whole result set:\nvar hash = Convert.ToHexString(\n    SHA256.HashData(\n        Encoding.UTF8.GetBytes(JsonSerializer.Serialize(products))));\nvar etag = new EntityTagHeaderValue($\"\\\"{hash}\\\"\");\n```\n\nETag benefits:\n- **Bandwidth savings** — 304 response has no body\n- **Client freshness** — client always gets fresh data when it changes\n- **Reduced server load** — skip serialization on 304 path\n\n**Rule of thumb:** Use ETags for large resources (product listings, reports)\nwhere the response body is kilobytes or more. For tiny payloads the overhead\nof hashing and the extra round-trip makes ETags counterproductive — a short\n`max-age` is simpler and faster.\n",15,null,{"description":11},"Caching interview questions — IMemoryCache, IDistributedCache, Redis, cache-aside pattern, output caching, cache invalidation, and cache stampede.","dotnet\u002Fperformance-deployment\u002Fcaching","Performance & Deployment","performance-deployment","2026-06-23","jRccySZ3LoYKXy7il4ZmR6dZmQ79CTBvLctoRRHMJq0",[95,96,99],{"subtopic":6,"path":21,"order":20},{"subtopic":97,"path":98,"order":12},"Logging & Monitoring","\u002Fdotnet\u002Fperformance-deployment\u002Flogging-monitoring",{"subtopic":100,"path":101,"order":102},"Deployment","\u002Fdotnet\u002Fperformance-deployment\u002Fdeployment",3,{"path":104,"title":105},"\u002Fblog\u002Fdotnet-caching","Caching in ASP.NET Core: IMemoryCache, Redis, and Output Cache",1782244120073]