🚀 کشینگ در 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
✨ ساخت آبجکت مثل یک حرفهای: قدرت Object Initializers در #C
یادتونه قدیما برای ساختن و مقداردهی یه آبجکت، باید چند خط کد پشت سر هم مینوشتیم؟ این روش هم طولانیه و هم ممکنه باعث بشه مقداردهی بعضی پراپرتیها رو فراموش کنیم.
سیشارپ یه راه حل خیلی شیک، مدرن و امن برای این کار داره: Object Initializers.
1️⃣ روش سنتی در برابر روش مدرن
فرض کنید این کلاس Bunny رو داریم:
public class Bunny
{
public string Name;
public bool LikesCarrots;
public bool LikesHumans;
public Bunny() {}
public Bunny(string n) => Name = n;
}
روش قدیمی و چند خطی: 👎
Bunny b1 = new Bunny();
b1.Name = "Bo";
b1.LikesCarrots = true;
b1.LikesHumans = false;
روش مدرن با Object Initializer: 👍
حالا با سینتکس {}، میتونیم تمام این کارها رو در یک دستور و به صورت خیلی خوانا انجام بدیم:
Bunny b2 = new Bunny
{
Name = "Bo",
LikesCarrots = true,
LikesHumans = false
};
این قابلیت حتی با سازندههایی که پارامتر دارن هم کار میکنه:
Bunny b3 = new Bunny("Bo")
{
LikesCarrots = true,
LikesHumans = false
};2️⃣ پشت صحنه چه خبره؟ (نکته حرفهای) ⚙️
شاید فکر کنید این سینتکس فقط یه خلاصه نویسیه، ولی کامپایلر پشت صحنه یه کار هوشمندانه برای امنیت در برابر خطاها (Exception Safety) انجام میده.
اون اول آبجکت رو تو یه متغیر موقت میسازه و بعد پراپرتیها رو ست میکنه. این کار تضمین میکنه که اگه وسط مقداردهی یکی از پراپرتیها خطایی رخ بده، شما هیچوقت با یه آبجکت نصفه و نیمه و ناقص مواجه نمیشید!
🔖 هشتگها:
#CSharp #Programming #Developer #DotNet #OOP #CleanCode #BestPractices
🔬 کالبدشکافی آبجکتها با Deconstructors در #C
تو پست قبلی دیدیم چطور با Object Initializers یه آبجکت رو به صورت شیک "بسازیم". حالا بیاید ببینیم چطور میتونیم یه آبجکت رو به همون زیبایی "بشکافیم" و اجزاش رو استخراج کنیم!
سیشارپ یه قابلیت مدرن و قدرتمند به اسم Deconstructor داره که دقیقاً برعکس سازنده (Constructor) عمل میکنه.
1️⃣ حالا Deconstructor چیست؟
این Deconstructor یه متد خاص به اسم Deconstruct هست که شما تو کلاستون تعریف میکنید. این متد، فیلدها و پراپرتیهای کلاس شما رو به مجموعهای از متغیرهای خروجی (out parameters) تبدیل میکنه.
public class Rectangle
{
public readonly float Width, Height;
public Rectangle(float width, float height)
{
Width = width;
Height = height;
}
// این متد Deconstructor ماست
public void Deconstruct(out float width, out float height)
{
width = Width;
height = Height;
}
}
2️⃣ جادوی سینتکس: کالبدشکافی در یک خط! ✨
حالا که متد Deconstruct رو داریم، #C به ما یه سینتکس فوقالعاده شیک و خوانا برای استفاده ازش میده:
var rect = new Rectangle(3, 4);
// جادو اینجا اتفاق میفته!
// این خط، متد Deconstruct رو صدا میزنه
(float width, float height) = rect;
Console.WriteLine($"Width: {width}, Height: {height}"); // خروجی: Width: 3, Height: 4
این سینتکس، کد شما رو به شدت تمیز و بیانگر میکنه.
3️⃣ ترفندهای خلاصهنویسی
این قابلیت چند تا ترفند برای خلاصهتر شدن هم داره:
استفاده از var:
// کامپایلر خودش نوعها رو تشخیص میده
var (width, height) = rect;
نادیده گرفتن با _ (Discard):
اگه فقط به یکی از مقادیر نیاز دارید، میتونید اون یکی رو با _ نادیده بگیرید:
// ما اینجا فقط به ارتفاع نیاز داریم
var (_, height) = rect;
تخصیص به متغیرهای موجود:
اگه متغیرها رو از قبل دارید، دیگه نیازی به تعریفشون نیست:
float w, h;
(w, h) = rect;
🤔 حرف حساب و تجربه شما
شما تا حالا از Deconstructors تو کدهاتون استفاده کردید؟ به نظرتون بهترین کاربرد این قابلیت کجاست؟
🔖 هشتگها:
#CSharp #Programming #Developer #DotNet #ModernCSharp #CleanCode #BestPractices
🏷 فیلترهای کوئری نامدار در EF 10 (چندین فیلتر کوئری برای هر انتیتی)
فیلترهای کوئری سراسری (global query filters) در Entity Framework Core از دیرباز راهی مناسب برای اعمال شرایط مشترک به تمام کوئریهای یک انتیتی بودهاند. این فیلترها به ویژه در سناریوهایی مانند حذف منطقی (soft deletion) 🗑 و چند-مستأجری (multi-tenancy) 🏢 مفید هستند، جایی که شما میخواهید همان دستور WHERE به صورت خودکار به هر کوئری اضافه شود.
با این حال، نسخههای قبلی EF Core از یک محدودیت بزرگ 😩 رنج میبردند: هر نوع انتیتی فقط میتوانست یک فیلتر تعریف شده داشته باشد. اگر نیاز به ترکیب چندین شرط داشتید، یا باید عبارات && صریح مینوشتید یا فیلترها را به صورت دستی در کوئریهای خاص غیرفعال و دوباره اعمال میکردید.
با EF 10، این وضعیت تغییر میکند. ✨
قابلیت جدید فیلترهای کوئری نامدار (named query filters) به شما امکان میدهد چندین فیلتر را به یک انتیتی متصل کرده و با نام به آنها ارجاع دهید. سپس میتوانید فیلترهای فردی را در صورت نیاز غیرفعال کنید، به جای اینکه همه فیلترها را یکجا خاموش کنید.
بیایید این قابلیت جدید، چرایی اهمیت آن و چند روش عملی برای استفاده از آن را بررسی کنیم.
🔎فیلترهای کوئری (Query Filters) چه هستند؟
اگر مدتی است که از EF Core استفاده میکنید، ممکن است از قبل با فیلترهای کوئری سراسری آشنا باشید. یک فیلتر کوئری، شرطی است که EF به طور خودکار به تمام کوئریها برای یک نوع انتیتی خاص اعمال میکند. در پشت صحنه، EF هر زمان که آن انتیتی کوئری میشود، یک دستور WHERE اضافه میکند. کاربردهای معمول عبارتند از:
• حذف منطقی: فیلتر کردن ردیفهایی که IsDeleted در آنها true است.
• چند-مستأجری: فیلتر کردن بر اساس TenantId تا هر مستأجر فقط دادههای خود را ببیند.
برای مثال، یک فیلتر حذف منطقی ممکن است اینگونه پیکربندی شود:
modelBuilder.Entity<Order>()
.HasQueryFilter(order => !order.IsDeleted);
با وجود این فیلتر، هر کوئری روی Orders به طور خودکار رکوردهای حذف شده منطقی را حذف میکند. برای شامل کردن دادههای حذف شده، میتوانید IgnoreQueryFilters() را روی کوئری فراخوانی کنید. عیب این کار این است که تمام فیلترهای روی آن انتیتی غیرفعال میشوند.
استفاده از چندین فیلتر کوئری ✅
تاکنون، EF فقط یک فیلتر کوئری برای هر انتیتی مجاز میدانست. برای ترکیب فیلترها باید یک عبارت واحد با && مینوشتید:
modelBuilder.Entity<Order>()
.HasQueryFilter(order => !order.IsDeleted && order.TenantId == tenantId);
💡این کار میکند اما غیرفعال کردن انتخابی یک شرط را غیرممکن میسازد. EF 10 یک جایگزین بهتر معرفی میکند: فیلترهای کوئری نامدار.
برای متصل کردن چندین فیلتر به یک انتیتی، HasQueryFilter را با یک نام برای هر فیلتر فراخوانی کنید:
modelBuilder.Entity<Order>()
.HasQueryFilter("SoftDeletionFilter", order => !order.IsDeleted)
.HasQueryFilter("TenantFilter", order => order.TenantId == tenantId);
اکنون میتوانید فقط فیلتر حذف منطقی را خاموش کنید در حالی که فیلتر مستأجر را فعال نگه میدارید:
// تمام سفارشات (شامل حذف شدههای منطقی) برای مستأجر فعلی را برمیگرداند
var allOrders = await context.Orders
.IgnoreQueryFilters(["SoftDeletionFilter"])
.ToListAsync();
💡 نکته: استفاده از ثابتها برای نام فیلترها
فیلترهای نامدار از کلیدهای رشتهای استفاده
میکنند. هاردکد کردن این نامها میتواند باعث ایجاد "رشتههای جادویی" (magic strings) شکننده شود. برای جلوگیری از این مشکل، ثابتها را برای نام فیلترهای خود تعریف کنید.
public static class OrderFilters
{
public const string SoftDelete = nameof(SoftDelete);
public const string Tenant = nameof(Tenant);
}
modelBuilder.Entity<Order>()
.HasQueryFilter(OrderFilters.SoftDelete, order => !order.IsDeleted)
.HasQueryFilter(OrderFilters.Tenant, order => order.TenantId == tenantId);
یک رویه بهتر دیگر، پیچیدن فراخوانی ignore در یک متد توسعه (extension method) است:
public static IQueryable<Order> IncludeSoftDeleted(this IQueryable<Order> query)
=> query.IgnoreQueryFilters([OrderFilters.SoftDelete]);
جمعبندی 📝
معرفی فیلترهای کوئری نامدار در EF 10 یکی از محدودیتهای دیرینه را برطرف میکند. شما اکنون میتوانید:
🔹 چندین فیلتر را به یک انتیتی متصل کرده و آنها را به صورت جداگانه مدیریت کنید.
🔹 فیلترهای خاصی را در یک کوئری LINQ با استفاده از IgnoreQueryFilters(["FilterName"]) به صورت انتخابی غیرفعال کنید.
🔹 الگوهای رایجی مانند حذف منطقی و چند-مستأجری را سادهسازی کنید.
فیلترهای کوئری نامدار میتوانند به ابزاری قدرتمند برای تمیز نگه داشتن کوئریها و کپسولهسازی منطق دامین شما تبدیل شوند.
آنها را در نسخه پیشنمایش امتحان کنید و به این فکر کنید که چگونه میتوانند پایگاه کد شما را سادهتر کنند.
معرفی فیلترهای کوئری نامدار در EF 10 یکی از محدودیتهای دیرینه را برطرف میکند. شما اکنون میتوانید:
🔹 چندین فیلتر را به یک انتیتی متصل کرده و آنها را به صورت جداگانه مدیریت کنید.
🔹 فیلترهای خاصی را در یک کوئری LINQ با استفاده از IgnoreQueryFilters(["FilterName"]) به صورت انتخابی غیرفعال کنید.
🔹 الگوهای رایجی مانند حذف منطقی و چند-مستأجری را سادهسازی کنید.
فیلترهای کوئری نامدار میتوانند به ابزاری قدرتمند برای تمیز نگه داشتن کوئریها و کپسولهسازی منطق دامین شما تبدیل شوند.
آنها را در نسخه پیشنمایش امتحان کنید و به این فکر کنید که چگونه میتوانند پایگاه کد شما را سادهتر کنند.
🔖 هشتگها:
#CSharp #Programming #Developer #DotNet #EntityFrameWork #EF
🏢 معماری چند-مستأجره (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
🔐 ذخیرهسازی امن اسرار برنامه در محیط توسعه در ASP.NET Core
این مقاله توضیح میدهد که چگونه دادههای حساس را برای یک اپلیکیشن ASP.NET Core روی یک ماشین توسعه مدیریت کنید. هرگز پسوردها یا دیگر دادههای حساس را در سورس کد یا فایلهای پیکربندی ذخیره نکنید. اسرار محیط پروداکشن نباید برای توسعه یا تست استفاده شوند. اسرار نباید همراه با اپلیکیشن دیپلوی شوند. اسرار پروداکشن باید از طریق ابزارهای کنترلشده مانند Azure Key Vault 🔑 قابل دسترسی باشند.
متغیرهای محیطی (Environment variables) 🌍
متغیرهای محیطی برای جلوگیری از ذخیرهسازی اسرار برنامه در کد یا فایلهای کانفیگ محلی استفاده میشوند. متغیرهای محیطی، مقادیر پیکربندی را برای تمام منابع پیکربندی که قبلاً مشخص شدهاند، بازنویسی (override) میکنند.
⚠️ هشدار
متغیرهای محیطی معمولاً به صورت متن ساده و رمزنگارینشده ذخیره میشوند. اگر ماشین یا فرآیند به خطر بیفتد، متغیرهای محیطی میتوانند توسط افراد غیرقابل اعتماد قابل دسترسی باشند.
جداکننده : با کلیدهای سلسلهمراتبی متغیرهای محیطی روی همه پلتفرمها کار نمیکند (مثلاً Bash از آن پشتیبانی نمیکند). اما آندرلاین دوتایی (__):
✅ توسط همه پلتفرمها پشتیبانی میشود.
✅ به طور خودکار با یک کالن (:) جایگزین میشود.
ابزار Secret Manager 🕵️♂️
ابزار Secret Manager دادههای حساس را در طول توسعه اپلیکیشن ذخیره میکند. در این زمینه، یک داده حساس، یک سرّ برنامه (app secret) است. اسرار برنامه در مکانی جدا از درخت پروژه ذخیره میشوند و به یک پروژه خاص مرتبط هستند. اسرار برنامه به سورس کنترل چکاین نمیشوند.
⚠️ هشدار
ابزار Secret Manager اسرار ذخیره شده را رمزنگاری نمیکند و نباید به عنوان یک مخزن قابل اعتماد در نظر گرفته شود. این ابزار فقط برای اهداف توسعه است. کلیدها و مقادیر در یک فایل کانفیگ JSON در پوشه پروفایل کاربر ذخیره میشوند.
نحوه کار ابزار Secret Manager 🪄
این ابزار جزئیات پیادهسازی مانند مکان و نحوه ذخیره مقادیر را پنهان میکند. مقادیر در یک فایل JSON در پوشه پروفایل کاربر ماشین محلی ذخیره میشوند:
Windows: 📁 %APPDATA%\Microsoft\UserSecrets\<user_secrets_id>\secrets.json
Linux / macOS: 📁 ~/.microsoft/usersecrets/<user_secrets_id>\secrets.json
کدی ننویسید که به مکان یا فرمت دادههای ذخیره شده با Secret Manager وابسته باشد.
فعال کردن ذخیرهسازی اسرار
از طریق CLI: ❯
در پوشه پروژه، دستور زیر را اجرا کنید:
dotnet user-secrets init
این دستور یک عنصر UserSecretsId به فایل csproj. شما اضافه میکند.
از طریق ویژوال استودیو: 🖱️
در Solution Explorer، روی پروژه راستکلیک کرده و Manage User Secrets را انتخاب کنید.
تنظیم یک سرّ ✍️
یک سرّ از یک کلید و مقدار تشکیل شده است. برای تنظیم آن، از دستور set استفاده کنید:
dotnet user-secrets set "Movies:ServiceApiKey" "12345"
کالن (:) در اینجا نشان میدهد که Movies یک آبجکت با پراپرتی ServiceApiKey است.
دسترسی به یک سرّ 📖
1️⃣ ثبت منبع پیکربندی User Secrets:
در اپلیکیشنهای وب مدرن ASP.NET Core، WebApplication.CreateBuilder(args) به صورت خودکار AddUserSecrets را در محیط Development فراخوانی میکند.
2️⃣ خواندن سرّ از طریق Configuration API: ⚙️
شما میتوانید اسرار را دقیقاً مانند مقادیر appsettings.json از طریق IConfiguration بخوانید.
var builder = WebApplication.CreateBuilder(args);
var movieApiKey = builder.Configuration["Movies:ServiceApiKey"];
var app = builder.Build();
app.MapGet("/", () => movieApiKey);
app.Run();
مثال در یک Page Model:
public class IndexModel : PageModel
{
private readonly IConfiguration _config;
public IndexModel(IConfiguration config)
{
_config = config;
}
public void OnGet()
{
var moviesApiKey = _config["Movies:ServiceApiKey"];
// ...
}
}
مپ کردن اسرار به یک POCO 🔗
مپ کردن یک آبجکت کامل به یک POCO (یک کلاس ساده #C با پراپرتیها) برای جمعآوری پراپرتیهای مرتبط مفید است.فرض کنید فایل secrets.json برنامه شامل دو سرّ زیر است:
{
"Movies:ConnectionString": "Server=(localdb)\\mssqllocaldb;Database=Movie-1;Trusted_Connection=True;MultipleActiveResultSets=true",
"Movies:ServiceApiKey": "12345"
}کد زیر به یک POCO سفارشی به نام MovieSettings متصل شده و به مقدار پراپرتی ServiceApiKey دسترسی پیدا میکند:
var moviesConfig =
Configuration.GetSection("Movies").Get<MovieSettings>();
var _moviesApiKey = moviesConfig.ServiceApiKey;
اسرار Movies:ConnectionString و Movies:ServiceApiKey به پراپرتیهای مربوطه در MovieSettings مپ میشوند:
public class MovieSettings
{
public string ConnectionString { get; set; }
public string ServiceApiKey { get; set; }
}
جایگزینی رشته با اسرار 🛠️
ذخیره پسوردها به صورت متن ساده ناامن است. پسورد را به عنوان یک سرّ ذخیره کنید و در زمان اجرا آن را به رشته اتصال اضافه کنید.
dotnet user-secrets set "DbPassword" "<secret value>"
using System.Data.SqlClient;
var builder = WebApplication.CreateBuilder(args);
var conStrBuilder = new SqlConnectionStringBuilder(
builder.Configuration.GetConnectionString("Movies"));
conStrBuilder.Password = builder.Configuration["DbPassword"];
var connection = conStrBuilder.ConnectionString;
var app = builder.Build();
app.MapGet("/", () => connection);
app.Run();
📑 دستورات CLI
لیست کردن اسرار list 📋
dotnet user-secrets list
حذف یک سرّ واحد remove 🗑️
dotnet user-secrets remove "Movies:ConnectionString"
حذف تمام اسرار clear ♻️
dotnet user-secrets clear
مدیریت اسرار کاربر با ویژوال استودیو :
برای مدیریت اسرار کاربر در ویژوال استودیو، در Solution Explorer روی پروژه راستکلیک کرده و Manage User Secrets را انتخاب کنید.
اسرار کاربر در اپلیکیشنهای غیر وب 🖥️
برای اپلیکیشنهایی مانند Console App، باید پکیجهای NuGet 📦 زیر را به صورت دستی اضافه کنید:
با استفاده از PowerShell:
Install-Package Microsoft.Extensions.Configuration
Install-Package Microsoft.Extensions.Configuration.UserSecrets
با استفاده از .NET CLI:
dotnet add package Microsoft.Extensions.Configuration
dotnet add package Microsoft.Extensions.Configuration.UserSecrets
پس از نصب پکیجها، پروژه را مقداردهی اولیه کرده و اسرار را همانند یک اپلیکیشن وب تنظیم کنید. مثال زیر یک اپلیکیشن کنسول را نشان میدهد که مقدار یک سرّ را که با کلید "AppSecret" تنظیم شده، بازیابی میکند:
using Microsoft.Extensions.Configuration;
class Program
{
static void Main(string[] args)
{
IConfigurationRoot config = new ConfigurationBuilder()
.AddUserSecrets<Program>()
.Build();
Console.WriteLine(config["AppSecret"]);
}
}
🚀 چرا 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
📌الگوی Saga چی هست و چرا بهش نیاز داریم؟
[Saga distributed transactions pattern]
🔹الگوی طراحی Saga به حفظ سازگاری دادهها در سیستمهای توزیعشده با هماهنگسازی تراکنشها بین چندین سرویس کمک میکند.
🔹یک Saga دنبالهای از تراکنشهای محلی (local transactions) است که در آن هر سرویس عملیات خود را انجام داده و مرحله بعد را از طریق رویدادها یا پیامها آغاز میکند
🔹اگر مرحلهای از این دنباله با شکست مواجه شود، Saga مجموعهای از تراکنشهای جبرانی (compensating transactions) را برای برگرداندن مراحل انجامشده اجرا میکند. این روش به حفظ سازگاری دادهها کمک میکند.
📍زمینه و مشکل (Context and problem)
🔹یک تراکنش، واحدی از کار است که میتواند شامل چندین عملیات باشد.
🔹در یک تراکنش، رویداد (event) به تغییر وضعیت اشاره دارد که بر یک موجودیت تأثیر میگذارد.
🔹یک فرمان (command) تمام اطلاعات لازم برای انجام یک عمل یا ایجاد رویداد بعدی را در خود دارد.
⚖️تراکنشها باید به اصول ACID پایبند باشند:
1️⃣ اتمی بودن (Atomicity): همه عملیاتها موفق میشوند یا هیچ عملیاتی موفق نمیشود.
2️⃣ سازگاری (Consistency): دادهها از یک حالت معتبر به حالت معتبر دیگری منتقل میشوند.
3️⃣ انزوا (Isolation): تراکنشهای همزمان نتایجی مشابه اجرای ترتیبی ایجاد میکنند.
4️⃣ ماندگاری (Durability): تغییرات پس از ثبت، حتی در صورت بروز خطا، باقی میمانند.
در یک سرویس واحد، تراکنشها اصول ACID را رعایت میکنند زیرا در یک پایگاه داده واحد عمل میکنند. با این حال، دستیابی به انطباق ACID در چندین سرویس دشوارتر است.
🏗چالشها در معماریهای مایکروسرویس
(Challenges in microservices architectures)
معماریهای مایکروسرویس معمولاً یک پایگاه داده اختصاصی به هر سرویس اختصاص میدهند. این روش مزایای زیر را دارد:
• هر سرویس دادههای خود را کپسوله میکند.
• هر سرویس میتواند فناوری و ساختار پایگاه داده مناسب خود را استفاده کند.
• پایگاه دادههای هر سرویس به صورت مستقل مقیاسپذیر هستند.
• خطا در یک سرویس از سایر سرویسها جدا میشود.
⚠️با وجود این مزایا، این معماری سازگاری داده بین سرویسها را پیچیده میکند. تضمینهای سنتی پایگاه داده مانند ACID به طور مستقیم در چندین پایگاه داده مستقل قابل اجرا نیستند. به همین دلیل، معماریهایی که به ارتباط بین پردازشی (IPC) یا مدلهای تراکنش سنتی مانند two-phase commit protocol وابستهاند، اغلب برای الگوی Saga مناسبتر هستند.
💡راهکار (Solution)
الگوی Saga تراکنشها را با تقسیم آنها به مجموعهای از تراکنشهای محلی مدیریت میکند.
هر تراکنش محلی:
• کار خود را بهصورت اتمی در یک سرویس انجام میدهد.
• پایگاه داده سرویس را بهروزرسانی میکند.
• تراکنش بعدی را از طریق رویداد یا پیام آغاز میکند.
• اگر یک تراکنش محلی شکست بخورد، Saga مجموعهای از تراکنشهای جبرانی را اجرا میکند تا تغییرات تراکنشهای قبلی را برگرداند.
🔑 مفاهیم کلیدی در الگوی Saga
(Key concepts in the Saga pattern)
تراکنشهای قابل جبران (Compensable Transactions):
تراکنشهایی که میتوان آنها را توسط تراکنشهای دیگر با اثر معکوس، جبران یا لغو کرد. اگر یک مرحله شکست بخورد، تراکنشهای جبرانی تغییرات اعمالشده را برمیگردانند.
تراکنش محوری (Pivot Transaction):
نقطه بدون بازگشت در Saga است. بعد از موفقیت تراکنش محوری، تراکنشهای جبرانی دیگر کاربردی ندارند و همه اقدامات بعدی باید برای رسیدن سیستم به حالت سازگار نهایی تکمیل شوند. تراکنش محوری میتواند:
• آخرین تراکنش قابل جبران باشد.
•اولین عملیات قابل تکرار (Retryable) در Saga باشد.
تراکنشهای قابل تکرار (Retryable Transactions):
پس از تراکنش محوری اجرا میشوند، idempotent هستند و تضمین میکنند که حتی در صورت بروز خطاهای موقت، Saga به حالت سازگار برسد.
رویکردهای پیادهسازی Saga
(Saga implementation approaches)
دو رویکرد رایج برای پیادهسازی Saga وجود دارد: Choreography و Orchestration.
1️⃣ Choreography
در این رویکرد، سرویسها بدون کنترلکننده مرکزی، از طریق رویدادها با هم تعامل دارند. هر تراکنش محلی یک رویداد دامنه منتشر میکند که تراکنش محلی دیگری در سرویس دیگر را فعال میکند.
مزایا:
✅مناسب برای جریانهای کاری ساده با سرویسهای کم و بدون نیاز به منطق هماهنگی.
✅عدم نیاز به سرویس اضافی برای هماهنگی.
✅بدون نقطه شکست مرکزی (distributed responsibility).
معایب:
⚠️با اضافه شدن مراحل جدید، جریان کاری پیچیده میشود.
⚠️ریسک ایجاد وابستگی حلقهای بین سرویسها.
⚠️تست یکپارچگی دشوار به دلیل نیاز به اجرای همه سرویسها برای شبیهسازی تراکنش.
2️⃣ Orchestration
در این رویکرد، یک کنترلکننده مرکزی (Orchestrator) مسئول مدیریت همه تراکنشها است و بر اساس رویدادها به سرویسها میگوید چه عملیاتی انجام دهند.
مزایا:
✅مناسب برای جریانهای کاری پیچیده یا زمانی که سرویسهای جدید اضافه میشوند.
✅جلوگیری از وابستگی حلقهای.
✅جداسازی واضح وظایف.
معایب:
⚠️ نیاز به پیادهسازی منطق هماهنگی (design complexity).
⚠️وجود نقطه شکست مرکزی.
🛠مشکلات و ملاحظات
(Problems and considerations)
● تغییر در تفکر طراحی (Design Thinking Shift)
● دشواری در دیباگ بهخصوص با افزایش سرویسها
● غیرقابل بازگشت بودن تغییرات محلی دیتابیس
● نیاز به مدیریت خطاهای موقت و تضمین idempotency
● نیاز به مانیتورینگ و ردیابی جریان Saga
● محدودیت در تراکنشهای جبرانی (همیشه موفق نمیشوند)
⚠️ناهنجاریهای دادهای در Saga
(Potential data anomalies in sagas)
به دلیل مدیریت مستقل دادهها توسط سرویسها، نبود ایزولهسازی بین سرویسها میتواند منجر به ناسازگاری داده شود:
📍بهروزرسانیهای از دست رفته (Lost Updates)
📍خواندن کثیف (Dirty Reads)
📍خواندن غیرتکرارپذیر (Nonrepeatable Reads)
🛡راهکارهای جلوگیری از ناهنجاریها
(Strategies to address data anomalies)
🔐 قفل معنایی (Semantic Lock)
🔄 بهروزرسانیهای جابجاپذیر (Commutative Updates)
🧑💻 دید بدبینانه (Pessimistic View)
📖 خواندن مجدد مقادیر (Reread Values)
🗃 نسخهبندی رکوردها (Version Files)
📌زمان استفاده از این الگو
(When to use this pattern)
مناسب:
✔️نیاز به سازگاری داده در سیستم توزیعشده بدون کوپلینگ شدید
✔️نیاز به جبران تغییرات در صورت شکست یک عملیات
نامناسب:
❌تراکنشهای به شدت وابسته
❌نیاز به جبران تراکنشها در مراحل اولیه
❌وجود وابستگی حلقهای
🔖 هشتگها:
#Microservices #SagaPattern #SoftwareArchitecture