Not içeriği yükleniyor...
// Markdown dosyası okunuyor
// İçerik işleniyor
// Syntax highlighting hazırlanıyor
// Markdown dosyası okunuyor
// İçerik işleniyor
// Syntax highlighting hazırlanıyor
C# ile In-Memory ve Redis Distributed Caching implementasyonu. Monolit mimari, Onion Architecture ve Repository Pattern ile cache entegrasyonu, best practices ve performance optimization.
Konu | Açıklama |
---|---|
In-Memory Caching | IMemoryCache ile temel cache implementasyonu |
Redis Distributed Caching | IDistributedCache ile Redis entegrasyonu |
Monolit Mimari Kurulumu | Tek uygulama içinde caching yapılandırması |
Shared Klasör Yapısı | Ortak kullanım için cache servislerinin organize edilmesi |
Onion Architecture | Temiz mimari prensipleri ile cache implementasyonu |
Repository Design Pattern | Repository katmanında caching entegrasyonu |
Best Practices | Performans, güvenlik ve bakım açısından en iyi yaklaşımlar |
Redis Distributed Caching için gerekli paketler:typescript<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
typescript<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="8.0.0" /> <PackageReference Include="StackExchange.Redis" Version="2.7.20" />
bashusing Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Distributed; using StackExchange.Redis; var builder = WebApplication.CreateBuilder(args); // In-Memory Cache Konfigürasyonu - TÜM OPTIONS builder.Services.AddMemoryCache(options => { // TEMEL BOYUT YÖNETİMİ options.SizeLimit = 1024; // Cache boyut sınırı (entry sayısı) options.CompactionPercentage = 0.25; // Temizlik oranı (%25) options.ExpirationScanFrequency = TimeSpan.FromMinutes(5); // Temizlik sıklığı // İSTATİSTİK VE İZLEME options.TrackStatistics = true; // Cache istatistiklerini topla (.NET 8.0+) options.TrackLinkedCacheEntries = true; // Linked entry'leri takip et (.NET 8.0+) }); // Redis Distributed Cache Konfigürasyonu - TÜM OPTIONS builder.Services.AddStackExchangeRedisCache(options => { // TEMEL BAĞLANTI YÖNETİMİ options.Configuration = builder.Configuration.GetConnectionString("Redis"); options.InstanceName = "MyApp"; // Redis instance adı (key prefix) // GELİŞMİŞ KONFIGÜRASYON options.ConfigurationOptions = new ConfigurationOptions { EndPoints = { "localhost:6379", "localhost:6380" }, // Cluster endpoints Password = "mypassword", // Authentication Database = 0, // Redis database numarası ConnectTimeout = 5000, // Bağlantı timeout (ms) SyncTimeout = 5000, // Sync operation timeout (ms) AsyncTimeout = 5000, // Async operation timeout (ms) ConnectRetry = 3, // Bağlantı retry sayısı AbortOnConnectFail = false, // Bağlantı hatada abort etme AllowAdmin = false, // Admin komutlarına izin Ssl = false, // SSL kullanımı SslHost = null, // SSL host adı ClientName = "MyApp-Cache", // Client identifier CommandMap = CommandMap.Default, // Komut mapping DefaultDatabase = 0, // Varsayılan database KeepAlive = 60, // Keep-alive interval (saniye) ReconnectRetryPolicy = new ExponentialRetry(1000), // Reconnect stratejisi ResolveDns = true, // DNS çözümleme ServiceName = null, // Sentinel service name TieBreaker = "__Booksleeve_TieBreaker", // Tie-breaker key Version = RedisVersion.Default // Redis version }; // FACTORY YÖNETİMİ options.ConnectionMultiplexerFactory = async () => { var config = ConfigurationOptions.Parse("localhost:6379"); config.ReconnectRetryPolicy = new ExponentialRetry(1000); return await ConnectionMultiplexer.ConnectAsync(config); }; // PROFILING VE İZLEME options.ProfilingSession = () => new ProfilingSession(); }); var app = builder.Build();
MemoryCache
diyoruz. Bu sistem, RAM üzerinde çalışır. Çok hızlıdır – çünkü bellekte durur. Ama geçicidir – uygulama kapanırsa silinir. Ve en önemlisi: sınırlı bir kapasitesi vardır.SizeLimit
– Dolabın Kapasitesini Belirlemek
MemoryCache
için de bir limit tanımlarız:bashvar cache = new MemoryCache(new MemoryCacheOptions { SizeLimit = 100 // Dolabın kapasitesi: 100 birimlik yer });
SetSize
– Bu Eşya Kaç Yer Kaplıyor?SetSize
denir. Her cache verisi yani her bir “entry” için boyut belirlemek şarttır:bashmemoryCache.Set("products", productList, new MemoryCacheEntryOptions { Size = 5 // Bu veri büyük, 5 birim yer kaplasın });
Kavram | Açıklama |
---|---|
MemoryCache | Dolap (RAM üzerinde çalışan hızlı geçici saklama alanı) |
SizeLimit | Dolabın toplam kapasitesi (Kaç birimlik eşya alabileceği) |
SetSize | Her bir eşyanın (verinin) kaç birim yer kapladığı |
SizeLimit = 10
Şimdi dolaba veri (eşya) ekliyoruz:Her şey güzel gidiyor. Dolap tamamen doldu. Ama sonra bir eşya daha eklemeye çalışıyoruz:bashcache.Set("kavanoz1", data1, new MemoryCacheEntryOptions { Size = 3 }); cache.Set("kavanoz2", data2, new MemoryCacheEntryOptions { Size = 4 }); cache.Set("kavanoz3", data3, new MemoryCacheEntryOptions { Size = 3 }); // Toplam: 3 + 4 + 3 = 10 birim ✅
bash// Toplam: 3 + 4 + 3 = 10 birim Doldurmuştuk. cache.Set("kavanoz4", data4, new MemoryCacheEntryOptions { Size = 2 }); // Toplam 12 olacak ❌
MemoryCache
, en eski veya az kullanılan verileri otomatik olarak çıkarır ve yerine yenisini koyar.
Bu sürece Eviction (taşma ve çıkarma) denir.Eviction
Nedir?bash// 1. Sınırsız Cache (DİKKATLİ KULLAN!) options.SizeLimit = null; // ❌ NEDEN TEHLİKELİ: Memory dolana kadar büyür, uygulama çöker // ✅ NE ZAMAN KULLAN: Sadece çok küçük veriler için, test ortamında // 2. Küçük Uygulamalar (100 entry) options.SizeLimit = 100; // ✅ NE İÇİN: Blog siteleri, kişisel projeler // 📊 MEMORY: ~1-2 MB kullanım // ⚡ PERFORMANS: Çok hızlı, sürekli temizlik // 3. Orta Ölçekli Uygulamalar (1024 entry) options.SizeLimit = 1024; // ✅ NE İÇİN: E-ticaret siteleri, kurumsal web uygulamaları // 📊 MEMORY: ~10-20 MB kullanım // ⚡ PERFORMANS: Dengeli, makul temizlik sıklığı // 4. Büyük Uygulamalar (10000 entry) options.SizeLimit = 10000; // ✅ NE İÇİN: Sosyal medya, büyük e-ticaret platformları // 📊 MEMORY: ~100-200 MB kullanım // ⚡ PERFORMANS: Yavaş temizlik, yüksek hit rate // 5. Enterprise Uygulamalar (50000 entry) options.SizeLimit = 50000; // ✅ NE İÇİN: Netflix, Amazon gibi mega platformlar // 📊 MEMORY: ~500MB-1GB kullanım // ⚡ PERFORMANS: Çok yavaş temizlik, maksimum cache hit
Pratik Örnekler:bash// Cache konfigürasyonu builder.Services.AddMemoryCache(options => { options.SizeLimit = 10000; // Toplam 10.000 birim }); // Farklı boyutlarda entry'ler ekleme cache.Set("small-data", smallData, new MemoryCacheEntryOptions { Size = 1 // 1 birim }); cache.Set("medium-data", mediumData, new MemoryCacheEntryOptions { Size = 100 // 100 birim }); cache.Set("large-data", largeData, new MemoryCacheEntryOptions { Size = 1000 // 1000 birim }); // Toplam: 1 + 100 + 1000 = 1101 birim (limit 10.000)
SetSize Olmadan vs SetSize İle:bash// 1. Küçük Veri (1KB - 1 birim) cache.Set("user-settings", userSettings, new MemoryCacheEntryOptions { Size = 1, AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1) }); // 2. Orta Boyut Veri (100KB - 100 birim) cache.Set("product-list", productList, new MemoryCacheEntryOptions { Size = 100, AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30) }); // 3. Büyük Veri (1MB - 1000 birim) cache.Set("large-report", reportData, new MemoryCacheEntryOptions { Size = 1000, AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10) }); // 4. Çok Büyük Veri (10MB - 10000 birim) cache.Set("database-backup", backupData, new MemoryCacheEntryOptions { Size = 10000, AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5) });
Memory Yönetimi Senaryoları:bash// ❌ SetSize olmadan (adil değil) cache.Set("tiny-data", tinyData); // 1 birim (gerçekte 1KB) cache.Set("huge-data", hugeData); // 1 birim (gerçekte 10MB) // Her ikisi de aynı değer, ama memory kullanımları çok farklı! // ✅ SetSize ile (adil) cache.Set("tiny-data", tinyData, new MemoryCacheEntryOptions { Size = 1 }); cache.Set("huge-data", hugeData, new MemoryCacheEntryOptions { Size = 10000 }); // Gerçek memory kullanımına göre sayım
bash// Senaryo 1: SizeLimit = 1000, SetSize kullanılmıyor // 1000 entry eklenebilir (hepsi 1 birim) // Ama memory kullanımı çok farklı olabilir // Senaryo 2: SizeLimit = 1000, SetSize kullanılıyor // Toplam 1000 birim eklenebilir // 1 büyük entry (1000 birim) VEYA // 1000 küçük entry (her biri 1 birim) VEYA // Karışık boyutlarda entry'ler // Senaryo 3: Gerçekçi örnek cache.Set("user-123", userData, new MemoryCacheEntryOptions { Size = 50 }); cache.Set("products", productList, new MemoryCacheEntryOptions { Size = 200 }); cache.Set("categories", categoryList, new MemoryCacheEntryOptions { Size = 100 }); cache.Set("orders", orderList, new MemoryCacheEntryOptions { Size = 500 }); // Toplam: 850 birim (limit 1000'den altında)
bash// 1. Conservative Temizlik (%10) options.CompactionPercentage = 0.1; // ✅ NE İÇİN: Cache hit rate'i yüksek olması gereken uygulamalar // 📊 ETKİ: 1000 entry varsa, sadece 100 tanesi silinir // ⚡ AVANTAJ: Cache'de daha fazla veri kalır // ❌ DEZAVANTAJ: Sık temizlik yapılır // 2. Balanced Temizlik (%25) - EN YAYGIN options.CompactionPercentage = 0.25; // ✅ NE İÇİN: Çoğu production uygulaması // 📊 ETKİ: 1000 entry varsa, 250 tanesi silinir // ⚡ AVANTAJ: Dengeli performans // 📈 ÖNERİ: Başlangıç için bu değeri kullanın // 3. Aggressive Temizlik (%50) options.CompactionPercentage = 0.5; // ✅ NE İÇİN: Memory sıkıntısı olan sistemler // 📊 ETKİ: 1000 entry varsa, 500 tanesi silinir // ⚡ AVANTAJ: Hızlı memory temizliği // ❌ DEZAVANTAJ: Cache hit rate düşer // 4. Very Aggressive Temizlik (%75) options.CompactionPercentage = 0.75; // ✅ NE İÇİN: Çok sınırlı memory'li sistemler // 📊 ETKİ: 1000 entry varsa, 750 tanesi silinir // ⚡ AVANTAJ: Çok hızlı memory temizliği // ❌ DEZAVANTAJ: Cache neredeyse boş kalır
bash// 1. Çok Hızlı Tarama (30 saniye) options.ExpirationScanFrequency = TimeSpan.FromSeconds(30); // ✅ NE İÇİN: Test ortamları, development // 📊 CPU KULLANIMI: Yüksek (her 30 saniyede bir tarama) // ⚡ AVANTAJ: Expired entry'ler hemen silinir // ❌ DEZAVANTAJ: CPU kaynağı tüketir // 2. Hızlı Tarama (1 dakika) options.ExpirationScanFrequency = TimeSpan.FromMinutes(1); // ✅ NE İÇİN: Development, küçük production sistemleri // 📊 CPU KULLANIMI: Orta-yüksek // ⚡ AVANTAJ: Hızlı cleanup // 📈 ÖNERİ: Development için ideal // 3. Normal Tarama (5 dakika) - EN YAYGIN options.ExpirationScanFrequency = TimeSpan.FromMinutes(5); // ✅ NE İÇİN: Çoğu production uygulaması // 📊 CPU KULLANIMI: Düşük // ⚡ AVANTAJ: Dengeli CPU kullanımı // 📈 ÖNERİ: Production için ideal // 4. Yavaş Tarama (15 dakika) options.ExpirationScanFrequency = TimeSpan.FromMinutes(15); // ✅ NE İÇİN: Yüksek performans gerektiren sistemler // 📊 CPU KULLANIMI: Çok düşük // ⚡ AVANTAJ: Minimum CPU kullanımı // ❌ DEZAVANTAJ: Expired entry'ler uzun süre memory'de kalır // 5. Çok Yavaş Tarama (30 dakika) options.ExpirationScanFrequency = TimeSpan.FromMinutes(30); // ✅ NE İÇİN: CPU kritik sistemler // 📊 CPU KULLANIMI: Minimal // ⚡ AVANTAJ: Maksimum CPU tasarrufu // ❌ DEZAVANTAJ: Memory leak riski // 6. Taramayı Devre Dışı Bırak options.ExpirationScanFrequency = TimeSpan.Zero; // ❌ NEDEN TEHLİKELİ: Expired entry'ler hiç silinmez // ✅ NE ZAMAN: Sadece özel durumlar için
bash// 1. İstatistikleri Aktif Et options.TrackStatistics = true; // ✅ NE İÇİN: Production sistemleri, monitoring gerektiren uygulamalar // 📊 VERİLER: Hit count, miss count, hit ratio // ⚡ AVANTAJ: Performance analizi yapabilirsiniz // 📈 ÖNERİ: Production'da mutlaka aktif edin // 2. İstatistikleri Devre Dışı Bırak options.TrackStatistics = false; // ✅ NE İÇİN: Çok küçük uygulamalar, test ortamları // 📊 VERİLER: Hiçbir istatistik toplanmaz // ⚡ AVANTAJ: Hafif memory footprint // ❌ DEZAVANTAJ: Performance analizi yapamazsınız // KULLANIM ÖRNEĞİ: var stats = memoryCache.GetCurrentStatistics(); if (stats != null) { Console.WriteLine($"Cache Hit Count: {stats.TotalHits}"); Console.WriteLine($"Cache Miss Count: {stats.TotalMisses}"); double hitRatio = (double)stats.TotalHits / (stats.TotalHits + stats.TotalMisses); Console.WriteLine($"Cache Hit Ratio: {hitRatio:P2}"); // Hit ratio %80'in altındaysa cache stratejinizi gözden geçirin if (hitRatio < 0.8) { Console.WriteLine("⚠️ Cache hit ratio düşük! Stratejinizi optimize edin."); } }
bash// 1. Linked Entry Tracking Aktif options.TrackLinkedCacheEntries = true; // ✅ NE İÇİN: Karmaşık cache stratejileri, dependency management // 📊 ÖRNEK: User silindiğinde user'ın tüm cache'leri de silinir // ⚡ AVANTAJ: Otomatik cleanup, tutarlılık // ❌ DEZAVANTAJ: Hafif performance overhead // 2. Linked Entry Tracking Devre Dışı options.TrackLinkedCacheEntries = false; // ✅ NE İÇİN: Basit cache stratejileri, performance kritik sistemler // 📊 ÖRNEK: Her entry bağımsız olarak yönetilir // ⚡ AVANTAJ: Maksimum performance // ❌ DEZAVANTAJ: Manuel cleanup gerekir // KULLANIM ÖRNEĞİ: // User ve user'ın order'ları arasında link kurma var userKey = "user:123"; var userOrdersKey = "user:123:orders"; // User'ı cache'le memoryCache.Set(userKey, user, new MemoryCacheEntryOptions { Size = 1, Priority = CacheItemPriority.Normal }); // User'ın order'larını cache'le ve user'a link'le memoryCache.Set(userOrdersKey, orders, new MemoryCacheEntryOptions { Size = 1, Priority = CacheItemPriority.Normal, // User silindiğinde orders da silinir PostEvictionCallbacks = { new PostEvictionCallbackRegistration { EvictionCallback = (key, value, reason, state) => { if (key.ToString() == userKey) { memoryCache.Remove(userOrdersKey); } } }} });
host:port[,options]
localhost:6379
→ Local Redis'e bağlanırredis.company.com:6380,ssl=true,password=mykey
→ Güvenli remote bağlantıredis1:6379,redis2:6379,redis3:6379
→ Cluster'a bağlanırbash// 1. Basit Local Bağlantı options.Configuration = "localhost:6379"; // ✅ NE İÇİN: Development, test ortamları // 📊 ÖZELLİKLER: Şifresiz, SSL yok, default port // ⚡ AVANTAJ: En basit kurulum // ❌ DEZAVANTAJ: Güvenlik yok, sadece local // 2. Şifreli Local Bağlantı options.Configuration = "localhost:6379,password=mypassword"; // ✅ NE İÇİN: Development, küçük production sistemleri // 📊 ÖZELLİKLER: Authentication var, SSL yok // ⚡ AVANTAJ: Basit güvenlik // 🔐 GÜVENLİK: Şifre connection string'de (environment variable kullanın!) // 3. SSL Remote Bağlantı options.Configuration = "redis.company.com:6380,ssl=true,password=mykey"; // ✅ NE İÇİN: Production sistemleri, cloud Redis // 📊 ÖZELLİKLER: SSL/TLS encryption, authentication // ⚡ AVANTAJ: Güvenli bağlantı // 📈 ÖNERİ: Production için ideal // 4. Redis Cluster Bağlantı options.Configuration = "redis1:6379,redis2:6379,redis3:6379"; // ✅ NE İÇİN: Yüksek availability gerektiren sistemler // 📊 ÖZELLİKLER: Multiple endpoints, failover support // ⚡ AVANTAJ: High availability // ❌ DEZAVANTAJ: Karmaşık setup // 5. Azure Redis Cache options.Configuration = "myredis.redis.cache.windows.net:6380,ssl=true,password=mykey"; // ✅ NE İÇİN: Azure cloud uygulamaları // 📊 ÖZELLİKLER: Managed Redis, SSL, authentication // ⚡ AVANTAJ: Managed service, otomatik backup // 💰 MALİYET: Azure pricing // 6. AWS ElastiCache options.Configuration = "mycluster.xxxxx.cache.amazonaws.com:6379,ssl=true"; // ✅ NE İÇİN: AWS cloud uygulamaları // 📊 ÖZELLİKLER: AWS managed Redis, SSL // ⚡ AVANTAJ: AWS integration, auto-scaling // 💰 MALİYET: AWS pricing // 7. Environment Variable ile Güvenli Bağlantı options.Configuration = Environment.GetEnvironmentVariable("REDIS_CONNECTION_STRING"); // ✅ NE İÇİN: Production sistemleri // 📊 ÖZELLİKLER: Güvenli credential management // ⚡ AVANTAJ: Şifreler kodda yok // 📈 ÖNERİ: En güvenli yöntem
"user:123"
key'i kullanır"MyApp:user:123"
olurMyApp:user:123
→ Web uygulamasının user cache'iMyApp-API:user:123
→ API uygulamasının user cache'iMyApp-BG:user:123
→ Background service'in user cache'ibash// 1. Production Prefix options.InstanceName = "MyApp"; // ✅ NE İÇİN: Production ortamları // 📊 ÖRNEK: user:123 → MyApp:user:123 // ⚡ AVANTAJ: Temiz ve anlaşılır // 📈 ÖNERİ: Production için ideal // 2. Development Prefix options.InstanceName = "MyApp-Dev"; // ✅ NE İÇİN: Development ortamları // 📊 ÖRNEK: user:123 → MyApp-Dev:user:123 // ⚡ AVANTAJ: Dev/Prod ayrımı // 🔍 DEBUGGING: Redis'te hangi key'in dev'e ait olduğu belli // 3. Test Prefix options.InstanceName = "MyApp-Test"; // ✅ NE İÇİN: Test ortamları // 📊 ÖRNEK: user:123 → MyApp-Test:user:123 // ⚡ AVANTAJ: Test verileri ayrı // 🧪 TESTING: Test verileri production'ı etkilemez // 4. Staging Prefix options.InstanceName = "MyApp-Staging"; // ✅ NE İÇİN: Staging ortamları // 📊 ÖRNEK: user:123 → MyApp-Staging:user:123 // ⚡ AVANTAJ: Pre-production testing // 🔄 DEPLOYMENT: Production'a geçmeden önce test // 5. Machine Specific Prefix options.InstanceName = $"MyApp-{Environment.MachineName}"; // ✅ NE İÇİN: Multi-server deployment // 📊 ÖRNEK: user:123 → MyApp-SERVER01:user:123 // ⚡ AVANTAJ: Hangi server'ın hangi key'i kullandığı belli // 🔍 DEBUGGING: Server-specific troubleshooting // 6. Environment Variable ile Dynamic Prefix options.InstanceName = Environment.GetEnvironmentVariable("REDIS_INSTANCE_NAME") ?? "MyApp"; // ✅ NE İÇİN: Flexible deployment // 📊 ÖRNEK: Environment variable ile prefix değiştirilebilir // ⚡ AVANTAJ: Deployment-time configuration // 📈 ÖNERİ: CI/CD pipeline'ları için ideal // 7. Multi-tenant Prefix options.InstanceName = $"MyApp-{tenantId}"; // ✅ NE İÇİN: Multi-tenant uygulamalar // 📊 ÖRNEK: user:123 → MyApp-TenantA:user:123 // ⚡ AVANTAJ: Tenant isolation // 🔐 SECURITY: Tenant'lar birbirinin verilerini göremez // PRATİK ÖRNEK - Aynı Redis'te Farklı Uygulamalar: // Web API: MyApp-API:user:123 // Background Service: MyApp-BG:user:123 // Admin Panel: MyApp-Admin:user:123 // Hepsi aynı Redis'te ama birbirini etkilemez!
bashoptions.ConfigurationOptions = new ConfigurationOptions { // 🌐 BAĞLANTI YÖNETİMİ EndPoints = { "redis1.company.com:6379", "redis2.company.com:6379", "redis3.company.com:6380" }, // ✅ NE İÇİN: Redis cluster, high availability // 📊 ÖZELLİKLER: Multiple server support, failover // ⚡ AVANTAJ: Bir server down olsa diğerleri çalışır // ❌ DEZAVANJ: Karmaşık setup gerektirir Password = Environment.GetEnvironmentVariable("REDIS_PASSWORD"), // ✅ NE İÇİN: Production sistemleri // 📊 ÖZELLİKLER: Güvenli credential management // ⚡ AVANTAJ: Şifre kodda yok // 📈 ÖNERİ: Environment variable kullanın Database = 2, // Farklı database kullan // ✅ NE İÇİN: Veri organizasyonu // 📊 ÖZELLİKLER: 0-15 arası database numarası // ⚡ AVANTAJ: Farklı veri türleri ayrı database'lerde // 📈 ÖNERİ: Cache için 0, session için 1, analytics için 2 // ⏱️ TIMEOUT YÖNETİMİ ConnectTimeout = 10000, // 10 saniye bağlantı timeout // ✅ NE İÇİN: Yavaş network bağlantıları // 📊 ETKİ: İlk bağlantı kurma süresi // ⚡ AVANTAJ: Network sorunlarında sabırlı // ❌ DEZAVANJ: Yavaş bağlantı tespiti SyncTimeout = 8000, // 8 saniye sync timeout // ✅ NE İÇİN: Synchronous Redis operasyonları // 📊 ETKİ: GET, SET gibi sync komutların timeout'u // ⚡ AVANTAJ: Sync operasyonlar için optimize // 📈 ÖNERİ: Async operasyonlar tercih edin AsyncTimeout = 12000, // 12 saniye async timeout // ✅ NE İÇİN: Asynchronous Redis operasyonları // 📊 ETKİ: Async GET, SET komutlarının timeout'u // ⚡ AVANTAJ: Async operasyonlar için optimize // 📈 ÖNERİ: Modern .NET uygulamaları için ideal // 🔄 RETRY VE RECONNECTION ConnectRetry = 5, // 5 kez bağlantı dene // ✅ NE İÇİN: Network instability durumları // 📊 ETKİ: İlk bağlantı başarısız olursa 5 kez dener // ⚡ AVANTAJ: Geçici network sorunlarında dayanıklı // ❌ DEZAVANJ: Başarısız bağlantılar için daha uzun bekleme ReconnectRetryPolicy = new LinearRetry(1000), // Her 1 saniyede retry // ✅ NE İÇİN: Bağlantı koptuğunda yeniden bağlanma // 📊 ETKİ: Her 1 saniyede bir retry yapar // ⚡ AVANTAJ: Hızlı reconnection // 📈 ALTERNATİF: new ExponentialRetry(1000) - exponential backoff AbortOnConnectFail = false, // İlk bağlantı hatada devam et // ✅ NE İÇİN: High availability gerektiren sistemler // 📊 ETKİ: İlk bağlantı başarısız olsa bile devam eder // ⚡ AVANTAJ: Cluster'da diğer node'ları dener // ❌ DEZAVANJ: Başarısız bağlantılar için daha uzun bekleme // 🔐 GÜVENLİK Ssl = true, // SSL/TLS kullan // ✅ NE İÇİN: Production sistemleri, cloud Redis // 📊 ÖZELLİKLER: Encrypted connection // ⚡ AVANTAJ: Veri güvenliği // 📈 ÖNERİ: Production'da mutlaka aktif edin SslHost = "*.redis.company.com", // SSL sertifika host // ✅ NE İÇİN: Custom SSL sertifikaları // 📊 ÖZELLİKLER: Certificate validation // ⚡ AVANTAJ: Man-in-the-middle saldırılarına karşı koruma // 📈 ÖNERİ: Wildcard sertifikalar için kullanın AllowAdmin = false, // Admin komutlarını engelle // ✅ NE İÇİN: Production güvenliği // 📊 ETKİ: FLUSHDB, CONFIG gibi admin komutları engellenir // ⚡ AVANTAJ: Güvenlik riski azalır // 📈 ÖNERİ: Sadece gerekli durumlarda true yapın // ⚡ PERFORMANS KeepAlive = 180, // 3 dakika keep-alive // ✅ NE İÇİN: Uzun süreli bağlantılar // 📊 ETKİ: Her 3 dakikada bir keep-alive paketi gönderir // ⚡ AVANTAJ: Firewall timeout'larını önler // 📈 ÖNERİ: Network timeout'larına göre ayarlayın DefaultDatabase = 0, // Varsayılan database // ✅ NE İÇİN: Database seçimi // 📊 ETKİ: Bağlantı kurulduğunda otomatik bu database seçilir // ⚡ AVANTAJ: Her seferinde SELECT komutu çalıştırmaya gerek yok // 📈 ÖNERİ: Cache için 0, session için 1 kullanın ClientName = "MyApp-WebAPI", // Client identifier // ✅ NE İÇİN: Monitoring ve debugging // 📊 ETKİ: Redis'te CLIENT LIST komutunda görünür // ⚡ AVANTAJ: Hangi uygulamanın bağlı olduğu belli // 📈 ÖNERİ: Anlamlı isimler verin // 🏰 SENTINEL SUPPORT ServiceName = "mymaster", // Sentinel service adı // ✅ NE İÇİN: Redis Sentinel high availability // 📊 ETKİ: Sentinel cluster'da hangi master'ı takip edeceği // ⚡ AVANTAJ: Automatic failover // 📈 ÖNERİ: Sentinel setup'ları için gerekli TieBreaker = "tiebreaker", // Split-brain önleme // ✅ NE İÇİN: Network partition durumları // 📊 ETKİ: Hangi master'ın aktif olacağını belirler // ⚡ AVANTAJ: Data consistency // 📈 ÖNERİ: Sentinel ile birlikte kullanın // 🌐 NETWORK ResolveDns = true, // DNS çözümle // ✅ NE İÇİN: Dynamic DNS environments // 📊 ETKİ: DNS değişikliklerini otomatik takip eder // ⚡ AVANTAJ: IP değişikliklerinde otomatik adaptasyon // 📈 ÖNERİ: Cloud environments için ideal CheckCertificateRevocation = false // SSL sertifika iptal kontrolü // ✅ NE İÇİN: Performance optimization // 📊 ETKİ: SSL sertifika iptal listesi kontrol edilmez // ⚡ AVANTAJ: Daha hızlı SSL handshake // ❌ DEZAVANJ: İptal edilmiş sertifikalar tespit edilmez };
2. Production Environment:bashoptions.ConfigurationOptions = new ConfigurationOptions { EndPoints = { "localhost:6379" }, ConnectTimeout = 2000, // Hızlı bağlantı SyncTimeout = 2000, // Hızlı sync AsyncTimeout = 3000, // Hızlı async ConnectRetry = 2, // Az retry AbortOnConnectFail = true, // Hızlı hata tespiti Ssl = false, // SSL yok (local) KeepAlive = 60, // Kısa keep-alive ClientName = "MyApp-Dev" };
3. High Performance Environment:bashoptions.ConfigurationOptions = new ConfigurationOptions { EndPoints = { "redis1.prod.company.com:6379", "redis2.prod.company.com:6379" }, Password = Environment.GetEnvironmentVariable("REDIS_PASSWORD"), ConnectTimeout = 10000, // Sabırlı bağlantı SyncTimeout = 8000, // Orta timeout AsyncTimeout = 12000, // Uzun async timeout ConnectRetry = 5, // Çok retry ReconnectRetryPolicy = new ExponentialRetry(1000), AbortOnConnectFail = false, // Cluster support Ssl = true, // SSL gerekli SslHost = "*.prod.company.com", AllowAdmin = false, // Güvenlik KeepAlive = 300, // Uzun keep-alive ClientName = "MyApp-Prod" };
bashoptions.ConfigurationOptions = new ConfigurationOptions { EndPoints = { "redis-cluster.company.com:6379" }, Password = Environment.GetEnvironmentVariable("REDIS_PASSWORD"), ConnectTimeout = 1000, // Çok hızlı SyncTimeout = 500, // Çok hızlı AsyncTimeout = 1000, // Çok hızlı ConnectRetry = 3, // Orta retry ReconnectRetryPolicy = new LinearRetry(500), // Hızlı retry AbortOnConnectFail = false, // Cluster support Ssl = true, // SSL gerekli KeepAlive = 60, // Kısa keep-alive ClientName = "MyApp-HighPerf" };
ProfilingSession: Performance profilingbash// Özel bağlantı mantığı options.ConnectionMultiplexerFactory = async () => { var config = new ConfigurationOptions { EndPoints = { GetRedisEndpoint() }, // Dynamic endpoint Password = await GetRedisPasswordFromVault(), // Güvenli şifre alma ConnectTimeout = 5000, ConnectRetry = 3 }; // Custom retry logic var multiplexer = await ConnectionMultiplexer.ConnectAsync(config); // Connection event handlers multiplexer.ConnectionFailed += (sender, args) => { Console.WriteLine($"Redis connection failed: {args.Exception}"); }; multiplexer.ConnectionRestored += (sender, args) => { Console.WriteLine("Redis connection restored"); }; return multiplexer; };
bash// Redis komut profiling'i options.ProfilingSession = () => { return new ProfilingSession(); // Her request için yeni session }; // Kullanım: // var session = new ProfilingSession(); // ConnectionMultiplexer.SetProfiler(() => session); // // Redis operations... // var commands = session.FinishProfiling();
bash// Memory Cache - Development options.SizeLimit = 100; options.CompactionPercentage = 0.5; options.ExpirationScanFrequency = TimeSpan.FromMinutes(1); options.TrackStatistics = true; // Redis Cache - Development options.Configuration = "localhost:6379"; options.InstanceName = "MyApp-Dev"; options.ConfigurationOptions.ConnectTimeout = 2000; options.ConfigurationOptions.SyncTimeout = 2000;
bash// Memory Cache - Production options.SizeLimit = 5000; options.CompactionPercentage = 0.25; options.ExpirationScanFrequency = TimeSpan.FromMinutes(10); options.TrackStatistics = true; // Redis Cache - Production options.Configuration = "redis-cluster.company.com:6379,ssl=true"; options.InstanceName = "MyApp-Prod"; options.ConfigurationOptions.ConnectRetry = 5; options.ConfigurationOptions.ReconnectRetryPolicy = new ExponentialRetry(1000); options.ConfigurationOptions.KeepAlive = 300;
bash// Memory Cache - High Performance options.SizeLimit = 20000; options.CompactionPercentage = 0.1; options.ExpirationScanFrequency = TimeSpan.FromMinutes(30); options.TrackLinkedCacheEntries = false; // Performance için devre dışı // Redis Cache - High Performance options.ConfigurationOptions.AsyncTimeout = 1000; options.ConfigurationOptions.SyncTimeout = 1000; options.ConfigurationOptions.ConnectTimeout = 1000; options.ConfigurationOptions.KeepAlive = 60;
SizeLimit = 1024
: Cache'de en fazla 1024 adet cache entry tutulabilirSizeLimit = null
: Sınırsız cache (tehlikeli olabilir, OutOfMemoryException riski)SizeLimit = 500
: Küçük uygulamalar için uygun, daha sık compaction yapılır
CompactionPercentage (0.25) - Cache boyutu sınıra ulaştığında ne kadar temizlik yapılacağını belirler. %25 değeri, cache'in %25'inin silineceği anlamına gelir:CompactionPercentage = 0.25
: Cache dolduğunda %25'i temizlenir (256 entry silinir)CompactionPercentage = 0.50
: Cache dolduğunda %50'si temizlenir (daha agresif temizlik)CompactionPercentage = 0.10
: Cache dolduğunda %10'u temizlenir (daha az temizlik)
ExpirationScanFrequency (5 dakika) - Süresi dolmuş cache entry'lerin ne sıklıkla taranıp silineceğini belirler:TimeSpan.FromMinutes(5)
: Her 5 dakikada bir expired entry'ler taranırTimeSpan.FromMinutes(1)
: Daha sık tarama (daha fazla CPU kullanımı)TimeSpan.FromMinutes(15)
: Daha az tarama (memory'de expired entry'ler daha uzun kalır)Key Prefix Neden Önemli:text// InstanceName = "MyApp" olduğunda: Uygulama cache key'i: "user:123" Redis'teki gerçek key: "MyApp:user:123" // Başka bir uygulama InstanceName = "BlogApp" kullanırsa: Uygulama cache key'i: "user:123" Redis'teki gerçek key: "BlogApp:user:123" // Bu sayede iki uygulama aynı Redis'i kullanabilir
csharp// Basit bağlantı "Redis": "localhost:6379" // Authentication ile "Redis": "localhost:6379,password=mypassword" // Cluster setup "Redis": "redis1:6379,redis2:6379,redis3:6379" // SSL ile "Redis": "myredis.cache.windows.net:6380,ssl=true,password=mykey"
Production Environment:textSizeLimit = 256 (küçük memory footprint) CompactionPercentage = 0.5 (agresif temizlik) ExpirationScanFrequency = 1 dakika (hızlı cleanup)
High Traffic Applications:textSizeLimit = 2048 veya daha fazla (yeterli capacity) CompactionPercentage = 0.25 (balanced cleanup) ExpirationScanFrequency = 5 dakika (CPU friendly)
textSizeLimit = null (Redis'e güven, memory cache minimal) CompactionPercentage = 0.1 (minimal disruption) ExpirationScanFrequency = 10 dakika (performance priority)
json{ "ConnectionStrings": { "Redis": "localhost:6379" }, "CacheSettings": { "DefaultExpirationMinutes": 30, "UseDistributedCache": true, "RedisSettings": { "Host": "localhost", "Port": 6379, "Password": "", "Database": 0, "ConnectTimeout": 5000, "SyncTimeout": 5000 }, "MemoryCacheSettings": { "SizeLimit": 1024, "CompactionPercentage": 0.25, "ExpirationScanFrequencyMinutes": 5 } } }
textMyApp/ ├── Controllers/ ├── Services/ │ ├── Interfaces/ │ │ └── ICacheService.cs │ └── CacheService.cs ├── Models/ ├── Configuration/ │ └── CacheConfiguration.cs └── Extensions/ └── ServiceCollectionExtensions.cs
bash// Services/Interfaces/ICacheService.cs public interface ICacheService { Task<T?> GetAsync<T>(string key) where T : class; Task SetAsync<T>(string key, T value, TimeSpan? expiration = null) where T : class; Task RemoveAsync(string key); Task RemoveByPatternAsync(string pattern); Task<bool> ExistsAsync(string key); Task<IEnumerable<string>> GetKeysAsync(string pattern); Task ClearAllAsync(); }
bash// Services/CacheService.cs using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Distributed; using System.Text.Json; public class CacheService : ICacheService { private readonly IMemoryCache _memoryCache; private readonly IDistributedCache _distributedCache; private readonly IConfiguration _configuration; private readonly ILogger<CacheService> _logger; private readonly bool _useDistributedCache; private readonly TimeSpan _defaultExpiration; private readonly JsonSerializerOptions _jsonOptions; public CacheService( IMemoryCache memoryCache, IDistributedCache distributedCache, IConfiguration configuration, ILogger<CacheService> logger) { _memoryCache = memoryCache; _distributedCache = distributedCache; _configuration = configuration; _logger = logger; _useDistributedCache = _configuration.GetValue<bool>("CacheSettings:UseDistributedCache"); _defaultExpiration = TimeSpan.FromMinutes( _configuration.GetValue<int>("CacheSettings:DefaultExpirationMinutes", 30)); _jsonOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, WriteIndented = false }; } public async Task<T?> GetAsync<T>(string key) where T : class { ArgumentException.ThrowIfNullOrEmpty(key); try { // İlk olarak Memory Cache'den kontrol et (L1 Cache) if (_memoryCache.TryGetValue(key, out T? memoryValue)) { _logger.LogDebug("Cache hit from memory for key: {Key}", key); return memoryValue; } // Distributed cache kullanılıyorsa Redis'ten kontrol et (L2 Cache) if (_useDistributedCache) { var distributedValue = await _distributedCache.GetStringAsync(key); if (!string.IsNullOrEmpty(distributedValue)) { var deserializedValue = JsonSerializer.Deserialize<T>(distributedValue, _jsonOptions); // Bulunan değeri Memory Cache'e de ekle (cache warming) var memoryCacheOptions = new MemoryCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5), Size = 1, Priority = CacheItemPriority.Normal }; _memoryCache.Set(key, deserializedValue, memoryCacheOptions); _logger.LogDebug("Cache hit from distributed for key: {Key}", key); return deserializedValue; } } _logger.LogDebug("Cache miss for key: {Key}", key); return null; } catch (Exception ex) { _logger.LogError(ex, "Error getting cache for key: {Key}", key); return null; } } public async Task SetAsync<T>(string key, T value, TimeSpan? expiration = null) where T : class { ArgumentException.ThrowIfNullOrEmpty(key); ArgumentNullException.ThrowIfNull(value); try { var cacheExpiration = expiration ?? _defaultExpiration; // Distributed Cache'e ekle if (_useDistributedCache) { var serializedValue = JsonSerializer.Serialize(value, _jsonOptions); var distributedCacheOptions = new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = cacheExpiration }; await _distributedCache.SetStringAsync(key, serializedValue, distributedCacheOptions); } // Memory Cache'e de ekle (L1 Cache) var memoryCacheOptions = new MemoryCacheEntryOptions { AbsoluteExpirationRelativeToNow = cacheExpiration, Size = 1, Priority = CacheItemPriority.Normal }; _memoryCache.Set(key, value, memoryCacheOptions); _logger.LogDebug("Cache set for key: {Key}, expiration: {Expiration}", key, cacheExpiration); } catch (Exception ex) { _logger.LogError(ex, "Error setting cache for key: {Key}", key); throw; // Cache set operasyonu kritik olabilir, hata fırlatılır } } public async Task RemoveAsync(string key) { ArgumentException.ThrowIfNullOrEmpty(key); try { // Her iki cache'den de sil _memoryCache.Remove(key); if (_useDistributedCache) { await _distributedCache.RemoveAsync(key); } _logger.LogDebug("Cache removed for key: {Key}", key); } catch (Exception ex) { _logger.LogError(ex, "Error removing cache for key: {Key}", key); } } public async Task RemoveByPatternAsync(string pattern) { ArgumentException.ThrowIfNullOrEmpty(pattern); try { // Memory cache'de pattern bazlı silme mümkün değil // Redis için StackExchange.Redis ile özel implementation gerekli _logger.LogWarning("RemoveByPatternAsync partially implemented for pattern: {Pattern}", pattern); // Gelecek implementasyonlar için placeholder await Task.CompletedTask; } catch (Exception ex) { _logger.LogError(ex, "Error removing cache by pattern: {Pattern}", pattern); } } public async Task<bool> ExistsAsync(string key) { ArgumentException.ThrowIfNullOrEmpty(key); try { // Memory cache'de varlık kontrolü if (_memoryCache.TryGetValue(key, out _)) return true; // Distributed cache'de varlık kontrolü if (_useDistributedCache) { var value = await _distributedCache.GetStringAsync(key); return !string.IsNullOrEmpty(value); } return false; } catch (Exception ex) { _logger.LogError(ex, "Error checking cache existence for key: {Key}", key); return false; } } public async Task<IEnumerable<string>> GetKeysAsync(string pattern) { // Bu metod Redis'e özel implementation gerektirir _logger.LogWarning("GetKeysAsync not implemented for current setup"); await Task.CompletedTask; return Enumerable.Empty<string>(); } public async Task ClearAllAsync() { try { // Memory cache tamamen temizlenir if (_memoryCache is MemoryCache mc) { mc.Compact(1.0); // %100 compaction } // Redis için tüm database'i temizleme (dikkatli kullanılmalı) _logger.LogWarning("ClearAllAsync called - this will clear all cache data"); await Task.CompletedTask; } catch (Exception ex) { _logger.LogError(ex, "Error clearing all cache"); } } }
bash// Extensions/ServiceCollectionExtensions.cs public static class ServiceCollectionExtensions { public static IServiceCollection AddCachingServices( this IServiceCollection services, IConfiguration configuration) { // Cache ayarlarını configuration'dan al var cacheSettings = configuration.GetSection("CacheSettings"); var useDistributedCache = cacheSettings.GetValue<bool>("UseDistributedCache"); // Memory Cache her zaman eklenir (L1 Cache olarak) services.AddMemoryCache(options => { var memoryCacheSettings = cacheSettings.GetSection("MemoryCacheSettings"); options.SizeLimit = memoryCacheSettings.GetValue<int>("SizeLimit", 1024); options.CompactionPercentage = memoryCacheSettings.GetValue<double>("CompactionPercentage", 0.25); options.ExpirationScanFrequency = TimeSpan.FromMinutes( memoryCacheSettings.GetValue<int>("ExpirationScanFrequencyMinutes", 5)); }); // Distributed Cache conditionally eklenir if (useDistributedCache) { var redisSettings = cacheSettings.GetSection("RedisSettings"); services.AddStackExchangeRedisCache(options => { var connectionString = $"{redisSettings["Host"]}:{redisSettings["Port"]}"; if (!string.IsNullOrEmpty(redisSettings["Password"])) { connectionString += $",password={redisSettings["Password"]}"; } options.Configuration = connectionString; options.InstanceName = configuration.GetValue<string>("ApplicationName", "MyApp"); // Advanced Redis configuration options.ConfigurationOptions = new StackExchange.Redis.ConfigurationOptions { EndPoints = { $"{redisSettings["Host"]}:{redisSettings["Port"]}" }, Password = redisSettings["Password"], Database = redisSettings.GetValue<int>("Database", 0), ConnectTimeout = redisSettings.GetValue<int>("ConnectTimeout", 5000), SyncTimeout = redisSettings.GetValue<int>("SyncTimeout", 5000), AbortOnConnectFail = false }; }); } // Cache Service'i register et services.AddScoped<ICacheService, CacheService>(); // Cache metrikleri için (opsiyonel) services.AddSingleton<ICacheMetricsService, CacheMetricsService>(); return services; } }
textShared/ ├── Caching/ │ ├── Interfaces/ │ │ ├── ICacheService.cs │ │ ├── ICacheKeyGenerator.cs │ │ ├── ICacheConfiguration.cs │ │ └── ICacheMetricsService.cs │ ├── Services/ │ │ ├── MemoryCacheService.cs │ │ ├── RedisCacheService.cs │ │ ├── HybridCacheService.cs │ │ ├── CacheKeyGenerator.cs │ │ └── CacheMetricsService.cs │ ├── Configuration/ │ │ ├── CacheSettings.cs │ │ ├── RedisCacheOptions.cs │ │ └── MemoryCacheOptions.cs │ ├── Extensions/ │ │ ├── CacheServiceCollectionExtensions.cs │ │ └── CacheServiceProviderExtensions.cs │ ├── Attributes/ │ │ ├── CacheableAttribute.cs │ │ └── CacheEvictAttribute.cs │ ├── Middleware/ │ │ └── CacheMiddleware.cs │ └── Exceptions/ │ ├── CacheException.cs │ └── CacheConnectionException.cs
bash// Shared/Caching/Interfaces/ICacheKeyGenerator.cs public interface ICacheKeyGenerator { string GenerateKey(string prefix, params object[] parameters); string GenerateUserSpecificKey(string userId, string prefix, params object[] parameters); string GenerateEntityKey<T>(object id) where T : class; string GenerateListKey<T>(string? filter = null, int? page = null, int? pageSize = null) where T : class; string GenerateHashKey(string algorithm, string input); bool IsValidKey(string key); } // Shared/Caching/Services/CacheKeyGenerator.cs public class CacheKeyGenerator : ICacheKeyGenerator { private const string SEPARATOR = ":"; private const int MAX_KEY_LENGTH = 250; // Redis key length limit private readonly string _applicationName; private readonly string _environment; public CacheKeyGenerator(IConfiguration configuration) { _applicationName = configuration.GetValue<string>("CacheSettings:ApplicationName", "MyApp"); _environment = configuration.GetValue<string>("Environment", "Development"); } public string GenerateKey(string prefix, params object[] parameters) { ArgumentException.ThrowIfNullOrEmpty(prefix); var keyParts = new List<string> { _applicationName, _environment, prefix }; if (parameters?.Length > 0) { keyParts.AddRange(parameters.Where(p => p != null).Select(p => p.ToString()!)); } var key = string.Join(SEPARATOR, keyParts); if (!IsValidKey(key)) { throw new ArgumentException($"Generated key '{key}' is invalid"); } return key; } public string GenerateUserSpecificKey(string userId, string prefix, params object[] parameters) { ArgumentException.ThrowIfNullOrEmpty(userId); ArgumentException.ThrowIfNullOrEmpty(prefix); var keyParts = new List<string> { _applicationName, _environment, "user", userId, prefix }; if (parameters?.Length > 0) { keyParts.AddRange(parameters.Where(p => p != null).Select(p => p.ToString()!)); } var key = string.Join(SEPARATOR, keyParts); if (!IsValidKey(key)) { throw new ArgumentException($"Generated user key '{key}' is invalid"); } return key; } public string GenerateEntityKey<T>(object id) where T : class { ArgumentNullException.ThrowIfNull(id); var entityName = typeof(T).Name.ToLowerInvariant(); return GenerateKey("entity", entityName, id); } public string GenerateListKey<T>(string? filter = null, int? page = null, int? pageSize = null) where T : class { var entityName = typeof(T).Name.ToLowerInvariant(); var parameters = new List<object> { "list", entityName }; if (!string.IsNullOrEmpty(filter)) parameters.Add($"filter:{filter}"); if (page.HasValue) parameters.Add($"page:{page}"); if (pageSize.HasValue) parameters.Add($"size:{pageSize}"); return GenerateKey("query", parameters.ToArray()); } public string GenerateHashKey(string algorithm, string input) { ArgumentException.ThrowIfNullOrEmpty(algorithm); ArgumentException.ThrowIfNullOrEmpty(input); using var hash = algorithm.ToUpperInvariant() switch { "MD5" => System.Security.Cryptography.MD5.Create(), "SHA1" => System.Security.Cryptography.SHA1.Create(), "SHA256" => System.Security.Cryptography.SHA256.Create(), _ => throw new NotSupportedException($"Hash algorithm '{algorithm}' is not supported") }; var hashBytes = hash.ComputeHash(System.Text.Encoding.UTF8.GetBytes(input)); var hashString = Convert.ToHexString(hashBytes).ToLowerInvariant(); return GenerateKey("hash", algorithm.ToLowerInvariant(), hashString); } public bool IsValidKey(string key) { if (string.IsNullOrEmpty(key)) return false; if (key.Length > MAX_KEY_LENGTH) return false; // Redis key naming restrictions return !key.Contains(' ') && !key.Contains('\t') && !key.Contains('\n') && !key.Contains('\r'); } }
bash// Shared/Caching/Attributes/CacheableAttribute.cs [AttributeUsage(AttributeTargets.Method)] public class CacheableAttribute : Attribute { public string? KeyPrefix { get; set; } public int ExpirationMinutes { get; set; } = 30; public bool UseDistributedCache { get; set; } = true; public string[]? Tags { get; set; } public CacheItemPriority Priority { get; set; } = CacheItemPriority.Normal; public bool IncludeParameters { get; set; } = true; /// <summary> /// Cache condition expression (SpEL benzeri) /// Örnek: "args[0] != null && args[0].IsActive" /// </summary> public string? Condition { get; set; } } // Shared/Caching/Attributes/CacheEvictAttribute.cs [AttributeUsage(AttributeTargets.Method)] public class CacheEvictAttribute : Attribute { public string? KeyPrefix { get; set; } public string[]? Keys { get; set; } public string[]? Tags { get; set; } public bool AllEntries { get; set; } = false; public bool BeforeInvocation { get; set; } = false; }
textMyApp/ ├── Core/ │ ├── Domain/ │ │ ├── Entities/ │ │ ├── ValueObjects/ │ │ └── Events/ │ └── Application/ │ ├── Interfaces/ │ │ ├── Repositories/ │ │ └── Services/ │ │ ├── ICacheService.cs │ │ └── ICacheKeyService.cs │ ├── Services/ │ ├── DTOs/ │ └── UseCases/ ├── Infrastructure/ │ ├── Persistence/ │ └── Caching/ │ ├── Services/ │ │ ├── RedisCacheService.cs │ │ ├── MemoryCacheService.cs │ │ └── CacheKeyService.cs │ └── Configuration/ │ └── CacheConfiguration.cs └── Presentation/ └── WebAPI/ ├── Controllers/ └── Middleware/
bash// Core/Application/Interfaces/Services/ICacheService.cs namespace MyApp.Core.Application.Interfaces.Services; public interface ICacheService { Task<T?> GetAsync<T>(string key, CancellationToken cancellationToken = default) where T : class; Task<T> GetOrSetAsync<T>(string key, Func<Task<T>> getItem, TimeSpan? expiration = null, CancellationToken cancellationToken = default) where T : class; Task SetAsync<T>(string key, T value, TimeSpan? expiration = null, CancellationToken cancellationToken = default) where T : class; Task RemoveAsync(string key, CancellationToken cancellationToken = default); Task RemoveByTagAsync(string tag, CancellationToken cancellationToken = default); Task<bool> ExistsAsync(string key, CancellationToken cancellationToken = default); Task RefreshAsync(string key, CancellationToken cancellationToken = default); } // Core/Application/Interfaces/Services/ICacheKeyService.cs public interface ICacheKeyService { string GenerateEntityKey<TEntity>(object id) where TEntity : class; string GenerateListKey<TEntity>(string? filter = null, int? page = null, int? pageSize = null) where TEntity : class; string GenerateUserKey(string userId, string operation); string GenerateAggregateKey<TAggregate>(object id) where TAggregate : class; string[] GenerateTagsForEntity<TEntity>(object id) where TEntity : class; }
bash// Infrastructure/Caching/Services/RedisCacheService.cs using MyApp.Core.Application.Interfaces.Services; using Microsoft.Extensions.Caching.Distributed; using System.Text.Json; namespace MyApp.Infrastructure.Caching.Services; public class RedisCacheService : ICacheService { private readonly IDistributedCache _distributedCache; private readonly ILogger<RedisCacheService> _logger; private readonly JsonSerializerOptions _jsonOptions; private readonly TimeSpan _defaultExpiration; public RedisCacheService( IDistributedCache distributedCache, ILogger<RedisCacheService> logger, IConfiguration configuration) { _distributedCache = distributedCache; _logger = logger; _defaultExpiration = TimeSpan.FromMinutes( configuration.GetValue<int>("CacheSettings:DefaultExpirationMinutes", 30)); _jsonOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, WriteIndented = false, DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull }; } public async Task<T?> GetAsync<T>(string key, CancellationToken cancellationToken = default) where T : class { ArgumentException.ThrowIfNullOrEmpty(key); try { var stopwatch = System.Diagnostics.Stopwatch.StartNew(); var cachedValue = await _distributedCache.GetStringAsync(key, cancellationToken); stopwatch.Stop(); if (string.IsNullOrEmpty(cachedValue)) { _logger.LogDebug("Cache miss for key: {Key} (took {ElapsedMs}ms)", key, stopwatch.ElapsedMilliseconds); return null; } var deserializedValue = JsonSerializer.Deserialize<T>(cachedValue, _jsonOptions); _logger.LogDebug("Cache hit for key: {Key} (took {ElapsedMs}ms)", key, stopwatch.ElapsedMilliseconds); return deserializedValue; } catch (Exception ex) { _logger.LogError(ex, "Error retrieving cache for key: {Key}", key); return null; } } public async Task<T> GetOrSetAsync<T>(string key, Func<Task<T>> getItem, TimeSpan? expiration = null, CancellationToken cancellationToken = default) where T : class { ArgumentException.ThrowIfNullOrEmpty(key); ArgumentNullException.ThrowIfNull(getItem); // Önce cache'den almaya çalış var cachedValue = await GetAsync<T>(key, cancellationToken); if (cachedValue != null) { return cachedValue; } // Cache'de yoksa, veriyi getir ve cache'le try { var value = await getItem(); if (value != null) { await SetAsync(key, value, expiration, cancellationToken); } return value; } catch (Exception ex) { _logger.LogError(ex, "Error in GetOrSetAsync for key: {Key}", key); throw; } } public async Task SetAsync<T>(string key, T value, TimeSpan? expiration = null, CancellationToken cancellationToken = default) where T : class { ArgumentException.ThrowIfNullOrEmpty(key); ArgumentNullException.ThrowIfNull(value); try { var stopwatch = System.Diagnostics.Stopwatch.StartNew(); var serializedValue = JsonSerializer.Serialize(value, _jsonOptions); var options = new DistributedCacheEntryOptions(); var cacheExpiration = expiration ?? _defaultExpiration; options.AbsoluteExpirationRelativeToNow = cacheExpiration; await _distributedCache.SetStringAsync(key, serializedValue, options, cancellationToken); stopwatch.Stop(); _logger.LogDebug("Cache set for key: {Key}, expiration: {Expiration} (took {ElapsedMs}ms)", key, cacheExpiration, stopwatch.ElapsedMilliseconds); } catch (Exception ex) { _logger.LogError(ex, "Error setting cache for key: {Key}", key); throw; } } public async Task RemoveAsync(string key, CancellationToken cancellationToken = default) { ArgumentException.ThrowIfNullOrEmpty(key); try { await _distributedCache.RemoveAsync(key, cancellationToken); _logger.LogDebug("Cache removed for key: {Key}", key); } catch (Exception ex) { _logger.LogError(ex, "Error removing cache for key: {Key}", key); } } public async Task RemoveByTagAsync(string tag, CancellationToken cancellationToken = default) { ArgumentException.ThrowIfNullOrEmpty(tag); try { // Redis tag-based invalidation için Redis Set kullanılabilir // Bu implementasyon StackExchange.Redis ile özel tag management gerektirir _logger.LogWarning("RemoveByTagAsync requires custom Redis tag implementation for tag: {Tag}", tag); await Task.CompletedTask; } catch (Exception ex) { _logger.LogError(ex, "Error removing cache by tag: {Tag}", tag); } } public async Task<bool> ExistsAsync(string key, CancellationToken cancellationToken = default) { ArgumentException.ThrowIfNullOrEmpty(key); try { var value = await _distributedCache.GetStringAsync(key, cancellationToken); return !string.IsNullOrEmpty(value); } catch (Exception ex) { _logger.LogError(ex, "Error checking cache existence for key: {Key}", key); return false; } } public async Task RefreshAsync(string key, CancellationToken cancellationToken = default) { ArgumentException.ThrowIfNullOrEmpty(key); try { await _distributedCache.RefreshAsync(key, cancellationToken); _logger.LogDebug("Cache refreshed for key: {Key}", key); } catch (Exception ex) { _logger.LogError(ex, "Error refreshing cache for key: {Key}", key); } } }
bash// Infrastructure/Caching/Services/CacheKeyService.cs using MyApp.Core.Application.Interfaces.Services; using MyApp.Core.Domain.Common; namespace MyApp.Infrastructure.Caching.Services; public class CacheKeyService : ICacheKeyService { private const string SEPARATOR = ":"; private readonly string _applicationPrefix; private readonly string _environmentPrefix; public CacheKeyService(IConfiguration configuration) { _applicationPrefix = configuration.GetValue<string>("CacheSettings:ApplicationPrefix", "MyApp"); _environmentPrefix = configuration.GetValue<string>("Environment", "Development"); } public string GenerateEntityKey<TEntity>(object id) where TEntity : class { ArgumentNullException.ThrowIfNull(id); var entityName = GetEntityName<TEntity>(); return $"{_applicationPrefix}{SEPARATOR}{_environmentPrefix}{SEPARATOR}entity{SEPARATOR}{entityName}{SEPARATOR}{id}"; } public string GenerateListKey<TEntity>(string? filter = null, int? page = null, int? pageSize = null) where TEntity : class { var entityName = GetEntityName<TEntity>(); var baseKey = $"{_applicationPrefix}{SEPARATOR}{_environmentPrefix}{SEPARATOR}list{SEPARATOR}{entityName}"; var parameters = new List<string>(); if (!string.IsNullOrEmpty(filter)) parameters.Add($"filter{SEPARATOR}{filter}"); if (page.HasValue) parameters.Add($"page{SEPARATOR}{page}"); if (pageSize.HasValue) parameters.Add($"size{SEPARATOR}{pageSize}"); if (parameters.Any()) baseKey += $"{SEPARATOR}{string.Join(SEPARATOR, parameters)}"; return baseKey; } public string GenerateUserKey(string userId, string operation) { ArgumentException.ThrowIfNullOrEmpty(userId); ArgumentException.ThrowIfNullOrEmpty(operation); return $"{_applicationPrefix}{SEPARATOR}{_environmentPrefix}{SEPARATOR}user{SEPARATOR}{userId}{SEPARATOR}{operation}"; } public string GenerateAggregateKey<TAggregate>(object id) where TAggregate : class { ArgumentNullException.ThrowIfNull(id); var aggregateName = GetEntityName<TAggregate>(); return $"{_applicationPrefix}{SEPARATOR}{_environmentPrefix}{SEPARATOR}aggregate{SEPARATOR}{aggregateName}{SEPARATOR}{id}"; } public string[] GenerateTagsForEntity<TEntity>(object id) where TEntity : class { ArgumentNullException.ThrowIfNull(id); var entityName = GetEntityName<TEntity>(); return new[] { $"entity{SEPARATOR}{entityName}", $"entity{SEPARATOR}{entityName}{SEPARATOR}{id}", $"type{SEPARATOR}{entityName}" }; } private static string GetEntityName<TEntity>() where TEntity : class { var entityType = typeof(TEntity); // Domain entity ise base class name'ini kullan if (entityType.IsSubclassOf(typeof(BaseEntity))) { return entityType.Name.ToLowerInvariant(); } // DTO ise "Dto" suffix'ini kaldır var name = entityType.Name; if (name.EndsWith("Dto", StringComparison.OrdinalIgnoreCase)) { name = name[..^3]; } return name.ToLowerInvariant(); } }
bash// Core/Application/Interfaces/Repositories/IRepository.cs using MyApp.Core.Domain.Common; namespace MyApp.Core.Application.Interfaces.Repositories; public interface IRepository<TEntity> where TEntity : BaseEntity { // Read Operations Task<TEntity?> GetByIdAsync(int id, bool useCache = true, CancellationToken cancellationToken = default); Task<IEnumerable<TEntity>> GetAllAsync(bool useCache = true, CancellationToken cancellationToken = default); Task<IEnumerable<TEntity>> GetPagedAsync(int page, int pageSize, bool useCache = true, CancellationToken cancellationToken = default); // Write Operations Task<TEntity> AddAsync(TEntity entity, CancellationToken cancellationToken = default); Task UpdateAsync(TEntity entity, CancellationToken cancellationToken = default); Task DeleteAsync(int id, CancellationToken cancellationToken = default); // Query Operations Task<IEnumerable<TEntity>> FindAsync(Expression<Func<TEntity, bool>> predicate, bool useCache = false, CancellationToken cancellationToken = default); Task<TEntity?> FirstOrDefaultAsync(Expression<Func<TEntity, bool>> predicate, bool useCache = false, CancellationToken cancellationToken = default); Task<int> CountAsync(Expression<Func<TEntity, bool>>? predicate = null, bool useCache = true, CancellationToken cancellationToken = default); Task<bool> ExistsAsync(Expression<Func<TEntity, bool>> predicate, bool useCache = true, CancellationToken cancellationToken = default); } // Core/Application/Interfaces/Repositories/IUserRepository.cs public interface IUserRepository : IRepository<User> { Task<User?> GetByEmailAsync(string email, bool useCache = true, CancellationToken cancellationToken = default); Task<User?> GetByUsernameAsync(string username, bool useCache = true, CancellationToken cancellationToken = default); Task<IEnumerable<User>> GetActiveUsersAsync(bool useCache = true, CancellationToken cancellationToken = default); Task<IEnumerable<User>> GetUsersByRoleAsync(string role, bool useCache = true, CancellationToken cancellationToken = default); Task<int> GetActiveUserCountAsync(bool useCache = true, CancellationToken cancellationToken = default); }
bash// Infrastructure/Persistence/Repositories/UserRepository.cs using MyApp.Core.Application.Interfaces.Repositories; using MyApp.Core.Application.Interfaces.Services; using MyApp.Core.Domain.Entities; using Microsoft.EntityFrameworkCore; using System.Linq.Expressions; namespace MyApp.Infrastructure.Persistence.Repositories; public class UserRepository : IUserRepository { private readonly ApplicationDbContext _context; private readonly ICacheService _cacheService; private readonly ICacheKeyService _cacheKeyService; private readonly ILogger<UserRepository> _logger; // Cache expiration strategies private readonly TimeSpan _entityCacheExpiration = TimeSpan.FromMinutes(30); private readonly TimeSpan _listCacheExpiration = TimeSpan.FromMinutes(15); private readonly TimeSpan _countCacheExpiration = TimeSpan.FromMinutes(10); public UserRepository( ApplicationDbContext context, ICacheService cacheService, ICacheKeyService cacheKeyService, ILogger<UserRepository> logger) { _context = context; _cacheService = cacheService; _cacheKeyService = cacheKeyService; _logger = logger; } public async Task<User?> GetByIdAsync(int id, bool useCache = true, CancellationToken cancellationToken = default) { var cacheKey = _cacheKeyService.GenerateEntityKey<User>(id); if (useCache) { var cachedUser = await _cacheService.GetAsync<User>(cacheKey, cancellationToken); if (cachedUser != null) { _logger.LogDebug("User {UserId} retrieved from cache", id); return cachedUser; } } // Database'den user'ı al var user = await _context.Users .Include(u => u.Profile) .Include(u => u.Roles) .FirstOrDefaultAsync(u => u.Id == id, cancellationToken); // Cache'e ekle if (user != null && useCache) { await _cacheService.SetAsync(cacheKey, user, _entityCacheExpiration, cancellationToken); _logger.LogDebug("User {UserId} cached", id); } return user; } public async Task<User?> GetByEmailAsync(string email, bool useCache = true, CancellationToken cancellationToken = default) { ArgumentException.ThrowIfNullOrEmpty(email); var cacheKey = _cacheKeyService.GenerateEntityKey<User>($"email:{email.ToLowerInvariant()}"); if (useCache) { var cachedUser = await _cacheService.GetAsync<User>(cacheKey, cancellationToken); if (cachedUser != null) { _logger.LogDebug("User with email {Email} retrieved from cache", email); return cachedUser; } } var user = await _context.Users .Include(u => u.Profile) .Include(u => u.Roles) .FirstOrDefaultAsync(u => u.Email.ToLower() == email.ToLower(), cancellationToken); if (user != null && useCache) { await _cacheService.SetAsync(cacheKey, user, _entityCacheExpiration, cancellationToken); _logger.LogDebug("User with email {Email} cached", email); } return user; } public async Task<IEnumerable<User>> GetAllAsync(bool useCache = true, CancellationToken cancellationToken = default) { var cacheKey = _cacheKeyService.GenerateListKey<User>(); if (useCache) { var cachedUsers = await _cacheService.GetAsync<List<User>>(cacheKey, cancellationToken); if (cachedUsers != null) { _logger.LogDebug("All users retrieved from cache"); return cachedUsers; } } var users = await _context.Users .Include(u => u.Profile) .Include(u => u.Roles) .OrderBy(u => u.CreatedAt) .ToListAsync(cancellationToken); if (useCache) { await _cacheService.SetAsync(cacheKey, users, _listCacheExpiration, cancellationToken); _logger.LogDebug("All users cached"); } return users; } public async Task<IEnumerable<User>> GetActiveUsersAsync(bool useCache = true, CancellationToken cancellationToken = default) { var cacheKey = _cacheKeyService.GenerateListKey<User>("active"); if (useCache) { var cachedUsers = await _cacheService.GetAsync<List<User>>(cacheKey, cancellationToken); if (cachedUsers != null) { _logger.LogDebug("Active users retrieved from cache"); return cachedUsers; } } var users = await _context.Users .Include(u => u.Profile) .Include(u => u.Roles) .Where(u => u.IsActive && !u.IsDeleted) .OrderBy(u => u.LastLoginAt ?? u.CreatedAt) .ToListAsync(cancellationToken); if (useCache) { await _cacheService.SetAsync(cacheKey, users, _listCacheExpiration, cancellationToken); _logger.LogDebug("Active users cached"); } return users; } public async Task<User> AddAsync(User entity, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(entity); // Database'e ekle _context.Users.Add(entity); await _context.SaveChangesAsync(cancellationToken); // Cache'leri invalidate et await InvalidateUserCaches(entity); _logger.LogDebug("User {UserId} added and related caches invalidated", entity.Id); return entity; } public async Task UpdateAsync(User entity, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(entity); // Database'i güncelle _context.Users.Update(entity); await _context.SaveChangesAsync(cancellationToken); // Cache'leri invalidate et ve yeni veriyi cache'e ekle await InvalidateUserCaches(entity); // Güncel veriyi cache'e ekle var entityKey = _cacheKeyService.GenerateEntityKey<User>(entity.Id); await _cacheService.SetAsync(entityKey, entity, _entityCacheExpiration, cancellationToken); _logger.LogDebug("User {UserId} updated and caches refreshed", entity.Id); } public async Task DeleteAsync(int id, CancellationToken cancellationToken = default) { var user = await _context.Users.FindAsync(new object[] { id }, cancellationToken); if (user != null) { // Soft delete user.IsDeleted = true; user.DeletedAt = DateTime.UtcNow; _context.Users.Update(user); await _context.SaveChangesAsync(cancellationToken); // Cache'leri invalidate et await InvalidateUserCaches(user); _logger.LogDebug("User {UserId} deleted and caches invalidated", id); } } public async Task<IEnumerable<User>> FindAsync(Expression<Func<User, bool>> predicate, bool useCache = false, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(predicate); // Dinamik query'ler genellikle cache'lenmez var users = await _context.Users .Include(u => u.Profile) .Include(u => u.Roles) .Where(predicate) .ToListAsync(cancellationToken); return users; } public async Task<User?> FirstOrDefaultAsync(Expression<Func<User, bool>> predicate, bool useCache = false, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(predicate); var user = await _context.Users .Include(u => u.Profile) .Include(u => u.Roles) .FirstOrDefaultAsync(predicate, cancellationToken); return user; } public async Task<int> CountAsync(Expression<Func<User, bool>>? predicate = null, bool useCache = true, CancellationToken cancellationToken = default) { var filterKey = predicate != null ? predicate.ToString().GetHashCode().ToString() : "all"; var cacheKey = _cacheKeyService.GenerateEntityKey<User>($"count:{filterKey}"); if (useCache) { var cachedCount = await _cacheService.GetAsync<int?>(cacheKey, cancellationToken); if (cachedCount.HasValue) { _logger.LogDebug("User count retrieved from cache for filter: {Filter}", filterKey); return cachedCount.Value; } } var count = predicate != null ? await _context.Users.CountAsync(predicate, cancellationToken) : await _context.Users.CountAsync(cancellationToken); if (useCache) { await _cacheService.SetAsync(cacheKey, count, _countCacheExpiration, cancellationToken); _logger.LogDebug("User count cached for filter: {Filter}", filterKey); } return count; } public async Task<bool> ExistsAsync(Expression<Func<User, bool>> predicate, bool useCache = true, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(predicate); var filterKey = predicate.ToString().GetHashCode().ToString(); var cacheKey = _cacheKeyService.GenerateEntityKey<User>($"exists:{filterKey}"); if (useCache) { var cachedExists = await _cacheService.GetAsync<bool?>(cacheKey, cancellationToken); if (cachedExists.HasValue) { _logger.LogDebug("User existence check retrieved from cache for filter: {Filter}", filterKey); return cachedExists.Value; } } var exists = await _context.Users.AnyAsync(predicate, cancellationToken); if (useCache) { await _cacheService.SetAsync(cacheKey, exists, _countCacheExpiration, cancellationToken); _logger.LogDebug("User existence cached for filter: {Filter}", filterKey); } return exists; } public async Task<IEnumerable<User>> GetPagedAsync(int page, int pageSize, bool useCache = true, CancellationToken cancellationToken = default) { var cacheKey = _cacheKeyService.GenerateListKey<User>(page: page, pageSize: pageSize); if (useCache) { var cachedUsers = await _cacheService.GetAsync<List<User>>(cacheKey, cancellationToken); if (cachedUsers != null) { _logger.LogDebug("Paged users retrieved from cache (page: {Page}, size: {PageSize})", page, pageSize); return cachedUsers; } } var users = await _context.Users .Include(u => u.Profile) .Include(u => u.Roles) .OrderBy(u => u.CreatedAt) .Skip((page - 1) * pageSize) .Take(pageSize) .ToListAsync(cancellationToken); if (useCache) { await _cacheService.SetAsync(cacheKey, users, _listCacheExpiration, cancellationToken); _logger.LogDebug("Paged users cached (page: {Page}, size: {PageSize})", page, pageSize); } return users; } public async Task<User?> GetByUsernameAsync(string username, bool useCache = true, CancellationToken cancellationToken = default) { ArgumentException.ThrowIfNullOrEmpty(username); var cacheKey = _cacheKeyService.GenerateEntityKey<User>($"username:{username.ToLowerInvariant()}"); if (useCache) { var cachedUser = await _cacheService.GetAsync<User>(cacheKey, cancellationToken); if (cachedUser != null) { _logger.LogDebug("User with username {Username} retrieved from cache", username); return cachedUser; } } var user = await _context.Users .Include(u => u.Profile) .Include(u => u.Roles) .FirstOrDefaultAsync(u => u.Username.ToLower() == username.ToLower(), cancellationToken); if (user != null && useCache) { await _cacheService.SetAsync(cacheKey, user, _entityCacheExpiration, cancellationToken); _logger.LogDebug("User with username {Username} cached", username); } return user; } public async Task<IEnumerable<User>> GetUsersByRoleAsync(string role, bool useCache = true, CancellationToken cancellationToken = default) { ArgumentException.ThrowIfNullOrEmpty(role); var cacheKey = _cacheKeyService.GenerateListKey<User>($"role:{role.ToLowerInvariant()}"); if (useCache) { var cachedUsers = await _cacheService.GetAsync<List<User>>(cacheKey, cancellationToken); if (cachedUsers != null) { _logger.LogDebug("Users with role {Role} retrieved from cache", role); return cachedUsers; } } var users = await _context.Users .Include(u => u.Profile) .Include(u => u.Roles) .Where(u => u.Roles.Any(r => r.Name.ToLower() == role.ToLower())) .OrderBy(u => u.CreatedAt) .ToListAsync(cancellationToken); if (useCache) { await _cacheService.SetAsync(cacheKey, users, _listCacheExpiration, cancellationToken); _logger.LogDebug("Users with role {Role} cached", role); } return users; } public async Task<int> GetActiveUserCountAsync(bool useCache = true, CancellationToken cancellationToken = default) { var cacheKey = _cacheKeyService.GenerateEntityKey<User>("count:active"); if (useCache) { var cachedCount = await _cacheService.GetAsync<int?>(cacheKey, cancellationToken); if (cachedCount.HasValue) { _logger.LogDebug("Active user count retrieved from cache"); return cachedCount.Value; } } var count = await _context.Users .CountAsync(u => u.IsActive && !u.IsDeleted, cancellationToken); if (useCache) { await _cacheService.SetAsync(cacheKey, count, _countCacheExpiration, cancellationToken); _logger.LogDebug("Active user count cached"); } return count; } /// <summary> /// User ile ilgili tüm cache entry'lerini invalidate eder /// </summary> private async Task InvalidateUserCaches(User user) { var keysToInvalidate = new List<string> { // Entity cache'leri _cacheKeyService.GenerateEntityKey<User>(user.Id), _cacheKeyService.GenerateEntityKey<User>($"email:{user.Email.ToLowerInvariant()}"), _cacheKeyService.GenerateEntityKey<User>($"username:{user.Username.ToLowerInvariant()}"), // List cache'leri _cacheKeyService.GenerateListKey<User>(), _cacheKeyService.GenerateListKey<User>("active"), // Count cache'leri _cacheKeyService.GenerateEntityKey<User>("count:all"), _cacheKeyService.GenerateEntityKey<User>("count:active") }; // Role bazlı cache'leri de temizle foreach (var role in user.Roles) { keysToInvalidate.Add(_cacheKeyService.GenerateListKey<User>($"role:{role.Name.ToLowerInvariant()}")); } // Tüm key'leri paralel olarak invalidate et var invalidationTasks = keysToInvalidate.Select(key => _cacheService.RemoveAsync(key)); await Task.WhenAll(invalidationTasks); _logger.LogDebug("Invalidated {Count} cache entries for user {UserId}", keysToInvalidate.Count, user.Id); } }
Kategori | Best Practice | Açıklama |
---|---|---|
Key Naming | Consistent naming convention kullanın | app:env:entity:id formatı tercih edin |
Expiration Strategy | Uygun TTL değerleri belirleyin | Frequently changing data için kısa, static data için uzun |
Cache Invalidation | Write operasyonlarında cache'i temizleyin | Add/Update/Delete sonrası ilgili cache'leri invalidate edin |
Error Handling | Cache hatalarında application'ı durdurmayın | Cache miss durumunda data source'a fallback yapın |
Memory Management | Cache boyutunu monitör edin | Memory leak'leri önlemek için size limit belirleyin |
Serialization | Lightweight serialization kullanın | JSON.NET yerine System.Text.Json tercih edin |
Performance Monitoring | Cache hit/miss oranlarını takip edin | Application Insights veya custom metrics kullanın |
Security | Sensitive data'yı cache'lemeyin | PII ve credentials cache'den uzak tutun |
Concurrency | Race condition'lara dikkat edin | Lock-free pattern'ler tercih edin |
Testing | Cache behavior'ını test edin | Unit test'lerde cache mock'ları kullanın |
Anti-Pattern | Neden Kötü | Doğru Yaklaşım |
---|---|---|
Cache-aside olmadan kullanım | Veri tutarsızlığı yaratır | Cache-aside pattern uygulayın |
Çok uzun TTL değerleri | Stale data sorununa yol açar | İş ihtiyacına uygun TTL belirleyin |
Cache'de sensitive data | Güvenlik riski oluşturur | Hassas verileri cache'lemeyin |
Büyük objeleri cache'leme | Memory tüketimi artar | Gerçekten gerekli olan alanları tutun |
Exception'ları swallow etmek | Debug zorlaşır | Logla ama application'ı durdurmayın |
Cache stampede | Aynı veri için concurrent request'ler | Lock mechanism veya queue kullanın |
Hot key problem | Tek key'e çok fazla erişim | Sharding veya local cache kullanın |
Cache dependency hell | Karmaşık invalidation dependencies | Simple invalidation strategy uygulayın |
bash// Optimal Configuration Örneği public class CacheConfiguration { public class MemoryCacheConfig { public int SizeLimit { get; set; } = 1024; public double CompactionPercentage { get; set; } = 0.25; public TimeSpan ExpirationScanFrequency { get; set; } = TimeSpan.FromMinutes(5); public bool EnableStatistics { get; set; } = true; } public class RedisCacheConfig { public string ConnectionString { get; set; } = ""; public string InstanceName { get; set; } = "MyApp"; public int Database { get; set; } = 0; public TimeSpan ConnectTimeout { get; set; } = TimeSpan.FromSeconds(5); public TimeSpan SyncTimeout { get; set; } = TimeSpan.FromSeconds(5); public int ConnectRetry { get; set; } = 3; public bool AbortOnConnectFail { get; set; } = false; public string? Password { get; set; } public bool UseSsl { get; set; } = false; } public class CacheSettings { public bool UseDistributedCache { get; set; } = true; public TimeSpan DefaultExpiration { get; set; } = TimeSpan.FromMinutes(30); public TimeSpan ShortExpiration { get; set; } = TimeSpan.FromMinutes(5); public TimeSpan LongExpiration { get; set; } = TimeSpan.FromHours(2); public string ApplicationPrefix { get; set; } = "MyApp"; public bool EnableCacheMetrics { get; set; } = true; public bool EnableDistributedLocking { get; set; } = true; public int MaxConcurrentRequests { get; set; } = 100; } public class CacheStrategies { public Dictionary<string, TimeSpan> EntityExpirations { get; set; } = new() { ["User"] = TimeSpan.FromMinutes(30), ["Product"] = TimeSpan.FromHours(1), ["Category"] = TimeSpan.FromHours(6), ["Configuration"] = TimeSpan.FromHours(12) }; public Dictionary<string, bool> CacheableEntities { get; set; } = new() { ["User"] = true, ["AuditLog"] = false, ["Session"] = false, ["TempData"] = false }; } }
bash// Cache Metrics Service public interface ICacheMetricsService { void RecordCacheHit(string cacheType, string operation, TimeSpan duration); void RecordCacheMiss(string cacheType, string operation); void RecordCacheSetTime(string cacheType, string operation, TimeSpan duration); void RecordCacheError(string cacheType, string operation, Exception exception); Task<CacheStatistics> GetStatisticsAsync(TimeSpan timeWindow); } public class CacheMetricsService : ICacheMetricsService { private readonly ILogger<CacheMetricsService> _logger; private readonly IMetricsLogger _metricsLogger; // Application Insights, Prometheus vs. private readonly ConcurrentDictionary<string, CacheOperationStats> _stats; public CacheMetricsService( ILogger<CacheMetricsService> logger, IMetricsLogger metricsLogger) { _logger = logger; _metricsLogger = metricsLogger; _stats = new ConcurrentDictionary<string, CacheOperationStats>(); } public void RecordCacheHit(string cacheType, string operation, TimeSpan duration) { var key = $"{cacheType}:{operation}"; _stats.AddOrUpdate(key, new CacheOperationStats { Hits = 1, TotalDuration = duration }, (k, existing) => { existing.Hits++; existing.TotalDuration = existing.TotalDuration.Add(duration); return existing; }); _logger.LogDebug("Cache hit: {CacheType} - {Operation} ({Duration}ms)", cacheType, operation, duration.TotalMilliseconds); _metricsLogger.LogMetric("cache.hit", 1, new Dictionary<string, string> { ["cache_type"] = cacheType, ["operation"] = operation }); _metricsLogger.LogMetric("cache.duration", duration.TotalMilliseconds, new Dictionary<string, string> { ["cache_type"] = cacheType, ["operation"] = operation, ["result"] = "hit" }); } public void RecordCacheMiss(string cacheType, string operation) { var key = $"{cacheType}:{operation}"; _stats.AddOrUpdate(key, new CacheOperationStats { Misses = 1 }, (k, existing) => { existing.Misses++; return existing; }); _logger.LogDebug("Cache miss: {CacheType} - {Operation}", cacheType, operation); _metricsLogger.LogMetric("cache.miss", 1, new Dictionary<string, string> { ["cache_type"] = cacheType, ["operation"] = operation }); } public void RecordCacheSetTime(string cacheType, string operation, TimeSpan duration) { _metricsLogger.LogMetric("cache.set.duration", duration.TotalMilliseconds, new Dictionary<string, string> { ["cache_type"] = cacheType, ["operation"] = operation }); } public void RecordCacheError(string cacheType, string operation, Exception exception) { var key = $"{cacheType}:{operation}"; _stats.AddOrUpdate(key, new CacheOperationStats { Errors = 1 }, (k, existing) => { existing.Errors++; return existing; }); _logger.LogError(exception, "Cache error: {CacheType} - {Operation}", cacheType, operation); _metricsLogger.LogMetric("cache.error", 1, new Dictionary<string, string> { ["cache_type"] = cacheType, ["operation"] = operation, ["exception_type"] = exception.GetType().Name }); } public Task<CacheStatistics> GetStatisticsAsync(TimeSpan timeWindow) { var statistics = new CacheStatistics { TotalHits = _stats.Values.Sum(s => s.Hits), TotalMisses = _stats.Values.Sum(s => s.Misses), TotalErrors = _stats.Values.Sum(s => s.Errors), AverageResponseTime = _stats.Values .Where(s => s.Hits > 0) .Average(s => s.TotalDuration.TotalMilliseconds / s.Hits), TimeWindow = timeWindow }; statistics.HitRatio = statistics.TotalHits > 0 || statistics.TotalMisses > 0 ? (double)statistics.TotalHits / (statistics.TotalHits + statistics.TotalMisses) : 0; return Task.FromResult(statistics); } private class CacheOperationStats { public long Hits { get; set; } public long Misses { get; set; } public long Errors { get; set; } public TimeSpan TotalDuration { get; set; } } } public class CacheStatistics { public long TotalHits { get; set; } public long TotalMisses { get; set; } public long TotalErrors { get; set; } public double HitRatio { get; set; } public double AverageResponseTime { get; set; } public TimeSpan TimeWindow { get; set; } }