C# Geeks (.NET) – Telegram
📖 سری آموزشی کتاب C# 12 in a Nutshell

🚩 الگوی [Flags] Enum در #C: مدیریت چندین گزینه با بیت‌ها

تو پست قبلی با Enumهای استاندارد آشنا شدیم. اما اگه بخوایم یه متغیر بتونه ترکیبی از چند عضو Enum رو همزمان نگه داره چی؟ مثلاً دسترسی‌های کاربر (خواندن، نوشتن، حذف کردن).

اینجاست که الگوی قدرتمند [Flags] وارد میشه.

1️⃣ [Flags] چیست و کی بهش نیاز داریم؟

[Flags]
یه اتریبیوته که به کامپایلر و ابزارها میگه اعضای این Enum رو میشه با هم مثل پرچم‌های مختلف ترکیب کرد. این برای مدیریت گزینه‌ها (options)، دسترسی‌ها (permissions) یا هر وضعیتی که می‌تونه چند حالت همزمان داشته باشه، عالیه.

2️⃣ نحوه پیاده‌سازی: قدرت اعداد توان ۲

کلید اصلی این الگو، مقداردهی اعضا با توان‌های ۲ هست (1, 2, 4, 8, 16, ...). این کار باعث میشه هر عضو، نماینده یک بیت منحصر به فرد در یک عدد باشه و بتونیم اون‌ها رو بدون تداخل با هم ترکیب کنیم.
[Flags] // این اتریبیوت مهمه!
public enum BorderSides
{
None = 0,
Left = 1, // 2^0
Right = 2, // 2^1
Top = 4, // 2^2
Bottom = 8, // 2^3
// می‌تونیم ترکیبات رایج رو هم از قبل تعریف کنیم
LeftRight = Left | Right,
All = Left | Right | Top | Bottom
}


3️⃣ کار با Flags: عملگرهای بیتی

برای کار با Flags، از عملگرهای بیتی (Bitwise Operators) استفاده می‌کنیم:

🔹️ | (OR):
برای اضافه کردن یا ترکیب کردن چند فلگ.

🔹️ & (AND):
برای چک کردن اینکه آیا یه فلگ خاص در مجموعه وجود داره یا نه.

🔹️ ^ (XOR):
برای حذف کردن (toggle) یک فلگ اگه وجود داشته باشه.
// ترکیب کردن دو فلگ
BorderSides leftRight = BorderSides.Left | BorderSides.Right;
// چک کردن وجود یک فلگ
if ((leftRight & BorderSides.Left) != 0)
{
Console.WriteLine("Includes Left"); // اجرا میشه
}

// حذف یک فلگ
leftRight ^= BorderSides.Left; // حالا فقط Right باقی می‌مونه

// جادوی ToString() با اتریبیوت [Flags]
Console.WriteLine(leftRight.ToString()); // خروجی: Right

اگه اتریبیوت [Flags] رو نذارید، ToString() به جای اسم اعضا، مقدار عددی اون‌ها رو برمی‌گردونه.

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

الگوی [Flags] یکی از اون ترفندهای کلاسیک و قدرتمند #C هست که به شما اجازه میده با حجم کمی از داده، اطلاعات زیادی رو به صورت بهینه مدیریت کنید.

🔖 هشتگ‌ها:
#CSharp #DotNet #Enum #Flags #Bitwise
حضرت تمام شد بلاخره🔥
خوش گذشت...بریم برای کتاب بعدی🤙🏻
شما چه کتابی پیشنهاد میدید؟؟
📖 سری آموزشی کتاب C# 12 in a Nutshell

📦 کلاس در کلاس: راهنمای کامل Nested Types در #C

تا حالا شده یه کلاسی بنویسید که اونقدر به یه کلاس دیگه وابسته باشه که انگار باید جزئی از اون باشه؟ یا بخواید یه کلاس کمکی بسازید که فقط و فقط توسط یک کلاس دیگه استفاده میشه؟

سی‌شارپ برای این سناریوها یه راه حل خیلی تمیز و قدرتمند داره: Nested Types (تایپ‌های تودرتو).

1️⃣ Nested Type چیست؟

یه Nested Type، یک تایپ (class, struct, enum, ...) است که داخل یک کلاس یا struct دیگه تعریف میشه. مثل یه جعبه‌ی کوچیک، داخل یه جعبه‌ی بزرگتر.
public class TopLevel
{
public class Nested { } // کلاس تودرتو
public enum Color { Red, Blue, Tan } // enum تودرتو
}

برای دسترسی به یک Nested Type از بیرون، باید اون رو با اسم کلاس بیرونی مشخص کنید:
TopLevel.Nested n = new TopLevel.Nested();
TopLevel.Color color = TopLevel.Color.Red;


2️⃣ ابرقدرت‌های Nested Types 🦸‍♂️

چرا باید از یه Nested Type استفاده کنیم؟ چون چند تا قدرت ویژه دارن که کلاس‌های معمولی ندارن:

دسترسی به اعضای private: 🔐

مهم‌ترین قدرتشون اینه که می‌تونن به اعضای private و protected کلاسی که داخلش هستن، دسترسی داشته باشن! این یعنی یه سطح کپسوله‌سازی فوق‌العاده.
public class TopLevel
{
private static int x = 10;
class Nested
{
// Nested به فیلد private کلاس TopLevel دسترسی داره!
static void Foo() => Console.WriteLine(TopLevel.x);
}
}


