⚡ .NET · CACHING DEEP DIVE

Caching in .NET — من الـ RAM للـ Redis

شرح شامل لكل أنواع الـ Caching في دوت نت — من الـ In-Memory للـ Distributed للـ Hybrid — مع ديجرامات وأكواد عملية لكل جزء.

IMemoryCache Redis IDistributedCache HybridCache Cache Invalidation Output Caching Response Caching

١ Why Caching? — ليه أصلاً بنعمل كاش؟

تخيل معايا كده إن عندك سيستم، وكل ما مستخدم يفتح الـ Home Page، بنروح نعمل Query معقدة في الـ Database تاخد ثانية كاملة عشان تجيب قايمة المنتجات. لو دخل 1000 مستخدم في نفس اللحظة، الـ DB هتكح تراب والسيستم هيقع! من هنا جت فكرة الـ Caching.

// diagram — المشكلة بدون كاش vs الحل مع الكاش
❌ بدون Caching — ضغط مرعب على الـ DB 1000 User App Server Database 1000 Query مرة واحدة! ⚠️ بطيء — Timeout 💥 DB تحت ضغط رهيب ✅ مع Caching — الـ DB تستريح 1000 User App Server Cache (RAM) البيانات جاهزة هنا! Database مرة واحدة بس! ✅ Cache Hit = جزء من الملي ثانية 📦 DB بتتكلم مرة واحدة أول مرة فقط ⚡ الـ RAM أسرع بـ 1000x من الـ DB RAM Speed: ~100ns DB Speed: ~1-100ms
💡

الـ Caching ببساطة: بدل ما نروح للمكان البطيء أو البعيد (الـ Database) كل شوية، بناخد نسخة من الداتا دي ونحطها في مكان سريع جداً وقريب مننا (الـ RAM)، عشان لما حد يطلبها تاني، نديها له في جزء من الملي ثانية.

٢ In-Memory Cache — التخزين داخل السيرفر

أول نوع هو الـ In-Memory Cache (وفي دوت نت بنستخدم له IMemoryCache). ده ببساطة بيخزن الداتا جوه الـ RAM بتاعة السيرفر اللي شايل الـ Application نفسه — مباشرة في نفس الـ Process.

// diagram — هيكل الـ In-Memory Cache داخل السيرفر
🖥️ Single Server .NET Application Process Controller / Endpoint API Layer IMemoryCache في الـ RAM مباشرة Cache Miss Database بس لو مالقاش في الكاش ⚡ أسرع Cache لأن الداتا في نفس الـ Process — لا Network Call — لا Serialization

✅ المميزات

  • ⚡ سريع سرعة الصاروخ — الداتا في نفس الـ Process
  • 🆓 مجاني — مش محتاج سيرفر تاني
  • 🧩 سهل جداً في الـ Setup والاستخدام
  • 📦 بيخزن C# Objects زي ما هي — مفيش Serialization

❌ العيوب

  • 💀 الداتا بتموت لو السيرفر حصله Restart
  • ⚠️ مشكلة Data Inconsistency لو عندك كذا سيرفر (Scale-out)
  • 🧠 بياكل من الـ RAM بتاعة الـ App نفسه
  • 🔒 مش بيتشارك بين السيرفرات
مثال عملي — IMemoryCache
1
تسجيل الـ Service في الـ DI Container Program.cs
// Program.cs — تسجيل الـ IMemoryCache في الـ DI Container
var builder = WebApplication.CreateBuilder(args);

// 👇 سطر واحد بس عشان تفعّل الـ In-Memory Cache
builder.Services.AddMemoryCache();

var app = builder.Build();
app.Run();
2
استخدام الكاش داخل Service أو Endpoint ProductService.cs
public class ProductService
{
    // Inject الـ IMemoryCache
    private readonly IMemoryCache _memoryCache;
    private readonly AppDbContext  _db;

    public ProductService(IMemoryCache memoryCache, AppDbContext db)
    {
        _memoryCache = memoryCache;
        _db = db;
    }

