🚀 کشینگ در ASP.NET Core (قسمت ۱):
مبانی و چرا باید اهمیت بدیم؟
یکی از سادهترین و در عین حال قدرتمندترین تکنیکها برای افزایش چشمگیر پرفورمنس اپلیکیشن شما، کشینگ (Caching) هست.
کشینگ یعنی ذخیره موقت دادهها در یک محل سریعتر (معمولاً حافظه RAM). شما معمولاً نتایج عملیاتهای سنگین یا دادههایی که به صورت مکرر بهشون نیاز دارید رو کش میکنید تا در درخواستهای بعدی، به جای مراجعه به منبع اصلی (مثل دیتابیس)، مستقیماً از کش استفاده کنید.
امروز قسمت اول از مینی-سریال جدیدمون رو شروع میکنیم و به مبانی این موضوع حیاتی میپردازیم.
1️⃣ کشینگ چطور پرفورمنس رو بهبود میده؟ ⚡️
🚀 دسترسی سریعتر به دادهها:
دادههای کش شده از حافظه RAM خوانده میشن که هزاران برابر سریعتر از دیتابیس یا یک API خارجیه.
📉 کاهش بار دیتابیس:
با کش کردن دادههایی که زیاد خونده میشن، تعداد کوئریها به دیتابیس به شدت کم میشه و فشار از روی دیتابیس برداشته میشه.
🧠 استفاده کمتر از CPU:
جلوی پردازشهای تکراری و سنگین (مثل ساختن یک صفحه وب پیچیده) رو میگیره.
💪 مقیاسپذیری بیشتر:
برنامه شما میتونه ترافیک بیشتری رو با منابع کمتر مدیریت کنه و به کاربران بیشتری سرویس بده.
2️⃣ ابزارهای کشینگ در ASP.NET Core 🛠
حالا ASP.NET Core دو تا ابزار (اینترفیس) اصلی برای کار با کش در اختیار ما میذاره:
• IMemoryCache:
برای کشینگ درون-حافظهای (In-Memory). دادهها رو در حافظه RAM همون سروری که اپلیکیشن روش اجرا شده، ذخیره میکنه. استفاده ازش خیلی سادهست ولی برای سناریوهایی که چند سرور دارید، مناسب نیست.
• IDistributedCache:
برای کشینگ توزیعشده (Distributed). به شما اجازه میده دادههای کش رو در یک سرور خارجی مثل Redis ذخیره کنید تا بین چندین نمونه (instance) از اپلیکیشن شما به اشتراک گذاشته بشه.
🔖 هشتگها:
#ASPNETCore #Caching
🚀 کشینگ در ASP.NET Core (قسمت ۲):
شیرجه عمیق در IMemoryCache
در قسمت اول با مبانی کشینگ آشنا شدیم. حالا وقتشه آستینها رو بالا بزنیم و به صورت عملی، اولین و سادهترین نوع کشینگ در ASP.NET Core یعنی کشینگ درون-حافظهای (In-Memory) رو با IMemoryCache پیادهسازی کنیم.
1️⃣ قدم اول: فعالسازی سرویس
مثل خیلی از قابلیتهای دیگه در ASP.NET Core، اول باید سرویس IMemoryCache رو در فایل Program.cs به اپلیکیشن خودمون اضافه کنیم تا بتونیم اون رو از طریق Dependency Injection (تزریق وابستگی) همه جا استفاده کنیم.
var builder = WebApplication.CreateBuilder(args);
// اضافه کردن سرویس کشینگ درون-حافظهای
builder.Services.AddMemoryCache();
// ... بقیه تنظیمات
2️⃣ قدم دوم: الگوی پیادهسازی (Try-Get-Set)
الگوی استاندارد برای کار با کش خیلی سادهست:
• امتحان کن (Try): سعی کن داده رو از کش بگیری.
• بگیر (Get): اگه تو کش بود، همونو برگردون.
• تنظیم کن (Set): اگه تو کش نبود، از منبع اصلی (مثل دیتابیس) بگیر و برای دفعههای بعدی، تو کش ذخیره کن.
این الگو رو در یک Minimal API ببینیم:
app.MapGet("products/{id}",
(int id, IMemoryCache cache, AppDbContext context) =>
{
// ۱. سعی میکنیم محصول رو از کش با کلید id بخونیم
if (!cache.TryGetValue(id, out Product product))
{
// ۲. اگه تو کش نبود (Cache Miss)، از دیتابیس میخونیم
product = context.Products.Find(id);
// ۳. داده رو در کش ذخیره میکنیم تا دفعه بعد استفاده بشه
// (گزینههای انقضا رو در بخش بعدی توضیح میدیم)
var cacheOptions = new MemoryCacheEntryOptions()
.SetSlidingExpiration(TimeSpan.FromMinutes(5));
cache.Set(id, product, cacheOptions);
}
// ۴. داده رو (چه از کش، چه از دیتابیس) برمیگردونیم
return Results.Ok(product);
});3️⃣ قدم سوم: مدیریت انقضای کش (Cache Expiration) ⏱️
دادهها نباید برای همیشه تو کش بمونن، چون ممکنه در دیتابیس تغییر کنن و کهنه (stale) بشن. ما باید به کش بگیم که چه زمانی این دادهها رو دور بریزه. دو تا سیاست اصلی برای این کار وجود داره:
✨️انقضای مطلق (Absolute Expiration):
یه تاریخ انقضای مشخص تعیین میکنه. مثلاً "۱۰ دقیقه دیگه، چه کسی از این داده استفاده کرد یا نکرد، حذفش کن."
.SetAbsoluteExpiration(TimeSpan.FromMinutes(10))
✨️انقضای لغزنده (Sliding Expiration):
یه بازه زمانی عدم فعالیت تعیین میکنه. مثلاً "اگه تا ۲ دقیقه کسی به این داده دست نزد، حذفش کن. ولی اگه کسی ازش استفاده کرد، عمرش رو ۲ دقیقه دیگه تمدید کن."
.SetSlidingExpiration(TimeSpan.FromMinutes(2))
🔖 هشتگها:
#ASPNETCore #Caching
🚀 کشینگ در ASP.NET Core (قسمت ۳):
الگوی حرفهای Cache-Aside
در قسمت قبل، با IMemoryCache آشنا شدیم. عالی بود، ولی یه مشکل بزرگ داشت: اگه چند تا سرور داشته باشیم، کش بینشون به اشتراک گذاشته نمیشه.
امروز میخوایم با الگوی Cache-Aside و اینترفیس IDistributedCache آشنا بشیم تا این مشکل رو حل کنیم و کدهامون رو برای کشینگ، خیلی تمیزتر و قابل استفاده مجدد کنیم.
1️⃣ الگوی Cache-Aside چیست؟ 🤓
این الگو، رایجترین و استانداردترین استراتژی برای کار با کشه. منطقش خیلی سادهست:
• اول کش رو چک کن: برنامه شما اول به کش نگاه میکنه.
• اگه تو کش بود، برش گردون: اگه داده اونجا بود (Cache Hit)، کار تمومه.
• اگه نبود، برو سراغ منبع اصلی: اگه داده تو کش نبود (Cache Miss)، برو از منبع اصلی (مثل دیتابیس) بخونش.
• کش رو آپدیت کن و برگردون: دادهای که از منبع اصلی گرفتی رو تو کش ذخیره کن تا برای دفعه بعد آماده باشه و بعد به کاربر برگردون.
2️⃣ ساخت یک ابزار حرفهای: متد توسعه GetOrCreateAsync 🛠
به جای اینکه این منطق ۴ مرحلهای رو هر بار تکرار کنیم، میتونیم یه متد توسعه (Extension Method) خفن برای IDistributedCache بنویسیم که این کار رو برامون انجام بده.
public static class DistributedCacheExtensions
{
// یه زمان انقضای پیشفرض تعریف میکنیم
public static DistributedCacheEntryOptions DefaultExpiration => new()
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(2)
};
public static async Task<T> GetOrCreateAsync<T>(
this IDistributedCache cache,
string key,
Func<Task<T>> factory, // تابعی که قراره داده رو از منبع اصلی بگیره
DistributedCacheEntryOptions? options = null)
{
var cachedData = await cache.GetStringAsync(key);
if (cachedData is not null)
{
// اگه داده تو کش بود، از JSON برش میگردونیم
return JsonSerializer.Deserialize<T>(cachedData);
}
// اگه نبود، تابع factory رو اجرا میکنیم تا از منبع اصلی بگیره
var data = await factory();
// داده جدید رو به صورت JSON در کش ذخیره میکنیم
await cache.SetStringAsync(
key,
JsonSerializer.Serialize(data),
options ?? DefaultExpiration);
return data;
}
}
3️⃣ نحوه استفاده از ابزار جدید ✨
حالا ببینید اون کد شلوغ قبلی، با این متد توسعه چقدر تمیز و خوانا میشه:
app.MapGet("products/{id}",
async (int id, IDistributedCache cache, AppDbContext context) =>
{
// فقط کافیه متد خودمون رو صدا بزنیم!
var product = await cache.GetOrCreateAsync($"products-{id}", async () =>
{
// این تابع فقط زمانی اجرا میشه که داده تو کش نباشه
return await context.Products.FindAsync(id);
});
return Results.Ok(product);
});💡نکته: برای اینکه این کد کار کنه، باید اول سرویس IDistributedCache رو در Program.cs ثبت کنیم:
builder.Services.AddDistributedMemoryCache();
🔖 هشتگها:
#ASPNETCore #Caching
🚀 کشینگ در ASP.NET Core (قسمت ۴):
قدرت توزیعشده با Redis
در قسمت قبل، یه متد توسعه خفن برای IDistributedCache نوشتیم. اما پیادهسازی پیشفرض اون (AddDistributedMemoryCache)، هنوز هم درون-حافظهای بود و کش رو بین سرورها به اشتراک نمیذاشت.
امروز وقتشه این مشکل رو برای همیشه حل کنیم و با Redis، یکی از محبوبترین و قدرتمندترین ابزارهای کشینگ توزیعشده، آشنا بشیم.
ردیس (Redis) چیست؟ 🧠
ردیس یک ذخیرهساز داده درون-حافظهای (in-memory) فوقالعاده سریعه که اغلب به عنوان یک کش توزیعشده با پرفورمنس بالا استفاده میشه. وقتی از Redis به عنوان کش استفاده میکنید، تمام نمونههای (instances) اپلیکیشن شما به یک سرور Redis مشترک وصل میشن و دادههای کش رو از اونجا میخونن و مینویسن.
این یعنی اگه ۱۰ تا سرور داشته باشید، کش بین همهشون یکسان و هماهنگه!
1️⃣ قدم اول: نصب پکیج
برای اینکه ASP.NET Core بتونه با Redis صحبت کنه، باید پکیج مخصوصش رو نصب کنیم:
Install-Package Microsoft.Extensions.Caching.StackExchangeRedis
این پکیج به ما اجازه میده Redis رو به راحتی به عنوان پیادهسازی IDistributedCache به پروژهمون اضافه کنیم.
2️⃣ قدم دوم: پیکربندی در Program.cs
حالا باید به برنامهمون بگیم که از Redis استفاده کنه. دو تا راه رایج برای این کار وجود داره:
👍🏻روش ساده (با Connection String):
این روش برای شروع عالیه. فقط کافیه آدرس سرور Redis رو بهش بدید.
var builder = WebApplication.CreateBuilder(args);
string redisConnectionString = builder.Configuration.GetConnectionString("Redis");
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = redisConnectionString;
});
💯روش حرفهایتر (با IConnectionMultiplexer):
این روش کنترل بیشتری به شما میده و بهترین راه برای پروژههای بزرگه. شما خودتون یک نمونه از ConnectionMultiplexer رو به صورت Singleton ثبت میکنید.
string redisConnectionString = builder.Configuration.GetConnectionString("Redis");
IConnectionMultiplexer connectionMultiplexer =
ConnectionMultiplexer.Connect(redisConnectionString);
builder.Services.AddSingleton(connectionMultiplexer);
builder.Services.AddStackExchangeRedisCache(options =>
{
options.ConnectionMultiplexerFactory =
() => Task.FromResult(connectionMultiplexer);
});جادو اتفاق افتاد! ✨
تمام شد! حالا هرجایی از کدتون که IDistributedCache رو تزریق کنید، در پشت صحنه به جای کش حافظه، از Redis استفاده خواهد شد، بدون اینکه نیاز به تغییر حتی یک خط از کدهای قبلیتون (مثل متد GetOrCreateAsync) داشته باشید. این قدرت انتزاع (Abstraction) در #C هست!
🔖 هشتگها:
#ASPNETCore #Caching #Redis
🚀 کشینگ در ASP.NET Core (قسمت ۵):
مشکل Cache Stampede و آینده کشینگ در 9 Net.
تا اینجا با انواع کشینگ آشنا شدیم. اما وقتی ترافیک سیستم خیلی بالا میره، یه مشکل خطرناک و پنهان به اسم Cache Stampede (ازدحام کش) میتونه تمام زحمات ما رو به باد بده!
در قسمت آخر این سری، با این مشکل و راه حلهای مدرنش آشنا میشیم.
1️⃣ مشکل Cache Stampede چیست؟ 🔥
تصور کنید یه آیتم خیلی پرطرفدار (مثلاً صفحه اول سایت) در کش شما منقضی میشه. در یک لحظه، صدها یا هزاران درخواست همزمان میبینن که کش خالیه و همهشون با هم به سمت دیتابیس هجوم میارن تا اون داده رو بگیرن!
این هجوم ناگهانی، دیتابیس و اپلیکیشن شما رو از پا در میاره و عملاً مزیت کشینگ رو از بین میبره.
2️⃣ یک راه حل (ناقص): قفلگذاری با SemaphoreSlim 🔒
یک راه حل رایج، استفاده از قفلگذاری (Locking) هست. یعنی فقط به اولین درخواست اجازه بدیم که بره داده رو از دیتابیس بگیره و بقیه منتظر بمونن تا کش دوباره پر بشه.
میتونیم متد GetOrCreateAsync خودمون رو با SemaphoreSlim به این شکل تغییر بدیم:
public static class DistributedCacheExtensions
{
private static readonly SemaphoreSlim Semaphore = new(1, 1);
public static async Task<T> GetOrCreateAsync<T>(
this IDistributedCache cache, string key, Func<Task<T>> factory)
{
var cachedData = await cache.GetStringAsync(key);
if (cachedData is not null) return JsonSerializer.Deserialize<T>(cachedData);
try
{
await Semaphore.WaitAsync(); // منتظر قفل بمون
// دوباره کش رو چک کن، شاید درخواست قبلی پرش کرده باشه
cachedData = await cache.GetStringAsync(key);
if (cachedData is not null) return JsonSerializer.Deserialize<T>(cachedData);
// اگه هنوز خالی بود، از دیتابیس بگیر و کش رو پر کن
var data = await factory();
await cache.SetStringAsync(key, JsonSerializer.Serialize(data), ...);
return data;
}
finally
{
Semaphore.Release(); // قفل رو آزاد کن
}
}
}
🚫مشکل این راه حل: این کد از یه قفل سراسری برای تمام کلیدها استفاده میکنه. یعنی اگه ۱۰۰ تا درخواست برای ۱۰۰ تا کلید مختلف هم بیان، باز هم باید برای هم صبر کنن! این کارایی رو به شدت کم میکنه. (راه حل بهتر، قفلگذاری بر اساس key هست که پیچیدهتره).
3️⃣ آینده کشینگ: HybridCache در 9 Net. ✨
تیم داتنت برای حل این مشکلات (و مشکلات دیگه)، در 9 Net. یه ابزار جدید و خیلی قدرتمند به اسم HybridCache معرفی کرده.
این ابزار به صورت داخلی، مشکل Cache Stampede رو به روشی بهینه حل میکنه و ترکیبی از IMemoryCache (برای سرعت) و IDistributedCache (برای توزیعشدگی) رو به بهترین شکل ممکن ارائه میده.
جمعبندی سری و نظر شما 🤔
با این پست، مینی-سریال ما در مورد کشینگ به پایان میرسه. ما از مبانی شروع کردیم و به پیشرفتهترین مشکلات و جدیدترین راه حلها رسیدیم.
🔖 هشتگها:
#ASPNETCore #Caching
🏢 معماری چند-مستأجره (Multi-Tenancy) با EF Core (قسمت ۱):
یک دیتابیس برای همه اپلیکیشنهای مدرن SaaS (نرمافزار به عنوان سرویس) یک ویژگی مشترک دارن: چند-مستأجری (Multi-Tenancy).
یعنی یک اپلیکیشن به چندین مشتری (مستأجر یا tenant) سرویس میده، ولی دادههای هر مشتری کاملاً از بقیه ایزوله هست. امروز در قسمت اول از این سری، با این معماری و روش پیادهسازی "یک دیتابیس برای همه" با EF Core آشنا میشیم.
1️⃣ دو رویکرد اصلی برای چند-مستأجری
برای ایزوله کردن دادههای مشتریان، دو راه اصلی وجود داره:
🧠 ایزولهسازی منطقی (Logical Isolation):
یک دیتابیس واحد برای همه مشتریان، که دادهها با یک شناسه مثل TenantId از هم جدا میشن. (موضوع این پست)
🏬 ایزولهسازی فیزیکی (Physical Isolation):
یک دیتابیس کاملاً مجزا برای هر مشتری.
انتخاب بین این دو، به نیاز پروژه بستگی داره. برای صنایعی مثل حوزه سلامت که به ایزولهسازی بالا نیاز دارن، روش دوم الزامیه.
2️⃣ ابزار اصلی ما: EF Core Query Filters 🔍
برای پیادهسازی روش "یک دیتابیس برای همه"، ابزار اصلی ما در EF Core، فیلترهای کوئری سراسری (Global Query Filters) هست. این قابلیت به ما اجازه میده یه شرط WHERE رو به صورت خودکار به تمام کوئریهای یک انتیتی خاص اضافه کنیم. یک بار پیادهسازیش میکنیم و دیگه تقریباً فراموشش میکنیم!
3️⃣ پیادهسازی قدم به قدم
برای این کار به دو چیز نیاز داریم: ۱. راهی برای شناسایی مستأجر فعلی. ۲. راهی برای فیلتر کردن دادهها برای اون مستأجر.
• قدم اول: شناسایی مستأجر با TenantProvider
اول از همه، باید بفهمیم درخواست فعلی برای کدوم مستأجره. ما این منطق رو تو یه کلاس جدا به اسم TenantProvider کپسوله میکنیم. این کلاس، TenantId رو از هدر (X-TenantId) درخواست HTTP میخونه.
public sealed class TenantProvider
{
private const string TenantIdHeaderName = "X-TenantId";
private readonly IHttpContextAccessor _httpContextAccessor;
public TenantProvider(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
public string TenantId => _httpContextAccessor
.HttpContext?
.Request
.Headers[TenantIdHeaderName];
}
💡نکته: راههای امنتر دیگری هم برای گرفتن TenantId وجود داره مثل JWT Claim یا API Key.
• قدم دوم: اعمال فیلتر سراسری در DbContext
حالا که میتونیم مستأجر فعلی رو پیدا کنیم، باید به EF Core بگیم که تمام کوئریها رو بر اساس اون فیلتر کنه. بهترین جا برای این کار، متد OnModelCreating در DbContext ماست.
public class OrdersDbContext : DbContext
{
private readonly string _tenantId;
public OrdersDbContext(
DbContextOptions<OrdersDbContext> options,
TenantProvider tenantProvider)
: base(options)
{
// TenantId رو از TenantProvider میگیریم
_tenantId = tenantProvider.TenantId;
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// این فیلتر به صورت خودکار به تمام کوئریهای انتیتی Order اضافه میشه
modelBuilder
.Entity<Order>()
.HasQueryFilter(o => o.TenantId == _tenantId);
}
}
حالا هر کوئریای که روی جدول Order زده بشه، EF Core به صورت خودکار شرط WHERE TenantId = 'current_tenant_id' رو بهش اضافه میکنه!
🔖 هشتگها:
#CSharp #ASPNETCore #DotNet #MultiTenancy #EntityFrameworkCore #SoftwareArchitecture #Backend
🏢 معماری چند-مستأجره با EF Core (قسمت ۲):
هر مستأجر، دیتابیس خودش
در قسمت اول، روش ایزولهسازی منطقی (یک دیتابیس برای همه) رو با Query Filters پیادهسازی کردیم. این روش برای خیلی از پروژهها عالیه.
اما گاهی اوقات به ایزولهسازی فیزیکی نیاز داریم؛ یعنی هر مشتری، دیتابیس کاملاً مجزای خودش رو داشته باشه تا دادهها به صورت فیزیکی از هم جدا باشن. امروز با این روش پیشرفته و امن آشنا میشیم.
1️⃣ چالش جدید: مدیریت Connection String داینامیک 🔌
در این سناریو، دیگه Query Filters به کارمون نمیاد، چون با دیتابیسهای مختلفی سر و کار داریم. چالش اصلی ما اینه که به ازای هر درخواست، بتونیم DbContext رو با Connection String مخصوص همون مستأجر پیکربندی کنیم.
2️⃣ پیادهسازی قدم به قدم
قدم اول: ذخیره اطلاعات مستأجرها ⚙️
اول از همه، باید اطلاعات هر مستأجر و Connection String دیتابیسش رو یه جا ذخیره کنیم. برای مثال، در فایل appsettings.json:
"Tenants": [
{ "Id": "tenant-1", "ConnectionString": "Host=tenant1.db;Database=db1" },
{ "Id": "tenant-2", "ConnectionString": "Host=tenant2.db;Database=db2" }
]
قدم دوم: آپدیت کردن TenantProvider 🕵️♂️حالا TenantProvider رو آپدیت میکنیم تا علاوه بر TenantId، بتونه Connection String مربوط به مستأجر فعلی رو هم از تنظیمات بخونه و به ما بده.
public sealed class TenantProvider
{
// ... (Properties for HttpContextAccessor and TenantSettings)
public TenantProvider(
IHttpContextAccessor httpContextAccessor,
IOptions<TenantSettings> tenantsOptions)
{
// ...
_tenantSettings = tenantsOptions.Value;
}
public string TenantId => /* ... gets tenantId from header ... */;
public string GetConnectionString()
{
return _tenantSettings.Tenants
.Single(t => t.Id == TenantId).ConnectionString;
}
}
قدم سوم: جادوی DI (ثبت داینامیک DbContext) ✨این مهمترین و جادوییترین بخش کاره! ما DbContext رو جوری در Program.cs ثبت میکنیم که به ازای هر درخواست، اول TenantProvider رو اجرا کنه، Connection String رو بگیره و بعد DbContext رو با اون پیکربندی کنه.
builder.Services.AddDbContext<OrdersDbContext>((serviceProvider, options) =>
{
// TenantProvider رو از DI میگیریم
var tenantProvider = serviceProvider.GetRequiredService<TenantProvider>();
// Connection String مخصوص مستأجر فعلی رو میگیریم
var connectionString = tenantProvider.GetConnectionString();
// و DbContext رو با اون کانفیگ میکنیم
options.UseSqlServer(connectionString);
});
🔐 نکته امنیتی مهم
قرار دادن Connection Stringها به صورت مستقیم در appsettings.json برای محیط پروداکشن امن نیست. همیشه از ابزارهای مدیریت secret مثل Azure Key Vault یا NET User Secrets. برای محیط توسعه استفاده کنید.
🤔 حرف حساب و تجربه شمابا این دو روش
شما الان جعبه ابزار کاملی برای پیادهسازی هر نوع معماری چند-مستأجره در ASP.NET Core دارید.
شما تو پروژههاتون با کدوم مدل چند-مستأجری کار کردید؟ تک دیتابیس یا چند دیتابیس؟ چالشها و مزایای هر کدوم از نظر شما چیه؟
🔖 هشتگها:
#CSharp #ASPNETCore #DotNet #MultiTenancy #EntityFrameworkCore #SoftwareArchitecture #Backend
🚀 چرا Connection String رو مستقیم تو appsettings.json نگذاریم؟
آشنایی کامل با Azure Key Vault
🔐 وقتی توی یک پروژه ASP.NET Core داری کار میکنی، معمولاً Connection String و Credentialهای حساس رو توی appsettings.json میزاری.
❌ اما این کار چند تا مشکل اساسی داره:
📂 فایل پیکربندی معمولاً داخل سورس کنترل (Git) هست → پس هرکسی به مخزن دسترسی داشته باشه، به دادههای حساس هم دسترسی پیدا میکنه.
🛠 حتی اگه تو gitignore. بگذاری، باز هم روی سرور یا محیطهای مشترک ممکنه نشت کنه.
🔓 توی Production، این اطلاعات ممکنه لاگ بشه یا با خطاها لو بره.
✅ راهحل امن و استاندارد: استفاده از Azure Key Vault
🔍 حالا Azure Key Vault چیه؟
یک سرویس ابری از Azure برای ذخیرهسازی امن:
🔑 Secrets (مثل Connection String، API Keys، Tokenها)
🗝 Keys (کلیدهای رمزنگاری)
📜 Certificates (گواهیها)
💯مزیتها:
• مدیریت مرکزی و امن دادههای حساس.
• کنترل سطح دسترسی به کمک Microsoft Entra ID.
• پشتیبانی از Hardware Security Module (HSM).
📦 پکیجهای لازم
dotnet add package Azure.Extensions.AspNetCore.Configuration.Secrets
dotnet add package Azure.Identity
🛠 راهاندازی در حالت Development
برای محیط لوکال، از Secret Manager استفاده کن:
<PropertyGroup>
<UserSecretsId>{GUID}</UserSecretsId>
</PropertyGroup>
اضافه کردن Secret:
dotnet user-secrets set "DbConnection" "Server=.;Database=Test;User Id=sa;Password=1234"
🏭 راهاندازی در Production با Azure Key Vault
1️⃣ ساخت Resource Group و Key Vault
az group create --name "MyGroup" --location "westeurope"
az keyvault create --name "myvault123" --resource-group "MyGroup" --location "westeurope"
2️⃣ ذخیره Secrets
az keyvault secret set --vault-name "myvault123" --name "DbConnection" --value "Server=sql.example.com;Database=Prod;..."
🗂 اتصال ASP.NET Core به Key Vault
روش ۱: با گواهی X.509
builder.Configuration.AddAzureKeyVault(
new Uri($"https://{builder.Configuration["KeyVaultName"]}.vault.azure.net/"),
new ClientCertificateCredential(
builder.Configuration["AzureADDirectoryId"],
builder.Configuration["AzureADApplicationId"],
x509Certificate));
روش 2️⃣ : با Managed Identity (پیشنهادی در Azure)
builder.Configuration.AddAzureKeyVault(
new Uri($"https://{builder.Configuration["KeyVaultName"]}.vault.azure.net/"),
new DefaultAzureCredential());
📌 بهترین نکات امنیتی
🔒 هر محیط (Dev/Prod) باید Key Vault جدا داشته باشه.
📛 از نامگذاری استاندارد برای Secrets استفاده کن (برای بخشها از -- به جای :).
🔄 برای تغییرات حساس، ReloadInterval رو تنظیم کن تا مقادیر بهروزرسانی بشن.
🛑 و Secrets رو هرگز در لاگها چاپ نکن.
🎯 نتیجه
با این روش:
• اطلاعات حساس هرگز در سورسکد یا فایلهای config ذخیره نمیشن.
• در صورت نشت سورس یا فایلها، اطلاعات Production در امانه.
• مدیریت و تغییر مقادیر حساس از طریق Azure Portal یا CLI انجام میشه، بدون نیاز به Deploy دوباره اپلیکیشن.
🔖 هشتگها:
#CSharp #ASPNetCore #Azure #KeyVault #CodeSafety #MicrosoftAzure #ConnectionString
زمانبندی Jobهای تکرارشونده 🔄
برای jobهای پسزمینه تکرارشونده، میتوانید از زمانبندی cron استفاده کنید:
app.MapPost("/api/reminders/schedule/recurring", async (
ISchedulerFactory schedulerFactory,
RecurringReminderRequest request) =>
{
// ... (Job creation is the same) ...
var trigger = TriggerBuilder.Create()
.WithIdentity($"recurring-trigger-{Guid.NewGuid()}", "recurring-reminders")
.WithCronSchedule(request.CronExpression)
.Build();
await scheduler.ScheduleJob(job, trigger);
return Results.Ok();
});تریگرهای Cron قدرتمندتر از تریگرهای ساده هستند. آنها به شما اجازه میدهند زمانبندیهای پیچیدهای مانند "هر روز کاری ساعت ۱۰ صبح" یا "هر ۱۵ دقیقه" را تعریف کنید.
راهاندازی پایداری Job (Job Persistence) 💾
بهطور پیشفرض، Quartz از ذخیرهسازی درون-حافظهای استفاده میکند، که یعنی jobهای شما با ریاستارت شدن اپلیکیشن از بین میروند. برای محیطهای پروداکشن، شما به یک فروشگاه پایدار (persistent store) نیاز دارید.
بیایید ببینیم چگونه ذخیرهسازی پایدار را با ایزولهسازی اسکیمای مناسب راهاندازی کنیم:
builder.Services.AddQuartz(options =>
{
options.AddJob<EmailReminderJob>(c => c
.StoreDurably()
.WithIdentity(EmailReminderJob.Name));
options.UsePersistentStore(persistenceOptions =>
{
persistenceOptions.UsePostgres(cfg =>
{
cfg.ConnectionString = connectionString;
cfg.TablePrefix = "scheduler.qrtz_";
});
persistenceOptions.UseNewtonsoftJsonSerializer();
});
});
تنظیم TablePrefix به سازماندهی جداول Quartz در دیتابیس شما کمک میکند - در این مورد، آنها را در یک اسکیمای اختصاصی scheduler قرار میدهد.
جاب های بادوام (Durable Jobs) 📌
توجه کنید که ما EmailReminderJob را با StoreDurably پیکربندی میکنیم. این یک الگوی قدرتمند است که به شما اجازه میدهد jobهای خود را یک بار تعریف کرده و با تریگرهای مختلف از آنها استفاده مجدد کنید.
public async Task ScheduleReminder(string userId, string message, DateTime scheduledTime)
{
var scheduler = await _schedulerFactory.GetScheduler();
// Reference the stored job by its identity
var jobKey = new JobKey(EmailReminderJob.Name);
var trigger = TriggerBuilder.Create()
.ForJob(jobKey) // Reference the durable job
.WithIdentity($"trigger-{Guid.NewGuid()}")
.UsingJobData("userId", userId)
.UsingJobData("message", message)
.StartAt(scheduledTime)
.Build();
await scheduler.ScheduleJob(trigger); // Note: just passing the trigger
}
خلاصه ✅
راهاندازی صحیح Quartz در NET. شامل موارد بیشتری از صرفاً افزودن پکیج NuGet است.
به این موارد توجه کنید:
🔹 تعریف صحیح job و مدیریت داده با JobDataMap
🔹 راهاندازی زمانبندی jobهای یکباره و تکرارشونده
🔹 پیکربندی ذخیرهسازی پایدار با ایزولهسازی اسکیمای مناسب
🔹 استفاده از jobهای بادوام برای حفظ تعاریف ثابت job
هر یک از این عناصر به یک سیستم پردازش پسزمینه قابل اعتماد کمک میکند که میتواند با نیازهای اپلیکیشن شما رشد کند.
🔖 هشتگها:
#CSharp #DotNet #ASPNETCore #QuartzNet #BackgroundJobs #TaskScheduling #Observability #SystemDesign
استفاده از ProblemDetailsService 🛠
فراخوانی AddProblemDetails یک پیادهسازی پیشفرض از IProblemDetailsService را ثبت میکند. این سرویس میتواند پاسخ را برای ما بنویسد.
در اینجا نحوه استفاده از آن در CustomExceptionHandler آمده است:
public class CustomExceptionHandler(IProblemDetailsService problemDetailsService) : IExceptionHandler
{
public async ValueTask<bool> TryHandleAsync(
HttpContext httpContext,
Exception exception,
CancellationToken cancellationToken)
{
var problemDetails = new ProblemDetails
{
Status = exception switch { /* ... */ },
// ...
};
return await problemDetailsService.TryWriteAsync(new ProblemDetailsContext
{
Exception = exception,
HttpContext = httpContext,
ProblemDetails = problemDetails
});
}
}
سفارشیسازی Problem Details 🎨
ما میتوانیم یک delegate به متد AddProblemDetails پاس دهیم تا CustomizeProblemDetails را تنظیم کنیم. شما میتوانید از این برای افزودن اطلاعات اضافی به تمام پاسخهای Problem Details استفاده کنید.
builder.Services.AddProblemDetails(options =>
{
options.CustomizeProblemDetails = context =>
{
context.ProblemDetails.Instance =
$"{context.HttpContext.Request.iss.onethod} {context.HttpContext.Request.Path}";
context.ProblemDetails.Extensions.TryAdd("requestId", context.HttpContext.TraceIdentifier);
Activity? activity = context.HttpContext.Features.Get<IHttpActivityFeature>()?.Activity;
context.ProblemDetails.Extensions.TryAdd("traceId", activity?.Id);
};
});
این سفارشیسازی مسیر درخواست، یک requestId و یک traceId را به هر پاسخ Problem Details اضافه میکند که قابلیت دیباگ و ردیابی خطاها را افزایش میدهد.
مدیریت استثناهای خاص (کدهای وضعیت) 🆕
حالا 9 Net. یک راه سادهتر برای مپ کردن استثناها به کدهای وضعیت معرفی میکند. شما میتوانید از StatusCodeSelector برای تعریف این مپینگها استفاده کنید.
app.UseExceptionHandler(new ExceptionHandlerOptions
{
StatusCodeSelector = ex => ex switch
{
ArgumentException => StatusCodes.Status400BadRequest,
NotFoundException => StatusCodes.Status404NotFound,
_ => StatusCodes.Status500InternalServerError
}
});
نکات پایانی ✅
پیادهسازی Problem Details در APIهای ASP.NET Core شما بیش از یک رویه بهتر است - این یک استاندارد برای بهبود تجربه توسعهدهنده مصرفکنندگان API شماست. با ارائه پاسخهای خطای یکپارچه، دقیق و با ساختار مناسب، شما درک و مدیریت سناریوهای خطا را برای کلاینتها آسانتر میکنید.
🔖 هشتگها:
#CSharp #DotNet #ASPNETCore #ErrorHandling #ExceptionHandling #WebAPI #ProblemDetails