C# Geeks (.NET) – Telegram
استفاده از کتابخانه‌های 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
🧱 راهنمای کامل 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ها: سازنده‌های گیج‌کننده و 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 به اپلیکیشن‌های 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 را قبل از پایدار کردن یک رکورد جدید در دیتابیس، انجام می‌دهد.
انتشار یک پیام با MassTransit 📨

در اینجا یک distributed trace دیگر وجود دارد که در آن ما UserRegisteredIntegrationEvent را از طریق یک message bus منتشر می‌کنیم. می‌توانید ببینید که توسط دو سرویس مختلف که مقداری داده را در دیتابیس می‌نویسند، مصرف می‌شود.
بررسی اطلاعات اضافی trace 💾

Distributed trace
ها می‌توانند شامل اطلاعات زمینه‌ای مفیدی باشند. در اینجا یک مثال از trace که یک دستور دیتابیس را نشان می‌دهد، آمده است. این از ابزار دقیق PostgreSQL می‌آید و ما می‌توانیم کوئری SQL را که در حال اجرا هستیم، ببینیم.
خلاصه 📝

درک اپلیکیشن‌های مدرن، به خصوص توزیع‌شده، می‌تواند واقعاً گیج‌کننده باشد. OpenTelemetry مانند داشتن دید اشعه ایکس 👁 به سیستم شماست.

در حالی که افزودن OpenTelemetry نیاز به مقداری کار اولیه دارد، آن را یک سرمایه‌گذاری در نظر بگیرید. این سرمایه‌گذاری زمانی که مشکلات بروز می‌کنند، به شدت نتیجه می‌دهد. به جای حدس و گمان‌های آشفته، شما داده‌های دقیقی برای تمرکز سریع بر روی مشکلات دارید.

🔖 هشتگ‌ها:
#OpenTelemetry #Distributed_Tracing
#Performance #MemoryManagement
📖 سری آموزشی کتاب C# 12 in a Nutshell

🛡راهنمای کامل 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 📚 بیندازید.
به‌کارگیری الگوی Result 🚀

حالا که کلاس Result را داریم، بیایید ببینیم چگونه آن را در عمل به کار ببریم.

در اینجا یک نسخه بازآرایی (refactor) شده از FollowerService آمده است. به چند نکته توجه کنید:

دیگر هیچ استثنایی پرتاب نمی‌شود.

نوع بازگشتی Result صریح است.

مشخص است که متد کدام خطاها را برمی‌گرداند.

مزیت دیگر مدیریت خطا با استفاده از الگوی Result این است که تست کردن آن آسان‌تر است.
public sealed class FollowerService
{
private readonly IFollowerRepository _followerRepository;

public FollowerService(IFollowerRepository followerRepository)
{
_followerRepository = followerRepository;
}

public async Task<Result> StartFollowingAsync(
User user,
User followed,
DateTime utcNow,
CancellationToken cancellationToken = default)
{
if (user.Id == followed.Id)
{
return Result.Failure(FollowerErrors.SameUser);
}
if (!followed.HasPublicProfile)
{
return Result.Failure(FollowerErrors.NonPublicProfile);
}
if (await _followerRepository.IsAlreadyFollowingAsync(
user.Id,
followed.Id,
cancellationToken))
{
return Result.Failure(FollowerErrors.AlreadyFollowing);
}

var follower = Follower.Create(user.Id, followed.Id, utcNow);
_followerRepository.Insert(follower);

return Result.Success();
}
}


مستندسازی خطاهای اپلیکیشن 📚

شما می‌توانید از کلاس Error برای مستندسازی تمام خطاهای ممکن در اپلیکیشن خود استفاده کنید.

یک رویکرد، ایجاد یک کلاس استاتیک به نام Errors است. این کلاس، کلاس‌های تودرتو در داخل خود خواهد داشت که حاوی خطاهای خاص هستند. نحوه استفاده به شکل Errors.Followers.NonPublicProfile خواهد بود.