    public async Task<Product> GetProductAsync(int id)
    {
        string cacheKey = $"product-{id}"; // مفتاح فريد لكل منتج

        // GetOrCreateAsync — يشوف الكاش الأول، لو مالقاش يعمل الـ Factory
        return await _memoryCache.GetOrCreateAsync(cacheKey, async entry =>
        {
            // ⏱️ AbsoluteExpiration — الكاش يموت بعد 5 دقايق مهما حصل
            entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5);

            // 🏋️ Priority — لو الـ RAM ضاقت، مبيشيلوش الأول
            entry.Priority = CacheItemPriority.High;

            // 👇 روح جيب من الـ DB بس لو الكاش فاضي
            return await _db.Products.FindAsync(id);
        })!;
    }
}
⚠️

مشكلة الـ Scale-out: لو عملت 3 سيرفرات وفي Load Balancer بيوزع عليهم، كل سيرفر هيبقى جواه الـ Cache الخاص بيه. ممكن سيرفر 1 يكون فيه داتا محدثة وسيرفر 2 فيه داتا قديمة — ده الـ Data Inconsistency وهو أخطر عيب في الـ In-Memory Cache.

٣ Distributed Cache — Redis والمخزن المركزي

عشان نحل مشكلة السيرفرات الكتير دي، بنروح للـ Distributed Cache. الفكرة هنا إني بفصل الـ Cache تماماً في سيرفر مستقل برا الـ Application خالص، وأشهر حاجة بتعمل ده هي Redis.

// diagram — هيكل الـ Distributed Cache مع Redis
Load Balancer بيوزع الطلبات App Server 1 .NET App App Server 2 .NET App App Server 3 .NET App 🔴 Redis Server مخزن مركزي موحد localhost:6379 ✅ كل السيرفرات بتكلم نفس الـ Redis — داتا موحدة — مفيش Inconsistency
تشغيل Redis بـ Docker — سطر واحد بس!

مش هتقعد تنزل Redis وتعمل Setup معقدة! بـ Docker، بسطر واحد بس في الـ Terminal بـ Pull الـ Official Image وبشغلها في Container معزول على بورت 6379.

🐳
تشغيل Redis بـ Docker Terminal
# سطر واحد بس — بيشغل Redis على بورت 6379
docker run -d -p 6379:6379 --name redis-cache redis:alpine

# التحقق إنه شغال
docker ps
1
تثبيت الـ Package وتسجيل الـ Service Terminal + Program.cs
# تثبيت Package الـ Redis
dotnet add package Microsoft.Extensions.Caching.StackExchangeRedis

// Program.cs
builder.Services.AddStackExchangeRedisCache(options =>
{
    // 👇 بنقوله يكلم الـ Redis Container بتاعنا
    options.Configuration = "localhost:6379";
    options.InstanceName  = "MyApp_";
});
2
استخدام IDistributedCache داخل Service ProductService.cs
public class ProductService
{
    private readonly IDistributedCache _distributedCache;
    private readonly AppDbContext       _db;

    public async Task<Product?> GetProductAsync(int id)
    {
        string cacheKey  = $"product-{id}";

        // 🔍 نشوف في الـ Redis الأول — بيرجع JSON String
        var cachedData = await _distributedCache.GetStringAsync(cacheKey);

        if (!string.IsNullOrEmpty(cachedData))
        {
            // Cache Hit — نحول الـ JSON لـ Object
            return JsonSerializer.Deserialize<Product>(cachedData);
        }

        // Cache Miss — نروح للـ DB
        var product = await _db.Products.FindAsync(id);

        if (product != null)
        {
            // ⏱️ نحدد الـ Expiration ونحفظ في Redis كـ JSON
            var options = new DistributedCacheEntryOptions
            {
                AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10)
            };
            await _distributedCache.SetStringAsync(
                cacheKey,
                JsonSerializer.Serialize(product),
                options
            );
        }

        return product;
    }
}
🧠

الـ Serialization Overhead: الـ Distributed Cache بيتعامل مع الداتا كـ byte[] أو string (JSON). ده معناه كل عملية قراءة أو كتابة بيحصل معاها CPU Overhead بسبب الـ Serialization والـ Deserialization، على عكس الـ In-Memory اللي بيخزن الـ C# Objects زي ما هي في الـ Managed Heap.