سطوح دسترسی بیشتر: 🛡

می‌تونن هر Access Modifierی داشته باشن (public, private, protected و...). این در حالیه که کلاس‌های معمولی فقط می‌تونن public یا internal باشن. پیش‌فرض دسترسی برای Nested Typeها، private هست.

3️⃣ قانون طلایی: کی از Nested Type استفاده کنیم؟ 🎯

این مهم‌ترین بخش ماجراست.

دلیل اشتباه: استفاده از Nested Type برای جلوگیری از شلوغ شدن namespace. برای این کار از namespace تودرتو استفاده کنید.

دلایل درست: فقط زمانی از Nested Type استفاده کنید که:

• کلاس داخلی شما به شدت به کلاس بیرونی وابسته است و به اعضای private اون نیاز مستقیم داره.

• می‌خواید با استفاده از Access Modifierها (مثل private یا protected)، اون کلاس رو از دنیای بیرون کاملاً مخفی کنید و به عنوان یه جزئیات پیاده‌سازی داخلی نگهش دارید.

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

Nested Types
یه ابزار قدرتمند برای کپسوله‌سازی پیشرفته هستن، به شرطی که به جا و درست ازشون استفاده بشه.

🔖 هشتگ‌ها:
#CSharp #DotNet #OOP #Encapsulation
📖 سری آموزشی کتاب C# 12 in a Nutshell

🧬 جادوی Generics در #C: نوشتن یک کد برای همه تایپ‌ها!

سی‌شارپ دو تا مکانیزم اصلی برای نوشتن کدهای قابل استفاده مجدد (reusable) داره: وراثت و جنریک‌ها. وراثت این کار رو با یک تایپ پایه انجام میده، ولی جنریک‌ها با یک "قالب" که شامل "نوع‌های جایگزین" (placeholder) هست، این کار رو می‌کنن.

امروز می‌خوایم ببینیم جنریک‌ها چی هستن و چرا به وجود اومدن.

1️⃣ مشکل چه بود؟ (چرا به جنریک‌ها نیاز داریم؟)

فرض کنید به یه ساختار داده مثل پشته (Stack) نیاز داریم. بدون جنریک‌ها، دو راه بد پیش روی ما بود:

راه حل اول: تکرار کد 👎

باید برای هر نوع داده‌ای که می‌خواستیم، یه کلاس جدا می‌نوشتیم (IntStack, StringStack, UserStack و...). این یعنی حجم وحشتناکی از کد تکراری.

راه حل دوم: استفاده از object 👎

می‌تونستیم یه ObjectStack بسازیم که هر نوعی رو قبول کنه (چون همه تایپ‌ها از object ارث می‌برن). اما این روش دو تا مشکل بزرگ داشت که تو پست قبلی دیدیم:

• عدم ایمنی نوع (Type Safety): شما می‌تونستید به اشتباه یه string رو تو پشته‌ای که برای int ساخته بودید، Push کنید و کامپایلر هیچ خطایی نمی‌گرفت!

• هزینه پرفورمنس Boxing/Unboxing : برای کار با Value Typeها (مثل int)، باید مدام هزینه کپی کردن داده روی Heap (Boxing) و برگردوندنش (Unboxing) رو پرداخت می‌کردیم.

2️⃣ جنریک‌ها (Generics): بهترین‌های هر دو دنیا

جنریک‌ها دقیقاً برای حل این مشکل به وجود اومدن. به ما اجازه میدن یه "قالب" کد بنویسیم که هم قابل استفاده مجدد باشه (مثل ObjectStack) و هم ایمنی نوع و پرفورمنس بالا داشته باشه (مثل IntStack).

شما یک نوع جنریک با یک پارامتر T می‌سازید. بعداً موقع استفاده، به جای T، نوع داده واقعی خودتون رو قرار میدید.
public class Stack<T> // T یک نوع جایگزین است
{
int position;
T[] data = new T[100]; // آرایه ما حالا از نوع T است
public void Push(T obj) => data[position++] = obj;
public T Pop() => data[--position];
}


3️⃣ مزایای بزرگ در عمل

حالا ببینیم این نسخه جنریک چطور مشکلات رو حل می‌کنه:
var stack = new Stack<int>();
stack.Push(5);
stack.Push(10);
// stack.Push("hello"); // خطای زمان کامپایل! کامپایلر به شما ایمنی نوع کامل میده.

int x = stack.Pop(); // بدون نیاز به کست. بدون هزینه Unboxing. (x is 10)
int y = stack.Pop(); // (y is 5)


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

جنریک‌ها یکی از قدرتمندترین قابلیت‌های #C هستن و اساس تمام کالکشن‌های مدرن دات‌نت (List<T>, Dictionary<TKey, TValue>) رو تشکیل میدن. تسلط بر اون‌ها برای نوشتن کدهای قابل استفاده مجدد، امن و بهینه، ضروریه.

🔖 هشتگ‌ها:
#CSharp #DotNet #OOP #Generics
معماری برش عمودی (Vertical Slice Architecture) 🔪