با این حال، رویکردی که من دوست دارم استفاده کنم، ایجاد یک کلاس خاص است که حاوی خطاها باشد.

در اینجا کلاس FollowerErrors آمده است که خطاهای ممکن برای انتیتی Follower را مستند می‌کند: 📝
public static class FollowerErrors
{
public static readonly Error SameUser = new Error(
"Followers.SameUser", "Can't follow yourself");

public static readonly Error NonPublicProfile = new Error(
"Followers.NonPublicProfile", "Can't follow non-public profiles");

public static readonly Error AlreadyFollowing = new Error(
"Followers.AlreadyFollowing", "Already following");
}


به جای فیلدهای استاتیک، شما همچنین می‌توانید از متدهای استاتیک که یک خطا برمی‌گردانند، استفاده کنید. شما این متد را با یک آرگومان مشخص فراخوانی می‌کنید تا یک نمونه Error دریافت کنید.
public static class FollowerErrors
{
public static Error NotFound(Guid id) => new Error(
"Followers.NotFound", $"The follower with Id '{id}' was not found");
}
تبدیل Resultها به پاسخ‌های API 🚀

آبجکت Result در نهایت به endpoint Minimal API (یا کنترلر) در ASP.NET Core خواهد رسید. Minimal APIها یک پاسخ IResult برمی‌گردانند و کنترلرها یک پاسخ IActionResult برمی‌گردانند. صرف نظر از این، شما باید نمونه Result را به یک پاسخ API معتبر تبدیل کنید.

رویکرد سرراست، بررسی وضعیت Result و برگرداندن یک پاسخ HTTP است. در اینجا یک مثال آمده است که در آن ما فلگ Result.IsFailure را بررسی می‌کنیم:
app.MapPost(
"users/{userId}/follow/{followedId}",
async (Guid userId, Guid followedId, FollowerService followerService) =>
{
var result = await followerService.StartFollowingAsync(
userId,
followedId,
DateTime.UtcNow);

if (result.IsFailure)
{
return Results.BadRequest(result.Error);
}

return Results.NoContent();
});

با این حال، این یک فرصت عالی برای یک رویکرد تابعی‌تر است. شما می‌توانید متد توسعه Match را برای ارائه یک callback برای هر وضعیت Result پیاده‌سازی کنید. متد Match، callback مربوطه را اجرا کرده و نتیجه را برمی‌گرداند.

در اینجا پیاده‌سازی Match آمده است: 👍
public static class ResultExtensions
{
public static T Match<T>(
this Result result,
Func<T> onSuccess,
Func<Error, T> onFailure)
{
return result.IsSuccess ? onSuccess() : onFailure(result.Error);
}
}

و اینگونه از متد Match در یک endpoint Minimal API استفاده می‌کنید:
app.MapPost(
"users/{userId}/follow/{followedId}",
async (Guid userId, Guid followedId, FollowerService followerService) =>
{
var result = await followerService.StartFollowingAsync(
userId,
followedId,
DateTime.UtcNow);

return result.Match(
onSuccess: () => Results.NoContent(),
onFailure: error => Results.BadRequest(error));
});


خلاصه 📝

اگر قرار است یک چیز را از مقاله امروز با خود ببرید، باید این باشد: استثناها برای شرایط استثنایی هستند. علاوه بر این، شما فقط باید از استثناها برای خطاهایی که نمی‌دانید چگونه مدیریت کنید، استفاده کنید. در تمام موارد دیگر، بیان واضح خطا با الگوی Result ارزشمندتر است.

استفاده از کلاس Result به شما اجازه می‌دهد تا:

نیت اینکه یک متد ممکن است شکست بخورد را بیان کنید.

یک خطای اپلیکیشن را در داخل آن کپسوله کنید.

یک راه تابعی برای مدیریت خطاها فراهم کنید.