٤ In-Memory vs Distributed — المقارنة الكاملة

وجه المقارنة 🔵 In-Memory Cache 🔴 Distributed Cache (Redis)
السرعة ⚡ الأسرع — في نفس الـ Process — نانوثانية ⚠️ أبطأ قليلاً بسبب Network Call
التكلفة ✅ مجاني — مفيش سيرفر إضافي 💰 محتاج سيرفر Redis منفصل
لو السيرفر عمل Restart ❌ الداتا بتتمسح — تروح للـ DB من أول ✅ الداتا محفوظة في Redis مش بتروح
مع Scale-out (كذا سيرفر) ❌ Data Inconsistency — كل سيرفر كاشه مختلف ✅ داتا موحدة — كلهم بيكلموا نفس Redis
Serialization ✅ مفيش — بيخزن Objects مباشرة ⚠️ لازم تحول لـ JSON/bytes
الاستخدام الأمثل سيرفر واحد — مشاريع صغيرة — داتا مش حرجة Microservices — كذا Instance — داتا مهمة ومستقرة
الخلاصة: الـ In-Memory أسرع وأبسط، بس بتاخد بيها لو عندك سيرفر واحد والداتا مش حرجة لو ضاعت. الـ Distributed (Redis) أبطأ بحاجة بسيطة، بس أساسي لو عندك Microservices أو كذا Instance للـ App وعاوز داتا مستقرة وموحدة.

٥ Hybrid Cache — أفضل العالمين في .NET 9

في .NET 9 نزلوا حاجة عظيمة اسمها Hybrid Cache. من اسمها كده 'هجين'، بتجمع بين مميزات الاتنين! بتعمل طابقين للتخزين (Two-Level Caching).

// diagram — Two-Level Caching Flow في الـ HybridCache
Request من المستخدم L1: In-Memory RAM السيرفر ⚡ أسرع مستوى Cache Hit? → Return Miss L2: Redis Distributed Cache 🔴 مخزن مركزي Cache Hit? → Return Miss Database المصدر الأصلي 🗄️ بس لو ماعندكش وبيملي L1 و L2 🔒 Cache Stampede Prevention: لو 1000 Request جوا مرة واحدة — Request واحد بس يروح للـ DB والباقي يستنوا Internal Locking — No Dogpile
L1

In-Memory (Level 1)

أول حاجة بيبص فيها — الـ RAM بتاعة السيرفر. لو لقى الداتا هنا رجعها في لمح البصر. ده أسرع مستوى ومحتاج مفيش Network Call.

L2

Redis (Level 2)

لو مالقاش في الـ L1، بيروح يدور في الـ Redis. لو لقى، بيرجعها وبيملي بيها الـ L1 برضو للمرة الجاية.

DB

Database (Last Resort)

لو مالقاش في L1 ولا L2، بيروح للـ DB — بيجيب الداتا، وبيملي L2 (Redis) وL1 (RAM) عشان المرة الجاية تبقى عنده.

🔒

Cache Stampede Prevention

لو الكاش خلص وجاء 1000 طلب في نفس اللحظة، الـ HybridCache ذكي — بيعمل Locking داخلي يخلي Request واحد بس يروح للـ DB والـ 999 التانيين يستنوا نتيجته.

مثال عملي — HybridCache في .NET 9
1
تثبيت الـ Package وتسجيل الـ Services Terminal + Program.cs
# تثبيت الـ Package
dotnet add package Microsoft.Extensions.Caching.Hybrid

// Program.cs — لازم تسجل الاتنين
builder.Services.AddStackExchangeRedisCache(options =>
    options.Configuration = "localhost:6379"); // Redis كـ L2

// 👇 ده اللي بيفعّل الـ HybridCache
builder.Services.AddHybridCache();
2
استخدام HybridCache — أبسط API ممكنة ProductService.cs
public class ProductService
{
    private readonly HybridCache _hybridCache;
    private readonly AppDbContext  _db;