معماری‌های لایه‌ای، پایه و اساس بسیاری از سیستم‌های نرم‌افزاری هستند. با این حال، معماری‌های لایه‌ای سیستم را حول لایه‌های فنی سازماندهی می‌کنند. و انسجام (cohesion) بین لایه‌ها پایین است.

چه می‌شد اگر به جای آن، می‌خواستید سیستم را حول ویژگی‌ها (features) سازماندهی کنید؟ 🤔

کوپلینگ (coupling) بین ویژگی‌های نامرتبط را به حداقل برسانید و کوپلینگ را در یک ویژگی واحد به حداکثر برسانید.

امروز می‌خواهم در مورد معماری برش عمودی صحبت کنم، که دقیقاً همین کار را انجام می‌دهد.

مشکل معماری‌های لایه‌ای 👎

معماری‌های لایه‌ای، سیستم نرم‌افزاری را به لایه‌ها یا طبقات (tiers) سازماندهی می‌کنند. هر یک از لایه‌ها معمولاً یک پروژه در سولوشن شماست. برخی از پیاده‌سازی‌های محبوب، معماری N-tier یا معماری تمیز (Clean architecture) هستند.

معماری‌های لایه‌ای بر روی تفکیک دغدغه‌های (separating the concerns) کامپوننت‌های مختلف تمرکز می‌کنند. این کار، درک و نگهداری سیستم نرم‌افزاری را آسان‌تر می‌کند. و مزایای زیادی برای طراحی نرم‌افزار ساختاریافته وجود دارد ، مانند قابلیت نگهداری، انعطاف‌پذیری و اتصال سست (loose coupling).
با این حال، معماری‌های لایه‌ای محدودیت‌ها یا قوانین سفت و سختی را نیز بر سیستم شما تحمیل می‌کنند. جهت وابستگی‌ها بین لایه‌ها از پیش تعیین شده است.

برای مثال، در معماری تمیز (Clean Architecture): ⛓️


🔹 Domain
نباید هیچ وابستگی داشته باشد.

🔹 لایه Application می‌تواند به Domain ارجاع دهد.

🔹 Infrastructure
می‌تواند هم به Application و هم به Domain ارجاع دهد.

🔹 Presentation
می‌تواند هم به Application و هم به Domain ارجاع دهد.

شما در نهایت به coupling بالا در داخل یک لایه و coupling پایین بین لایه‌ها می‌رسید. این به این معنی نیست که معماری‌های لایه‌ای بد هستند. اما به این معنی است که شما انتزاع‌های (abstractions) زیادی بین لایه‌های مجزا خواهید داشت. و انتزاع‌های بیشتر به معنای افزایش پیچیدگی است 🤯 زیرا کامپوننت‌های بیشتری برای نگهداری وجود دارد.

معماری برش عمودی (Vertical Slice Architecture) چیست؟ 🤔

من برای اولین بار در مورد معماری برش عمودی از جیمی بوگارد شنیدم. او همچنین خالق برخی کتابخانه‌های متن‌باز محبوب مانند MediatR و Automapper است.

معماری برش عمودی از درد کار با معماری‌های لایه‌ای متولد شد. آن‌ها شما را مجبور می‌کنند برای پیاده‌سازی یک ویژگی، تغییراتی در لایه‌های بسیار متفاوتی ایجاد کنید.

بیایید تصور کنیم افزودن یک ویژگی جدید در یک معماری لایه‌ای چگونه به نظر می‌رسد: 📝

🔹 به‌روزرسانی مدل دامین

🔹 اصلاح منطق اعتبارسنجی

🔹 ایجاد یک مورد استفاده با MediatR

🔹 ارائه یک endpoint API از یک کنترلر

انسجام پایین است زیرا شما در حال ایجاد فایل‌های زیادی در لایه‌های مختلف هستید.

برش‌های عمودی رویکرد متفاوتی را در پیش می‌گیرند:

کوپلینگ بین برش‌ها را به حداقل برسانید و کوپلینگ را در یک برش به حداکثر برسانید. 🎯
در این عکس نحوه تجسم برش‌های عمودی آمده است:
تمام فایل‌ها برای یک مورد استفاده واحد، در داخل یک پوشه گروه‌بندی می‌شوند. بنابراین، انسجام برای یک مورد استفاده واحد بسیار بالا است. 👍 این کار تجربه توسعه را ساده می‌کند. پیدا کردن تمام کامپوننت‌های مرتبط برای هر ویژگی آسان است، زیرا آن‌ها به هم نزدیک هستند.

🎯پیاده‌سازی برش‌های عمودی

اگر در حال ساخت یک API هستید، سیستم از قبل به کامندها (POST/PUT/DELETE) و کوئری‌ها (GET) تقسیم می‌شود. با تقسیم کردن درخواست‌ها به کامندها و کوئری‌ها، شما از مزایای الگوی CQRS بهره‌مند می‌شوید. 🚀

برش‌های عمودی به طور محدود روی یک ویژگی واحد تمرکز می‌کنند. این به شما اجازه می‌دهد تا هر مورد استفاده را به صورت جداگانه در نظر گرفته و پیاده‌سازی را متناسب با نیازمندی‌های خاص، سفارشی کنید. یک برش عمودی می‌تواند از EF Core برای پیاده‌سازی یک درخواست GET استفاده کند. برش عمودی دیگری می‌تواند از Dapper با کوئری‌های SQL خام استفاده کند.