علاوه بر این، شما می‌توانید تمام خطاهای اپلیکیشن را با کلاس Error مستندسازی کنید. این برای توسعه‌دهندگان مفید است تا بدانند کدام خطاها را باید مدیریت کنند.

شما حتی می‌توانید این را به مستندات واقعی تبدیل کنید. 📚 برای مثال، من یک برنامه ساده نوشتم که پروژه را برای تمام فیلدهای Error اسکن می‌کند. سپس این را به فرمت جدول تبدیل کرده و در یک صفحه Confluence آپلود می‌کند.

بنابراین من شما را تشویق می‌کنم که الگوی Result را امتحان کنید و ببینید چگونه می‌تواند کد شما را بهبود ببخشد.
😅🤦🏻‍♂️
📖 سری آموزشی کتاب C# 12 in a Nutshell

📜 قراردادهای کدنویسی در #C: راهنمای کامل اینترفیس‌ها (Interfaces)

چطور می‌تونیم برای کلاس‌هامون یه "قرارداد" تعریف کنیم و بگیم "هر کلاسی که می‌خواد با من کار کنه، باید این قابلیت‌ها رو داشته باشه"؟

ابزار اصلی ما برای این کار در دنیای شیءگرایی، اینترفیس (Interface) هست. اینترفیس‌ها ستون فقرات معماری‌های تمیز، انعطاف‌پذیر و تست‌پذیر هستن.

1️⃣ اینترفیس چیست؟ یک قرارداد، بدون پیاده‌سازی

اینترفیس فقط رفتار (Behavior) رو تعریف می‌کنه، نه وضعیت (State). یعنی فقط امضای متدها و پراپرتی‌ها رو داره، نه فیلدهای داده و نه بدنه پیاده‌سازی برای اعضاش.

تفاوت‌های کلیدی با کلاس:

🔹 فقط توابع (متد، پراپرتی، ایونت، ایندکسر) را تعریف می‌کند، نه فیلدها.

🔹 اعضایش به صورت پیش‌فرض public و abstract هستند.

🔹 یک کلاس می‌تواند چندین اینترفیس را پیاده‌سازی کند (برخلاف وراثت از کلاس که فقط یکیه).

مثال (اینترفیس IEnumerator):
public interface IEnumerator
{
bool MoveNext();
object Current { get; }
void Reset();
}


2️⃣ پیاده‌سازی اینترفیس: امضای قرارداد

وقتی یه کلاس، اینترفیسی رو پیاده‌سازی می‌کنه، قول میده که برای تمام اعضای اون اینترفیس، یک پیاده‌سازی public ارائه بده.
internal class Countdown : IEnumerator
{
int count = 11;
public bool MoveNext() => count-- > 0;
public object Current => count;
public void Reset() { throw new NotSupportedException(); }
}

حالا می‌تونید یک نمونه از Countdown رو در متغیری از نوع IEnumerator بریزید:
IEnumerator e = new Countdown();
while (e.MoveNext())
{
Console.Write(e.Current + " "); // 10 9 8 7 6 5 4 3 2 1 0
}


3️⃣ جادوی پیاده‌سازی صریح (Explicit Implementation) 🎭

حالا اگه یه کلاس، دو تا اینترفیس رو پیاده‌سازی کنه که متدهایی با اسم یکسان ولی امضای متفاوت دارن، چی میشه؟ یا اگه بخوایم یه متد از اینترفیس رو از دید عمومی کلاس مخفی کنیم؟

اینجا پای پیاده‌سازی صریح (Explicit Implementation) به میدون باز میشه. در این حالت، شما اسم اینترفیس رو قبل از اسم متد میارید.

مثال (حل تداخل):
interface I1 { void Foo(); }
interface I2 { int Foo(); }
public class Widget : I1, I2
{
public void Foo() // پیاده‌سازی پیش‌فرض برای I1
{
Console.WriteLine("Widget's implementation of I1.Foo");
}

// پیاده‌سازی صریح برای حل تداخل با I1.Foo
int I2.Foo()
{
Console.WriteLine("Widget's implementation of I2.Foo");
return 42;
}
}