    public async Task<Product> GetProductAsync(int id)
    {
        string cacheKey = $"product-{id}";

        // 🪄 سطر واحد بيعمل كل حاجة:
        // 1. يشوف في L1 (RAM)
        // 2. لو مالقاش يشوف في L2 (Redis)
        // 3. لو مالقاش يروح للـ DB ويملي الاتنين
        return await _hybridCache.GetOrCreateAsync<Product>(
            cacheKey,
            async cancelToken => await _db.Products.FindAsync(id),
            new HybridCacheEntryOptions
            {
                Expiration         = TimeSpan.FromMinutes(5),  // L2 (Redis)
                LocalCacheExpiration = TimeSpan.FromMinutes(1) // L1 (RAM) أقصر
            }
        );
    }
}

٦ Cache Invalidation — أصعب حاجة في البرمجة!

"There are only two hard things in Computer Science: cache invalidation and naming things."
— Phil Karlton

تخيل كاشت داتا، والداتا دي اتعدلت في الـ DB.. كده الـ Cache شايل داتا قديمة (Stale Data). بنحل ده إزاي؟
// diagram — طرق الـ Cache Invalidation
⏱️ TTL (Time To Live) Absolute وقت ثابت — بعد 5 دقايق الكاش يموت حتماً Sliding الوقت بيتجدد لو اتطلب جوا الـ Window الداتا بتتحدث لوحدها بعد الوقت المحدد ✅ سهل — ❌ ممكن يشيل stale data لفترة 🗑️ On-Demand Eviction UPDATE/DELETE في الـ DB امسح الكاش فوراً بالكود الـ Request الجاي هيجيب الجديد تلقائياً ✅ فوري — ✅ دقيق — ⚠️ محتاج كود أكتر
مثال عملي — TTL و On-Demand Eviction
TTL
تحديد Absolute و Sliding Expiration مع بعض ProductService.cs
// الـ Best Practice — تجمع الاتنين مع بعض
var cacheOptions = new MemoryCacheEntryOptions
{
    // لو اتطلب جوا دقيقتين، الوقت بيتجدد
    SlidingExpiration = TimeSpan.FromMinutes(2),

    // بس هيموت حتماً بعد 10 دقايق مهما اتطلب!
    // ده بيمنع الداتا من الإستمرار لأبد الآبدين
    AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10)
};

_memoryCache.Set(cacheKey, product, cacheOptions);
🗑️
On-Demand Eviction — مسح الكاش لما الداتا تتغير ProductService.cs
// لما المنتج يتعدل — امسح الكاش بتاعه فوراً
public async Task UpdateProductAsync(Product product)
{
    // 1. عدّل في الـ DB
    _db.Products.Update(product);
    await _db.SaveChangesAsync();

    // 2. امسح من الـ Cache فوراً — نفس الـ Key اللي استخدمناه
    string cacheKey = $"product-{product.Id}";
    _memoryCache.Remove(cacheKey);
    // أو في IDistributedCache:
    // await _distributedCache.RemoveAsync(cacheKey);

    // 3. الـ Request الجاي هيلاقي Cache Miss ويجيب الجديد من الـ DB
}

٧ Response Cache vs Output Cache — كاش الـ HTTP Response

كل اللي فات كنا بنتكلم عن كاش الداتا (Objects). طب لو عاوزين نكاش الـ Web Response نفسه (الـ JSON اللي راجع للعميل)؟ هنا بيجي نوعين مختلفين.

// diagram — Response Cache (Client-Side) vs Output Cache (Server-Side)
⚡ Response Caching — Client-Side Browser Server Cache-Control: max-age=120 Request 2 من الكاش! ✅ Request مابيوصلش للسيرفر أصلاً ❌ السيرفر ملوش تحكم بعد ما تطلع 🚀 Output Caching — Server-Side Browser Server Output Cache هنا! Cache Store على السيرفر ✅ السيرفر عنده Full Control ✅ Eviction فورية بـ Tags ✅ مش محتاج تبعته للـ Client
Response Caching — HTTP Headers

بتعتمد على الـ HTTP Headers (زي Cache-Control). هنا الـ Backend بيقول للمتصفح: "يا متصفح، الـ Response ده سيفه عندك وماتطلبوش مني تاني لمدة دقيقتين".