یکی دیگر از مزایای پیاده‌سازی برش‌های عمودی به این شکل این است:

ویژگی‌های جدید فقط کد اضافه می‌کنند، شما کد اشتراکی را تغییر نمی‌دهید و نگران عوارض جانبی نیستید.

با این حال، برش‌های عمودی مجموعه چالش‌های خاص خود را دارند. ⚠️ از آنجایی که شما بخش زیادی از منطق بیزینس را داخل یک مورد استفاده واحد پیاده‌سازی می‌کنید، باید قادر به تشخیص code smells باشید. با رشد مورد استفاده، ممکن است در نهایت کار بیش از حدی انجام دهد. شما مجبور خواهید بود با انتقال منطق به دامین، کد را بازآرایی (refactor) کنید.
ساختار سولوشن با الگوی REPR 🧩

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

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

یک رویکرد جالب برای ساختاربندی APIها حول ویژگی‌ها، استفاده از الگوی REPR است. این مخفف Request-EndPoint-Response است. این کاملاً با ایده برش‌های عمودی هماهنگ است. شما می‌توانید برای مثال، این را با کتابخانه MediatR به دست آورید.

الگوی REPR تعریف می‌کند که endpointهای وب API باید سه کامپوننت داشته باشند:

1️⃣ Request (درخواست)

2️⃣ Endpoint (نقطه پایانی)

3️⃣ Response (پاسخ)

در اینجا یک مثال از ساختار سولوشن در NET. آمده است. شما پوشه Features را مشاهده خواهید کرد که حاوی برش‌های عمودی است. هر برش عمودی یک درخواست API (یا مورد استفاده) را پیاده‌سازی می‌کند.
📂 RunTracker.API
| 📁 Database
| 📁 Entities
| #️⃣ Activity.cs
| #️⃣ Workout.cs
| #️⃣ ...
| 📁 Features
| 📁 Activities
| 📁 GetActivity
| #️⃣ ActivityResponse.cs
| #️⃣ GetActivityEndpoint.cs
| #️⃣ GetActivityQuery.cs
| #️⃣ GetActivityQueryHandler.cs
| 📁 CreateActivity
| #️⃣ CreateActivity.cs
| #️⃣ CreateActivity.Command.cs
| #️⃣ CreateActivity.Endpoint.cs
| #️⃣ CreateActivity.Handler.cs
| #️⃣ CreateActivity.Validator.cs
| 📁 Workouts
| 📁 ...
| 📁 Middleware
| 📄 appsettings.json
| 📄 appsettings.Development.json
| #️⃣ Program.cs


چند کتابخانه دیگر برای پیاده‌سازی الگوی REPR:

🔹 FastEndpoints

🔹 ApiEndpoints

قدم‌های بعدی 🚀

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

ممنون برای مطالعه، و عالی بمانید!🤍
📖 سری آموزشی کتاب C# 12 in a Nutshell

🧬 متدهای جنریک در #C: نوشتن الگوریتم‌های همه‌کاره

تو پست قبلی، با تایپ‌های جنریک مثل <Stack<T آشنا شدیم. اما قدرت جنریک‌ها به کلاس‌ها محدود نمیشه! ما می‌تونیم متدهایی بنویسیم که خودشون پارامترهای نوعی (<T>) داشته باشن.

به این‌ها متدهای جنریک (Generic Methods) میگن و به ما اجازه میدن الگوریتم‌های بنیادی رو به صورت کاملاً قابل استفاده مجدد و Type-Safe بنویسیم.

1️⃣ مثال کلاسیک: متد <Swap<T

فرض کنید می‌خوایم یه متد بنویسیم که محتوای دو تا متغیر رو با هم عوض کنه. بدون جنریک‌ها، باید برای int، string و هر نوع دیگه‌ای یه نسخه جدا می‌نوشتیم (تکرار کد!). اما با یه متد جنریک، این کار رو فقط یک بار و برای همه تایپ‌ها انجام میدیم:
static void Swap<T>(ref T a, ref T b)
{
T temp = a;
a = b;
b = temp;
}

2️⃣ جادوی استنتاج نوع (Type Inference)

بهترین بخش ماجرا اینه که در ۹۹٪ مواقع، شما نیازی ندارید که نوع T رو موقع صدا زدن متد مشخص کنید (<Swap<int). کامپایلر #C اونقدر هوشمنده که خودش از روی نوع آرگومان‌هایی که به متد پاس میدید، T رو تشخیص میده.
int x = 5;
int y = 10;
// نیازی به نوشتن Swap<int>(ref x, ref y) نیست!
Swap(ref x, ref y);
string s1 = "Hello";
string s2 = "World";
// کامپایلر خودش می‌فهمه T اینجا از نوع string هست
Swap(ref s1, ref s2);


3️⃣ قوانین و جزئیات مهم جنریک‌ها ⚖️

چه چیزهایی می‌توانند جنریک باشند؟
در #C، پارامترهای نوعی (<T>) فقط در تعریف کلاس‌ها، استراکت‌ها، اینترفیس‌ها، دلیگیت‌ها و متدها قابل استفاده هستن. اعضای دیگه مثل پراپرتی‌ها یا سازنده‌ها نمی‌تونن پارامتر نوعی جدیدی تعریف کنن، ولی می‌تونن از پارامتر نوعی که کلاسشون تعریف کرده، استفاده کنن.

