پیادهسازی الگوی Outbox 📬
در سیستمهای توزیعشده، ما اغلب با چالش همگامسازی دیتابیس و سیستمهای خارجی مواجه هستیم. تصور کنید سفارشی را در دیتابیس ذخیره کرده و سپس پیامی را به یک message broker منتشر میکنید. اگر هر یک از این عملیاتها شکست بخورد، سیستم شما در وضعیتی ناسازگار (inconsistent) قرار میگیرد.
الگوی Outbox این مشکل را با در نظر گرفتن انتشار پیام به عنوان بخشی از تراکنش دیتابیس شما حل میکند. به جای انتشار مستقیم پیامها، ما آنها را در یک جدول Outbox در دیتابیس خود ذخیره میکنیم و از عملیات اتمیک اطمینان حاصل میکنیم. سپس یک فرآیند جداگانه این پیامها را به طور قابل اعتماد منتشر میکند.
در این مقاله، ما به پیادهسازی این الگو در NET.، از راهاندازی تا مقیاسپذیری، خواهیم پرداخت.
چرا به الگوی Outbox نیاز داریم؟ 🤔
الگوی transactional Outbox یک مشکل رایج را در سیستمهای توزیعشده حل میکند. این مشکل زمانی رخ میدهد که شما باید دو کار را همزمان انجام دهید: ذخیره داده و ارتباط با یک کامپوننت خارجی.
برای مثال، یک میکروسرویس را تصور کنید که باید:
🔹️ یک سفارش جدید را در دیتابیس خود ذخیره کند.
🔹️ در مورد این سفارش جدید به سیستمهای دیگر اطلاع دهد.
🔹️ اگر یکی از این مراحل شکست بخورد، سیستم شما میتواند در وضعیتی ناسازگار قرار گیرد.
// کد با مشکل سازگاری بالقوه
public async Task<OrderDto> Handle(CreateOrderCommand request, CancellationToken cancellationToken)
{
// ...
await unitOfWork.CommitAsync(cancellationToken);
// تراکنش دیتابیس در این نقطه کامل شده است
// 💥 اگر برنامه اینجا کرش کند یا event bus در دسترس نباشد چه؟
await eventBus.Send(new OrderCreatedIntegrationEvent(order.Id));
return new OrderDto { Id = order.Id, Total = order.Total };
}
الگوی transactional Outbox به حل این مشکل کمک میکند. 📈 ما هم سفارش و هم پیام Outbox را در یک تراکنش دیتابیس واحد ذخیره میکنیم. این یک عملیات "همه یا هیچ" است.
سپس یک پردازشگر Outbox جداگانه، ارسال واقعی پیام را مدیریت میکند. ⚠️ نکته مهمی که باید در اینجا متوجه شوید این است که الگوی Outbox به ما تحویل حداقل-یکباره (at-least-once delivery) را میدهد. این یعنی ما باید مصرفکنندگان پیام خود را idempotent بسازیم.
پیادهسازی الگوی Outbox 💾
ابتدا، جدول Outbox خود را ایجاد میکنیم:
CREATE TABLE outbox_messages (
id UUID PRIMARY KEY,
type VARCHAR(255) NOT NULL,
content JSONB NOT NULL,
occurred_on_utc TIMESTAMP WITH TIME ZONE NOT NULL,
processed_on_utc TIMESTAMP WITH TIME ZONE NULL,
error TEXT NULL
);
و کلاس #C برای نمایش ورودی Outbox:
public sealed class OutboxMessage
{
public Guid Id { get; init; }
public string Type { get; init; }
public string Content { get; init; }
public DateTime OccurredOnUtc { get; init; }
public DateTime? ProcessedOnUtc { get; init; }
public string? Error { get; init; }
}
✨️یک رویکرد زیبا برای پیادهسازی این، استفاده از domain events برای نمایش نوتیفیکیشنها است. قبل از تکمیل تراکنش، میتوانید تمام رویدادها را برداشته و آنها را به عنوان پیامهای Outbox ذخیره کنید.
پردازش Outbox ⚙️
پردازشگر Outbox کامپوننت بعدی است که به آن نیاز داریم. این میتواند یک فرآیند فیزیکی جداگانه یا یک background worker در همان فرآیند باشد.
من از Quartz برای زمانبندی background jobها برای پردازش Outbox استفاده خواهم کرد.
[DisallowConcurrentExecution]
public class OutboxProcessorJob(...) : IJob
{
public async Task Execute(IJobExecutionContext context)
{
// پیامهای پردازش نشده را از دیتابیس واکشی میکند
var messages = await connection.QueryAsync<OutboxMessage>(...);
foreach (var message in messages)
{
try
{
// پیام را deserialize کرده و به message broker منتشر میکند
await publishEndpoint.Publish(deserializedMessage);
// پیام را به عنوان پردازش شده علامتگذاری میکند
await connection.ExecuteAsync(
"UPDATE outbox_messages SET processed_on_utc = @ProcessedOnUtc WHERE id = @Id",
...);
}
catch (Exception ex)
{
// خطا را لاگ میکند
}
}
await transaction.CommitAsync();
}
}
یک راه جایگزین برای پردازش پیامهای Outbox، استفاده از Transaction log tailing است. ما میتوانیم این را با استفاده از Postgres logical replication پیادهسازی کنیم.
ملاحظات و مزایا و معایب 🧐
الگوی Outbox، ضمن کارآمدی، پیچیدگی و نوشتنهای اضافی در دیتابیس را به همراه دارد.
🔹 توصیه میکنم مکانیزمهای تلاش مجدد (retry) را در پردازشگر Outbox برای بهبود قابلیت اطمینان پیادهسازی کنید.
🔹 ضروری است که مصرفکنندگان پیام idempotent را پیادهسازی کنید.
🔹 با گذشت زمان، جدول Outbox میتواند به طور قابل توجهی رشد کند. مهم است که یک استراتژی آرشیو از همان ابتدا پیادهسازی کنید.
مقیاسپذیری پردازش Outbox 🚀
با رشد سیستم شما، ممکن است یک پردازشگر Outbox نتواند حجم پیامها را مدیریت کند.
🔹 یک رویکرد ساده، افزایش فرکانس اجرای job پردازشگر Outbox است.
🔹 یک استراتژی مؤثر دیگر، افزایش اندازه بچ (batch size) هنگام واکشی پیامهای پردازش نشده است.
🔹 برای سیستمهای با حجم بالا، پردازش Outbox به صورت موازی میتواند بسیار مؤثر باشد. یک مکانیزم قفلگذاری برای ادعای بچهای پیام پیادهسازی کنید.
جمعبندی ✅
الگوی Outbox یک ابزار قدرتمند برای حفظ سازگاری داده در سیستمهای توزیعشده است. با جداسازی عملیات دیتابیس از انتشار پیام، الگوی Outbox تضمین میکند که سیستم شما حتی در مواجهه با شکستها، قابل اعتماد باقی بماند.
به یاد داشته باشید که مصرفکنندگان خود را idempotent نگه دارید، استراتژیهای مقیاسپذیری مناسب را پیادهسازی کنید، و رشد جدول Outbox خود را مدیریت کنید.
🔖 هشتگها:
#SoftwareArchitecture #SystemDesign #Microservices #DistributedSystems #OutboxPattern #EventDrivenArchitecture #DataConsistency
📖 سری آموزشی کتاب C# 12 in a Nutshell
وقتی کلاسهای فرزند میخوان آبجکت بسازن، چه اتفاقی پشت صحنه میفته؟ امروز به عمیقترین قوانین ساخت و ساز در سلسلهمراتب وراثت شیرجه میزنیم.
base
به شما اجازه میده از داخل کلاس فرزند، به اعضای کلاس پدر دسترسی داشته باشید. دو کاربرد اصلی داره:
صدا زدن عضو بازنویسی شده:
اگه متدی رو override کردید، با base میتونید به پیادهسازی اصلی در کلاس پدر دسترسی پیدا کنید.
صدا زدن سازنده پدر:
با سینتکس : base(...) صراحتاً سازنده مورد نظرتون در کلاس پدر رو صدا میزنید.
• قانون اول: سازنده کلاس پدر همیشه قبل از سازنده کلاس فرزند اجرا میشه.
• قانون دوم (تله مهم): اگه شما : base(...) رو ننویسید، #C به صورت خودکار سعی میکنه سازنده بدون پارامتر پدر رو صدا بزنه. اگه کلاس پدر سازنده بدون پارامتر نداشته باشه، خطای زمان کامپایل میگیرید!
جایگزین مدرن ✨
گاهی وقتا زنجیره سازندهها خیلی طولانی میشه. از #C 11 به بعد، میتونید پراپرتیها یا فیلدهایی رو با کلمه کلیدی required مشخص کنید. این یعنی هر کسی که از کلاس شما آبجکت میسازه، مجبوره که این اعضا رو با استفاده از Object Initializer مقداردهی کنه.
💡نکته: برای اینکه به یک سازنده اجازه بدید این قانون رو دور بزنه، میتونید از اتریبیوت
وقتی یه آبجکت فرزند ساخته میشه، ترتیب اجرای دقیق عملیات به این صورته:
فاز اول (از فرزند به پدر):
•فیلدهای کلاس فرزند مقداردهی اولیه میشن.
• آرگومانهای پاس داده شده به base(...) ارزیابی میشن.
• این روند تا بالاترین کلاس در سلسلهمراتب ادامه پیدا میکنه.
فاز دوم (از پدر به فرزند):
• حالا بدنهی سازندهها، از کلاس پدر شروع شده و به سمت کلاس فرزند اجرا میشن.
مثال کتاب برای درک بهتر این ترتیب:
🤔 حرف حساب و تجربه شما
درک این جزئیات، شما رو به یک متخصص واقعی در طراحی کلاسهای شیءگرا تبدیل میکنه.
🏛 کالبدشکافی وراثت در #C: سازندهها، required و ترتیب اجرا
وقتی کلاسهای فرزند میخوان آبجکت بسازن، چه اتفاقی پشت صحنه میفته؟ امروز به عمیقترین قوانین ساخت و ساز در سلسلهمراتب وراثت شیرجه میزنیم.
1️⃣ کلیدواژه base: صحبت با کلاس پدر
base
به شما اجازه میده از داخل کلاس فرزند، به اعضای کلاس پدر دسترسی داشته باشید. دو کاربرد اصلی داره:
صدا زدن عضو بازنویسی شده:
اگه متدی رو override کردید، با base میتونید به پیادهسازی اصلی در کلاس پدر دسترسی پیدا کنید.
public override decimal Liability => base.Liability + Mortgage;
صدا زدن سازنده پدر:
با سینتکس : base(...) صراحتاً سازنده مورد نظرتون در کلاس پدر رو صدا میزنید.
public class Subclass : Baseclass
{
public Subclass(int x) : base(x) { }
}
2️⃣ قوانین سازندهها در وراثت ⚖️
• قانون اول: سازنده کلاس پدر همیشه قبل از سازنده کلاس فرزند اجرا میشه.
• قانون دوم (تله مهم): اگه شما : base(...) رو ننویسید، #C به صورت خودکار سعی میکنه سازنده بدون پارامتر پدر رو صدا بزنه. اگه کلاس پدر سازنده بدون پارامتر نداشته باشه، خطای زمان کامپایل میگیرید!
3️⃣ required members (از C# 11):
جایگزین مدرن ✨
گاهی وقتا زنجیره سازندهها خیلی طولانی میشه. از #C 11 به بعد، میتونید پراپرتیها یا فیلدهایی رو با کلمه کلیدی required مشخص کنید. این یعنی هر کسی که از کلاس شما آبجکت میسازه، مجبوره که این اعضا رو با استفاده از Object Initializer مقداردهی کنه.
public class Asset
{
public required string Name;
}
// ✅ درسته
Asset a1 = new Asset { Name = "House" };
// ❌ خطای زمان کامپایل! چون Name مقداردهی نشده
Asset a2 = new Asset();
💡نکته: برای اینکه به یک سازنده اجازه بدید این قانون رو دور بزنه، میتونید از اتریبیوت
[SetsRequiredMembers] استفاده کنید.4️⃣ ترتیب دقیق ساخت (Deep Dive) 🔬
وقتی یه آبجکت فرزند ساخته میشه، ترتیب اجرای دقیق عملیات به این صورته:
فاز اول (از فرزند به پدر):
•فیلدهای کلاس فرزند مقداردهی اولیه میشن.
• آرگومانهای پاس داده شده به base(...) ارزیابی میشن.
• این روند تا بالاترین کلاس در سلسلهمراتب ادامه پیدا میکنه.
فاز دوم (از پدر به فرزند):
• حالا بدنهی سازندهها، از کلاس پدر شروع شده و به سمت کلاس فرزند اجرا میشن.
مثال کتاب برای درک بهتر این ترتیب:
public class B
{
int x = 1; // مرحله ۳: این اجرا میشه
public B(int x)
{
// مرحله ۴: بدنه سازنده پدر اجرا میشه
}
}
public class D : B
{
int y = 1; // مرحله ۱: این اول از همه اجرا میشه
public D(int x)
: base(x + 1) // مرحله ۲: آرگومان base ارزیابی میشه
{
// مرحله ۵: بدنه سازنده فرزند در آخر اجرا میشه
}
}
🤔 حرف حساب و تجربه شما
درک این جزئیات، شما رو به یک متخصص واقعی در طراحی کلاسهای شیءگرا تبدیل میکنه.
🔖 هشتگها:
#OOP #Inheritance #BestPractices #Constructor
C# Geeks (.NET)
الگوی CQRS به روشی که از ابتدا باید میبود 🚀 📢 MediatR در حال تجاری شدن است. جیمی بوگارد اعلام کرد که MediatR برای شرکتهای بالاتر از یک اندازه مشخص، یک مدل لایسنس تجاری اتخاذ خواهد کرد. برای بسیاری از تیمها، این یک محرک برای ارزیابی مجدد استفاده از آن…
پست بالا رو حتما برسی کنید🤍
✨️Here is how to implement CQRS in the application.
📌Free Clean Architecture Template(Link)
✨️Here is how to implement CQRS in the application.
📌Free Clean Architecture Template(Link)
Enumها به عنوان رشته در EF Core:
کدی خواناتر برای دیتابیس شما 📜
وقتی از Enumها در Entity Framework Core استفاده میکنید، به صورت پیشفرض به شکل عدد (0, 1, 2) در دیتابیس ذخیره میشن. این کار از نظر پرفورمنس خوبه، ولی وقتی مستقیم به دیتابیس نگاه میکنید، این عددها هیچ معنایی ندارن! 🧐
اما یه راه حل خیلی ساده و تمیز برای افزایش خوانایی و قابلیت نگهداری دیتابیس وجود داره: ذخیره کردن Enumها به صورت رشته.
جادوی HasConversion ✨
با استفاده از متد HasConversion در Fluent API، میتونید به راحتی به EF Core بگید که مقادیر Enum رو به جای عدد، به صورت نام رشتهای اونها ذخیره کنه.
1️⃣ Enum شما:
public enum OrderStatus
{
Pending,
Completed,
Cancelled
}
2️⃣ انتیتی شما:
public class Order
{
public int Id { get; set; }
public OrderStatus Status { get; set; }
}
3️⃣ پیکربندی در DbContext:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Order>()
.Property(o => o.Status)
.HasConversion<string>(); // تمام جادو اینجاست!
}
حالت پیشفرض (بدون HasConversion): 👎
| Id | Status |
| :-- | :--- |
| 1 | 0 |
| 2 | 1 |
حالت جدید (با HasConversion): 👍
| Id | Status |
| :-- | :--- |
| 1 | "Pending" |
| 2 | "Completed" |
🤔 حرف حساب و تجربه شما
این تغییر کوچیک، دیباگ کردن و کار مستقیم با دیتابیس رو خیلی راحتتر میکنه. با اینکه ذخیرهسازی به صورت عدد کمی بهینهتره، اما در اکثر پروژهها، خوانایی بالاتر Enum به صورت رشته، ارزشش رو داره.
</Link>
🔖 هشتگها:
#EntityFrameworkCore #EFCore #Database
6️⃣ نکته برای بهبود مهارتهای نرم به عنوان یک مهندس نرمافزار 💡
1️⃣ خودآگاهی (Self-Awareness)
بازخورد گرفتن از منتور، همکاران و مدیر، یک ابزار عالی برای ساختن خودآگاهیه.
2️⃣ ارتباطات (Communication)
ارتباط واضح و موثر، برای موفقیت در توسعه نرمافزار حیاتیه.
🔹 اطلاعات پیچیده رو به بخشهای کوچیکتر تقسیم کنید.
🔹 از روشهای ارتباطی مناسب استفاده کنید (چت، تماس، جلسه).
🔹 مخاطب خودتون رو در نظر بگیرید و پیامتون رو متناسب با اونها تنظیم کنید.
3️⃣ گوش دادن فعال (Active Listening)
این بخش ضروری از ارتباطه و فقط به معنی شنیدن نیست، بلکه درک کردن پیامه.
🔹 سوالات شفافکننده بپرسید.
🔹 تماس چشمی برقرار کنید.
🔹 چیزی که شنیدید رو خلاصه کنید تا مطمئن بشید درست فهمیدید.
🔹 قبل از اینکه سریع جواب بدید، یکی دو ثانیه مکث کنید.
4️⃣ ایجاد همدلی (Empathy)
همدلی یعنی توانایی درک احساسات دیگران. ایجاد همدلی با کاربران به شما کمک میکنه نیازهاشون رو بهتر بفهمید.
🔹 تحقیقات کاربر انجام بدید.
🔹 خودتون رو جای اونها بذارید! 👟
🔹 بازخوردهای مشتریان رو برای درک بهتر، مرور کنید.
🔹 از ابزارهای آنالیتیکس برای مشاهده رفتار مشتری استفاده کنید.
5️⃣ همکاری (Collaboration)
کار تیمی موثر برای موفقیت پروژهها حیاتیه. حتی اگه تنها دولوپر تیم باشید، باز هم باید با بقیه ذینفعان همکاری کنید!
🔹 تنوع رو بپذیرید! 🧑🤝🧑
🔹 فرهنگ همکاری رو تقویت کنید.
🔹 روی ایجاد اعتماد و احترام تمرکز کنید.
🔹 مسئولیتپذیر باشید و در مورد "شکستها" شفاف باشید.
6️⃣ سازگاری (Adaptability)
پذیرای تغییر اولویتها و تطبیق با نیازمندیهای جدید باشید. من نمیتونم چیزهای زیادی رو به شما قول بدم، جز اینکه اولویتها همیشه تغییر خواهند کرد.
🔹 بپذیرید که تغییر، اجتنابناپذیره. 🔄
🔹 انعطافپذیری رو با نیازهای بیزینس متعادل کنید.
🔹 پذیرای بازخورد و انتقاد سازنده باشید.
🔹 روی راههای موفقیت تمرکز کنید، نه روی موانع. 🚀
string link ="Link";
📖 سری آموزشی کتاب C# 12 in a Nutshell
در دنیای شیءگرایی، دو تا از قدرتمندترین مفاهیم، Overloading (چند متد با اسم یکسان) و Polymorphism (چندریختی) هستن. اما وقتی این دو با هم روبرو میشن، چه اتفاقی میفته؟ کامپایلر چطور تصمیم میگیره کدوم متد رو اجرا کنه؟
جواب این سوال، یکی از ظریفترین و مهمترین نکات #C هست که درک عمیق شما از زبان رو نشون میده.
فرض کنید این کلاسها و دو متد Foo رو داریم که override نشدن، بلکه overload شدن:
قانون اینه: انتخاب بین متدهای Overload شده، در زمان کامپایل و بر اساس نوع متغیر شما اتفاق میفته، نه نوع واقعی آبجکتی که در زمان اجرا داخل اون متغیره.
این رفتار با override کردن کاملاً متفاوته!
حالا اگه بخوایم این تصمیم رو به زمان اجرا موکول کنیم تا بر اساس نوع واقعی آبجکت تصمیم گرفته بشه، میتونیم متغیر رو به dynamic کست کنیم. این کار به DLR (Dynamic Language Runtime) میگه که در زمان اجرا، بهترین متد رو پیدا کنه.
🤔 حرف حساب و تجربه شما
این تفاوت ظریف، نشون میده که Overloading یک نوع چندریختی در زمان کامپایل (Static Polymorphism) هست، در حالی که override کردن، چندریختی در زمان اجرا (Dynamic Polymorphism) رو پیاده میکنه.
⚔️ دوئل Overloading و Polymorphism: کدام متد اجرا میشود؟
در دنیای شیءگرایی، دو تا از قدرتمندترین مفاهیم، Overloading (چند متد با اسم یکسان) و Polymorphism (چندریختی) هستن. اما وقتی این دو با هم روبرو میشن، چه اتفاقی میفته؟ کامپایلر چطور تصمیم میگیره کدوم متد رو اجرا کنه؟
جواب این سوال، یکی از ظریفترین و مهمترین نکات #C هست که درک عمیق شما از زبان رو نشون میده.
1️⃣ صحنه نبرد: دو متد Overload شده
فرض کنید این کلاسها و دو متد Foo رو داریم که override نشدن، بلکه overload شدن:
public class Asset { }
public class House : Asset { }
static void Foo(Asset a) => Console.WriteLine("Foo(Asset) called");
static void Foo(House h) => Console.WriteLine("Foo(House) called");2️⃣ قانون بازی: انتخاب در زمان کامپایل! ⚖️
قانون اینه: انتخاب بین متدهای Overload شده، در زمان کامپایل و بر اساس نوع متغیر شما اتفاق میفته، نه نوع واقعی آبجکتی که در زمان اجرا داخل اون متغیره.
این رفتار با override کردن کاملاً متفاوته!
House h = new House();
// اینجا نوع متغیر h در زمان کامپایل House است، پس کامپایلر Foo(House) رو انتخاب میکنه.
Foo(h);
// خروجی: Foo(House) called
Asset a = new House();
// مهم! اینجا با اینکه آبجکت داخل a از نوع House است،
// اما نوع متغیر a در زمان کامپایل Asset است!
// پس کامپایلر، Foo(Asset) رو انتخاب میکنه.
Foo(a);
// خروجی: Foo(Asset) called
3️⃣ راه فرار: استفاده از dynamic 🚀
حالا اگه بخوایم این تصمیم رو به زمان اجرا موکول کنیم تا بر اساس نوع واقعی آبجکت تصمیم گرفته بشه، میتونیم متغیر رو به dynamic کست کنیم. این کار به DLR (Dynamic Language Runtime) میگه که در زمان اجرا، بهترین متد رو پیدا کنه.
Asset a = new House();
Foo((dynamic)a);
// خروجی: Foo(House) called
🤔 حرف حساب و تجربه شما
این تفاوت ظریف، نشون میده که Overloading یک نوع چندریختی در زمان کامپایل (Static Polymorphism) هست، در حالی که override کردن، چندریختی در زمان اجرا (Dynamic Polymorphism) رو پیاده میکنه.
🔖 هشتگها:
#DotNet #OOP #Polymorphism #Overloading
5️⃣ قابلیت EF Core که باید بدانید 💡
بیایید صادق باشیم. همه ما میلیونها کار روی سرمان ریخته و شیرجه عمیق در هر گوشه و کنار EF Core ممکن است در بالای لیست اولویتهای شما نباشد.
اما قضیه این است: EF Core قدرتمند است و دانستن چند ویژگی کلیدی میتواند زمان و خستگی زیادی را از شما بگیرد.
بنابراین، من شما را با تک تک ویژگیهای EF Core بمباران نخواهم کرد.
در عوض، من پنج مورد ضروری را که واقعاً باید بدانید، دستچین کردهام.
ما موارد زیر را بررسی خواهیم کرد:
1️⃣ Query Splitting
- بهترین دوست جدید دیتابیس شما
2️⃣ Bulk Updates and Deletes
- کارایی به توان دو
3️⃣ Raw SQL Queries
- وقتی نیاز دارید یاغی شوید
4️⃣ Query Filters
- برای تمیز و مرتب نگه داشتن همه چیز
5️⃣ Eager Loading
- چون تنبلی همیشه هم خوب نیست
بزن بریم!
1️⃣ Query Splitting (تقسیم کوئری) ✂️
تقسیم کوئری یکی از آن ویژگیهای EF Core است که به ندرت به آن نیاز دارید. تا اینکه یک روز، به آن نیاز پیدا میکنید. Query splitting در سناریوهایی که چندین کالکشن را eager load میکنید، مفید است. این به ما کمک میکند تا از مشکل انفجار کارتزین (cartesian explosion) جلوگیری کنیم.
Department department =
context.Departments
.Include(d => d.Teams)
.Include(d => d.Employees)
.Where(d => d.Id == departmentId)
.AsSplitQuery() // جادو اینجاست
.First();
با AsSplitQuery، EF Core برای هر navigation کالکشن، یک کوئری SQL اضافی اجرا خواهد کرد. با این حال، مراقب باشید که بیش از حد از آن استفاده نکنید.
2️⃣ Bulk Updates and Deletes (آپدیتها و حذفهای دستهای) ⚡️
EF Core 7
دو API جدید برای انجام آپدیتها و حذفهای دستهای اضافه کرد، ExecuteUpdate و ExecuteDelete. آنها به شما اجازه میدهند تعداد زیادی از ردیفها را در یک رفت و برگشت به دیتابیس به طور موثر آپدیت کنید.
// افزایش ۵٪ حقوق تمام کارمندان فروش در یک کوئری
context.Employees
.Where(e => e.Department == "Sales")
.ExecuteUpdate(s => s.SetProperty(e => e.Salary, e => e.Salary * 1.05m));
// حذف تمام سبدهای خرید قدیمیتر از یک سال در یک کوئری
context.Carts
.Where(o => o.CreatedOn < DateTime.Now.AddYears(-1))
.ExecuteDelete();
این کار بدون بارگذاری انتیتیها در حافظه، مستقیماً دستور UPDATE یا DELETE را در دیتابیس اجرا میکند.
3️⃣ Raw SQL Queries (کوئریهای SQL خام) 👨💻
EF Core 8
یک ویژگی جدید اضافه کرد که به ما اجازه میدهد انواع داده مپنشده را با SQL خام کوئری بزنیم.
public class ProductSummary
{
public int ProductId { get; set; }
public string ProductName { get; set; }
public decimal TotalSales { get; set; }
}
var productSummaries = await context.Database
.SqlQuery<ProductSummary>(
$"""
SELECT p.ProductId, p.ProductName, SUM(oi.Quantity * oi.UnitPrice) AS TotalSales
FROM Products p ...
""")
.ToListAsync();
متد SqlQuery یک IQueryable برمیگرداند، که به شما اجازه میدهد کوئریهای SQL خام را با LINQ ترکیب کنید.
4️⃣ Query Filters (فیلترهای کوئری) 🔍
فیلترهای کوئری مانند دستورات WHERE قابل استفاده مجدد هستند که میتوانید به انتیتیهای خود اعمال کنید. این فیلترها به طور خودکار به کوئریهای LINQ هر زمان که انتیتیهای نوع مربوطه را بازیابی میکنید، اضافه میشوند.
کاربردهای رایج:
• حذف منطقی (Soft Deletes)
• چند-مستأجری (Multi-tenancy)
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// _currentTenantId بر اساس درخواست/زمینه فعلی تنظیم میشود
modelBuilder.Entity<Product>().HasQueryFilter(p => p.TenantId == _currentTenantId);
}
// حالا، کوئریها به طور خودکار بر اساس مستأجر فیلتر میشوند
var productsForCurrentTenant = context.Products.ToList();
5️⃣ Eager Loading (بارگذاری مشتاقانه) ⏬
بارگذاری مشتاقانه قابلیتی در EF Core است که به شما اجازه میدهد انتیتیهای مرتبط را به همراه انتیتی اصلی خود در یک کوئری دیتابیس واحد بارگذاری کنید.
internal sealed class
VerifyEmail(AppDbContext context)
{
public async Task<bool> Handle(Guid tokenId)
{
EmailVerificationToken? token = await context.EmailVerificationTokens
.Include(e => e.User) // User مرتبط را همزمان لود کن
.FirstOrDefaultAsync(e => e.Id == tokenId);
// ...
}
}
EF Core
یک کوئری SQL واحد تولید میکند که جداول EmailVerificationToken و User را join میکند.
خلاصه 📝
پس، این هم از این! پنج ویژگی EF Core که، صراحتاً، نمیتوانید از ندانستنشان شانه خالی کنید. به یاد داشته باشید، تسلط بر EF Core زمان میبرد، اما این ویژگیها یک پایه محکم برای ساختن فراهم میکنند.
یک توصیه دیگر این است که عمیقاً درک کنید دیتابیس شما چگونه کار میکند. تسلط بر SQL همچنین به شما اجازه میدهد بیشترین ارزش را از EF Core بدست آورید.
🔖 هشتگها:
#EntityFrameworkCore #EFCore #Performance #Database #SQL
💡 تکنیک Clean Code: جایگزینی if های پیچیده با متدهای گویا
شرطهای if پیچیده، خوانایی کد را به شدت پایین میآورند. 😫
وقتی چند شرط مجزا با هم ترکیب میشوند، درک منطق در نگاه اول حتی سختتر هم میشود.
اما میتوانیم این مشکل را با یک بازآرایی (refactoring) ساده حل کنیم.
راه حل: ↙️
منطق شرط را به یک متد یا پراپرتی با اسم گویا منتقل کنید.
👍🏻حالا نام توصیفی متد، توضیح میدهد که شرط چیست.
به یاد داشته باشید: درک زبان طبیعی برای انسانها، همیشه راحتتر از درک کد است.
📖 سری آموزشی کتاب C# 12 in a Nutshell
تا حالا شده بخواید یه کالکشن بسازید که بتونه هر نوع دادهای رو تو خودش نگه داره، از int و bool گرفته تا string و کلاسهای خودتون؟
این کار به لطف پدر همه تایپها در داتنت، یعنی System.Object، ممکنه. اما این قابلیت، یه راز عملکردی مهم به اسم Boxing و Unboxing رو تو دل خودش داره.
در #C، هر نوع دادهای، چه Value Type (مثل int) و چه Reference Type (مثل string)، به صورت پنهان از کلاس System.Object ارثبری میکنه. این یعنی شما میتونید هر متغیری رو به یه متغیر از نوع object تبدیل کنید (Upcast).
مثال (یک Stack همهکاره):
سوال اینجاست: چطور یه int که Value Type هست، میتونه مثل یه object که Reference Type هست رفتار کنه؟ با جادوی Boxing و Unboxing.
• Boxing (بستهبندی):
وقتی شما یه Value Type (مثل int) رو داخل یه متغیر object میریزید، CLR یه "جعبه" روی هیپ (Heap) میسازه، کپی از مقدار شما رو داخل اون میذاره و رفرنس اون جعبه رو به شما میده.
• Unboxing (باز کردن بسته):
وقتی میخواید مقدار رو از اون جعبه در بیارید، عملیات برعکس یعنی Unboxing اتفاق میفته که نیاز به کست صریح داره.
Unboxing
باید به نوع دقیق و اصلی انجام بشه. اگه سعی کنید یه int باکس شده رو به long آنباکس کنید، با خطای InvalidCastException مواجه میشید.
نکته حیاتی: Boxing یه کپی از مقدار شما رو میسازه. این یعنی اگه بعداً متغیر اصلی رو تغییر بدید، مقدار داخل جعبه دستنخورده باقی میمونه!
دونستن مفهوم Boxing و Unboxing برای درک پرفورمنس در #C حیاتیه، چون این عملیاتها هزینه حافظه و پردازش دارن. به همین دلیله که جنریکها (Generics) اختراع شدن تا جلوی این اتفاق رو بگیرن (که بعداً بهش میرسیم).
👑 پدر همه تایپها: object و راز Boxing/Unboxing در #C
تا حالا شده بخواید یه کالکشن بسازید که بتونه هر نوع دادهای رو تو خودش نگه داره، از int و bool گرفته تا string و کلاسهای خودتون؟
این کار به لطف پدر همه تایپها در داتنت، یعنی System.Object، ممکنه. اما این قابلیت، یه راز عملکردی مهم به اسم Boxing و Unboxing رو تو دل خودش داره.
1️⃣ object: جد بزرگ همه!
در #C، هر نوع دادهای، چه Value Type (مثل int) و چه Reference Type (مثل string)، به صورت پنهان از کلاس System.Object ارثبری میکنه. این یعنی شما میتونید هر متغیری رو به یه متغیر از نوع object تبدیل کنید (Upcast).
مثال (یک Stack همهکاره):
public class Stack
{
int position;
object[] data = new object[10];
public void Push(object obj) => data[position++] = obj;
public object Pop() => data[--position];
}
// --- نحوه استفاده ---
var stack = new Stack();
stack.Push("hello");
stack.Push(123); // حتی int!
stack.Push(false); // حتی bool!
// موقع خروج، باید Downcast کنیم
int myNumber = (int)stack.Pop(); // 123
string myString = (string)stack.Pop(); // "hello"
2️⃣ جادوی پشت پرده: Boxing و Unboxing 🎁
سوال اینجاست: چطور یه int که Value Type هست، میتونه مثل یه object که Reference Type هست رفتار کنه؟ با جادوی Boxing و Unboxing.
• Boxing (بستهبندی):
وقتی شما یه Value Type (مثل int) رو داخل یه متغیر object میریزید، CLR یه "جعبه" روی هیپ (Heap) میسازه، کپی از مقدار شما رو داخل اون میذاره و رفرنس اون جعبه رو به شما میده.
• Unboxing (باز کردن بسته):
وقتی میخواید مقدار رو از اون جعبه در بیارید، عملیات برعکس یعنی Unboxing اتفاق میفته که نیاز به کست صریح داره.
int x = 9;
object obj = x; // Boxing: مقدار x در یک جعبه روی هیپ کپی میشود
int y = (int)obj; // Unboxing: مقدار از جعبه کپی شده و به y ریخته میشود
3️⃣ تلهها و نکات مهم ⚠️ تله InvalidCastException:
Unboxing
باید به نوع دقیق و اصلی انجام بشه. اگه سعی کنید یه int باکس شده رو به long آنباکس کنید، با خطای InvalidCastException مواجه میشید.
object obj = 9; // این یک int باکس شده است
// long l = (long)obj; // ❌ InvalidCastException!
تله کپی شدن مقدار:
نکته حیاتی: Boxing یه کپی از مقدار شما رو میسازه. این یعنی اگه بعداً متغیر اصلی رو تغییر بدید، مقدار داخل جعبه دستنخورده باقی میمونه!
int i = 3;
object boxed = i; // یک کپی از 3 در boxed قرار گرفت
i = 5; // تغییر i اصلی، تأثیری روی مقدار باکس شده ندارد
Console.WriteLine(boxed); // خروجی: 3
🤔 حرف حساب و تجربه شما
دونستن مفهوم Boxing و Unboxing برای درک پرفورمنس در #C حیاتیه، چون این عملیاتها هزینه حافظه و پردازش دارن. به همین دلیله که جنریکها (Generics) اختراع شدن تا جلوی این اتفاق رو بگیرن (که بعداً بهش میرسیم).
🔖 هشتگها:
#OOP #MemoryManagement #Boxing
پیادهسازی حذف نرم (Soft Delete) با EF Core 💾
حذف کردن یا نکردن، مسئله این است. 🤔
روش سنتی برای حذف اطلاعات در پایگاه داده، از طریق "حذف سخت" (hard delete) است. یک حذف سخت، یک رکورد را برای همیشه از جدول پایگاه داده پاک میکند. اگرچه این روش ساده به نظر میرسد، اما ریسک قابل توجهی دارد: وقتی دادهای حذف شد، برای همیشه از بین رفته است. 💥
به جای حذف فیزیکی یک رکورد، در "حذف نرم" (soft delete) آن را به عنوان حذف شده علامتگذاری میکنیم، معمولاً با قرار دادن یک فلگ مانند IsDeleted به مقدار true. رکورد در پایگاه داده باقی میماند، اما به طور موثر از کوئریهای عادی برنامه پنهان میشود. 🛡️
امروز، به جزئیات نحوه پیادهسازی حذف نرم با استفاده از EF Core خواهیم پرداخت. در مورد فیلترهای کوئری سراسری بحث خواهیم کرد، روشهای کارآمد برای مدیریت دادههای حذف نرم شده را بررسی میکنیم و مزایا و معایب آن را میسنجیم. 🚀
حذف نرم (Soft Delete) چیست؟ 🧐
حذف نرم یک استراتژی پایداری داده است که از حذف دائمی رکوردها از پایگاه داده شما جلوگیری میکند. به جای حذف داده از پایگاه داده، یک فلگ روی رکورد تنظیم میشود که آن را به عنوان "حذف شده" مشخص میکند.
این رویکرد به برنامه اجازه میدهد تا در کوئریهای عادی این رکوردها را نادیده بگیرد. با این حال، در صورت لزوم میتوانید این رکوردها را بازیابی کنید. حذف نرم همچنین زمانی کاربردی است که بخواهید قیود کلید خارجی (foreign key constraints) را حفظ کنید. حذف نرم یک عملیات "غیرمخرب" است، در مقابل حذف سخت که در آن دادهها به طور کامل حذف میشوند. 🔄
یک حذف سخت از دستور DELETE در SQL استفاده میکند:
DELETE FROM bookings.Reviews
WHERE Id = @BookingId;
از طرف دیگر، یک حذف نرم از دستور UPDATE استفاده میکند:
UPDATE bookings.Reviews
SET IsDeleted = 1, DeletedOnUtc = @UtcNow
WHERE Id = @BookingId;
دادهها هنوز در پایگاه داده وجود دارند و این عملیات قابل بازگشت است.
اما باید به خاطر داشته باشید که هنگام کوئری گرفتن از پایگاه داده، دادههای حذف نرم شده را فیلتر کنید:
SELECT *
FROM bookings.Reviews
WHERE IsDeleted = 0;
بیایید ببینیم چگونه میتوانیم حذف نرم را با EF Core پیادهسازی کنیم. 👨💻
حذف نرم با استفاده از رهگیرها (Interceptors) در EF Core ⚙️
رهگیرهای EF Core یک مکانیزم قدرتمند برای رهگیری و تغییر عملیات پایگاه داده فراهم میکنند. به عنوان مثال، میتوانید عملیات ذخیره تغییرات (saving changes) را برای پیادهسازی قابلیت حذف نرم رهگیری کنید.
بیایید یک اینترفیس نشانگر ISoftDeletable برای نمایش موجودیتهای قابل حذف نرم ایجاد کنیم:
public interface ISoftDeletable
{
bool IsDeleted { get; set; }
DateTime? DeletedOnUtc { get; set; }
}
موجودیتهایی که باید از حذف نرم پشتیبانی کنند، این اینترفیس را پیادهسازی خواهند کرد. شما باید مایگریشن مربوطه را برای ایجاد این ستونها در پایگاه داده اعمال کنید.
مولفه بعدی که نیاز داریم یک SaveChangesInterceptor است که به ما اجازه میدهد به متد SavingChangesAsync (یا SavingChanges) متصل شویم. ما میتوانیم به ChangeTracker دسترسی پیدا کنیم و به دنبال ورودیهایی بگردیم که ISoftDeletable را پیادهسازی کرده و برای حذف شدن علامتگذاری شدهاند. این موضوع را میتوانیم با بررسی اینکه وضعیت موجودیت (entity state) برابر با EntityState.Deleted است، تشخیص دهیم.
وقتی موجودیتهایی را که برای حذف علامتگذاری شدهاند پیدا کردیم، در یک حلقه آنها را پیمایش کرده و وضعیتشان را به EntityState.Modified تغییر میدهیم. همچنین باید مقادیر مربوط به پراپرتیهای IsDeleted و DeletedOnUtc را تنظیم کنید. این کار باعث میشود EF به جای عملیات DELETE، یک عملیات UPDATE ایجاد کند.
این رویکرد تضمین میکند که تمام عملیات حذف در سراسر برنامه از سیاست حذف نرم پیروی میکنند.
شما باید SoftDeleteInterceptor را با تزریق وابستگی (dependency injection) ثبت کرده و آن را با ApplicationDbContext پیکربندی کنید.
برای اطمینان از اینکه رکوردهای حذف نرم شده به طور خودکار از کوئریها حذف میشوند، میتوانیم از فیلترهای کوئری سراسری (global query filters) در EF Core استفاده کنیم. ما میتوانیم فیلترهای کوئری را با استفاده از متد OnModelCreating روی موجودیتها اعمال کنیم تا رکوردهای علامتگذاری شده به عنوان حذف شده، به طور خودکار مستثنی شوند. این ویژگی نوشتن کوئریها را به شدت ساده میکند.
در اینجا نحوه پیکربندی فیلتر کوئری حذف نرم آمده است:
یک محدودیت این است که شما نمیتوانید بیش از یک فیلتر کوئری برای هر موجودیت پیکربندی کنید.
با این حال، گاهی اوقات لازم است که رکوردهای حذف نرم شده را به صراحت در کوئریها لحاظ کنید. شما میتوانید این کار را با استفاده از متد IgnoreQueryFilters انجام دهید.
برای بهبود عملکرد کوئری، به ویژه در جداولی که تعداد زیادی رکورد حذف نرم شده دارند، میتوانید از ایندکسهای فیلتر شده (filtered indexes) استفاده کنید. یک ایندکس فیلتر شده فقط شامل رکوردهایی است که معیارهای مشخص شده را برآورده میکنند. این کار اندازه ایندکس را کاهش داده و زمان اجرای کوئری را برای عملیاتی که رکوردهای فیلتر شده را مستثنی میکنند، بهبود میبخشد. اکثر پایگاه دادههای محبوب از ایندکسهای فیلتر شده پشتیبانی میکنند.
در اینجا نحوه پیکربندی یک ایندکس فیلتر شده با EF Core آمده است:
public sealed class SoftDeleteInterceptor : SaveChangesInterceptor
{
public override ValueTask<InterceptionResult<int>> SavingChangesAsync(
DbContextEventData eventData,
InterceptionResult<int> result,
CancellationToken cancellationToken = default)
{
if (eventData.Context is null)
{
return base.SavingChangesAsync(
eventData, result, cancellationToken);
}
IEnumerable<EntityEntry<ISoftDeletable>> entries =
eventData
.Context
.ChangeTracker
.Entries<ISoftDeletable>()
.Where(e => e.State == EntityState.Deleted);
foreach (EntityEntry<ISoftDeletable> softDeletable in entries)
{
softDeletable.State = EntityState.Modified;
softDeletable.Entity.IsDeleted = true;
softDeletable.Entity.DeletedOnUtc = DateTime.UtcNow;
}
return base.SavingChangesAsync(eventData, result, cancellationToken);
}
}
این رویکرد تضمین میکند که تمام عملیات حذف در سراسر برنامه از سیاست حذف نرم پیروی میکنند.
شما باید SoftDeleteInterceptor را با تزریق وابستگی (dependency injection) ثبت کرده و آن را با ApplicationDbContext پیکربندی کنید.
services.AddSingleton<SoftDeleteInterceptor>();
services.AddDbContext<ApplicationDbContext>(
(sp, options) => options
.UseSqlServer(connectionString)
.AddInterceptors(
sp.GetRequiredService<SoftDeleteInterceptor>()));
فیلتر کردن خودکار دادههای حذف نرم شده 🔍
برای اطمینان از اینکه رکوردهای حذف نرم شده به طور خودکار از کوئریها حذف میشوند، میتوانیم از فیلترهای کوئری سراسری (global query filters) در EF Core استفاده کنیم. ما میتوانیم فیلترهای کوئری را با استفاده از متد OnModelCreating روی موجودیتها اعمال کنیم تا رکوردهای علامتگذاری شده به عنوان حذف شده، به طور خودکار مستثنی شوند. این ویژگی نوشتن کوئریها را به شدت ساده میکند.
در اینجا نحوه پیکربندی فیلتر کوئری حذف نرم آمده است:
public sealed class ApplicationDbContext(
DbContextOptions<UsersDbContext> options) : DbContext(options)
{
public DbSet<Review> Reviews { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Review>().HasQueryFilter(r => !r.IsDeleted);
}
}
یک محدودیت این است که شما نمیتوانید بیش از یک فیلتر کوئری برای هر موجودیت پیکربندی کنید.
با این حال، گاهی اوقات لازم است که رکوردهای حذف نرم شده را به صراحت در کوئریها لحاظ کنید. شما میتوانید این کار را با استفاده از متد IgnoreQueryFilters انجام دهید.
dbContex.Reviews
.IgnoreQueryFilters()
.Where(r => r.ApartmentId == apartmentId)
.ToList();
کوئریهای سریعتر با استفاده از ایندکس فیلتر شده (Filtered Index) 🚀
برای بهبود عملکرد کوئری، به ویژه در جداولی که تعداد زیادی رکورد حذف نرم شده دارند، میتوانید از ایندکسهای فیلتر شده (filtered indexes) استفاده کنید. یک ایندکس فیلتر شده فقط شامل رکوردهایی است که معیارهای مشخص شده را برآورده میکنند. این کار اندازه ایندکس را کاهش داده و زمان اجرای کوئری را برای عملیاتی که رکوردهای فیلتر شده را مستثنی میکنند، بهبود میبخشد. اکثر پایگاه دادههای محبوب از ایندکسهای فیلتر شده پشتیبانی میکنند.
در اینجا نحوه پیکربندی یک ایندکس فیلتر شده با EF Core آمده است:
public sealed class ApplicationDbContext(
DbContextOptions<UsersDbContext> options) : DbContext(options)
{
public DbSet<Review> Reviews { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Review>().HasQueryFilter(r => !r.IsDeleted);
modelBuilder.Entity<Review>()
.HasIndex(r => r.IsDeleted)
.HasFilter("IsDeleted = 0");
}
}
متد HasFilter فیلتر SQL را برای رکوردهایی که در ایندکس قرار خواهند گرفت، میپذیرد.
شما همچنین میتوانید یک ایندکس فیلتر شده را با استفاده از SQL ایجاد کنید:
شما میتوانید از طریق مستندات، اطلاعات بیشتری در مورد ایندکسهای فیلتر شده کسب کنید.
ارزشش را دارد که فکر کنید آیا اصلاً به حذف نرم رکوردها نیاز دارید یا خیر.
در سیستمهای سازمانی (enterprise)، شما معمولاً به "حذف" داده فکر نمیکنید. مفاهیم تجاری وجود دارند که شامل حذف داده نمیشوند. چند مثال عبارتند از: لغو یک سفارش، بازپرداخت یک پرداخت، یا باطل کردن یک فاکتور. این عملیاتهای "مخرب" سیستم را به حالت قبلی بازمیگردانند. اما از دیدگاه تجاری، شما واقعاً در حال حذف داده نیستید. 💼
حذف نرم در صورتی مفید است که خطر حذف تصادفی وجود داشته باشد. این قابلیت به شما امکان میدهد رکوردهای حذف نرم شده را به راحتی بازیابی کنید.
در هر صورت، در نظر بگیرید که آیا حذف نرم از دیدگاه تجاری منطقی است یا خیر.
حذف نرم یک شبکه ایمنی ارزشمند برای بازیابی اطلاعات ارائه میدهد و میتواند ردیابی دادههای تاریخی را بهبود بخشد.
با این حال، ارزیابی اینکه آیا این روش واقعاً با نیازمندیهای خاص برنامه شما همخوانی دارد، بسیار مهم است. عواملی مانند اهمیت بازیابی دادههای حذف شده، نیازهای ممیزی (auditing) و مقررات صنعت خود را در نظر بگیرید. ایجاد یک ایندکس فیلتر شده میتواند عملکرد کوئری را در جداول دارای رکوردهای حذف نرم شده بهبود بخشد.
اگر تصمیم گرفتید که حذف نرم برای شما مناسب است، EF Core ابزارهای لازم را برای یک پیادهسازی روان و ساده فراهم میکند.
شما همچنین میتوانید یک ایندکس فیلتر شده را با استفاده از SQL ایجاد کنید:
CREATE INDEX IX_Reviews_IsDeleted
ON bookings.Reviews (IsDeleted)
WHERE IsDeleted = 0;
شما میتوانید از طریق مستندات، اطلاعات بیشتری در مورد ایندکسهای فیلتر شده کسب کنید.
آیا واقعاً به حذف نرم نیاز دارید؟ 🤔
ارزشش را دارد که فکر کنید آیا اصلاً به حذف نرم رکوردها نیاز دارید یا خیر.
در سیستمهای سازمانی (enterprise)، شما معمولاً به "حذف" داده فکر نمیکنید. مفاهیم تجاری وجود دارند که شامل حذف داده نمیشوند. چند مثال عبارتند از: لغو یک سفارش، بازپرداخت یک پرداخت، یا باطل کردن یک فاکتور. این عملیاتهای "مخرب" سیستم را به حالت قبلی بازمیگردانند. اما از دیدگاه تجاری، شما واقعاً در حال حذف داده نیستید. 💼
حذف نرم در صورتی مفید است که خطر حذف تصادفی وجود داشته باشد. این قابلیت به شما امکان میدهد رکوردهای حذف نرم شده را به راحتی بازیابی کنید.
در هر صورت، در نظر بگیرید که آیا حذف نرم از دیدگاه تجاری منطقی است یا خیر.
نکات کلیدی (Takeaway) 📌
حذف نرم یک شبکه ایمنی ارزشمند برای بازیابی اطلاعات ارائه میدهد و میتواند ردیابی دادههای تاریخی را بهبود بخشد.
با این حال، ارزیابی اینکه آیا این روش واقعاً با نیازمندیهای خاص برنامه شما همخوانی دارد، بسیار مهم است. عواملی مانند اهمیت بازیابی دادههای حذف شده، نیازهای ممیزی (auditing) و مقررات صنعت خود را در نظر بگیرید. ایجاد یک ایندکس فیلتر شده میتواند عملکرد کوئری را در جداول دارای رکوردهای حذف نرم شده بهبود بخشد.
اگر تصمیم گرفتید که حذف نرم برای شما مناسب است، EF Core ابزارهای لازم را برای یک پیادهسازی روان و ساده فراهم میکند.
🔖 هشتگها:
#EntityFrameworkCore #EFCore #SoftDelete #Database #DataPersistence #SQL
📖 سری آموزشی کتاب C# 12 in a Nutshell
چطور #C جلوی خطاهای مربوط به نوع داده رو میگیره؟ این زبان از یه سیستم امنیتی دو لایه استفاده میکنه: بررسی استاتیک در زمان کامپایل و بررسی در زمان اجرا.
امروز میخوایم با این دو لایه امنیتی و ابزارهای بازرسیشون آشنا بشیم.
این اولین خط دفاعی شماست. کامپایلر #C قبل از اینکه حتی برنامه رو اجرا کنید، کد شما رو بررسی میکنه تا مطمئن بشه که شما نوعهای داده رو به درستی استفاده کردید. این کار جلوی یه عالمه باگ رو در نطفه خفه میکنه.
بعضی وقتا کامپایلر نمیتونه از همه چیز مطمئن باشه (مثلاً موقع Downcasting). اینجا CLR (محیط اجرای داتنت) وارد عمل میشه.
هر آبجکتی که روی هیپ ساخته میشه، یه "توکن نوع" با خودش داره. CLR در زمان اجرا، از این توکن برای چک کردن صحت عملیات کستینگ استفاده میکنه.
برای اینکه خودمون اطلاعات یه نوع رو بدست بیاریم، دو تا ابزار اصلی داریم:
🔹️ GetType() (برای زمان اجرا):
این یه متده که روی یک نمونه (instance) از آبجکت صدا زده میشه و نوع دقیق اون آبجکت رو در زمان اجرا به ما میده.
🔹️ typeof (برای زمان کامپایل):
این یه اپراتوره که اسم یه نوع (Type) رو میگیره و آبجکت System.Type مربوط به اون رو در زمان کامپایل مشخص میکنه.
این سیستم بررسی نوع دو مرحلهای، یکی از دلایل اصلی قدرت و امنیت زبان #C هست.
🛡 بازرسی هویت در #C :
Type Checking در زمان کامپایل و اجرا
چطور #C جلوی خطاهای مربوط به نوع داده رو میگیره؟ این زبان از یه سیستم امنیتی دو لایه استفاده میکنه: بررسی استاتیک در زمان کامپایل و بررسی در زمان اجرا.
امروز میخوایم با این دو لایه امنیتی و ابزارهای بازرسیشون آشنا بشیم.
1️⃣ بررسی استاتیک (Static Type Checking): نگهبان زمان کامپایل 👮♂️
این اولین خط دفاعی شماست. کامپایلر #C قبل از اینکه حتی برنامه رو اجرا کنید، کد شما رو بررسی میکنه تا مطمئن بشه که شما نوعهای داده رو به درستی استفاده کردید. این کار جلوی یه عالمه باگ رو در نطفه خفه میکنه.
// کامپایلر اینجا جلوی شما رو میگیره و اجازه نمیده برنامه ساخته بشه
// چون نمیتونید یه رشته رو تو یه متغیر int بریزید.
int x = "5"; // ❌ خطای زمان کامپایل!
2️⃣ بررسی در زمان اجرا (Runtime Type Checking): نگهبان CLR 🏃♂️
بعضی وقتا کامپایلر نمیتونه از همه چیز مطمئن باشه (مثلاً موقع Downcasting). اینجا CLR (محیط اجرای داتنت) وارد عمل میشه.
هر آبجکتی که روی هیپ ساخته میشه، یه "توکن نوع" با خودش داره. CLR در زمان اجرا، از این توکن برای چک کردن صحت عملیات کستینگ استفاده میکنه.
object y = "5";
// در زمان اجرا، CLR میبینه که آبجکت داخل y یک string است، نه int
// و یک خطای InvalidCastException پرتاب میکنه.
int z = (int)y; // 💥 خطای زمان اجرا!
3️⃣ ابزارهای بازرسی: GetType در برابر typeof 🔎
برای اینکه خودمون اطلاعات یه نوع رو بدست بیاریم، دو تا ابزار اصلی داریم:
🔹️ GetType() (برای زمان اجرا):
این یه متده که روی یک نمونه (instance) از آبجکت صدا زده میشه و نوع دقیق اون آبجکت رو در زمان اجرا به ما میده.
🔹️ typeof (برای زمان کامپایل):
این یه اپراتوره که اسم یه نوع (Type) رو میگیره و آبجکت System.Type مربوط به اون رو در زمان کامپایل مشخص میکنه.
public class Point { public int X, Y; }
// ...
Point p = new Point();
// GetType روی نمونه کار میکنه
Console.WriteLine(p.GetType().Name); // خروجی: Point
// typeof روی خودِ نوع کار میکنه
Console.WriteLine(typeof(Point).Name); // خروجی: Point
// میتونیم نتایج رو با هم مقایسه کنیم
Console.WriteLine(p.GetType() == typeof(Point)); // خروجی: Trueاین سیستم بررسی نوع دو مرحلهای، یکی از دلایل اصلی قدرت و امنیت زبان #C هست.
🔖 هشتگها:
#DotNet #OOP #BestPractices #TypeSystem
📖 سری آموزشی کتاب C# 12 in a Nutshell
تا حالا شده یه آبجکت از کلاس خودتون رو Console.WriteLine کنید و با یه خروجی بیمعنی مثل MyProject.Panda مواجه بشید؟ 😑
راه حل این مشکل، بازنویسی (override) کردن یکی از مهمترین متدهای کلاس object یعنی ToStringهست.
ToString()
یه متد virtual در کلاس System.Object هست. وظیفهش اینه که یه نمایش متنی و خوانا از آبجکت شما برگردونه. این متد به خصوص موقع دیباگ کردن و لاگ انداختن فوقالعاده به درد میخوره، چون به شما اجازه میده وضعیت داخلی آبجکت رو به راحتی ببینید.
کافیه تو کلاس خودتون، متد ToString رو override کنید و هر رشتهای که دوست دارید و وضعیت آبجکت رو بهتر توصیف میکنه، برگردونید.
اگه
یه نکته خیلی مهم در مورد پرفورمنس: وقتی ToString رو مستقیماً روی یه Value Type (مثل int) صدا میزنید، هیچ عمل Boxing اتفاق نمیفته. Boxing (که هزینه پرفورمنس داره) فقط زمانی رخ میده که شما اول اون Value Type رو به object کست کنید.
عادت کردن به override کردن ToString در تمام کلاسهاتون، یکی از بهترین کارهاییه که میتونید برای خودتون (و همتیمیهاتون در آینده) انجام بدید.
🖨 چاپ زیبا با ()ToString: معرفی کردن آبجکتهای شما به دنیا
تا حالا شده یه آبجکت از کلاس خودتون رو Console.WriteLine کنید و با یه خروجی بیمعنی مثل MyProject.Panda مواجه بشید؟ 😑
راه حل این مشکل، بازنویسی (override) کردن یکی از مهمترین متدهای کلاس object یعنی ToStringهست.
1️⃣ ToString() چیست و چرا مهمه؟
ToString()
یه متد virtual در کلاس System.Object هست. وظیفهش اینه که یه نمایش متنی و خوانا از آبجکت شما برگردونه. این متد به خصوص موقع دیباگ کردن و لاگ انداختن فوقالعاده به درد میخوره، چون به شما اجازه میده وضعیت داخلی آبجکت رو به راحتی ببینید.
2️⃣ چطور ToString رو بازنویسی (Override) کنیم؟
کافیه تو کلاس خودتون، متد ToString رو override کنید و هر رشتهای که دوست دارید و وضعیت آبجکت رو بهتر توصیف میکنه، برگردونید.
public class Panda
{
public string Name;
// بازنویسی متد ToString
public override string ToString() => Name;
}
// --- نحوه استفاده ---
Panda p = new Panda { Name = "Petey" };
// Console.WriteLine به صورت خودکار ToString() رو روی آبجکتها صدا میزنه
Console.WriteLine(p);
// خروجی:
// Petey
اگه
ToString رو بازنویسی نکنید، خروجی پیشفرض، اسم کامل تایپ خواهد بود.3️⃣ نکته فنی: ToString و تلهی Boxing ⚠️
یه نکته خیلی مهم در مورد پرفورمنس: وقتی ToString رو مستقیماً روی یه Value Type (مثل int) صدا میزنید، هیچ عمل Boxing اتفاق نمیفته. Boxing (که هزینه پرفورمنس داره) فقط زمانی رخ میده که شما اول اون Value Type رو به object کست کنید.
int x = 1;
// ✅ بدون Boxing: بهینه و سریع
string s1 = x.ToString();
// ❌ با Boxing: هزینه پرفورمنس داره
object box = x;
string s2 = box.ToString();
🤔 حرف حساب و تجربه شما
عادت کردن به override کردن ToString در تمام کلاسهاتون، یکی از بهترین کارهاییه که میتونید برای خودتون (و همتیمیهاتون در آینده) انجام بدید.
🔖 هشتگها:
#DotNet #OOP #ToString
Forwarded from thisisnabi.dev [Farsi]
به نظر من، بهینه سازی اینجوریه که هرچقدر شما جلوتر میرید باید زمان بیشتری صرف تغییرات کمتر کنید.
به زبان ساده میشه گفت همون قانون ۸۰/۲۰ یا پارتو اینجا صدق میکنه؛ یعنی معمولاً ۸۰ درصد نتیجه رو توی ۲۰ درصد تلاش اولیه میشه گرفت. اما وقتی جلوتر میری، برای همون چند درصد باقیمونده باید کلی زمان و انرژی بیشتری بذاری تا تغییر کوچیکی اتفاق بیفته.
به زبان ساده میشه گفت همون قانون ۸۰/۲۰ یا پارتو اینجا صدق میکنه؛ یعنی معمولاً ۸۰ درصد نتیجه رو توی ۲۰ درصد تلاش اولیه میشه گرفت. اما وقتی جلوتر میری، برای همون چند درصد باقیمونده باید کلی زمان و انرژی بیشتری بذاری تا تغییر کوچیکی اتفاق بیفته.
بررسیهای سلامت (Health Checks) در ASP.NET Core برای مانیتورینگ اپلیکیشنهای شما 🩺
همه ما میخواهیم اپلیکیشنهای قوی و قابل اعتماد بسازیم که بتوانند به طور نامحدود مقیاسپذیر باشند و هر تعداد درخواستی را مدیریت کنند.
اما با افزایش پیچیدگی سیستمهای توزیعشده و معماریهای میکروسرویس، مانیتور کردن سلامت اپلیکیشنهای ما به طور فزایندهای دشوارتر میشود.
حیاتی است که شما سیستمی برای دریافت بازخورد سریع از سلامت اپلیکیشن خود داشته باشید.
اینجاست که health checks وارد میشوند.
بررسیهای سلامت راهی برای مانیتور و تأیید سلامت کامپوننتهای مختلف یک اپلیکیشن فراهم میکنند، از جمله:
🔹 دیتابیسها
🔹 APIها
🔹 کشها
🔹 سرویسهای خارجی
در این مقاله به شما نشان خواهم داد:
🔹 Health checks چه هستند
🔹 افزودن یک health check سفارشی
🔹 استفاده از کتابخانههای health check موجود
🔹 سفارشیسازی فرمت پاسخ health checks
بیایید ببینیم چگونه health checks را در ASP.NET Core پیادهسازی کنیم.
Health Checks چه هستند؟ 🤔
یک مکانیزم پیشگیرانه برای مانیتورینگ و تأیید سلامت و در دسترس بودن یک اپلیکیشن در ASP.NET Core هستند.
در اینجا پیکربندی اولیه آمده است که سرویسهای health check را ثبت کرده و HealthCheckMiddleware را برای پاسخگویی در URL مشخص شده اضافه میکند.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHealthChecks();
var app = builder.Build();
app.MapHealthChecks("/health");
app.Run();
health check
یک مقدار HealthStatus را برمیگرداند که سلامت سرویس را نشان میدهد.
سه مقدار متمایز HealthStatus وجود دارد:
🟢 HealthStatus.Healthy
🟡 HealthStatus.Degraded
🔴 HealthStatus.Unhealthy
شما میتوانید از HealthStatus برای نشان دادن حالتهای مختلف اپلیکیشن خود استفاده کنید.
برای مثال، اگر اپلیکیشن کندتر از حد انتظار عمل میکند، میتوانید HealthStatus.Degraded را برگردانید.
افزودن Health Checks سفارشی 👨🔧
شما میتوانید health checks سفارشی را با پیادهسازی اینترفیس IHealthCheck ایجاد کنید.
برای مثال، میتوانید یک چک برای بررسی در دسترس بودن دیتابیس SQL خود پیادهسازی کنید.
مهم است که از یک کوئری استفاده کنید که بتواند به سرعت در دیتابیس کامل شود، مانند SELECT 1.
در اینجا یک مثال از پیادهسازی health check سفارشی در کلاس SqlHealthCheck آمده است:
public class SqlHealthCheck : IHealthCheck
{
private readonly string _connectionString;
public SqlHealthCheck(IConfiguration configuration)
{
_connectionString = configuration.GetConnectionString("Database");
}
public async Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context,
CancellationToken cancellationToken = default)
{
try
{
using var sqlConnection = new SqlConnection(_connectionString);
await sqlConnection.OpenAsync(cancellationToken);
using var command = sqlConnection.CreateCommand();
command.CommandText = "SELECT 1";
await command.ExecuteScalarAsync(cancellationToken);
return HealthCheckResult.Healthy();
}
catch(Exception ex)
{
return HealthCheckResult.Unhealthy(
context.Registration.FailureStatus,
exception: ex);
}
}
}
پس از پیادهسازی health check سفارشی، باید آن را ثبت کنید.
فراخوانی قبلی به AddHealthChecks اکنون به این شکل میشود:
builder.Services.AddHealthChecks()
.AddCheck<SqlHealthCheck>("custom-sql", HealthStatus.Unhealthy);
ما به آن یک نام سفارشی میدهیم و مشخص میکنیم که کدام وضعیت به عنوان نتیجه شکست در
HealthCheckContext.Registration.FailureStatus
استفاده شود.
اما یک لحظه بایستید و فکر کنید.
آیا میخواهید برای هر سرویس خارجی که دارید، یک health check سفارشی را خودتان پیادهسازی کنید؟
البته که نه! یک راه حل بهتر وجود دارد.
استفاده از کتابخانههای Health Check موجود 📚
قبل از اینکه برای همه چیز یک health check سفارشی پیادهسازی کنید، ابتدا باید ببینید آیا از قبل کتابخانه موجودی وجود دارد یا نه.
در ریپازیتوری AspNetCore.Diagnostics.HealthChecks میتوانید مجموعه وسیعی از پکیجهای health check برای سرویسها و کتابخانههای پرکاربرد را پیدا کنید.
در اینجا فقط چند مثال آمده است:
SQL Server -
AspNetCore.HealthChecks.SqlServer
Postgres -
AspNetCore.HealthChecks.Npgsql
Redis -
AspNetCore.HealthChecks.Redis
RabbitMQ -
AspNetCore.HealthChecks.RabbitMQ
در اینجا نحوه افزودن health checks برای PostgreSQL و RabbitMQ آمده است:
builder.Services.AddHealthChecks()
.AddCheck<SqlHealthCheck>("custom-sql", HealthStatus.Unhealthy);
.AddNpgSql(pgConnectionString)
.AddRabbitMQ(rabbitConnectionString)
فرمتدهی پاسخ Health Checks 🎨
بهطور پیشفرض، endpointی که وضعیت health check شما را برمیگرداند، یک مقدار رشتهای را که نماینده یک HealthStatus است، برمیگرداند.
این عملی نیست اگر چندین health check پیکربندی کرده باشید، زیرا میخواهید وضعیت سلامت را به صورت جداگانه برای هر سرویس مشاهده کنید.
بدتر از آن، اگر یکی از سرویسها در حال شکست باشد، کل پاسخ Unhealthy برمیگرداند و شما نمیدانید چه چیزی باعث مشکل شده است.
شما میتوانید این مشکل را با ارائه یک ResponseWriter حل کنید، و یک نمونه موجود در کتابخانه AspNetCore.HealthChecks.UI.Client وجود دارد.
پکیج NuGet را نصب کنید:
Install-Package AspNetCore.HealthChecks.UI.Client
و باید فراخوانی به MapHealthChecks را کمی بهروز کنید تا از ResponseWriter این کتابخانه استفاده کند:
app.MapHealthChecks(
"/health",
new HealthCheckOptions
{
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
});
پس از این تغییرات، پاسخ از endpoint health check به این شکل خواهد بود:
{
"status": "Unhealthy",
"totalDuration": "00:00:00.3285211",
"entries": {
"npgsql": { "status": "Healthy", ... },
"rabbitmq": { "status": "Healthy", ... },
"custom-sql": { "status": "Unhealthy", ... }
}
}نکات پایانی 📝
مانیتورینگ اپلیکیشن برای ردیابی در دسترس بودن، استفاده از منابع و تغییرات عملکرد در اپلیکیشن شما مهم است.
مانیتور کردن سلامت اپلیکیشنهای ASP.NET Core شما با ارائه health checks برای سرویسهایتان آسان است.
شما میتوانید تصمیم بگیرید health checks سفارشی را پیادهسازی کنید، اما ابتدا در نظر بگیرید که آیا راهحلهای موجود وجود دارد یا نه.