⚡
Response Caching — بالـ Attribute ReportsController.cs
// Program.cs — تسجيل الـ Middleware
app.UseResponseCaching();

// في الـ Endpoint
[HttpGet("report")]
// Duration = 120 ثانية = دقيقتين
// VaryByHeader — يعمل كاش مختلف لو المتصفح مختلف
[ResponseCache(
    Duration     = 120,
    Location     = ResponseCacheLocation.Any,
    VaryByHeader = "User-Agent"
)]
public IActionResult GetReport()
{
    return Ok(new { Data = "Big Financial Report..." });
}
⚠️

عيب كبير: السيرفر ملوش أي تحكم عليها بعد ما تطلع. لو الداتا اتغيرت على السيرفر، العميل هيفضل يشوف القديم لحد ما الوقت يخلص. مفيش طريقة تقوله "اعمل Refresh دلوقتي!"

Output Caching — التحكم الكامل على السيرفر (.NET 7+)

بتكاش الـ Response بالكامل بس على السيرفر عندك مش عند العميل! ميزتها المرعبة إن السيرفر ليه تحكم كامل — تقدر تعمل Tagging: تعلم كذا Response بتاج معين، وأول ما الداتا تتغير تمسحهم كلهم في ثانية واحدة!

1
تسجيل الـ Output Caching Program.cs
// Program.cs
builder.Services.AddOutputCache();

var app = builder.Build();
app.UseOutputCache(); // لازم قبل الـ Routing
2
كاش الـ Endpoint بتاج ProductsController.cs
// Endpoint اللي هنكاش Response بتاعه
[HttpGet("all-products")]
[OutputCache(Duration = 600)] // كاش لمدة 10 دقايق
public IActionResult GetProducts(HttpContext context)
{
    // 🏷️ بنعلم الكاش ده بتاج — ده اللي بيخلينا نمسحه بعدين
    context.Features
           .Get<IOutputCacheFeature>()?
           .Context.Tags
           .Add("products-tag");

    return Ok(_db.Products.ToList());
}
3
Eviction لحظي بالتاج لما الداتا تتغير ProductsController.cs
// Endpoint الإضافة — بعد ما نضيف منتج نمسح الكاش القديم
[HttpPost("add-product")]
public async Task<IActionResult> AddProduct(
    Product product,
    IOutputCacheStore cacheStore)
{
    _db.Products.Add(product);
    await _db.SaveChangesAsync();

    // 🗑️ امسح كل الـ Output Caches اللي عليها "products-tag"
    // في ثانية واحدة — العميل هيشوف المنتج الجديد فوراً
    await cacheStore.EvictByTagAsync("products-tag", default);

    return Ok();
}
المقارنة الكاملة — كل أنواع الكاش في جدول واحد
النوع مكان التخزين الاستخدام الأمثل التحكم في Invalidation الـ Overhead
IMemoryCache RAM السيرفر سيرفر واحد — بيانات صغيرة ✅ TTL + Manual Remove لا شيء — أسرع
IDistributedCache Redis (خارجي) كذا سيرفر — Microservices ✅ TTL + Manual Remove Serialization + Network
HybridCache RAM + Redis (L1+L2) الأفضل في الحالتين ✅ TTL + Tags L2 فقط لو Cache Miss
Response Cache Browser / Proxy بيانات ثابتة لا تتغير ❌ مفيش — TTL فقط مفيش — مابيجيش للسيرفر
Output Cache Server Memory Responses تحتاج تحكم ✅ Tags + Instant Eviction شوية Memory على السيرفر
الخلاصة النهائية: مفيش نوع واحد يناسب كل المواقف. الـ In-Memory أسرع وأبسط لو عندك سيرفر واحد. الـ Redis أساسي لو اتسعت. الـ HybridCache في .NET 9 هو المستقبل لأنه بياخد أحسن ما في الاتنين. الـ Output Cache أقوى من Response Cache لأنك تقدر تتحكم فيه لحظياً بالـ Tags. اختار الأداة الصح للمشكلة الصح.