🔹️ چندین پارامتر جنریک:
یک تایپ یا متد جنریک می‌تونه چند تا پارامتر نوعی داشته باشه:
// TKey و TValue دو پارامتر نوعی متفاوت هستن
class Dictionary<TKey, TValue> { /* ... */ }


🔹️ اورلودینگ بر اساس تعداد پارامترها:
شما می‌تونید اسم یک تایپ یا متد رو بر اساس تعداد پارامترهای جنریکش اورلود کنید. (به تعداد پارامترهای جنریک، "Arity" گفته میشه).
class A {}           // Arity = 0
class A<T> {} // Arity = 1
class A<T1, T2> {} // Arity = 2


🔹️ قرارداد نام‌گذاری:
طبق قرارداد، برای پارامترهای نوعی تک حرفی از T استفاده میشه. اگه پارامترهای بیشتری دارید، اون‌ها رو با اسم‌های توصیفی‌تر و با پیشوند T نام‌گذاری کنید (مثل TKey, TValue).

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

متدهای جنریک به شما اجازه میدن الگوریتم‌های بنیادی رو یک بار بنویسید و همه جا به صورت type-safe ازشون استفاده کنید، که این اساس نوشتن کدهای تمیز و قابل استفاده مجدده.

🔖 هشتگ‌ها:
#CSharp #DotNet #OOP #Generics
Forwarded from thisisnabi.dev [Farsi]
کتاب خوب بخونید.
📖 سری آموزشی کتاب C# 12 in a Nutshell

⚙️ قدرت where: راهنمای کامل Generic Constraints در #C


تو پست قبلی با جنریک‌ها آشنا شدیم. اما پارامتر T به صورت پیش‌فرض یک "لوح سفید" هست و کامپایلر هیچ‌چیزی در موردش نمی‌دونه. چطور می‌تونیم به کامپایلر بگیم که T باید چه قابلیت‌هایی داشته باشه؟

با استفاده از قیدهای جنریک (Generic Constraints) و کلمه کلیدی where.

1️⃣ چرا به قیدها (Constraints) نیاز داریم؟

به صورت پیش‌فرض، شما می‌تونید هر نوعی رو جای T قرار بدید. قیدها این امکان رو محدود می‌کنن، اما هدف اصلی‌شون فعال کردن قابلیت‌های جدیده. وقتی شما یه قید میذارید، به کامپایلر میگید: "من قول میدم که T حتماً این قابلیت‌ها رو داره" و کامپایلر هم در عوض به شما اجازه میده از اون قابلیت‌ها استفاده کنید (مثلاً متدهای یک اینترفیس رو روی T صدا بزنید).

2️⃣ انواع قیدهای جنریک

این لیست کامل قیدهاییه که می‌تونید با where استفاده کنید:

🔹️ where T : base-class
(قید کلاس پایه: T باید از این کلاس ارث‌بری کند)

🔹️ where T : interface
(قید اینترفیس: T باید این اینترفیس را پیاده‌سازی کند)

🔹️ where T : class
(قید نوع ارجاعی: T باید یک کلاس باشد)

🔹️ where T : struct
(قید نوع مقداری: T باید یک struct باشد)

🔹️ where T : new()
(قید سازنده: T باید یک سازنده بدون پارامتر داشته باشد)

🔹️ where U : T
(قید نوع عریان: پارامتر جنریک U باید از T ارث‌بری کند)

3️⃣ مثال‌های کاربردی

قید اینترفیس (: <IComparable<T):
فرض کنید می‌خوایم یه متد Max بنویسیم. برای این کار، باید مطمئن باشیم که T قابلیت مقایسه شدن رو داره.
static T Max<T>(T a, T b) where T : IComparable<T>
{
// حالا که قید گذاشتیم، کامپایلر اجازه میده متد CompareTo رو صدا بزنیم
return a.CompareTo(b) > 0 ? a : b;
}
// استفاده:
int z = Max(5, 10); // 10


قید نوع مقداری (: struct):

این قید تضمین می‌کنه که T باید یک Value Type باشد. بهترین مثالش، خودِ System.Nullable<T> هست که فقط برای Value Typeها معنی داره.
struct Nullable<T> where T : struct { /* ... */ }


قید سازنده (: ()new):

این قید تضمین می‌کنه که T یک سازنده عمومی بدون پارامتر داره. این به ما اجازه میده که بتونیم با new T() ازش نمونه بسازیم.
static void Initialize<T>(T[] array) where T : new()
{
for (int i = 0; i < array.Length; i++)
array[i] = new T();
}


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

قیدهای جنریک، ابزار اصلی شما برای ساختن APIهای جنریک قدرتمند، امن و کارآمد هستن.

🔖 هشتگ‌ها:
#CSharp #DotNet #OOP #Generics
📖 سری آموزشی کتاب C# 12 in a Nutshell

🧬 وراثت در دنیای جنریک‌ها: ساختن تایپ‌های پیچیده

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

1️⃣ ارث‌بری از کلاس‌های جنریک

