🧩 کد نهایی
در اینجا نسخهی نهایی منطق cache را میبینی:
public static class CurrencyConversion
{
private record CacheEntry(decimal Rate, DateTime CreatedAt);
private static readonly ConcurrentDictionary<string, CacheEntry> Cache = new();
private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(5);
private static readonly ConcurrentDictionary<string, SemaphoreSlim> Locks = new();
public static async Task<IResult> Handle(
string currencyCode,
decimal amount,
CurrencyApiClient currencyClient)
{
// Validate currency code format (3 uppercase letters)
if (string.IsNullOrWhiteSpace(currencyCode)||
currencyCode.Length != 3 ||
!currencyCode.All(char.IsLetter))
{
return Results.BadRequest(
new { error = "Currency code must be a 3-letter uppercase code (e.g., EUR, GBP)" });
}
// Validate amount (must be positive)
if (amount < 0)
{
return Results.BadRequest(new { error = "Amount must be a positive number" });
}
decimal? rate;
var semaphore = Locks.GetOrAdd(currencyCode, _ => new SemaphoreSlim(1, 1));
if (!Cache.TryGetValue(currencyCode, out var cachedRate) &&
DateTime.UtcNow - cachedRate?.CreatedAt < CacheDuration)
{
var acquired = await semaphore.WaitAsync(TimeSpan.FromSeconds(10));
if (!acquired)
{
throw new Exception("Could not acquire lock to fetch exchange rate.");
}
try
{
// Double-check locking pattern: check again inside the lock
if (!Cache.TryGetValue(currencyCode, out cachedRate) &&
DateTime.UtcNow - cachedRate?.CreatedAt < CacheDuration)
{
rate = await currencyClient.GetExchangeRateAsync(currencyCode);
if (rate == null)
{
return Results.NotFound(
new { error = $"Exchange rate for {currencyCode} not found or API error occurred" });
}
Cache.AddOrUpdate(currencyCode,
_ => new CacheEntry(rate.Value, DateTime.UtcNow),
(_, _) => new CacheEntry(rate.Value, DateTime.UtcNow));
}
else
{
rate = cachedRate!.Rate;
}
}
finally
{
semaphore.Release();
}
}
else
{
rate = cachedRate!.Rate;
}
var convertedAmount = amount * rate.Value;
return Results.Ok(new ExchangeRateResponse(
Currency: currencyCode,
BaseCurrency: "USD",
Rate: rate.Value,
Amount: amount,
ConvertedAmount: convertedAmount
));
}
}
گام بعدی این است که منطق اصلی cache را به یک کلاس reusable جداگانه استخراج کنی 🧩.
به این شکل میتوانی آن را در بخشهای دیگر برنامه هم استفاده کنی 🔁.
🎯 جمعبندی (Takeaway)
چرا اصلاً این همه دردسر؟ 🤔
دیدن یک ConcurrentDictionary ساده خیلی وسوسهبرانگیز است 😌 و فکر میکنی کار تمام شده.
اما همانطور که دیدیم، فاصلهی بین «کار میکند» و «مقیاسپذیر است»
پر از edge caseهایی است که میتوانند یک سیستم production را زمینگیر کنند 💥.
وقتی از یک library استفاده میکنی، این edge caseها برایت مدیریت میشوند 🛡.
اما ساختن آن با دست خودت، به تو سه ستون اصلی کد با کارایی بالا را یاد میدهد 🏛:
🧵 Thread safety
🔒 Lock contention
🛡 Resource protection
گاهی اوقات، «کسلکنندهترین» بخشهای زیرساخت، مثل cache،
در واقع جالبترین بخشها از نظر معماری هستند 🧠✨.
و آن ۱٪ مواقعی هم هست که به یک راهحل کاملاً سفارشی نیاز داری
که هیچ libraryای ارائهاش نمیدهد 🛠.
برای همین دانستن اصول پایه واقعاً ارزشمند است.
کتابخانههای مدرنی مثل HybridCache یا FusionCache این کارها را برایت انجام میدهند 🚀،
اما درک این الگوها باعث میشود دقیقاً بدانی چرا برنامهات تحت load آنطور که میبینی رفتار میکند 📊.