نکته حیاتی: ⚠️ متدی که به صورت صریح پیاده‌سازی شده، دیگه به صورت عمومی در دسترس نیست. برای صدا زدنش، باید اول آبجکت رو به اون اینترفیس خاص کست کنید:
Widget w = new Widget();
w.Foo(); // I1.Foo صدا زده میشه
((I1)w).Foo(); // I1.Foo صدا زده میشه
((I2)w).Foo(); // I2.Foo صدا زده میشه


🤔 حرف حساب و تجربه شما

اینترفیس‌ها، اساس طراحی‌های ماژولار، تست‌پذیر و مبتنی بر اصول SOLID هستن.

🔖 هشتگ‌ها:
#CSharp #DotNet #OOP #Interface #CleanCode
📖 سری آموزشی کتاب C# 12 in a Nutshell
🧩 وراثت و اینترفیس‌ها در #C: بازپیاده‌سازی و الگوهای حرفه‌ای

تو پست‌ قبلی، وراثت و اینترفیس‌ها رو جداگونه بررسی کردیم. اما وقتی این دو تا با هم ترکیب میشن، دنیایی از نکات ظریف و الگوهای پیشرفته به وجود میاد.

امروز می‌خوایم یاد بگیریم چطور یه عضو اینترفیس رو در سلسله‌مراتب وراثت به درستی override کنیم.

1️⃣ روش استاندارد: virtual و override

به صورت پیش‌فرض، وقتی یه عضو اینترفیس رو پیاده‌سازی می‌کنید، اون sealed (مهر و موم شده) هست. برای اینکه به کلاس‌های فرزند اجازه override کردنش رو بدید، باید اون رو صراحتاً virtual مشخص کنید. این کار به شما اجازه میده از قدرت کامل چندریختی (Polymorphism) استفاده کنید.
public interface IUndoable { void Undo(); }
public class TextBox : IUndoable
{
public virtual void Undo() => Console.WriteLine("TextBox.Undo");
}

public class RichTextBox : TextBox
{
public override void Undo() => Console.WriteLine("RichTextBox.Undo");
}
// --- نتایج ---
RichTextBox r = new RichTextBox();
r.Undo(); // RichTextBox.Undo
((IUndoable)r).Undo(); // RichTextBox.Undo
((TextBox)r).Undo(); // RichTextBox.Undo (رفتار یکپارچه و درست)


2️⃣ تکنیک خطرناک: بازپیاده‌سازی
(Re-implementation) ⚠️

حالا فرض کنید کلاس پدر، متد رو virtual نکرده. یه راه برای "override" کردنش، بازپیاده‌سازی اینترفیس در کلاس فرزنده. این کار، پیاده‌سازی رو فقط وقتی که آبجکت از طریق خود اینترفیس صدا زده بشه، "هایجک" می‌کنه.

تله‌ی بزرگ: ☠️ اگه پیاده‌سازی در کلاس پدر به صورت ضمنی (public) باشه، این الگو باعث رفتار متناقض و خطرناک میشه!
public class TextBox : IUndoable
{
// این متد virtual نیست!
public void Undo() => Console.WriteLine("TextBox.Undo");
}
public class RichTextBox : TextBox, IUndoable // بازپیاده‌سازی اینترفیس
{
// اینجا new هم می‌تونستیم بذاریم
public void Undo() => Console.WriteLine("RichTextBox.Undo");
}

// --- نتایج متناقض و خطرناک ---
RichTextBox r = new RichTextBox();
r.Undo(); // RichTextBox.Undo
((IUndoable)r).Undo(); // RichTextBox.Undo (هایجک شد)
((TextBox)r).Undo(); // TextBox.Undo (فاجعه! رفتار چندریختی شکست)


3️⃣ الگوهای حرفه‌ای (جایگزین‌های بهتر)