یک کلاس جنریک می‌تونه مثل هر کلاس دیگه‌ای به ارث برده بشه. کلاس فرزند (subclass) در این حالت چند تا انتخاب داره:

باز گذاشتن پارامتر نوعی: 📖

در این حالت، کلاس فرزند هم جنریک باقی می‌مونه و پارامتر نوعی T رو از پدر به ارث می‌بره.
class Stack<T> { /* ... */ }
class SpecialStack<T> : Stack<T> { /* ... */ }


بستن پارامتر نوعی:
شما می‌تونید با یه نوع مشخص، پارامتر جنریک کلاس پدر رو ببندید. در این حالت، کلاس فرزند شما دیگه جنریک نیست.
class IntStack : Stack<int> { /* ... */ }

معرفی پارامترهای نوعی جدید: کلاس فرزند می‌تونه علاوه بر پارامترهای پدر، پارامترهای جنریک جدیدی هم برای خودش تعریف کنه.
class List<T> { /* ... */ }
class KeyedList<T, TKey> : List<T> { /* ... */ }


2️⃣ الگوی پیشرفته: Self-Referencing Generics 🤯

یکی از الگوهای خیلی قدرتمند و رایج، اینه که یک تایپ، یک اینترفیس جنریک رو با خودش به عنوان پارامتر نوعی، پیاده‌سازی کنه. این کار برای تعریف قراردادهایی مثل قابلیت مقایسه شدن (<IEquatable<T) عالیه و به شما ایمنی نوع کامل در زمان کامپایل میده.

مثال کتاب (Balloon):
public interface IEquatable<T> 
{
bool Equals(T obj);
}
// کلاس Balloon، قرارداد مقایسه شدن با یک Balloon دیگر را پیاده‌سازی می‌کند
public class Balloon : IEquatable
{
public string Color { get; set; }
public int CC { get; set; }

public bool Equals(Balloon b)
{
if (b == null) return false;
return b.Color == Color && b.CC == CC;
}
}


این الگو خیلی قدرتمنده و تو قیدهای جنریک هم استفاده میشه:
// این کلاس جنریک، فقط تایپ‌هایی رو به عنوان T قبول می‌کنه
// که خودشون قابل مقایسه با خودشون باشن!
class Foo<T> where T : IComparable<T> { /* ... */ }


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

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

🔖 هشتگ‌ها:
#CSharp #DotNet #OOP #Generics
چگونه Middlewareهای سفارشی در ASP.NET Core بسازیم 🔧


معماری middleware در ASP.NET Core راهی قدرتمند برای ساخت و پیکربندی پایپ‌لاین درخواست HTTP در اپلیکیشن‌های شما ارائه می‌دهد. در این پست، شما بررسی خواهید کرد که middleware چیست و چگونه middlewareهای سفارشی در ASP.NET Core بسازید.

Middleware در ASP.NET Core چیست؟ 🤔


میدلور در ASP.NET Core یک کامپوننت نرم‌افزاری است که بخشی از پایپ‌لاین اپلیکیشن است که درخواست‌ها و پاسخ‌ها را مدیریت می‌کند. در ASP.NET Core چندین middleware وجود دارد که با یکدیگر در یک زنجیره ترکیب شده‌اند. ⛓️

هر کامپوننت middleware در پایپ‌لاین مسئول فراخوانی کامپوننت بعدی در توالی است. هر middleware می‌تواند در صورت لزوم با short-circuit کردن زنجیره، اجرای middlewareهای دیگر را متوقف کند. Middlewareها در ASP.NET Core یک پیاده‌سازی کلاسیک از الگوی طراحی chain of responsibility هستند.

ASP.NET Core
تعداد زیادی middleware داخلی دارد و بسیاری نیز توسط پکیج‌های Nuget ارائه شده‌اند. ترتیبی که middlewareها به پایپ‌لاین اپلیکیشن اضافه می‌شوند، حیاتی است. ⚠️ این ترتیب تعریف می‌کند که چگونه درخواست‌های HTTP ورودی از طریق پایپ‌لاین عبور می‌کنند و پاسخ‌ها با چه توالی‌ای بازگردانده می‌شوند.

میدلور ها به ترتیبی که به پایپ‌لاین در آبجکت WebApplication اضافه شده‌اند، اجرا می‌شوند.

چگونه یک Middleware سفارشی در ASP.NET Core بسازیم ✍️

شما می‌توانید یک middleware سفارشی را به روش‌های زیر ایجاد کنید:

1️⃣ ارائه یک delegate برای متد Use در کلاس WebApplication.

2️⃣ ایجاد یک کلاس Middleware بر اساس قرارداد (by convention).

3️⃣ ایجاد یک کلاس Middleware با ارث‌بری از اینترفیس IMiddleware.
1️⃣ با یک متد Use در کلاس WebApplication

شما می‌توانید یک متد Use را روی کلاس WebApplication فراخوانی کنید تا یک middleware بسازید:
var builder = WebApplication.CreateBuilder(args);
builder.Logging.AddConsole();
var app = builder.Build();

app.Use(async (context, next) =>
{
Console.WriteLine("Request is starting...");
await next();
Console.WriteLine("Request has finished");
});

