C# Geeks (.NET) – Telegram
📌پست بعدی با دقت خوانده شود...
به دردتون میخوره
مدیریت خطای تابعی (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;
}
2️⃣: ادغام دستورات If برای بهبود خوانایی 🔄

اصل بازگشت زودهنگام، متد Process را خواناتر می‌کند.
اما نیازی نیست که یک guard clause پس از دیگری داشته باشیم.
بنابراین می‌توانیم همه آن‌ها را در یک دستور if ادغام کنیم.
رفتار متد Process بدون تغییر باقی می‌ماند، اما ما مقدار زیادی کد اضافی را حذف می‌کنیم.
public void Process(Order? order)
{
if (order is null
!order.IsVerified
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;
}

3️⃣: استفاده از LINQ برای کد خلاصه‌تر

یک بهبود سریع می‌تواند استفاده از LINQ برای خلاصه‌تر و گویاتر کردن کد باشد.
به جای بررسی Items.Count == 0، من ترجیح می‌دهم از متد Any در LINQ استفاده کنم.
شما می‌توانید استدلال کنید که LINQ عملکرد بدتری دارد، اما من همیشه برای خوانایی بهینه‌سازی می‌کنم.
عملیات بسیار پرهزینه‌تری در یک اپلیکیشن نسبت به یک فراخوانی متد ساده وجود دارد.
public void Process(Order? order)
{
if (order is null
!order.IsVerified
!order.Items.Any()) // استفاده از Any()
{
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;
}


4️⃣: عبارت بولین با متد توصیفی 🗣

ادغام چندین شرط در یک دستور if به معنای نوشتن کد کمتر است، اما می‌تواند خوانایی را در شرایط پیچیده کاهش دهد.
با این حال، شما می‌توانید این مشکل را برطرف کرده و خوانایی را با استفاده از یک متغیر یا متد با نام توصیفی بهبود ببخشید.
من استفاده از متدها را ترجیح می‌دهم، بنابراین متد IsProcessable را برای نمایش چک پیش‌شرط معرفی خواهم کرد.
public void Process(Order? order)
{
if (!IsProcessable(order))
{
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;
}

static bool IsProcessable(Order? order)
{
return order is not null &&
order.IsVerified &&
order.Items.Any();
}
5️⃣: ترجیح دادن پرتاب استثناهای سفارشی (Custom Exceptions) 💥

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

با این حال، اگر شما می‌خواهید از استثناها برای کنترل جریان استفاده کنید، بهتر است از استثناهای سفارشی استفاده کنید.
شما می‌توانید اطلاعات زمینه‌ای ارزشمندی را معرفی کرده و دلیل پرتاب استثنا را بهتر توصیف کنید.
و اگر می‌خواهید این استثناها را به صورت سراسری مدیریت کنید، می‌توانید یک کلاس پایه ایجاد کنید تا بتوانید استثناهای خاصی را catch کنید.
public void Process(Order? order)
{
if (!IsProcessable(order))
{
return;
}
if (order.Items.Count > 15)
{
throw new TooManyLineItemsException(order.Id);
}
if (order.Status != "ReadyToProcess")
{
throw new NotReadyForProcessingException(order.Id);
}
order.IsProcessed = true;
}
static bool IsProcessable(Order? order)
{
return order is not null &&
order.IsVerified &&
order.Items.Any();
}

6️⃣: رفع اعداد جادویی (Magic Numbers) با ثابت‌ها (Constants) 🔢

یک code smell رایج که من می‌بینم، استفاده از اعداد جادویی است.
آن‌ها معمولاً به راحتی قابل تشخیص هستند زیرا برای بررسی اعمال شدن یک شرط عددی استفاده می‌شوند.
مشکل اعداد جادویی این است که هیچ معنایی ندارند.
استدلال در مورد کد دشوارتر و مستعد خطا می‌شود.
رفع اعداد جادویی باید سرراست باشد و یک راه‌حل، معرفی یک ثابت (constant) است.
const int MaxNumberOfLineItems = 15;

public void Process(Order? order)
{
if (!IsProcessable(order))
{
return;
}
if (order.Items.Count > MaxNumberOfLineItems)
{
throw new TooManyLineItemsException(order.Id);
}
if (order.Status != "ReadyToProcess")
{
throw new NotReadyForProcessingException(order.Id);
}
order.IsProcessed = true;
}
static bool IsProcessable(Order? order)
{
return order is not null &&
order.IsVerified &&
order.Items.Any();
}


7️⃣: رفع رشته‌های جادویی (Magic Strings) با Enumها 📜

مشابه اعداد جادویی، ما code smell رشته‌های جادویی را داریم.
یک مورد استفاده معمول برای رشته‌های جادویی، نمایش نوعی از وضعیت (state) است.
شما متوجه خواهید شد که ما مقدار Order.Status را با یک رشته جادویی مقایسه می‌کنیم تا بررسی کنیم آیا سفارش آماده پردازش است یا نه.

چند مشکل با رشته‌های جادویی:

🔹 امکان اشتباه کردن (اشتباه تایپی) آسان است.

🔹 عدم وجود تایپ قوی (strong typing).

🔹 در برابر بازآرایی (refactoring) مقاوم نیستند.

بیایید یک enum به نام OrderStatus برای نمایش وضعیت‌های ممکن ایجاد کنیم: 🏷
enum OrderStatus
{
Pending = 0,
ReadyToProcess = 1,
Processed = 2
}

و حالا باید از OrderStatus مناسب در چک استفاده کنیم:
const int MaxNumberOfLineItems = 15;

public void Process(Order? order)
{
if (!IsProcessable(order))
{
return;
}
if (order.Items.Count > MaxNumberOfLineItems)
{
throw new TooManyLineItemsException(order.Id);
}
if (order.Status != OrderStatus.ReadyToProcess)
{
throw new NotReadyForProcessingException(order.Id);
}
order.IsProcessed = true;
order.Status = OrderStatus.Processed;
}
static bool IsProcessable(Order? order)
{
return order is not null &&
order.IsVerified &&
order.Items.Any();
}
8️⃣: از الگوی آبجکت نتیجه (Result Object Pattern) استفاده کنید 🎁

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

شما می‌توانید از یک کلاس جنریک Result برای نمایش انواع نتایج یا یک کلاس خاص مانند ProcessOrderResult استفاده کنید.
برای اینکه آبجکت‌های نتیجه شما کپسوله شوند، مجموعه‌ای از متدهای factory برای ایجاد نوع نتیجه مشخص، ارائه دهید.
public class ProcessOrderResult
{
private ProcessOrderResult(
ProcessOrderResultType type,
long orderId,
string message)
{
Type = type;
OrderId = orderId;
Message = message;
}

public ProcessOrderResultType Type { get; }
public long OrderId { get; }
public string? Message { get; }
public static ProcessOrderResult NotProcessable() =>
new(ProcessOrderResultType.NotProcessable, default, "Not processable");
public static ProcessOrderResult TooManyLineItems(long oderId) =>
new(ProcessOrderResultType.TooManyLineItems, orderId, "Too many items");
public static ProcessOrderResult NotReadyForProcessing(long oderId) =>
new(ProcessOrderResultType.NotReadyForProcessing, oderId, "Not ready");
public static ProcessOrderResult Success(long oderId) =>
new(ProcessOrderResultType.Success, orderId, "Success");
}

استفاده از یک enum مانند ProcessOrderResultType، مصرف کردن آبجکت نتیجه را با switch expressions آسان‌تر می‌کند. در اینجا enum برای نمایش ProcessOrderResult.Type آمده است:
public enum ProcessOrderResultType
{
NotProcessable = 0,
TooManyLineItems = 1,
NotReadyForProcessing = 2,
Success = 3
}

و حالا متد Process به این شکل در می‌آید:
const int MaxNumberOfLineItems = 15;

public ProcessOrderResult Process(Order? order)
{
if (!IsProcessable(order))
{
return ProcessOrderResult.NotProcessable();
}
if (order.Items.Count > MaxNumberOfLineItems)
{
return ProcessOrderResult.TooManyLineItems(order.Id);
}
if (order.Status != OrderStatus.ReadyToProcess)
{
return ProcessOrderResult.NotReadyForProcessing(order.Id);
}

order.IsProcessed = true;
order.Status = OrderStatus.Processed;

return ProcessOrderResult.Success(order.Id);
}

static bool IsProcessable(Order? order)
{
return order is not null &&
order.IsVerified &&
order.Items.Any();
}
`

در اینجا نحوه استفاده از یک enum برای ProcessOrderResult.Type به شما اجازه می‌دهد یک switch expression بنویسید:
var result = Process(order);

result.Type switch
{
ProcessOrderResultType.TooManyLineItems =>
Console.WriteLine($"Too many line items: {result.OrderId}"),
ProcessOrderResultType.NotReadyForProcessing =>
Console.WriteLine($"Not ready for processing {result.OrderId}"),
ProcessOrderResultType.Success =>
Console.WriteLine($"Processed successfully {result.OrderId}"),
_ => Console.WriteLine("Failed to process: {OrderId}", result.OrderId),
};


نکات پایانی 📝

تمام شد، ۸ نکته برای نوشتن کد تمیز:

1️⃣ اصل بازگشت زودهنگام

2️⃣ ادغام چندین دستور if

3️⃣ استفاده از LINQ برای اختصار

4️⃣ جایگزینی عبارت بولین با متد

5️⃣ ترجیح دادن پرتاب استثناهای سفارشی

6️⃣ جایگزینی اعداد جادویی با ثابت‌ها

7️⃣ جایگزینی رشته جادویی با enumها

8️⃣ استفاده از الگوی آبجکت نتیجه

نوشتن کد تمیز، موضوعی از تمرین هدفمند و تجربه است.
اکثر مردم در مورد اصول کدنویسی تمیز می‌خوانند، اما تعداد کمی تلاش می‌کنند تا آن‌ها را روزانه به کار ببرند.
اینجاست که شما می‌توانید خود را متمایز کنید.

امیدوارم این مطلب مفید بوده باشد.

اقدام عملی امروز: 🚀

نگاهی به پروژه خود بیندازید و ببینید آیا برخی از اشتباهاتی که من در اینجا برجسته کردم را مرتکب می‌شوید یا نه. و سپس آن‌ها را با استفاده از نکات کد تمیزی که با شما به اشتراک گذاشتم، برطرف کنید.
📖 سری آموزشی کتاب C# 12 in a Nutshell

🏛 کلاس در برابر اینترفیس: کدام را و چه زمانی انتخاب کنیم؟

یکی از اساسی‌ترین سوالات در طراحی شیءگرا اینه: کی باید از یه class و وراثت استفاده کنیم و کی باید بریم سراغ interface؟

این فقط یه انتخاب سینتکسی نیست، یه تصمیم مهم معماریه. بیاید با یه قانون ساده و یه مثال عالی، این موضوع رو برای همیشه روشن کنیم.

1️⃣ قانون طلایی: پیاده‌سازی مشترک در برابر رفتار مشترک ⚖️

به عنوان یک قانون کلی و کاربردی:

از class و وراثت استفاده کن:
وقتی تایپ‌های شما به صورت طبیعی، پیاده‌سازی مشترکی (shared implementation) دارند. یعنی کدهای مشترکی بینشون هست که می‌خواید از تکرارش جلوگیری کنید.

از interface استفاده کن:
وقتی تایپ‌های شما رفتار مشترکی (shared behavior) دارند، ولی هر کدوم پیاده‌سازی مستقل (independent implementation) و متفاوتی از اون رفتار رو ارائه میدن.

2️⃣ مثال عملی: دنیای حیوانات 🦅

فرض کنید می‌خوایم موجودات مختلف رو مدل‌سازی کنیم.

مشکل (استفاده فقط از کلاس): 👎
یه "عقاب" هم "پرنده" است، هم "موجود پرنده" و هم "گوشتخوار". چون #C وراثت چندگانه از کلاس‌ها رو پشتیبانی نمی‌کنه، کد زیر کامپایل نمیشه!
abstract class Animal {}
abstract class Bird : Animal {}
abstract class FlyingCreature : Animal {}
abstract class Carnivore : Animal {}
// خطای کامپایل! یک کلاس نمی‌تونه از چند کلاس ارث‌بری کنه
class Eagle : Bird, FlyingCreature, Carnivore {}

راه حل (ترکیب کلاس و اینترفیس): 👍
حالا قانون طلایی رو به کار می‌بریم. "پرنده" بودن احتمالاً یه سری پیاده‌سازی و ویژگی مشترک داره، پس Bird یه class باقی می‌مونه. اما "پرواز کردن" یا "گوشتخوار بودن"، رفتارهایی هستن که موجودات مختلف (مثل حشره و پرنده) به شکل‌های کاملاً متفاوتی انجام میدن. پس این‌ها بهترین کاندیدا برای اینترفیس هستن!
// اینترفیس‌ها فقط "رفتار" رو تعریف می‌کنن
interface IFlyingCreature { }
interface ICarnivore { }
abstract class Animal {}
// کلاس‌ها "پیاده‌سازی" مشترک رو ارائه میدن
abstract class Bird : Animal {}

// حالا درسته!
// عقاب از کلاس Bird ارث می‌بره و دو اینترفیس رو پیاده‌سازی می‌کنه
class Eagle : Bird, IFlyingCreature, ICarnivore {}


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

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

🔖 هشتگ‌ها:
#CSharp #DotNet #OOP #SoftwareArchitecture #Interface #CleanCode
📖 سری آموزشی کتاب C# 12 in a Nutshell
🚀 انقلاب اینترفیس‌ها در #C مدرن: Default و Static Members

تو پست قبلی، یاد گرفتیم که کی باید از اینترفیس استفاده کنیم. اما داستان اینترفیس‌ها به تعریف چند متد ختم نمیشه! در نسخه‌های مدرن #C، اینترفیس‌ها به قابلیت‌های فوق‌العاده قدرتمندی مجهز شدن که قواعد بازی رو عوض می‌کنن.

امروز با این ابرقدرت‌ها آشنا میشیم.

1️⃣ Default Interface Members (C# 8):
ارتقاء بدون شکستن کد!


تصور کنید یه کتابخونه نوشتید و هزاران نفر دارن از اینترفیس ILogger شما استفاده می‌کنن. حالا اگه بخواید یه متد جدید به این اینترفیس اضافه کنید، کد تمام اون هزار نفر می‌شکنه!

Default Interface Members
این مشکل رو حل می‌کنه. شما می‌تونید یه متد جدید رو با پیاده‌سازی پیش‌فرض به اینترفیس اضافه کنید. این یعنی کلاس‌هایی که از اینترفیس شما استفاده می‌کنن، مجبور نیستن فوراً این متد جدید رو پیاده‌سازی کنن.

نکته کلیدی: ⚠️ این پیاده‌سازی‌های پیش‌فرض، به صورت صریح (explicit) هستن. یعنی اگه کلاسی اون متد رو پیاده‌سازی نکنه، برای صدا زدنش باید حتماً آبجکت رو به خود اینترفیس کست کنید.
public interface ILogger
{
// متد جدید با پیاده‌سازی پیش‌فرض
void Log(string text) => Console.WriteLine(text);
void LogError(Exception ex); // متد قدیمی بدون پیاده‌سازی
}

public class MyLogger : ILogger
{
// این کلاس فقط مجبوره LogError رو پیاده‌سازی کنه
public void LogError(Exception ex) { /* ... */ }
}

// --- نحوه استفاده ---
var logger = new MyLogger();
// logger.Log("hi"); // خطای زمان کامپایل!
((ILogger)logger).Log("hi"); // درسته!


2️⃣ Static Interface Members:
ابزارهای کمکی و پلی‌مورفیسم استاتیک ⚙️

بله، درست خوندید! اینترفیس‌ها در #C مدرن حتی می‌تونن اعضای استاتیک هم داشته باشن!

اعضای استاتیک غیرمجازی:
این اعضا (مثل فیلدها و متدهای استاتیک) برای تعریف مقادیر ثابت یا متدهای کمکی که به خود اینترفیس مرتبطن، عالین.
public interface ILogger
{
static string DefaultPrefix = "[INFO]: ";
void Log(string text) => Console.WriteLine(DefaultPrefix + text);
}


اعضای استاتیک مجازی/انتزاعی (از 11 #C):

این یکی از پیشرفته‌ترین قابلیت‌های #C هست و درهای دنیای جدیدی به اسم "چندریختی استاتیک" (Static Polymorphism) رو باز می‌کنه (مثلاً برای Generic Math). کلاس‌هایی که این اینترفیس رو پیاده‌سازی می‌کنن، باید اون عضو استاتیک رو هم پیاده‌سازی کنن.

3️⃣ نکته فنی: اینترفیس‌ها و Boxing

یه نکته پرفورمنسی مهم: وقتی شما یه struct (که Value Type هست) رو به یک اینترفیسی که پیاده‌سازی کرده کست می‌کنید، عمل Boxing اتفاق میفته و اون struct روی هیپ کپی میشه. این کار هزینه داره!
interface I { void Foo(); }
struct S : I { public void Foo() {} }
S s = new S();
s.Foo(); // بدون Boxing

I i = s; // Boxing اتفاق میفته!
i.Foo();


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

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

🔖 هشتگ‌ها:
#CSharp #DotNet #OOP #Interface #ModernCSharp
ساخت اتریبیوت‌های سفارشی در #C 🏷

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

اتریبیوت چیست؟ 🤔

اتریبیوت‌ها در #C راهی برای افزودن اطلاعات اعلانی (declarative) به کد شما هستند. آن‌ها متادیتا فراهم می‌کنند که می‌توان از آن برای کنترل جنبه‌های مختلف رفتار برنامه شما در زمان اجرا یا زمان کامپایل استفاده کرد.

اتریبیوت‌ها می‌توانند به عناصر مختلف برنامه مانند موارد زیر اعمال شوند:

🔹 اسمبلی‌ها، ماژول‌ها، کلاس‌ها، اینترفیس‌ها، استراکت‌ها، enumها

🔹 متدها، سازنده‌ها، delegateها، ایونت‌ها

🔹 پراپرتی‌ها، فیلدها، پارامترها، پارامترهای جنریک، مقادیر بازگشتی

🔹 یا همه عناصر ذکر شده

شما می‌توانید یک یا چند اتریبیوت را به این عناصر برنامه اعمال کنید.
شما هر روز هنگام ساخت اپلیکیشن در NET. از اتریبیوت‌ها استفاده می‌کنید. برای مثال، اتریبیوت‌های زیر را بسیار دیده‌اید و استفاده کرده‌اید: 👇
[Serializable]
public class User
{
}

[ApiController]
[Route("api/users")]
public class UsersController : ControllerBase
{
}


چگونه یک اتریبیوت سفارشی بسازیم؟ ✍️

اتریبیوت در #C کلاسی است که از کلاس پایه Attribute ارث‌بری می‌کند.
بیایید نگاهی به نحوه ساخت یک اتریبیوت سفارشی بیندازیم:
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
public class CustomAttribute : Attribute
{
}

[Custom]
public class MyClass
{
}

نام کلاس اتریبیوت باید پسوند Attribute داشته باشد. هنگام اعمال به هر عنصری، این پسوند حذف می‌شود.

هنگام ایجاد یک کلاس اتریبیوت، شما از یک اتریبیوت داخلی برای مشخص کردن جایی که اتریبیوت می‌تواند اعمال شود، استفاده می‌کنید: 🎯
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true)]
public class CustomAttribute : Attribute
{
}

شما می‌توانید چندین تارگت را با عملگر | ترکیب کنید تا مشخص کنید یک اتریبیوت کجا می‌تواند اعمال شود.

پارامتر Inherited نشان می‌دهد که آیا اتریبیوت سفارشی می‌تواند توسط کلاس‌های مشتق شده به ارث برده شود یا نه. مقدار پیش‌فرض true است. 🧬
[AttributeUsage(AttributeTargets.Class, Inherited = true)]
public class CustomInheritedAttribute : Attribute
{
}

[AttributeUsage(AttributeTargets.Class, Inherited = false)]
public class CustomNonInheritedAttribute : Attribute
{
}

هنگام اعمال اتریبیوت inherited به کلاس ParentA، آن توسط کلاس ChildA به ارث برده می‌شود:
[CustomInherited]
public class ParentA
{
}

public class ChildA : ParentA
{
}

در اینجا، کلاس ChildA یک اتریبیوت CustomInherited دارد.

در حالی که در مثال زیر، هنگام استفاده از یک اتریبیوت non-inherited برای ParentB، آن به کلاس ChildB اعمال نمی‌شود:
[CustomNonInherited]
public class ParentB
{
}

public class ChildB : ParentB
{
}


پراپرتی‌های اتریبیوت ⚙️

پراپرتی‌های اتریبیوت می‌توانند یا الزامی (پارامترهای اجباری) یا غیرالزامی (پارامترهای اختیاری) باشند. اتریبیوت‌ها می‌توانند آرگومان‌ها را به همان روشی که متدها و پراپرتی‌ها می‌پذیرند، قبول کنند.
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = false)]
public class CustomWithParametersAttribute : Attribute
{
public string RequiredProperty { get; }
public int OptionalProperty { get; set; }

public CustomWithParametersAttribute(string requiredProperty)
{
RequiredProperty = requiredProperty;
}
}

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

هنگام اعمال چنین اتریبیوتی به یک کلاس، شما باید مقادیر اتریبیوت را به ترتیبی که در سازنده تعریف شده‌اند، تنظیم کنید. پراپرتی‌های اختیاری باید با نامشان مشخص شوند: 👇
[CustomWithParameters("some text here", OptionalProperty = 5)]
public class ExampleClass
{
}
مثال عملی از استفاده از اتریبیوت‌ها 🚀

بیایید یک اتریبیوت سفارشی برای مشخص کردن نقش‌های مجاز برای دسترسی به یک متد کنترلر ایجاد کنیم: 🛡
[AttributeUsage(AttributeTargets.Method, Inherited = false)]
public class AuthorizeRolesAttribute : Attribute
{
public string[] Roles { get; }

public AuthorizeRolesAttribute(params string[] roles)
{
Roles = roles;
}
}

سپس، ما AuthorizeRolesAttribute را به متدهای یک کلاس اعمال می‌کنیم و نقش‌هایی را که مجاز به دسترسی به هر متد هستند، مشخص می‌کنیم:
public class AccountController
{
[AuthorizeRoles("Admin", "Manager")]
public void AdminOnlyAction()
{
Console.WriteLine("Admin or Manager can access this method.");
}

[AuthorizeRoles("User")]
public void UserOnlyAction()
{
Console.WriteLine("Only users can access this method.");
}

public void PublicAction()
{
Console.WriteLine("Everyone can access this method.");
}
}


برای استفاده از این اتریبیوت، می‌توانیم از reflection 🔍 برای گرفتن اطلاعات در مورد اینکه چه اتریبیوت‌هایی به یک متد اعمال شده‌اند و چه پراپرتی‌هایی دارند، استفاده کنیم:
public class RoleBasedAccessControl
{
public void ExecuteAction(object controller, string methodName, string userRole)
{
var method = controller.GetType().GetMethod(methodName);
var attribute = method?.GetCustomAttribute<AuthorizeRolesAttribute>();

if (attribute is null || attribute.Roles.Contains(userRole))
{
method?.Invoke(controller, null);
}
else
{
Console.WriteLine("Access denied. User does not have the required role.");
}
}
}

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

در نهایت، می‌توانیم منطق کنترل دسترسی مبتنی بر نقش را با نقش‌های مختلف کاربر تست کنیم: 🧪
var controller = new AccountController();
var accessControl = new RoleBasedAccessControl();

Console.WriteLine("Testing with Admin role:");
accessControl.ExecuteAction(controller, nameof(AccountController.AdminOnlyAction), "Admin");

Console.WriteLine("\nTesting with User role:");
accessControl.ExecuteAction(controller, nameof(AccountController.UserOnlyAction), "User");

Console.WriteLine("\nTesting with Guest role:");
accessControl.ExecuteAction(controller, nameof(AccountController.AdminOnlyAction), "Guest");

Console.WriteLine("\nTesting public method with Guest role:");
accessControl.ExecuteAction(controller, nameof(AccountController.PublicAction), "Guest");

خروجی: 🖥
Testing with Admin role:
Admin or Manager can access this method.

Testing with User role:
Only users can access this method.

Testing with Guest role:
Access denied. User does not have the required role.

Testing public method with Guest role:
Everyone can access this method.

در این مثال، AuthorizeRolesAttribute برای مشخص کردن نقش‌های مجاز برای دسترسی به هر متد در AccountController استفاده می‌شود. کلاس RoleBasedAccessControl این محدودیت‌ها را با بررسی نقش کاربر در برابر نقش‌های تعریف شده در اتریبیوت، اعمال می‌کند. این نشان می‌دهد که چگونه می‌توان از اتریبیوت‌های سفارشی به روشی عملی و مفید در یک سناریوی دنیای واقعی استفاده کرد.
مهم اینه که به کدنویسی علاقه داره😅
📖 سری آموزشی کتاب C# 12 in a Nutshell

🏷 راهنمای کامل Enum در #C: فراتر از چند اسم ثابت

وقتی می‌خواید یه مجموعه از مقادیر ثابت و مرتبط رو تعریف کنید (مثل جهت‌ها، رنگ‌ها، یا وضعیت‌ها)، اولین چیزی که باید به ذهنتون برسه Enum هست. Enumها به کد شما خوانایی و ایمنی نوع (Type Safety) میدن.

بیاید با تمام جزئیاتشون آشنا بشیم.

1️⃣ Enum چیست و چه ساختاری دارد؟

Enum
یک نوع داده ارزشی (Value Type) خاصه که به شما اجازه میده برای یک گروه از ثابت‌های عددی، اسم‌های معنادار تعریف کنید.
public enum BorderSide { Left, Right, Top, Bottom }

پشت صحنه: 🧐
هر عضو Enum یک مقدار عددی صحیح داره. به صورت پیش‌فرض:

نوع داده زیرین، int است.

مقادیر به ترتیب از 0 شروع میشن (Left=0, Right=1, Top=2, ...).

سفارشی‌سازی:
شما می‌تونید هم نوع داده زیرین و هم مقدار هر عضو رو خودتون مشخص کنید:
public enum BorderSide : byte // تغییر نوع به بایت
{
Left = 1,
Right, // مقدارش میشه 2 (یکی بیشتر از قبلی)
Top = 10,
Bottom // مقدارش میشه 11 (یکی بیشتر از قبلی)
}


2️⃣ تبدیل‌ها (Conversions)

شما می‌تونید یه Enum رو به مقدار عددی زیرینش و برعکس، به صورت صریح (explicit) کست کنید.
BorderSide side = BorderSide.Left; // side = 1
int i = (int)side; // i = 1
BorderSide side2 = (BorderSide)i; // side2 = BorderSide.Left


نکته جالب: 💡 لیترال عددی 0 تنها عددیه که برای انتساب به Enum نیاز به کست نداره.

3️⃣ تله بزرگ: Type-Safety و مقادیر نامعتبر! ⚠️

این مهم‌ترین نکته در مورد Enumهاست. با اینکه Enumها به ما Type Safety میدن، ولی به خاطر قابلیت کست شدن از عدد، یه متغیر Enum می‌تونه یه مقدار عددی نامعتبر (خارج از مقادیر تعریف شده) رو تو خودش نگه داره!
// این کد کامپایل و اجرا میشه، ولی b حاوی یک مقدار بی‌معنیه
BorderSide b = (BorderSide)12345;
Console.WriteLine(b); // خروجی عددی: 12345

این رفتار می‌تونه منطق if-else یا switch شما رو که انتظار مقادیر مشخصی رو دارن، کاملاً به هم بریزه.

4️⃣ راه حل: اعتبارسنجی با Enum.IsDefined

برای اینکه مطمئن بشید یه مقدار Enum معتبره، می‌تونید از متد استاتیک Enum.IsDefined استفاده کنید.
BorderSide side = (BorderSide)12345;
bool isValid = Enum.IsDefined(typeof(BorderSide), side);
Console.WriteLine(isValid); // خروجی: False


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

درک این جزئیات و تله‌های Enum، به شما کمک می‌کنه کدهای امن‌تر و قابل اعتمادتری بنویسید.

🔖 هشتگ‌ها:
#CSharp #DotNet #Enum #TypeSafety
📖 سری آموزشی کتاب 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