بازپیاده‌سازی معمولاً یه راه حل ضعیف و نشانه‌ی طراحی بده. دو الگوی خیلی بهتر برای طراحی کلاس‌های توسعه‌پذیر وجود داره:

الگوی اول: اگه پیاده‌سازی ضمنیه، همیشه virtual ـش کنید (همون روش شماره ۱).

الگوی دوم (برای پیاده‌سازی صریح): پیاده‌سازی صریح (explicit) اینترفیس رو به یک متد protected virtual وصل کنید. این الگو، قدرت کامل رو به کلاس‌های فرزند میده تا رفتار رو به صورت امن override کنن.
public class TextBox : IUndoable
{
// پیاده‌سازی صریح، کار را به یک متد مجازی و محافظت‌شده می‌سپارد
void IUndoable.Undo() => Undo();

protected virtual void Undo() => Console.WriteLine("TextBox.Undo");
}
public class RichTextBox : TextBox
{
protected override void Undo() => Console.WriteLine("RichTextBox.Undo");
}


🤔 حرف حساب و تجربه شما

این الگوها، تفاوت بین یه کتابخونه قابل اعتماد و یه کتابخونه شکننده رو رقم می‌زنن. طراحی برای توسعه‌پذیری، نشانه یک معمار نرم‌افزار حرفه‌ایه.

🔖 هشتگ‌ها:
#CSharp #DotNet #OOP #Interface #Inheritance
8️⃣ نکته برای نوشتن کد تمیز 🧼

کد تمیز، کدی است که خواندن، نگهداری و درک آن آسان است.

نقطه شروع 🏁

من دوست دارم هنگام یادگیری مفاهیم جدید، با یک مشکل شروع کنم.
و هر چه مشکل گویاتر باشد، بهتر است.
بنابراین ما از یک کد با نوشتار ضعیف به عنوان نقطه شروع برای بازآرایی خود استفاده خواهیم کرد.
و در هر مرحله، من مشخص خواهم کرد که مشکل فعلی چیست و چگونه آن را برطرف خواهیم کرد.

این چیزی است که من وقتی به متد Process نگاه می‌کنم، می‌بینم:

nesting
تو در توی عمیق کد - دقیقاً ۴ سطح.

چک‌های پیش‌شرط (Precondition) یکی پس از دیگری اعمال می‌شوند.

پرتاب استثنا (Exception) برای نمایش یک شکست.

چگونه می‌توانیم این را به کد تمیز تبدیل کنیم؟ 👎
public void Process(Order? order)
{
if (order != null)
{
if (order.IsVerified)
{
if (order.Items.Count > 0)
{
if (order.Items.Count > 15)
{
throw new Exception(
"The order " + order.Id + " has too many items");
}
if (order.Status != "ReadyToProcess")
{
throw new Exception(
"The order " + order.Id + " isn't ready to process");
}

order.IsProcessed = true;
}
}
}
}


1️⃣: اصل بازگشت زودهنگام (Early Return Principle) 🚪


تا الان باید به طرز دردناکی واضح باشد که نسخه اولیه به دلیل دستورات if که چک‌های پیش‌شرط را اعمال می‌کنند، به شدت تودرتو است.
ما این مشکل را با استفاده از اصل بازگشت زودهنگام حل خواهیم کرد، که بیان می‌کند ما باید به محض برآورده شدن شرایط، از یک متد return کنیم.

در مورد متد Process، این به معنای حرکت از یک ساختار به شدت تودرتو به مجموعه‌ای از guard clauses (شرط‌های محافظ) است. 👍
public void Process(Order? order)
{
if (order is null)
{
return;
}

if (!order.IsVerified)
{
return;
}

if (order.Items.Count == 0)
{
return;
}

if (order.Items.Count > 15)
{
throw new Exception(
"The order " + order.Id + " has too many items");
}

if (order.Status != "ReadyToProcess")
{
throw new Exception(
"The order " + order.Id + " isn't ready to process");
}

order.IsProcessed = true;
}