app.MapGet("/api/books", () =>
{
var books = SeedService.GetBooks(10);
return Results.Ok(books);
});

await app.RunAsync();

در این مثال هنگام فراخوانی یک endpoint به آدرس /api/books، ابتدا middleware تعریف شده در متد Use فراخوانی می‌شود. await next.Invoke() خودِ endpoint کتاب‌ها را فراخوانی می‌کند، اما قبل و بعد از آن ما یک پیام در کنسول لاگ کرده‌ایم:
Request is starting...
Request has finished


میدلور ها به ترتیبی که به پایپ‌لاین در آبجکت WebApplication اضافه شده‌اند، اجرا می‌شوند. هر middleware می‌تواند عملیاتی را قبل و بعد از middleware بعدی انجام دهد:

🔹 قبل: اجرای عملیات قبل از فراخوانی middleware بعدی می‌تواند شامل تسک‌هایی مانند لاگینگ، احراز هویت، اعتبارسنجی و غیره باشد.

🔹 بعد: عملیات پس از فراخوانی middleware بعدی می‌تواند شامل تسک‌هایی مانند تغییر پاسخ یا مدیریت خطا باشد.

قدرت واقعی middlewareها این است که شما می‌توانید آن‌ها را آزادانه به هر ترتیبی که می‌خواهید، زنجیره‌ای کنید. برای متوقف کردن اجرای درخواست و short-cut کردن زنجیره middleware (متوقف کردن اجرای middlewareهای دیگر) - یک پاسخ را مستقیماً در HttpContext بنویسید به جای فراخوانی متد await next.Invoke():
await context.Response.WriteAsync("Some response here");


2️⃣ با یک کلاس Middleware بر اساس قرارداد (By Convention)

شما می‌توانید یک middleware را به یک کلاس جداگانه که از قرارداد خاصی پیروی می‌کند، استخراج کنید:
public class LoggingMiddleware
{
private readonly RequestDelegate _next;

public LoggingMiddleware(RequestDelegate next)
{
_next = next;
}

public async Task InvokeAsync(HttpContext context)
{
Console.WriteLine($"Request: {context.Request.Method} {context.Request.Path}");
await _next(context);
Console.WriteLine($"Response: {context.Response.StatusCode}");
}
}

برای افزودن این middleware به پایپ‌لاین، متد UseMiddleware را روی کلاس WebApplication فراخوانی کنید:
app.Use(async (context, next) =>
{
// Middleware از مثال قبلی
});

app.UseMiddleware<LoggingMiddleware>();

در نتیجه اجرای این middleware، موارد زیر هنگام اجرای یک endpoint به آدرس /api/books در کنسول لاگ خواهد شد:
Request is starting...
Request: GET /api/books
Response: 200
Request has finished

این رویکرد "بر اساس قرارداد" نامیده می‌شود، زیرا کلاس middleware باید از این قوانین پیروی کند:

کلاس middleware باید یک متد InvokeAsync با یک آرگومان الزامی HttpContext داشته باشد.

کلاس middleware باید یک RequestDelegate بعدی را در سازنده تزریق کند.

کلاس middleware، delegate RequestDelegate بعدی را فراخوانی کرده و آرگومان HttpContext را به آن پاس دهد.
3️⃣ با یک کلاس Middleware که اینترفیس IMiddleware را پیاده‌سازی می‌کند 🛡

رویکرد قبلی معایب خود را دارد: توسعه‌دهنده باید یک کلاس middleware بسازد که از تمام قوانین ذکر شده در بالا پیروی کند، در غیر این صورت middleware کار نخواهد کرد. اما یک راه امن‌تر برای ایجاد middleware وجود دارد: پیاده‌سازی اینترفیس IMiddleware:
public class ExecutionTimeMiddleware : IMiddleware
{
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
var watch = Stopwatch.StartNew();

await next(context);

watch.Stop();

Console.WriteLine($"Request executed in {watch.ElapsedMilliseconds}ms");
}
}

این رویکرد بسیار امن‌تر است زیرا کامپایلر به شما می‌گوید که کلاس middleware چگونه باید باشد.
برای این رویکرد شما باید به صورت دستی ExecutionTimeMiddleware را در کانتینر DI ثبت کنید:
builder.Services.AddScoped<ExecutionTimeMiddleware>();

برای افزودن این middleware به پایپ‌لاین، متد UseMiddleware را روی کلاس WebApplication فراخوانی کنید:
app.Use(async (context, next) =>
{
// Middleware از مثال قبلی
});
app.UseMiddleware<LoggingMiddleware>();
app.UseMiddleware<ExecutionTimeMiddleware>();

در نتیجه اجرای این middleware، موارد زیر هنگام اجرای یک endpoint به آدرس /api/books در کنسول لاگ خواهد شد:
Request is starting...
Request: GET /api/books
Request executed in 68ms
Response: 200
Request has finished



میدلرور ها و تزریق وابستگی (Dependency Injection) 💉

میدلور هایی که بر اساس قرارداد ساخته می‌شوند، به طور پیش‌فرض طول عمر Singleton دارند و تمام وابستگی‌های تزریق شده در سازنده نیز باید singleton باشند. همانطور که می‌دانیم، middlewareها به ازای هر درخواست اجرا می‌شوند و شما می‌توانید وابستگی‌های scoped را در متد InvokeAsync بعد از HttpContext تزریق کنید. در اینجا ما یک ILoggingService را که به عنوان سرویس scoped در DI ثبت شده است، تزریق می‌کنیم:
builder.Services.AddScoped<ILoggingService, ConsoleLoggingService>();

