وقتی موجودیتهایی را که برای حذف علامتگذاری شدهاند پیدا کردیم، در یک حلقه آنها را پیمایش کرده و وضعیتشان را به 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 سفارشی را پیادهسازی کنید، اما ابتدا در نظر بگیرید که آیا راهحلهای موجود وجود دارد یا نه.
📖 سری آموزشی کتاب C# 12 in a Nutshell
در #C، ما دو نوع اصلی برای ساختن تایپهای خودمون داریم: class و struct. اکثر ما به صورت پیشفرض از class استفاده میکنیم، اما دونستن اینکه struct چیه و کی باید ازش استفاده کنیم، یه مهارت کلیدیه که کد شما رو بهینهتر و خواناتر میکنه.
دو تا تفاوت بنیادی وجود داره که همه چیز از اونها نشأت میگیره:
این مهمترین تفاوته. struct یک Value Type هست؛ یعنی وقتی اون رو به یه متغیر دیگه یا یه متد پاس میدید، کل آبجکت کپی میشه. class یک Reference Type هست؛ یعنی فقط رفرنس (آدرس حافظه) اون کپی میشه.
استراکت ها از وراثت پشتیبانی نمیکنن. شما نمیتونید یه struct بسازید که از یه struct یا class دیگه ارثبری کنه (البته همهشون به صورت پنهان از System.ValueType ارث میبرن). به همین دلیل، اعضای struct نمیتونن virtual یا abstract باشن.
قانون کلی اینه: وقتی تایپ شما بیشتر شبیه به یک "مقدار" ساده هست تا یک "موجودیت" پیچیده با هویت خاص، struct انتخاب خوبیه.
از این چکلیست استفاده کنید:
✅ برای تایپهای کوچک و داده-محور: مثل Point (نقطه)، Color (رنگ)، یا یک زوج مرتب KeyValuePair.
✅ وقتی تغییرناپذیری (Immutability) مهمه: چون structها کپی میشن، تغییر یک نمونه روی بقیه تأثیر نمیذاره و این باعث امنیت بیشتر کد میشه.
✅ وقتی پرفورمنس خیلی مهمه: ساختن structها (مخصوصاً در آرایههای بزرگ) هزینه حافظه کمتری داره چون سربار آبجکتهای روی هیپ رو ندارن و باعث کاهش فشار روی Garbage Collector میشن.
برای اینکه structهای خودتون رو امنتر و واقعاً تغییرناپذیر کنید، از 8 #C به بعد میتونید از کلمه کلیدی readonly قبل از تعریف struct استفاده کنید. این به کامپایلر میگه که تمام فیلدهای این struct باید readonly باشن.
این کار هم "نیت" شما رو برای ساختن یک تایپ تغییرناپذیر نشون میده و هم به کامپایلر اجازه بهینهسازیهای بیشتری رو میده.
استراکت ها ابزارهای قدرتمندی برای بهینهسازی و نوشتن کدهای خوانا هستن، به شرطی که درست و به جا ازشون استفاده بشه.
🧱 راهنمای کامل struct در #C: کی و چرا از آن استفاده کنیم؟
در #C، ما دو نوع اصلی برای ساختن تایپهای خودمون داریم: class و struct. اکثر ما به صورت پیشفرض از class استفاده میکنیم، اما دونستن اینکه struct چیه و کی باید ازش استفاده کنیم، یه مهارت کلیدیه که کد شما رو بهینهتر و خواناتر میکنه.
1️⃣ struct در برابر class: دوئل اصلی
دو تا تفاوت بنیادی وجود داره که همه چیز از اونها نشأت میگیره:
Value Type در برابر Reference Type: 📜
این مهمترین تفاوته. struct یک Value Type هست؛ یعنی وقتی اون رو به یه متغیر دیگه یا یه متد پاس میدید، کل آبجکت کپی میشه. class یک Reference Type هست؛ یعنی فقط رفرنس (آدرس حافظه) اون کپی میشه.
عدم پشتیبانی از وراثت: 🚫
استراکت ها از وراثت پشتیبانی نمیکنن. شما نمیتونید یه struct بسازید که از یه struct یا class دیگه ارثبری کنه (البته همهشون به صورت پنهان از System.ValueType ارث میبرن). به همین دلیل، اعضای struct نمیتونن virtual یا abstract باشن.
2️⃣ چه زمانی باید از struct استفاده کنیم؟
قانون کلی اینه: وقتی تایپ شما بیشتر شبیه به یک "مقدار" ساده هست تا یک "موجودیت" پیچیده با هویت خاص، struct انتخاب خوبیه.
از این چکلیست استفاده کنید:
✅ برای تایپهای کوچک و داده-محور: مثل Point (نقطه)، Color (رنگ)، یا یک زوج مرتب KeyValuePair.
✅ وقتی تغییرناپذیری (Immutability) مهمه: چون structها کپی میشن، تغییر یک نمونه روی بقیه تأثیر نمیذاره و این باعث امنیت بیشتر کد میشه.
✅ وقتی پرفورمنس خیلی مهمه: ساختن structها (مخصوصاً در آرایههای بزرگ) هزینه حافظه کمتری داره چون سربار آبجکتهای روی هیپ رو ندارن و باعث کاهش فشار روی Garbage Collector میشن.
3️⃣ readonly struct: بهترین شیوه مدرن 🛡
برای اینکه structهای خودتون رو امنتر و واقعاً تغییرناپذیر کنید، از 8 #C به بعد میتونید از کلمه کلیدی readonly قبل از تعریف struct استفاده کنید. این به کامپایلر میگه که تمام فیلدهای این struct باید readonly باشن.
این کار هم "نیت" شما رو برای ساختن یک تایپ تغییرناپذیر نشون میده و هم به کامپایلر اجازه بهینهسازیهای بیشتری رو میده.
public readonly struct Point
{
// تمام فیلدها باید readonly باشن
public readonly int X;
public readonly int Y;
public Point(int x, int y)
{
X = x;
Y = y;
}
// متدهایی که وضعیت رو تغییر نمیدن رو هم میتونید readonly مشخص کنید
public readonly double DistanceFromOrigin()
{
return Math.Sqrt(X * X + Y * Y);
}
}
🤔 حرف حساب و تجربه شما
استراکت ها ابزارهای قدرتمندی برای بهینهسازی و نوشتن کدهای خوانا هستن، به شرطی که درست و به جا ازشون استفاده بشه.
🔖 هشتگها:
#OOP #Struct #Performance
📖 سری آموزشی کتاب C# 12 in a Nutshell
تو پست قبلی، با اصول اولیه و کاربردی structها آشنا شدیم. امروز وقتشه که کلاه غواصی رو سرمون کنیم و به دو تا از عمیقترین و تخصصیترین مباحث مربوط به structها شیرجه بزنیم: رفتار عجیب سازندهها و قابلیت ref struct.
این یکی از گیجکنندهترین بخشهای کار با structهاست. یک struct همیشه یک سازنده پیشفرض بدون پارامتر داره که تمام فیلدها رو صفر میکنه (همون default).
حالا اگه شما خودتون یه سازنده بدون پارامتر بنویسید (که از 10 #C به بعد ممکنه)، اون سازنده پیشفرض حذف نمیشه و هنوز از راههای دیگهای مثل ساختن آرایه، قابل دسترسه!
این کد رو ببینید تا کامل متوجه بشید:
توصیه حرفهای: بهترین کار اینه که
این یه قابلیت خیلی خاص و پیشرفته برای بهینهسازیهای سطح پایینه. یه ref struct، نوعی از struct هست که کامپایلر تضمین میکنه فقط و فقط روی Stack زندگی کنه و هرگز به Heap منتقل نشه.
چرا این خوبه؟ چون به ما اجازه میده با حافظه Stack به صورت خیلی بهینه کار کنیم و از فشار روی Garbage Collector کم کنیم، مثل کاری که <Span<T انجام میده.
محدودیتهای ref struct:
چون ref struct هرگز نباید روی هیپ قرار بگیره، محدودیتهای زیر رو داره:
🚫 نمیتونه عضو یک class باشه.
🚫 نمیتونه عنصر یک آرایه باشه.
🚫 نمیتونه Boxed بشه (به object تبدیل بشه).
🚫 نمیتونه اینترفیس پیادهسازی کنه.
🚫 نمیتونه در متدهای async استفاده بشه.
🤔 حرف حساب و تجربه شما
این دو مفهوم، نهایت عمق و قدرت structها در #C رو نشون میدن.
🚀 شیرجه عمیق در structها: سازندههای گیجکننده و ref struct
تو پست قبلی، با اصول اولیه و کاربردی structها آشنا شدیم. امروز وقتشه که کلاه غواصی رو سرمون کنیم و به دو تا از عمیقترین و تخصصیترین مباحث مربوط به structها شیرجه بزنیم: رفتار عجیب سازندهها و قابلیت ref struct.
1️⃣ تلهی سازندهها: دوگانگی new() و default 🤯
این یکی از گیجکنندهترین بخشهای کار با structهاست. یک struct همیشه یک سازنده پیشفرض بدون پارامتر داره که تمام فیلدها رو صفر میکنه (همون default).
حالا اگه شما خودتون یه سازنده بدون پارامتر بنویسید (که از 10 #C به بعد ممکنه)، اون سازنده پیشفرض حذف نمیشه و هنوز از راههای دیگهای مثل ساختن آرایه، قابل دسترسه!
این کد رو ببینید تا کامل متوجه بشید:
struct Point
{
int x = 1;
int y;
// سازنده سفارشی بدون پارامتر
public Point() => y = 1;
}
// --- نتایج عجیب ---
// سازنده صریح و سفارشی ما صدا زده میشه
Point p1 = new Point();
Console.WriteLine($"p1: ({p1.x}, {p1.y})");
// خروجی: p1: (1, 1)
// سازنده پیشفرضِ صفرکننده صدا زده میشه
Point p2 = default;
Console.WriteLine($"p2: ({p2.x}, {p2.y})");
// خروجی: p2: (0, 0)
// آرایهها هم از سازنده پیشفرض و صفرکننده استفاده میکنن
Point[] points = new Point[1];
Console.WriteLine($"points[0]: ({points[0].x}, {points[0].y})");
// خروجی: points[0]: (0, 0)
توصیه حرفهای: بهترین کار اینه که
structهاتون رو جوری طراحی کنید که حالت پیشفرض و صفر شدهشون، یک حالت معتبر و قابل استفاده باشه.2️⃣ ref struct: زندگی فقط روی Stack! ⚡️
این یه قابلیت خیلی خاص و پیشرفته برای بهینهسازیهای سطح پایینه. یه ref struct، نوعی از struct هست که کامپایلر تضمین میکنه فقط و فقط روی Stack زندگی کنه و هرگز به Heap منتقل نشه.
چرا این خوبه؟ چون به ما اجازه میده با حافظه Stack به صورت خیلی بهینه کار کنیم و از فشار روی Garbage Collector کم کنیم، مثل کاری که <Span<T انجام میده.
محدودیتهای ref struct:
چون ref struct هرگز نباید روی هیپ قرار بگیره، محدودیتهای زیر رو داره:
🚫 نمیتونه عضو یک class باشه.
🚫 نمیتونه عنصر یک آرایه باشه.
🚫 نمیتونه Boxed بشه (به object تبدیل بشه).
🚫 نمیتونه اینترفیس پیادهسازی کنه.
🚫 نمیتونه در متدهای async استفاده بشه.
🤔 حرف حساب و تجربه شما
این دو مفهوم، نهایت عمق و قدرت structها در #C رو نشون میدن.
🔖 هشتگها:
#AdvancedCSharp #Struct #Performance #MemoryManagement
مقدمهای بر Distributed Tracing با OpenTelemetry در NET. 📡
اگر در حال ساخت یا نگهداری اپلیکیشنهای توزیعشده NET. هستید، درک نحوه رفتار آنها کلید تضمین قابلیت اطمینان و عملکرد است.
سیستمهای توزیعشده انعطافپذیری ارائه میدهند اما پیچیدگی را نیز به همراه دارند و عیبیابی را به یک سردرد تبدیل میکنند. 🤯 درک چگونگی جریان درخواستها در سیستم شما برای دیباگ کردن و بهینهسازی عملکرد، حیاتی است.
OpenTelemetry
یک فریمورک observability متنباز است که این امر را ممکن میسازد. ✨
در این مقاله، ما به عمق این موضوع میپردازیم که OpenTelemetry چیست، چگونه از آن در پروژههای NET. خود استفاده کنیم و چه بینشهای قدرتمندی را فراهم میکند.
معرفی OpenTelemetry
OpenTelemetry (OTel)
یک استاندارد متنباز و بیطرف نسبت به فروشندگان (vendor-neutral) برای ابزار دقیق (instrumenting) اپلیکیشنها به منظور تولید دادههای تلهمتری است. OpenTelemetry شامل APIها، SDKها، ابزارها و یکپارچهسازیهایی برای ایجاد و مدیریت این دادههای تلهمتری (traces, metrics, and logs) است.
دادههای تلهمتری شامل:
Traces (ردیابیها) 📈:
جریان درخواستها را در سیستمهای توزیعشده نشان میدهند و زمانبندیها و روابط بین سرویسها را نمایش میدهند.
Metrics (معیارها) 📊:
اندازهگیریهای عددی از رفتار سیستم در طول زمان (مانند تعداد درخواستها، نرخ خطا، استفاده از حافظه).
Logs (لاگها) 📝:
رکوردهای متنی از رویدادها با اطلاعات زمینهای غنی. لاگهای ساختاریافته.
OpenTelemetry
یک راه یکپارچه برای جمعآوری این دادهها فراهم میکند، که درک رفتار و سلامت اپلیکیشنهای توزیعشده پیچیده را آسانتر میکند.
ما میتوانیم دادههای تلهمتری که جمعآوری میکنیم را به سرویسی که قادر به پردازش آن است و یک اینترفیس برای تحلیل آن به ما ارائه میدهد، صادر (export) کنیم.
ما قصد داریم OpenTelemetry را طوری پیکربندی کنیم که traceها را مستقیماً به Jaeger صادر کند.
OpenTelemetry
کتابخانهها و SDKهایی برای افزودن کد (instrumentation) به اپلیکیشنهای NET. شما فراهم میکند. این instrumentationها به طور خودکار traces، metrics و logs مورد علاقه ما را ضبط میکنند.
ما قصد داریم پکیجهای NuGet زیر را نصب کنیم: 📦
هنگامی که این پکیجهای NuGet را نصب کردیم، زمان پیکربندی برخی سرویسها فرا میرسد.
🌐 AddAspNetCoreInstrumentation -
این ابزار دقیق (instrumentation) ASP.NET Core را فعال میکند.
📤 AddHttpClientInstrumentation -
این ابزار دقیق HttpClient را برای درخواستهای خروجی فعال میکند.
💾 AddEntityFrameworkCoreInstrumentation -
این ابزار دقیق EF Core را فعال میکند.
🔥 AddRedisInstrumentation -
این ابزار دقیق Redis را فعال میکند.
🐘 AddNpgsql -
این ابزار دقیق PostgreSQL را فعال میکند.
با پیکربندی تمام این ابزارهای دقیق، اپلیکیشن ما شروع به جمعآوری بسیاری از ردیابیهای (traces) ارزشمند در زمان اجرا خواهد کرد.
ما همچنین باید یک متغیر محیطی را برای exporter اضافه شده با AddOtlpExporter پیکربندی کنیم تا به درستی کار کند. ما میتوانیم OTEL_EXPORTER_OTLP_ENDPOINT را از طریق تنظیمات اپلیکیشن تنظیم کنیم. آدرس مشخص شده در اینجا به یک نمونه محلی Jaeger اشاره خواهد کرد.
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
Jaeger
یک پلتفرم متنباز و توزیعشده برای ردیابی است. Jaeger جریان درخواستها و دادهها را حین عبور از یک سیستم توزیعشده، ترسیم میکند.
در اینجا نحوه اجرای Jaeger داخل یک کانتینر Docker آمده است:
ما از ایمیج jaegertracing/all-in-one:latest استفاده میکنیم و پورت 4317 را برای پذیرش دادههای تلهمتری باز میکنیم. رابط کاربری Jaeger روی پورت 16686 در دسترس خواهد بود.
پس از نصب کتابخانههای OpenTelemetry و پیکربندی tracing در اپلیکیشنهایمان، میتوانیم چند درخواست برای تولید دادههای تلهمتری ارسال کنیم. سپس میتوانیم به Jaeger دسترسی پیدا کرده و شروع به تحلیل distributed traceهای خود کنیم.
ثبت نام یک کاربر جدید 👤
انتشار یک پیام با MassTransit 📨
بررسی اطلاعات اضافی trace 💾
traceهای توزیعشده پیچیده 🕸
یک راه یکپارچه برای جمعآوری این دادهها فراهم میکند، که درک رفتار و سلامت اپلیکیشنهای توزیعشده پیچیده را آسانتر میکند.
ما میتوانیم دادههای تلهمتری که جمعآوری میکنیم را به سرویسی که قادر به پردازش آن است و یک اینترفیس برای تحلیل آن به ما ارائه میدهد، صادر (export) کنیم.
ما قصد داریم OpenTelemetry را طوری پیکربندی کنیم که traceها را مستقیماً به Jaeger صادر کند.
افزودن OpenTelemetry به اپلیکیشنهای NET.🔧
OpenTelemetry
کتابخانهها و SDKهایی برای افزودن کد (instrumentation) به اپلیکیشنهای NET. شما فراهم میکند. این instrumentationها به طور خودکار traces، metrics و logs مورد علاقه ما را ضبط میکنند.
ما قصد داریم پکیجهای NuGet زیر را نصب کنیم: 📦
# Automatic tracing, metrics
Install-Package OpenTelemetry.Extensions.Hosting
# Telemetry data exporter
Install-Package OpenTelemetry.Exporter.OpenTelemetryProtocol
# Instrumentation packages
Install-Package OpenTelemetry.Instrumentation.Http
Install-Package OpenTelemetry.Instrumentation.AspNetCore
هنگامی که این پکیجهای NuGet را نصب کردیم، زمان پیکربندی برخی سرویسها فرا میرسد.
services
.AddOpenTelemetry()
.ConfigureResource(resource => resource.AddService(serviceName))
.WithTracing(tracing =>
{
tracing
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddEntityFrameworkCoreInstrumentation()
.AddRedisInstrumentation()
.AddNpgsql();
tracing.AddOtlpExporter();
});
🌐 AddAspNetCoreInstrumentation -
این ابزار دقیق (instrumentation) ASP.NET Core را فعال میکند.
📤 AddHttpClientInstrumentation -
این ابزار دقیق HttpClient را برای درخواستهای خروجی فعال میکند.
💾 AddEntityFrameworkCoreInstrumentation -
این ابزار دقیق EF Core را فعال میکند.
🔥 AddRedisInstrumentation -
این ابزار دقیق Redis را فعال میکند.
🐘 AddNpgsql -
این ابزار دقیق PostgreSQL را فعال میکند.
با پیکربندی تمام این ابزارهای دقیق، اپلیکیشن ما شروع به جمعآوری بسیاری از ردیابیهای (traces) ارزشمند در زمان اجرا خواهد کرد.
ما همچنین باید یک متغیر محیطی را برای exporter اضافه شده با AddOtlpExporter پیکربندی کنیم تا به درستی کار کند. ما میتوانیم OTEL_EXPORTER_OTLP_ENDPOINT را از طریق تنظیمات اپلیکیشن تنظیم کنیم. آدرس مشخص شده در اینجا به یک نمونه محلی Jaeger اشاره خواهد کرد.
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
اجرای Jaeger به صورت محلی 🐳
Jaeger
یک پلتفرم متنباز و توزیعشده برای ردیابی است. Jaeger جریان درخواستها و دادهها را حین عبور از یک سیستم توزیعشده، ترسیم میکند.
در اینجا نحوه اجرای Jaeger داخل یک کانتینر Docker آمده است:
docker run -d -p 4317:4317 -p 16686:16686 jaegertracing/all-in-one:latest
ما از ایمیج jaegertracing/all-in-one:latest استفاده میکنیم و پورت 4317 را برای پذیرش دادههای تلهمتری باز میکنیم. رابط کاربری Jaeger روی پورت 16686 در دسترس خواهد بود.
Distributed Tracing (ردیابی توزیعشده)
پس از نصب کتابخانههای OpenTelemetry و پیکربندی tracing در اپلیکیشنهایمان، میتوانیم چند درخواست برای تولید دادههای تلهمتری ارسال کنیم. سپس میتوانیم به Jaeger دسترسی پیدا کرده و شروع به تحلیل distributed traceهای خود کنیم.
ثبت نام یک کاربر جدید 👤
انتشار یک پیام با MassTransit 📨
بررسی اطلاعات اضافی trace 💾
traceهای توزیعشده پیچیده 🕸
ثبت نام یک کاربر جدید 👤
در اینجا مثالی از ثبت نام یک کاربر جدید در سیستم آمده است. ما در حال دسترسی به سرویس API gateway (Evently.Gateway) هستیم، که درخواست را به سرویس Evently.Api پروکسی میکند. و میتوانید ببینید که سرویس Evently.Api چند درخواست HTTP را قبل از پایدار کردن یک رکورد جدید در دیتابیس، انجام میدهد.
خلاصه 📝
درک اپلیکیشنهای مدرن، به خصوص توزیعشده، میتواند واقعاً گیجکننده باشد. OpenTelemetry مانند داشتن دید اشعه ایکس 👁 به سیستم شماست.
در حالی که افزودن OpenTelemetry نیاز به مقداری کار اولیه دارد، آن را یک سرمایهگذاری در نظر بگیرید. این سرمایهگذاری زمانی که مشکلات بروز میکنند، به شدت نتیجه میدهد. به جای حدس و گمانهای آشفته، شما دادههای دقیقی برای تمرکز سریع بر روی مشکلات دارید.
🔖 هشتگها:
#OpenTelemetry #Distributed_Tracing
#Performance #MemoryManagement
📖 سری آموزشی کتاب C# 12 in a Nutshell
در برنامهنویسی شیءگرا، کپسولهسازی (Encapsulation) یعنی مخفی کردن جزئیات پیادهسازی و فقط نمایش دادن چیزهای ضروری. ابزار اصلی ما برای این کار در C# Access Modifiers هست.
این کلمات کلیدی، نگهبانان کد شما هستن و مشخص میکنن که هر کلاس یا عضو اون، از کجا قابل دسترسیه.
معرفی نگهبانان
دربهای کاملاً باز! هر کسی از هر جایی (چه داخل اسمبلی و چه بیرون) میتونه ببینه و استفاده کنه.
فقط خودیها! فقط کدهای داخل همون اسمبلی (پروژه) میتونن ببینن. این حالت پیشفرض برای کلاسهای غیر تودرتو است.
راز شخصی! فقط کدهای داخل همون کلاس یا struct میتونن ببینن. این حالت پیشفرض برای اعضای کلاسها (مثل فیلدها و متدها) هست.
فقط خانواده! فقط کدهای داخل همون کلاس و کلاسهای فرزندی که ازش ارثبری کردن، میتونن ببینن.
خودیها و خانواده! اجتماع protected و internal. یعنی هم از داخل اسمبلی جاری دیده میشه و هم توسط کلاسهای فرزند (حتی اگه تو یه اسمبلی دیگه باشن).
فقط خانوادهی خودی! اشتراک protected و internal. یعنی فقط توسط کلاسهای فرزندی که در همون اسمبلی هستن، دیده میشه. این سطح دسترسی از protected و internal به تنهایی، محدودتره.
فقط همین فایل! اعضایی که با file مشخص میشن، فقط در همون فایلی که تعریف شدن، قابل مشاهده هستن. این بیشتر برای Source Generatorها کاربرد داره.
نکات حرفهای (Pro Tips) 💡
Friend Assemblies:
گاهی وقتا میخواید به یه پروژه دیگه (مثل پروژه تست) اجازه بدید که به اعضای internal شما دسترسی داشته باشه. با اتریبیوت [assembly: InternalsVisibleTo("FriendAssemblyName")] در فایل AssemblyInfo.cs یا .csproj میتونید این کار رو انجام بدید.
Accessibility Capping (سقف دسترسی):
سطح دسترسی یک تایپ، سطح دسترسی اعضای public اون رو محدود میکنه. یعنی یه متد public داخل یه کلاس internal، در عمل internal حساب میشه.
انتخاب درست Access Modifier، یکی از مهمترین تصمیمها در طراحی APIهای تمیز و قابل نگهداریه.
🛡راهنمای کامل Access Modifiers در #C: چه کسی کد شما را میبیند؟
در برنامهنویسی شیءگرا، کپسولهسازی (Encapsulation) یعنی مخفی کردن جزئیات پیادهسازی و فقط نمایش دادن چیزهای ضروری. ابزار اصلی ما برای این کار در C# Access Modifiers هست.
این کلمات کلیدی، نگهبانان کد شما هستن و مشخص میکنن که هر کلاس یا عضو اون، از کجا قابل دسترسیه.
معرفی نگهبانان
🌍 public (عمومی):
دربهای کاملاً باز! هر کسی از هر جایی (چه داخل اسمبلی و چه بیرون) میتونه ببینه و استفاده کنه.
🏢 internal (داخلی):
فقط خودیها! فقط کدهای داخل همون اسمبلی (پروژه) میتونن ببینن. این حالت پیشفرض برای کلاسهای غیر تودرتو است.
🔐 private (خصوصی):
راز شخصی! فقط کدهای داخل همون کلاس یا struct میتونن ببینن. این حالت پیشفرض برای اعضای کلاسها (مثل فیلدها و متدها) هست.
👨👩👧 protected (محافظت شده):
فقط خانواده! فقط کدهای داخل همون کلاس و کلاسهای فرزندی که ازش ارثبری کردن، میتونن ببینن.
🤝 protected internal:
خودیها و خانواده! اجتماع protected و internal. یعنی هم از داخل اسمبلی جاری دیده میشه و هم توسط کلاسهای فرزند (حتی اگه تو یه اسمبلی دیگه باشن).
🤫 private protected:
فقط خانوادهی خودی! اشتراک protected و internal. یعنی فقط توسط کلاسهای فرزندی که در همون اسمبلی هستن، دیده میشه. این سطح دسترسی از protected و internal به تنهایی، محدودتره.
📄 file (از C# 11):
فقط همین فایل! اعضایی که با file مشخص میشن، فقط در همون فایلی که تعریف شدن، قابل مشاهده هستن. این بیشتر برای Source Generatorها کاربرد داره.
نکات حرفهای (Pro Tips) 💡
Friend Assemblies:
گاهی وقتا میخواید به یه پروژه دیگه (مثل پروژه تست) اجازه بدید که به اعضای internal شما دسترسی داشته باشه. با اتریبیوت [assembly: InternalsVisibleTo("FriendAssemblyName")] در فایل AssemblyInfo.cs یا .csproj میتونید این کار رو انجام بدید.
Accessibility Capping (سقف دسترسی):
سطح دسترسی یک تایپ، سطح دسترسی اعضای public اون رو محدود میکنه. یعنی یه متد public داخل یه کلاس internal، در عمل internal حساب میشه.
🤔 حرف حساب و تجربه شما
انتخاب درست Access Modifier، یکی از مهمترین تصمیمها در طراحی APIهای تمیز و قابل نگهداریه.
🔖 هشتگها:
#OOP #Encapsulation
مدیریت خطای تابعی (Functional) در NET. با الگوی Result ✅
چگونه باید خطاها را در کد خود مدیریت کنید؟
این موضوع بحثهای زیادی بوده است و من میخواهم نظر خود را به اشتراک بگذارم.
یک مکتب فکری استفاده از استثناها (exceptions) را برای کنترل جریان (flow control) پیشنهاد میکند. 🤯 این رویکرد خوبی نیست زیرا استدلال در مورد کد را دشوارتر میکند. فراخواننده (caller) باید جزئیات پیادهسازی و اینکه کدام استثناها را باید مدیریت کند، بداند.
استثناها برای شرایط استثنایی هستند.
امروز، میخواهم به شما نشان دهم چگونه مدیریت خطا را با استفاده از الگوی Result پیادهسازی کنید. ✨
این یک رویکرد تابعی برای مدیریت خطا است که کد شما را گویاتر میکند.
استثناها برای کنترل جریان ⚡️
استفاده از استثناها برای کنترل جریان، رویکردی برای پیادهسازی اصل fail-fast است.
به محض اینکه با خطایی در کد مواجه میشوید، یک استثنا پرتاب میکنید — که به طور موثر متد را خاتمه میدهد و فراخواننده را مسئول مدیریت استثنا میکند.
مشکل این است که فراخواننده باید بداند کدام استثناها را مدیریت کند. و این تنها از امضای متد مشخص نیست.
یک مورد استفاده رایج دیگر، پرتاب استثناها برای خطاهای اعتبارسنجی است.
در اینجا یک مثال در FollowerService آمده است: 👨💻
public sealed class FollowerService
{
private readonly IFollowerRepository _followerRepository;
public FollowerService(IFollowerRepository followerRepository)
{
_followerRepository = followerRepository;
}
public async Task StartFollowingAsync(
User user,
User followed,
DateTime createdOnUtc,
CancellationToken cancellationToken = default)
{
if (user.Id == followed.Id)
{
throw new DomainException("Can't follow yourself");
}
if (!followed.HasPublicProfile)
{
throw new DomainException("Can't follow non-public profile");
}
if (await _followerRepository.IsAlreadyFollowingAsync(
user.Id,
followed.Id,
cancellationToken))
{
throw new DomainException("Already following");
}
var follower = Follower.Create(user.Id, followed.Id, createdOnUtc);
_followerRepository.Insert(follower);
}
}
از استثناها برای شرایط استثنایی استفاده کنید ⚡️
یک قانون سرانگشتی که من دنبال میکنم این است که از استثناها برای شرایط استثنایی استفاده کنم. از آنجایی که شما از قبل انتظار خطاهای بالقوه را دارید، چرا آن را صریح نکنید؟
شما میتوانید تمام خطاهای اپلیکیشن را به دو گروه تقسیم کنید:
✅ خطاهایی که میدانید چگونه مدیریت کنید.
❓ خطاهایی که نمیدانید چگونه مدیریت کنید.
استثناها یک راهحل عالی برای خطاهایی هستند که نمیدانید چگونه مدیریت کنید. و شما باید آنها را در پایینترین سطح ممکن catch کرده و مدیریت کنید.
در مورد خطاهایی که میدانید چگونه مدیریت کنید چطور؟
شما میتوانید آنها را به روش تابعی با الگوی Result مدیریت کنید. این صریح است و به وضوح این نیت را بیان میکند که متد میتواند شکست بخورد. نقطه ضعف این است که فراخواننده باید به صورت دستی بررسی کند که آیا عملیات شکست خورده است یا نه.
بیان خطاها با استفاده از الگوی Result ✨
اولین چیزی که نیاز خواهید داشت، یک کلاس Error برای نمایش خطاهای اپلیکیشن است.
🔹️ Code -
نام منحصر به فرد برای خطا در اپلیکیشن.
🔹️ Denoscription -
شامل جزئیات توسعهدهنده-پسند در مورد خطا.
public sealed record Error(string Code, string Denoscription)
{
public static readonly Error None = new(string.Empty, string.Empty);
}
سپس، میتوانید کلاس Result را با استفاده از Error برای توصیف شکست، پیادهسازی کنید. این پیادهسازی بسیار ساده است و شما میتوانید ویژگیهای بسیار بیشتری به آن اضافه کنید. در اکثر موارد، شما همچنین به یک کلاس جنریک Result<T> نیاز دارید که یک مقدار را در داخل خود بپیچد.
در اینجا ظاهر کلاس Result آمده است: 🎁
public class Result
{
private Result(bool isSuccess, Error error)
{
if (isSuccess && error != Error.None ||
!isSuccess && error == Error.None)
{
throw new ArgumentException("Invalid error", nameof(error));
}
IsSuccess = isSuccess;
Error = error;
}
public bool IsSuccess { get; }
public bool IsFailure => !IsSuccess;
public Error Error { get; }
public static Result Success() => new(true, Error.None);
public static Result Failure(Error error) => new(false, error);
}
تنها راه برای ایجاد یک نمونه Result، استفاده از متدهای استاتیک است:
🔹️ Success -
یک نتیجه موفقیتآمیز ایجاد میکند.
🔹️ Failure -
یک نتیجه شکست با Error مشخص شده ایجاد میکند.
اگر میخواهید از ساختن کلاس Result خودتان اجتناب کنید، نگاهی به کتابخانه FluentResults 📚 بیندازید.