[{"data":1,"prerenderedAt":855},["ShallowReactive",2],{"blog-\u002Fblog\u002Fdotnet-caching":3},{"id":4,"title":5,"body":6,"description":840,"difficulty":841,"extension":842,"framework":843,"frameworkSlug":844,"meta":845,"navigation":66,"order":51,"path":846,"qaPath":847,"seo":848,"stem":849,"subtopic":850,"topic":851,"topicSlug":852,"updated":853,"__hash__":854},"blog\u002Fblog\u002Fdotnet-caching.md","Caching in ASP.NET Core: IMemoryCache, Redis, and Output Cache",{"type":7,"value":8,"toc":830},"minimark",[9,14,27,31,38,163,173,178,182,188,310,317,321,324,439,447,451,454,464,498,504,542,545,549,552,558,564,595,609,613,616,750,757,761,769,808,811,826],[10,11,13],"h2",{"id":12},"why-caching-matters-in-net-interviews","Why caching matters in .NET interviews",[15,16,17,18,22,23,26],"p",{},"Caching questions reveal whether a candidate thinks about production performance,\nnot just correctness. Interviewers look for understanding of ",[19,20,21],"em",{},"where"," to cache\n(in-process vs distributed), ",[19,24,25],{},"when"," to invalidate, and the failure modes that\nemerge under load — particularly cache stampede. This article covers every caching\nmechanism in the ASP.NET Core ecosystem.",[10,28,30],{"id":29},"imemorycache-in-process-caching","IMemoryCache: in-process caching",[15,32,33,37],{},[34,35,36],"code",{},"IMemoryCache"," stores data in the process's heap. It is fast (sub-millisecond),\nrequires no external infrastructure, and is registered automatically in ASP.NET Core.",[39,40,45],"pre",{"className":41,"code":42,"language":43,"meta":44,"style":44},"language-csharp shiki shiki-themes github-light github-dark","\u002F\u002F Registration (included by default, but explicit is clearer):\nbuilder.Services.AddMemoryCache(opts => opts.SizeLimit = 1024);\n\n\u002F\u002F GetOrCreateAsync — atomic \"get or populate\":\npublic async Task\u003CProduct?> GetProductAsync(int id)\n    => await _cache.GetOrCreateAsync($\"product:{id}\", async entry =>\n    {\n        entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10);\n        entry.SlidingExpiration               = TimeSpan.FromMinutes(2);\n        entry.Size                            = 1;\n        return await _db.Products.FindAsync(id);\n    });\n\n\u002F\u002F Invalidate on write:\npublic async Task UpdateProductAsync(Product p)\n{\n    await _db.SaveChangesAsync();\n    _cache.Remove($\"product:{p.Id}\");\n}\n","csharp","",[34,46,47,55,61,68,74,80,86,92,98,104,110,116,122,127,133,139,145,151,157],{"__ignoreMap":44},[48,49,52],"span",{"class":50,"line":51},"line",1,[48,53,54],{},"\u002F\u002F Registration (included by default, but explicit is clearer):\n",[48,56,58],{"class":50,"line":57},2,[48,59,60],{},"builder.Services.AddMemoryCache(opts => opts.SizeLimit = 1024);\n",[48,62,64],{"class":50,"line":63},3,[48,65,67],{"emptyLinePlaceholder":66},true,"\n",[48,69,71],{"class":50,"line":70},4,[48,72,73],{},"\u002F\u002F GetOrCreateAsync — atomic \"get or populate\":\n",[48,75,77],{"class":50,"line":76},5,[48,78,79],{},"public async Task\u003CProduct?> GetProductAsync(int id)\n",[48,81,83],{"class":50,"line":82},6,[48,84,85],{},"    => await _cache.GetOrCreateAsync($\"product:{id}\", async entry =>\n",[48,87,89],{"class":50,"line":88},7,[48,90,91],{},"    {\n",[48,93,95],{"class":50,"line":94},8,[48,96,97],{},"        entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10);\n",[48,99,101],{"class":50,"line":100},9,[48,102,103],{},"        entry.SlidingExpiration               = TimeSpan.FromMinutes(2);\n",[48,105,107],{"class":50,"line":106},10,[48,108,109],{},"        entry.Size                            = 1;\n",[48,111,113],{"class":50,"line":112},11,[48,114,115],{},"        return await _db.Products.FindAsync(id);\n",[48,117,119],{"class":50,"line":118},12,[48,120,121],{},"    });\n",[48,123,125],{"class":50,"line":124},13,[48,126,67],{"emptyLinePlaceholder":66},[48,128,130],{"class":50,"line":129},14,[48,131,132],{},"\u002F\u002F Invalidate on write:\n",[48,134,136],{"class":50,"line":135},15,[48,137,138],{},"public async Task UpdateProductAsync(Product p)\n",[48,140,142],{"class":50,"line":141},16,[48,143,144],{},"{\n",[48,146,148],{"class":50,"line":147},17,[48,149,150],{},"    await _db.SaveChangesAsync();\n",[48,152,154],{"class":50,"line":153},18,[48,155,156],{},"    _cache.Remove($\"product:{p.Id}\");\n",[48,158,160],{"class":50,"line":159},19,[48,161,162],{},"}\n",[15,164,165,168,169,172],{},[34,166,167],{},"AbsoluteExpiration"," evicts after a fixed wall-clock time. ",[34,170,171],{},"SlidingExpiration","\nextends the deadline on each read. Combining both ensures entries never live\nforever even if accessed constantly.",[15,174,175,177],{},[34,176,36],{}," is per-process. In a multi-server deployment, each instance has\nits own cache — one server may serve stale data after another server's write\ninvalidates its own copy. This is the key limitation.",[10,179,181],{"id":180},"idistributedcache-and-redis-shared-caching","IDistributedCache and Redis: shared caching",[15,183,184,187],{},[34,185,186],{},"IDistributedCache"," is an abstraction for a shared, out-of-process cache. All\napplication instances read and write the same store.",[39,189,191],{"className":41,"code":190,"language":43,"meta":44,"style":44},"\u002F\u002F Register Redis:\nbuilder.Services.AddStackExchangeRedisCache(opts =>\n{\n    opts.Configuration = \"localhost:6379,abortConnect=false\";\n    opts.InstanceName  = \"myapp:\";\n});\n\n\u002F\u002F Usage — serialize manually (unlike IMemoryCache which stores objects):\npublic async Task\u003CProduct?> GetProductAsync(int id)\n{\n    var key  = $\"product:{id}\";\n    var json = await _cache.GetStringAsync(key);\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, JsonSerializer.Serialize(product),\n        new DistributedCacheEntryOptions\n            { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10) });\n\n    return product;\n}\n",[34,192,193,198,203,207,212,217,222,226,231,235,239,244,249,254,259,263,268,273,277,282,288,294,299,305],{"__ignoreMap":44},[48,194,195],{"class":50,"line":51},[48,196,197],{},"\u002F\u002F Register Redis:\n",[48,199,200],{"class":50,"line":57},[48,201,202],{},"builder.Services.AddStackExchangeRedisCache(opts =>\n",[48,204,205],{"class":50,"line":63},[48,206,144],{},[48,208,209],{"class":50,"line":70},[48,210,211],{},"    opts.Configuration = \"localhost:6379,abortConnect=false\";\n",[48,213,214],{"class":50,"line":76},[48,215,216],{},"    opts.InstanceName  = \"myapp:\";\n",[48,218,219],{"class":50,"line":82},[48,220,221],{},"});\n",[48,223,224],{"class":50,"line":88},[48,225,67],{"emptyLinePlaceholder":66},[48,227,228],{"class":50,"line":94},[48,229,230],{},"\u002F\u002F Usage — serialize manually (unlike IMemoryCache which stores objects):\n",[48,232,233],{"class":50,"line":100},[48,234,79],{},[48,236,237],{"class":50,"line":106},[48,238,144],{},[48,240,241],{"class":50,"line":112},[48,242,243],{},"    var key  = $\"product:{id}\";\n",[48,245,246],{"class":50,"line":118},[48,247,248],{},"    var json = await _cache.GetStringAsync(key);\n",[48,250,251],{"class":50,"line":124},[48,252,253],{},"    if (json is not null)\n",[48,255,256],{"class":50,"line":129},[48,257,258],{},"        return JsonSerializer.Deserialize\u003CProduct>(json);\n",[48,260,261],{"class":50,"line":135},[48,262,67],{"emptyLinePlaceholder":66},[48,264,265],{"class":50,"line":141},[48,266,267],{},"    var product = await _db.Products.FindAsync(id);\n",[48,269,270],{"class":50,"line":147},[48,271,272],{},"    if (product is null) return null;\n",[48,274,275],{"class":50,"line":153},[48,276,67],{"emptyLinePlaceholder":66},[48,278,279],{"class":50,"line":159},[48,280,281],{},"    await _cache.SetStringAsync(key, JsonSerializer.Serialize(product),\n",[48,283,285],{"class":50,"line":284},20,[48,286,287],{},"        new DistributedCacheEntryOptions\n",[48,289,291],{"class":50,"line":290},21,[48,292,293],{},"            { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10) });\n",[48,295,297],{"class":50,"line":296},22,[48,298,67],{"emptyLinePlaceholder":66},[48,300,302],{"class":50,"line":301},23,[48,303,304],{},"    return product;\n",[48,306,308],{"class":50,"line":307},24,[48,309,162],{},[15,311,312,313,316],{},"The serialization boilerplate motivates the cache-aside helper pattern (see below),\nor the ",[34,314,315],{},"HybridCache"," abstraction in .NET 9 that handles this automatically.",[10,318,320],{"id":319},"the-cache-aside-pattern","The cache-aside pattern",[15,322,323],{},"Cache-aside (lazy loading) is the dominant caching pattern: check the cache, fetch\nfrom source on miss, populate the cache, return to caller.",[39,325,327],{"className":41,"code":326,"language":43,"meta":44,"style":44},"\u002F\u002F Generic helper eliminates per-method boilerplate:\npublic 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) return JsonSerializer.Deserialize\u003CT>(json);\n\n    var value = await factory();\n    if (value is not null)\n        await cache.SetStringAsync(key, JsonSerializer.Serialize(value),\n            new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = expiry });\n\n    return value;\n}\n\n\u002F\u002F Call site:\nvar product = await _cache.GetOrSetAsync(\n    $\"product:{id}\",\n    () => _db.Products.FindAsync(id).AsTask(),\n    TimeSpan.FromMinutes(10));\n",[34,328,329,334,339,344,349,354,359,363,368,373,377,382,387,392,397,401,406,410,414,419,424,429,434],{"__ignoreMap":44},[48,330,331],{"class":50,"line":51},[48,332,333],{},"\u002F\u002F Generic helper eliminates per-method boilerplate:\n",[48,335,336],{"class":50,"line":57},[48,337,338],{},"public static async Task\u003CT?> GetOrSetAsync\u003CT>(\n",[48,340,341],{"class":50,"line":63},[48,342,343],{},"    this IDistributedCache cache,\n",[48,345,346],{"class":50,"line":70},[48,347,348],{},"    string key,\n",[48,350,351],{"class":50,"line":76},[48,352,353],{},"    Func\u003CTask\u003CT?>> factory,\n",[48,355,356],{"class":50,"line":82},[48,357,358],{},"    TimeSpan expiry)\n",[48,360,361],{"class":50,"line":88},[48,362,144],{},[48,364,365],{"class":50,"line":94},[48,366,367],{},"    var json = await cache.GetStringAsync(key);\n",[48,369,370],{"class":50,"line":100},[48,371,372],{},"    if (json is not null) return JsonSerializer.Deserialize\u003CT>(json);\n",[48,374,375],{"class":50,"line":106},[48,376,67],{"emptyLinePlaceholder":66},[48,378,379],{"class":50,"line":112},[48,380,381],{},"    var value = await factory();\n",[48,383,384],{"class":50,"line":118},[48,385,386],{},"    if (value is not null)\n",[48,388,389],{"class":50,"line":124},[48,390,391],{},"        await cache.SetStringAsync(key, JsonSerializer.Serialize(value),\n",[48,393,394],{"class":50,"line":129},[48,395,396],{},"            new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = expiry });\n",[48,398,399],{"class":50,"line":135},[48,400,67],{"emptyLinePlaceholder":66},[48,402,403],{"class":50,"line":141},[48,404,405],{},"    return value;\n",[48,407,408],{"class":50,"line":147},[48,409,162],{},[48,411,412],{"class":50,"line":153},[48,413,67],{"emptyLinePlaceholder":66},[48,415,416],{"class":50,"line":159},[48,417,418],{},"\u002F\u002F Call site:\n",[48,420,421],{"class":50,"line":284},[48,422,423],{},"var product = await _cache.GetOrSetAsync(\n",[48,425,426],{"class":50,"line":290},[48,427,428],{},"    $\"product:{id}\",\n",[48,430,431],{"class":50,"line":296},[48,432,433],{},"    () => _db.Products.FindAsync(id).AsTask(),\n",[48,435,436],{"class":50,"line":301},[48,437,438],{},"    TimeSpan.FromMinutes(10));\n",[15,440,441,442,446],{},"The alternative — ",[443,444,445],"strong",{},"write-through"," — updates the cache on every write so reads\nnever miss. It's more complex and adds write latency, but is useful when staleness\nis unacceptable.",[10,448,450],{"id":449},"response-caching-vs-output-caching","Response caching vs output caching",[15,452,453],{},"These two mechanisms both cache HTTP responses but work very differently.",[15,455,456,459,460,463],{},[443,457,458],{},"Response caching"," sets ",[34,461,462],{},"Cache-Control"," headers and delegates caching to the\nHTTP client or a CDN\u002Fproxy:",[39,465,467],{"className":41,"code":466,"language":43,"meta":44,"style":44},"app.UseResponseCaching();\n\n[HttpGet(\"products\")]\n[ResponseCache(Duration = 60, VaryByQueryKeys = new[] { \"page\" })]\npublic IActionResult GetProducts(int page = 1) => Ok(_service.GetPage(page));\n\u002F\u002F Sets: Cache-Control: public, max-age=60\n",[34,468,469,474,478,483,488,493],{"__ignoreMap":44},[48,470,471],{"class":50,"line":51},[48,472,473],{},"app.UseResponseCaching();\n",[48,475,476],{"class":50,"line":57},[48,477,67],{"emptyLinePlaceholder":66},[48,479,480],{"class":50,"line":63},[48,481,482],{},"[HttpGet(\"products\")]\n",[48,484,485],{"class":50,"line":70},[48,486,487],{},"[ResponseCache(Duration = 60, VaryByQueryKeys = new[] { \"page\" })]\n",[48,489,490],{"class":50,"line":76},[48,491,492],{},"public IActionResult GetProducts(int page = 1) => Ok(_service.GetPage(page));\n",[48,494,495],{"class":50,"line":82},[48,496,497],{},"\u002F\u002F Sets: Cache-Control: public, max-age=60\n",[15,499,500,503],{},[443,501,502],{},"Output caching"," (.NET 7+) caches the server-rendered response in the server's\nown memory, bypassing the controller on hits regardless of client cache headers:",[39,505,507],{"className":41,"code":506,"language":43,"meta":44,"style":44},"builder.Services.AddOutputCache();\napp.UseOutputCache();\n\napp.MapGet(\"\u002Fproducts\", GetProducts).CacheOutput(\"5min\");\n\n\u002F\u002F Tag-based invalidation — evict groups of entries atomically:\nawait _outputCacheStore.EvictByTagAsync(\"products\", ct);\n",[34,508,509,514,519,523,528,532,537],{"__ignoreMap":44},[48,510,511],{"class":50,"line":51},[48,512,513],{},"builder.Services.AddOutputCache();\n",[48,515,516],{"class":50,"line":57},[48,517,518],{},"app.UseOutputCache();\n",[48,520,521],{"class":50,"line":63},[48,522,67],{"emptyLinePlaceholder":66},[48,524,525],{"class":50,"line":70},[48,526,527],{},"app.MapGet(\"\u002Fproducts\", GetProducts).CacheOutput(\"5min\");\n",[48,529,530],{"class":50,"line":76},[48,531,67],{"emptyLinePlaceholder":66},[48,533,534],{"class":50,"line":82},[48,535,536],{},"\u002F\u002F Tag-based invalidation — evict groups of entries atomically:\n",[48,538,539],{"class":50,"line":88},[48,540,541],{},"await _outputCacheStore.EvictByTagAsync(\"products\", ct);\n",[15,543,544],{},"Output caching is strictly more powerful: it works with authenticated requests,\nis server-controlled, and supports tag-based invalidation. Use response caching\nonly when you need CDN or browser caching semantics.",[10,546,548],{"id":547},"cache-invalidation-strategies","Cache invalidation strategies",[15,550,551],{},"The three main approaches for keeping caches consistent with the source of truth:",[15,553,554,557],{},[443,555,556],{},"TTL (time-to-live):"," The simplest strategy. Entries expire after a fixed duration.\nStale data is guaranteed but brief. Best for reference data (product catalog,\ncountry lists) where occasional staleness is acceptable.",[15,559,560,563],{},[443,561,562],{},"Explicit removal on write:"," Remove the cache entry immediately when the\nunderlying data changes. Gives you freshness at the cost of implementation effort —\nevery write path must also remove the corresponding cache key.",[39,565,567],{"className":41,"code":566,"language":43,"meta":44,"style":44},"public async Task UpdateProductAsync(Product p)\n{\n    await _db.SaveChangesAsync();\n    await _cache.RemoveAsync($\"product:{p.Id}\");\n    await _cache.RemoveAsync(\"products:all\"); \u002F\u002F also bust collection caches\n}\n",[34,568,569,573,577,581,586,591],{"__ignoreMap":44},[48,570,571],{"class":50,"line":51},[48,572,138],{},[48,574,575],{"class":50,"line":57},[48,576,144],{},[48,578,579],{"class":50,"line":63},[48,580,150],{},[48,582,583],{"class":50,"line":70},[48,584,585],{},"    await _cache.RemoveAsync($\"product:{p.Id}\");\n",[48,587,588],{"class":50,"line":76},[48,589,590],{},"    await _cache.RemoveAsync(\"products:all\"); \u002F\u002F also bust collection caches\n",[48,592,593],{"class":50,"line":82},[48,594,162],{},[15,596,597,600,601,604,605,608],{},[443,598,599],{},"Tag-based eviction (output cache):"," Associate entries with semantic tags\n(",[34,602,603],{},"\"products\"",", ",[34,606,607],{},"\"orders:tenant-42\"","). Evicting a tag purges all entries in the group.",[10,610,612],{"id":611},"preventing-cache-stampede","Preventing cache stampede",[15,614,615],{},"A cache stampede occurs when a popular entry expires and many concurrent requests\nall miss simultaneously, flooding the data source.",[39,617,619],{"className":41,"code":618,"language":43,"meta":44,"style":44},"\u002F\u002F Problem — naive cache-aside under concurrency:\nif (!_cache.TryGetValue(key, out var value))\n{\n    value = await _db.Products.ToListAsync(); \u002F\u002F 1000 threads all hit here at once!\n    _cache.Set(key, value, TimeSpan.FromMinutes(5));\n}\n\n\u002F\u002F Solution: SemaphoreSlim with double-check:\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        if (_cache.TryGetValue(\"products\", out cached)) \u002F\u002F double-check after lock\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",[34,620,621,626,631,635,640,645,649,653,658,663,667,672,676,681,686,690,695,700,704,709,714,718,723,728,733,739,745],{"__ignoreMap":44},[48,622,623],{"class":50,"line":51},[48,624,625],{},"\u002F\u002F Problem — naive cache-aside under concurrency:\n",[48,627,628],{"class":50,"line":57},[48,629,630],{},"if (!_cache.TryGetValue(key, out var value))\n",[48,632,633],{"class":50,"line":63},[48,634,144],{},[48,636,637],{"class":50,"line":70},[48,638,639],{},"    value = await _db.Products.ToListAsync(); \u002F\u002F 1000 threads all hit here at once!\n",[48,641,642],{"class":50,"line":76},[48,643,644],{},"    _cache.Set(key, value, TimeSpan.FromMinutes(5));\n",[48,646,647],{"class":50,"line":82},[48,648,162],{},[48,650,651],{"class":50,"line":88},[48,652,67],{"emptyLinePlaceholder":66},[48,654,655],{"class":50,"line":94},[48,656,657],{},"\u002F\u002F Solution: SemaphoreSlim with double-check:\n",[48,659,660],{"class":50,"line":100},[48,661,662],{},"private static readonly SemaphoreSlim _lock = new(1, 1);\n",[48,664,665],{"class":50,"line":106},[48,666,67],{"emptyLinePlaceholder":66},[48,668,669],{"class":50,"line":112},[48,670,671],{},"public async Task\u003CIEnumerable\u003CProduct>> GetProductsAsync()\n",[48,673,674],{"class":50,"line":118},[48,675,144],{},[48,677,678],{"class":50,"line":124},[48,679,680],{},"    if (_cache.TryGetValue(\"products\", out IEnumerable\u003CProduct>? cached))\n",[48,682,683],{"class":50,"line":129},[48,684,685],{},"        return cached!;\n",[48,687,688],{"class":50,"line":135},[48,689,67],{"emptyLinePlaceholder":66},[48,691,692],{"class":50,"line":141},[48,693,694],{},"    await _lock.WaitAsync();\n",[48,696,697],{"class":50,"line":147},[48,698,699],{},"    try\n",[48,701,702],{"class":50,"line":153},[48,703,91],{},[48,705,706],{"class":50,"line":159},[48,707,708],{},"        if (_cache.TryGetValue(\"products\", out cached)) \u002F\u002F double-check after lock\n",[48,710,711],{"class":50,"line":284},[48,712,713],{},"            return cached!;\n",[48,715,716],{"class":50,"line":290},[48,717,67],{"emptyLinePlaceholder":66},[48,719,720],{"class":50,"line":296},[48,721,722],{},"        cached = await _db.Products.ToListAsync();\n",[48,724,725],{"class":50,"line":301},[48,726,727],{},"        _cache.Set(\"products\", cached, TimeSpan.FromMinutes(5));\n",[48,729,730],{"class":50,"line":307},[48,731,732],{},"        return cached;\n",[48,734,736],{"class":50,"line":735},25,[48,737,738],{},"    }\n",[48,740,742],{"class":50,"line":741},26,[48,743,744],{},"    finally { _lock.Release(); }\n",[48,746,748],{"class":50,"line":747},27,[48,749,162],{},[15,751,752,753,756],{},"In .NET 9, ",[34,754,755],{},"HybridCache.GetOrCreateAsync"," has stampede protection built in —\nconcurrent misses for the same key coalesce into a single database call.",[10,758,760],{"id":759},"hybridcache-net-9","HybridCache (.NET 9)",[15,762,763,765,766,768],{},[34,764,315],{}," combines L1 (in-process, ",[34,767,36],{},") and L2 (distributed, Redis)\nin one abstraction with built-in stampede protection and automatic serialization:",[39,770,772],{"className":41,"code":771,"language":43,"meta":44,"style":44},"builder.Services.AddHybridCache();\n\npublic async ValueTask\u003CProduct?> GetByIdAsync(int id, CancellationToken ct)\n    => await _hybridCache.GetOrCreateAsync(\n        $\"product:{id}\",\n        async cancel => await _db.Products.FindAsync(new object[] { id }, cancel),\n        cancellationToken: ct);\n",[34,773,774,779,783,788,793,798,803],{"__ignoreMap":44},[48,775,776],{"class":50,"line":51},[48,777,778],{},"builder.Services.AddHybridCache();\n",[48,780,781],{"class":50,"line":57},[48,782,67],{"emptyLinePlaceholder":66},[48,784,785],{"class":50,"line":63},[48,786,787],{},"public async ValueTask\u003CProduct?> GetByIdAsync(int id, CancellationToken ct)\n",[48,789,790],{"class":50,"line":70},[48,791,792],{},"    => await _hybridCache.GetOrCreateAsync(\n",[48,794,795],{"class":50,"line":76},[48,796,797],{},"        $\"product:{id}\",\n",[48,799,800],{"class":50,"line":82},[48,801,802],{},"        async cancel => await _db.Products.FindAsync(new object[] { id }, cancel),\n",[48,804,805],{"class":50,"line":88},[48,806,807],{},"        cancellationToken: ct);\n",[15,809,810],{},"A cache hit serves from L1 in \u003C 1 ms. An L1 miss checks L2 before falling back\nto the database. Only one concurrent call per missing key reaches the database.",[15,812,813,816,817,819,820,822,823,825],{},[443,814,815],{},"Rule of thumb:"," In .NET 9+, ",[34,818,315],{}," is the default choice — it replaces\nthe manual L1 + L2 wiring pattern. For earlier versions, use ",[34,821,36],{}," for\nsingle-server deployments and ",[34,824,186],{}," with a stampede guard for\nmulti-server deployments.",[827,828,829],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":44,"searchDepth":57,"depth":57,"links":831},[832,833,834,835,836,837,838,839],{"id":12,"depth":57,"text":13},{"id":29,"depth":57,"text":30},{"id":180,"depth":57,"text":181},{"id":319,"depth":57,"text":320},{"id":449,"depth":57,"text":450},{"id":547,"depth":57,"text":548},{"id":611,"depth":57,"text":612},{"id":759,"depth":57,"text":760},"Caching in ASP.NET Core — when to use IMemoryCache vs Redis, how cache-aside differs from write-through, output caching in .NET 7+, and the stampede problem that hits every high-traffic app eventually.","medium","md",".NET Core","dotnet",{},"\u002Fblog\u002Fdotnet-caching","\u002Fdotnet\u002Fperformance-deployment\u002Fcaching",{"title":5,"description":840},"blog\u002Fdotnet-caching","Caching","Performance & Deployment","performance-deployment","2026-06-23","JjomTw_MJYfI1bt-AmGo2wTzfITYTDaH7KpEYduo9sg",1782244083919]