در مرحله بعد:
public class LoggingMiddleware
{
private readonly RequestDelegate _next;
public LoggingMiddleware(RequestDelegate next)
{
_next = next;
}

public async Task InvokeAsync(HttpContext context, ILoggingService loggingService)
{
loggingService.LogRequest(context.Request.Method, context.Request.Path);
await _next(context);
loggingService.LogResponse(context.Response.StatusCode);
}
}
این رویکرد فقط برای کلاس‌های middleware که بر اساس قرارداد ایجاد شده‌اند، مناسب است. برای تزریق سرویس‌های scoped به کلاس‌های middleware که اینترفیس IMiddleware را پیاده‌سازی می‌کنند، به سادگی از سازنده استفاده کنید:

public class ExecutionTimeMiddleware : IMiddleware
{
private readonly ILoggingService _loggingService;
public ExecutionTimeMiddleware(ILoggingService loggingService)
{
_loggingService = loggingService;
}
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
// ...
}
}


📌 نکته: هنگام ایجاد یک کلاس middleware که اینترفیس IMiddleware را پیاده‌سازی می‌کند - شما مسئول انتخاب یک طول عمر DI مناسب برای آن هستید. شما می‌توانید middleware را به صورت Singleton, Scoped یا Transient ایجاد کنید، آنچه را که در هر مورد استفاده بهترین است، انتخاب کنید.

خلاصه 📝

شما می‌توانید یک middleware سفارشی را به روش‌های زیر ایجاد کنید:

🔹 ارائه یک delegate برای متد Use در کلاس WebApplication.

🔹 ایجاد یک کلاس Middleware بر اساس قرارداد.

🔹 ایجاد یک کلاس Middleware با ارث‌بری از اینترفیس IMiddleware.

انتخاب ترجیحی من، ایجاد یک middleware با ارث‌بری از اینترفیس IMiddleware است. این رویکرد یک راه امن‌تر و راحت‌تر برای ایجاد middlewareها و یک استراتژی تزریق وابستگی سرراست از طریق سازنده ارائه می‌دهد. و همچنین کنترل کاملی بر روی طول عمر middleware به شما می‌دهد.

امیدوارم این مقاله برایتان مفید باشد.👋🏻
📖 سری آموزشی کتاب C# 12 in a Nutshell

🔬 نکات عمیق جنریک‌ها: Static Data و مقدار default

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

این نکات، درک شما رو از جنریک‌ها کامل می‌کنه.

1️⃣ مقدار پیش‌فرض T (default(T))

چطور می‌تونیم مقدار پیش‌فرض یک پارامتر جنریک T رو بدست بیاریم؟ چون T می‌تونه هر چیزی باشه (value type یا reference type)، ما نمی‌تونیم مستقیماً null یا 0 بهش بدیم.

راه حل #C، کلمه کلیدی default هست.

default(T)
به ما مقدار پیش‌فرض T رو میده:

🔹 اگه T یک Reference Type باشه، مقدارش null میشه.

🔹 اگه T یک Value Type باشه، مقدارش صفر میشه (نتیجه صفر کردن بیت‌های حافظه).
static void Zap<T>(T[] array)
{
for (int i = 0; i < array.Length; i++)
{
// مقدار پیش‌فرض T را در آرایه قرار می‌دهد
array[i] = default(T);
}
}
// از #C 7.1 به بعد، می‌تونیم خلاصه‌تر بنویسیم:
// array[i] = default;


2️⃣ تله‌ی داده‌های استاتیک در جنریک‌ها ⚠️

این یکی از اون نکات خیلی مهمه که خیلی‌ها رو غافلگیر می‌کنه.

قانون اینه: داده‌های استاتیک (Static Fields/Properties) برای هر نوع بسته‌ی جنریک (Closed Type)، منحصر به فرد و جداگانه هستن.

یعنی static فیلدِ <Bob<int هیچ ربطی به static فیلدِ <Bob<string نداره و هر کدوم در حافظه، جای خودشون رو دارن.

مثال کتاب برای درک بهتر این موضوع:
class Bob<T> 
{
public static int Count;
}
// --- نتایج ---
Console.WriteLine(++Bob.Count); // خروجی: 1 (شمارنده مخصوص int)
Console.WriteLine(++Bob.Count); // خروجی: 2 (شمارنده مخصوص int)

Console.WriteLine(++Bob.Count); // خروجی: 1 (شمارنده مخصوص string، کاملاً جداست!)

Console.WriteLine(++Bob.Count); // خروجی: 1 (شمارنده مخصوص object، این هم جداست!)


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

این جزئیات فنی، نشون میده که جنریک‌ها در #C فقط یه جایگزینی ساده متن نیستن، بلکه یه مکانیزم قدرتمند در سطح CLR هستن که تایپ‌های کاملاً جدیدی رو در زمان اجرا تولید می‌کنن.

🔖 هشتگ‌ها:
#CSharp #DotNet #OOP #Generics #AdvancedCSharp