C# Geeks (.NET) – Telegram
روش مدرن: IExceptionHandler

ASP.NET Core 8
اینترفیس IExceptionHandler را معرفی کرد، و این یک تغییردهنده بازی است. به جای یک middleware عظیم که همه چیز را مدیریت می‌کند، می‌توانیم handlerهای متمرکزی برای انواع خاص استثناها ایجاد کنیم.

اینطور کار می‌کند:
internal sealed class GlobalExceptionHandler(
IProblemDetailsService problemDetailsService,
ILogger<GlobalExceptionHandler> logger) : IExceptionHandler
{
public async ValueTask<bool> TryHandleAsync(
HttpContext httpContext,
Exception exception,
CancellationToken cancellationToken)
{
logger.LogError(exception, "Unhandled exception occurred");

httpContext.Response.StatusCode = exception switch
{
ApplicationException => StatusCodes.Status400BadRequest,
_ => StatusCodes.Status500InternalServerError
};

return await problemDetailsService.TryWriteAsync(new ProblemDetailsContext
{
// ...
});
}
}


نکته کلیدی در اینجا مقدار بازگشتی است. اگر handler شما بتواند استثنا را مدیریت کند، true برگردانید. اگر نه، false برگردانید و اجازه دهید handler بعدی تلاش کند.

فراموش نکنید آن را با DI و در پایپ‌لاین درخواست ثبت کنید:
builder.Services.AddExceptionHandler<GlobalExceptionHandler>();
builder.Services.AddProblemDetails();

// و در پایپ‌لاین شما
app.UseExceptionHandler();


این رویکرد بسیار تمیزتر است. هر handler یک وظیفه دارد و کد به راحتی قابل تست و نگهداری است.

زنجیره‌ای کردن Exception Handlerها ⛓️

شما می‌توانید چندین exception handler را با هم زنجیره‌ای کنید و آن‌ها به ترتیبی که ثبت کرده‌اید اجرا می‌شوند. ASP.NET Core از اولین handler که true از TryHandleAsync برگرداند، استفاده خواهد کرد.

مثال: یکی برای خطاهای اعتبارسنجی، یکی به عنوان fallback سراسری.
builder.Services.AddExceptionHandler<ValidationExceptionHandler>();
builder.Services.AddExceptionHandler<GlobalExceptionHandler>();

بیایید بگوییم شما از FluentValidation استفاده می‌کنید (و باید هم استفاده کنید). در اینجا یک راه‌اندازی کامل آمده است:
internal sealed class ValidationExceptionHandler(
IProblemDetailsService problemDetailsService,
ILogger<ValidationExceptionHandler> logger) : IExceptionHandler
{
public async ValueTask<bool> TryHandleAsync(
HttpContext httpContext,
Exception exception,
CancellationToken cancellationToken)
{
if (exception is not ValidationException validationException)
{
return false;
}

logger.LogError(exception, "Unhandled exception occurred");

httpContext.Response.StatusCode = StatusCodes.Status400BadRequest;

var context = new ProblemDetailsContext { /* ... */ };

var errors = validationException.Errors
.GroupBy(e => e.PropertyName)
.ToDictionary(
g => g.Key.ToLowerInvariant(),
g => g.Select(e => e.ErrorMessage).ToArray()
);

context.ProblemDetails.Extensions.Add("errors", errors);

return await problemDetailsService.TryWriteAsync(context);
}
}


ترتیب اجرا مهم است. فریم‌ورک هر handler را به ترتیبی که ثبت کرده‌اید امتحان می‌کند. پس handlerهای خاص‌تر خود را اول و handler catch-all خود را آخر قرار دهید.

خلاصه 📝
ما راه درازی را از روزهای ساخت دستی پاسخ‌های خطا در middleware پیموده‌ایم. تکامل به این شکل است:
1️⃣ Middleware:
ساده، همه جا کار می‌کند، اما سریع پیچیده می‌شود.
2️⃣ IProblemDetailsService:
فرمت پاسخ را استاندارد می‌کند، هنوز قابل مدیریت است.
3️⃣ IExceptionHandler:
مدرن، قابل تست، و به زیبایی مقیاس‌پذیر است.

نکته کلیدی؟ 🔑

اجازه ندهید مدیریت خطا یک فکر آخر باشد. آن را زود راه‌اندازی کنید، یکپارچه‌اش کنید، و کاربران شما (و خود آینده‌تان) وقتی همه چیز به ناچار خراب شد، از شما تشکر خواهند کرد.


🔖 هشتگ‌ها:
#CSharp #DotNet #ErrorHandling #ExceptionHandling #SoftwareArchitecture
مدیریت متمرکز پکیج‌ها (CPM) در NET. : یک بار برای همیشه!


روزهایی رو به یاد میارم که مدیریت پکیج‌های NuGet در چندین پروژه یک دردسر واقعی بود. میدونید چی میگم - یه سولوشن بزرگ رو باز می‌کنی و می‌بینی هر پروژه از یه نسخه متفاوت از همون پکیج استفاده می‌کنه. اصلاً جالب نیست! 😫

بذارید بهتون نشون بدم که چطور مدیریت متمرکز پکیج‌ها (CPM) در NET. می‌تونه این مشکل رو یک بار برای همیشه حل کنه.

مشکلی که باید حل کنیم 💥
من اغلب با سولوشن‌هایی کار می‌کنم که پروژه‌های زیادی دارن. سولوشن‌هایی با ۳۰ یا بیشتر پروژه غیرمعمول نیستن. هر کدوم به پکیج‌های مشابهی مثل Serilog یا Polly نیاز دارن. قبل از CPM، پیگیری نسخه‌های پکیج یه افتضاح بود:

🔹 یک پروژه از Serilog 4.1.0 استفاده می‌کرد.

🔹 دیگری از Serilog 4.0.2.

🔹 و یه جوری، سومی از Serilog 3.1.1!

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

مدیریت متمرکز پکیج‌ها چگونه کمک می‌کند؟ 🎮

CPM
رو مثل یک مرکز کنترل برای تمام نسخه‌های پکیج‌تون در نظر بگیرید. به جای تنظیم نسخه‌ها در هر پروژه، اون‌ها رو یک بار در یک جا تنظیم می‌کنید. بعد، فقط به پکیجی که می‌خواید استفاده کنید، بدون مشخص کردن نسخه، ارجاع میدید. به همین سادگی!

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


NuGet نسخه 6.2 یا جدیدتر

.NET SDK نسخه 6.0.300 یا جدیدتر

اگر از ویژوال استودیو استفاده می‌کنید، نسخه 2022 17.2 یا جدیدتر

راه‌اندازی آن 📁

اول، یک فایل به نام Directory.Packages.props در پوشه اصلی سولوشن خود ایجاد کنید:
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>

<ItemGroup>
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
<PackageVersion Include="Serilog" Version="4.1.0" />
<PackageVersion Include="Polly" Version="8.5.0" />
</ItemGroup>
</Project>

در فایل‌های پروژه‌تون، می‌تونید پکیج‌ها رو با استفاده از PackageReference بدون نسخه لیست کنید:
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" />
<PackageReference Include="AutoMapper" />
<PackageReference Include="Polly" />
</ItemGroup>

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

✨️کارهای باحالی که می‌تونید انجام بدید
نیاز به نسخه‌ای متفاوت برای یک پروژه دارید؟ 🎯

مشکلی نیست! فقط این رو به فایل پروژه‌تون اضافه کنید:
<PackageReference Include="Serilog" VersionOverride="3.1.1" />

📍پراپرتی VersionOverride به شما اجازه میده نسخه خاصی که می‌خواید رو تعریف کنید.

یه پکیج رو تو همه پروژه‌ها می‌خواید؟ 🌍

اگه پکیج‌هایی دارید که هر پروژه‌ای بهشون نیاز داره، می‌تونید اون‌ها رو سراسری کنید. یک GlobalPackageReference در فایل props خود تعریف کنید:
<ItemGroup>
<GlobalPackageReference Include="SonarAnalyzer.CSharp" Version="10.3.0.106239" />
</ItemGroup>

حالا هر پروژه‌ای به طور خودکار این پکیج رو دریافت می‌کنه!

مهاجرت پروژه‌های موجود به CPM 🚚

1️⃣ فایل Directory.Packages.props رو در ریشه سولوشن ایجاد کنید.
2️⃣ تمام نسخه‌های پکیج رو از فایل‌های .csproj خود به اونجا منتقل کنید.
3️⃣ ویژگی Version رو از عناصر PackageReference حذف کنید.
4️⃣ سولوشن خود را بیلد کرده و هرگونه تداخل نسخه رو برطرف کنید.
5️⃣ قبل از کامیت کردن، به طور کامل تست کنید.

همچنین یه ابزار CLI به نام CentralisedPackageConverter وجود داره که می‌تونید برای اتوماتیک کردن مهاجرت ازش استفاده کنید.

چه زمانی باید از CPM استفاده کنید؟ 🤔
من دلیل قانع‌کننده‌ای برای استفاده نکردن پیش‌فرض از این قابلیت نمی‌بینم.
من توصیه می‌کنم از CPM استفاده کنید وقتی:

• پروژه‌های زیادی دارید که پکیج‌های مشترک دارن.

• از رفع باگ‌های مربوط به نسخه خسته شده‌اید.

• می‌خواید مطمئن بشید همه از نسخه‌های یکسان استفاده می‌کنن.

جمع‌بندی 📝
نکات من برای موفقیت با مدیریت متمرکز پکیج‌ها:


💡 وقتی CPM رو به یه سولوشن موجود اضافه می‌کنید، این کار رو در یک change/PR جداگانه انجام بدید.

💡 اگه نسخه‌ای رو override می‌کنید، یه کامنت بذارید که دلیلش رو توضیح بده.

💡 نسخه‌های پکیج خود را به طور منظم برای آپدیت‌ها چک کنید.

💡 فقط پکیج‌هایی رو سراسری کنید که واقعاً همه جا بهشون نیاز دارید.

🔖 هشتگ‌ها:
#CSharp #DotNet #NuGet #DependencyManagement #BestPractices #CleanCode #Developer #VisualStudio
الگوی CQRS به روشی که از ابتدا باید می‌بود 🚀


📢 MediatR
در حال تجاری شدن است.

جیمی بوگارد اعلام کرد که MediatR برای شرکت‌های بالاتر از یک اندازه مشخص، یک مدل لایسنس تجاری اتخاذ خواهد کرد.
برای بسیاری از تیم‌ها، این یک محرک برای ارزیابی مجدد استفاده از آن و احتمالاً جستجو برای جایگزین‌ها است.

و زمان بدی هم برای این کار نیست. MediatR تقریباً با CQRS در NET. مترادف شده است، با وجود این واقعیت که CQRS و MediatR یک چیز نیستند. اکثر پروژه‌ها از آن به عنوان یک لایه ارسال (dispatching) نازک برای کامندها و کوئری‌ها استفاده می‌کنند - یک مورد استفاده که می‌تواند با چند انتزاع (abstraction) ساده پوشش داده شود.

با حذف MediatR، شما به دست می‌آورید:


کنترل کامل بر زیرساخت CQRS خود
ارسال قابل پیش‌بینی و صریح به handlerها
دیباگ و آنبوردینگ ساده‌تر
راه‌اندازی تمیزتر DI و تست‌پذیری بهتر

📌ما پوشش خواهیم داد:

🔹 تعریف قراردادهای ICommand, IQuery و handlerها
🔹 افزودن پشتیبانی برای دکوراتورها (لاگینگ، اعتبارسنجی و غیره)
🔹 ثبت همه چیز با DI
🔹 یک مثال کامل و کاربردی در یک سناریوی دنیای واقعی

کامندها، کوئری‌ها و Handlerها 🧱

بیایید با تعریف قراردادهای پایه برای کامندها و کوئری‌ها شروع کنیم.
// ICommand.cs
public interface ICommand;
public interface ICommand<TResponse>;

// IQuery.cs
public interface IQuery<TResponse>;


این اینترفیس‌ها صرفاً به عنوان نشانگر وجود دارند. آن‌ها به ما اجازه می‌دهند منطق اپلیکیشن را حول نیت ساختار دهیم - عملیات نوشتن از طریق ICommand، عملیات خواندن از طریق IQuery.

اینترفیس‌های handler از همان مدل پیروی می‌کنند:
// ICommandHandler.cs
public interface ICommandHandler<in TCommand> where TCommand : ICommand
{
Task<Result> Handle(TCommand command, CancellationToken cancellationToken);
}
public interface ICommandHandler<in TCommand, TResponse> where TCommand : ICommand<TResponse>
{
Task<Result<TResponse>> Handle(TCommand command, CancellationToken cancellationToken);
}

// IQueryHandler.cs
public interface IQueryHandler<in TQuery, TResponse> where TQuery : IQuery<TResponse>
{
Task<Result<TResponse>> Handle(TQuery query, CancellationToken cancellationToken);
}


این‌ها تقریباً با APIهای IRequest و IRequestHandler MediatR یکسان هستند، که مهاجرت را در صورت خروج از MediatR بسیار ساده می‌کند.

مثال عملی: Command Handler 👨‍💻

برای دیدن این انتزاع‌ها در عمل، بیایید یک کامند را پیاده‌سازی کنیم که یک آیتم todo را به عنوان تکمیل شده علامت‌گذاری می‌کند.
// CompleteTodoCommand.cs
public sealed record CompleteTodoCommand(Guid TodoItemId) : ICommand;

// CompleteTodoCommandHandler.cs
internal sealed class CompleteTodoCommandHandler(...) : ICommandHandler<CompleteTodoCommand>
{
public async Task<Result> Handle(CompleteTodoCommand command, CancellationToken cancellationToken)
{
// ... (منطق بیزینس برای تکمیل کردن todo) ...
return Result.Success();
}
}


💡چند نکته مهم:


• کامند یک آبجکت مقدار تغییرناپذیر است (فقط داده، بدون رفتار).

• هندلر تمام منطق بیزینس را کپسوله می‌کند: اعتبارسنجی، تغییر وضعیت، ایجاد domain events و پایداری.

• هیچ mediator، ISender یا ارسال پنهانی وجود ندارد. handler مستقیماً از طریق انتزاع‌های سفارشی ما فراخوانی می‌شود.

دکوراتورها 🎨

برای پشتیبانی از دغدغه‌های مشترک (cross-cutting concerns) مانند لاگینگ، اعتبارسنجی و تراکنش‌ها، ما الگوی دکوراتور را در اطراف handlerهای خود اعمال می‌کنیم.

بیایید به دو مثال نگاه کنیم: یکی برای لاگینگ، یکی برای اعتبارسنجی.

دکوراتور لاگینگ: 📝

internal sealed clasa LoggingCommandHandler<TCommand, TResponse>(...)
: ICommandHandler<TCommand, TResponse>
where TCommand : ICommand<TResponse>
{
public async Task<Result<TResponse>> Handle(TCommand command, CancellationToken cancellationToken)
{
// ... (منطق لاگ کردن قبل و بعد از اجرای handler اصلی) ...
}
}

این کلاس هر ICommandHandler را می‌پیچد و لاگینگ ساختاریافته را در اطراف اجرای کامند اضافه می‌کند.
دکوراتور اعتبارسنجی با FluentValidation:


internal sealed class ValidationCommandHandler<TCommand, TResponse>(...)
: ICommandHandler<TCommand, TResponse>
where TCommand : ICommand<TResponse>
{
public async Task<Result<TResponse>> Handle(TCommand command, CancellationToken cancellationToken)
{
// ... (منطق اعتبارسنجی قبل از اجرای handler اصلی) ...
}
}


⚠️ مهم: از آنجایی که ما با اینترفیس‌های جنریک کار می‌کنیم، هر دکوراتور باید صراحتاً همان قرارداد جنریک را هدف قرار دهد.

در بخش بعدی، این را با استفاده از Scrutor به هم متصل خواهیم کرد.

راه‌اندازی DI ⚙️

با handlerها و دکوراتورهایمان، می‌توانیم همه چیز را با استفاده از Scrutor ثبت کنیم.
services.Scan(scan => scan.FromAssembliesOf(typeof(DependencyInjection))
.AddClasses(classes => classes.AssignableTo(typeof(IQueryHandler<,>)), publicOnly: false)
.AsImplementedInterfaces()
.WithScopedLifetime()
// ... ثبت بقیه handlerها ...
);

🔹این کد اسمبلی اپلیکیشن را اسکن کرده و تمام command و query handlerها را ثبت می‌کند.

🔹سپس، دکوراتورها را برای اعتبارسنجی و لاگینگ اعمال می‌کنیم:
services.Decorate(typeof(ICommandHandler<,>), typeof(ValidationDecorator.CommandHandler<,>));
services.Decorate(typeof(ICommandHandler<,>), typeof(LoggingDecorator.CommandHandler<,>));

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

استفاده از Minimal API 🎯

هنگامی که همه چیز متصل شد، استفاده از یک command handler از یک endpoint Minimal API ساده است:
app.MapPut("todos/{id:guid}/complete", async (
Guid id,
ICommandHandler<CompleteTodoCommand> handler,
CancellationToken cancellationToken) =>
{
var command = new CompleteTodoCommand(id);
Result result = await handler.Handle(command, cancellationToken);
return result.Match(Results.NoContent, CustomResults.Problem);
})


ما ICommandHandler مناسب را مستقیماً به endpoint تزریق می‌کنیم. نیازی به ISender، لایه mediator یا جستجوی زمان اجرا نیست.

نتیجه‌گیری 👍

CQRS
به یک فریم‌ورک پیچیده نیاز ندارد.
با چند اینترفیس کوچک، چند کلاس دکوراتور و یک راه‌اندازی تمیز DI، می‌توانید یک پایپ‌لاین ساده و انعطاف‌پذیر برای مدیریت کامندها و کوئری‌ها بسازید. درک، تست و توسعه آن آسان است.

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

🔖 هشتگ‌ها:
#CSharp #DotNet #CQRS #SoftwareArchitecture #CleanArchitecture #DesignPatterns #BestPractices #MediatR
📖 سری آموزشی کتاب C# 12 in a Nutshell

👇کلیدواژه this در #C:

'خود' آبجکت کیست؟ در دنیای شیءگرایی، گاهی وقتا یه آبجکت نیاز داره به "خودش" اشاره کنه. ابزار #C برای این کار، کلمه کلیدی ساده ولی قدرتمند this هست. this در واقع ضمیر "من" برای یک آبجکته و به نمونه فعلی (current instance) خودش اشاره می‌کنه.

1️⃣ کاربرد اول: رفع ابهام (Disambiguation)
این رایج‌ترین کاربرد this هست. وقتی اسم پارامتر سازنده، هم‌اسم یکی از فیلدهای کلاس باشه، برای اینکه به کامپایلر بفهمونیم منظورمون فیلد کلاسه، از this استفاده می‌کنیم.
public class Test
{
private string name;
public Test(string name)
{
// this.name به فیلد کلاس اشاره داره
// name به پارامتر ورودی اشاره داره
this.name = name;
}
}


2️⃣ کاربرد دوم: پاس دادن خودِ آبجکت 🤝
گاهی وقتا لازمه یه آبجکت، رفرنس خودش رو به یه آبجکت یا متد دیگه بده.
public class Panda
{
public Panda Mate;
public void Marry(Panda partner)
{
Mate = partner;
// 'خودم' رو به عنوان جفتِ شریکم معرفی می‌کنم
partner.Mate = this;
}
}


قانون مهم ⚠️

this
فقط در اعضای غیر استاتیک (non-static) یک کلاس یا struct معتبره.

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

🏦 پراپرتی‌ها (Properties) در #C:

دروازه‌های هوشمند کلاس شما چرا تو کدنویسی حرفه‌ای، تقریباً هیچوقت فیلدهای یه کلاس رو public نمی‌کنن؟ چون این کار یعنی از دست دادن کنترل! هر کسی از بیرون می‌تونه هر مقدار نامعتبری رو توش بریزه.

راه حل #C برای این مشکل، یه قابلیت فوق‌العاده به اسم پراپرتی (Property) هست.

1️⃣ پراپرتی چیست؟ فیلد با ماسک متد! 🎭

پراپرتی‌ها از بیرون شبیه فیلدهای معمولی به نظر میرسن و به همون سادگی استفاده میشن، ولی در داخل، در واقع متدهای خاصی هستن که بهشون اکسسور (accessor) گفته میشه. این به ما کنترل کامل روی خوندن و نوشتن مقدار رو میده.
Stock msft = new Stock();
msft.CurrentPrice = 30; // اکسسور set صدا زده میشه

msft.CurrentPrice -= 3;

Console.WriteLine(msft.CurrentPrice); // اکسسور get صدا زده میشه


2️⃣ کالبدشکافی یک پراپرتی 🔬

در رایج‌ترین حالت، یک پراپرتی از دو بخش تشکیل شده:

• فیلد پشتیبان (Backing Field): یک فیلد private که داده واقعی رو نگه میداره.

• پراپرتی public: دروازه‌ای که به دنیای بیرون اجازه دسترسی کنترل‌شده به اون فیلد رو میده.

✨️این پراپرتی، دو اکسسور داره:

🔹️get:
وقتی پراپرتی رو می‌خونیم، این بلوک اجرا میشه.

🔹️set:
وقتی مقداری رو به پراپرتی اختصاص میدیم، این بلوک اجرا میشه. کلمه کلیدی value در اینجا، به مقداری که داره ست میشه، اشاره داره.
public class Stock
{
// ۱. فیلد پشتیبان (private)
private decimal _currentPrice;
// ۲. پراپرتی عمومی (public)
public decimal CurrentPrice
{
get { return _currentPrice; }
set { _currentPrice = value; }
}
}


3️⃣ قدرت واقعی: کپسوله‌سازی (Encapsulation) 🛡

جادوی پراپرتی‌ها اینجاست که می‌تونید تو اکسسورهای get و set، منطق دلخواهتون رو پیاده کنید. مثلاً اعتبارسنجی (validation).
public class Stock
{
private decimal _currentPrice;
public decimal CurrentPrice
{
get { return _currentPrice; }
set
{
// منطق اعتبارسنجی
if (value < 0)
{
throw new ArgumentException("Price cannot be negative!");
}
_currentPrice = value;
}
}
}


🤔 حرف حساب و قانون طلایی
قانون طلایی شیءگرایی: فیلدها رو همیشه private نگه دارید و با پراپرتی‌های public اون‌ها رو در معرض دید بذارید. این کار به شما کنترل کامل روی کلاس‌هاتون میده و اساس کپسوله‌سازیه.

🔖 هشتگ‌ها:
#CSharp #Programming #DotNet #OOP #Properties #Encapsulation
⚡️ پست‌های سریالی جدید: Background Tasks در NET.

بچه‌ها سلام! یه مبحث خیلی خفن و کاربردی براتون آماده کردم 😎
توی دنیای واقعی برنامه‌نویسی، خیلی وقت‌ها لازمه یه سری کارها رو در پس‌زمینه (Background) انجام بدیم. کارهایی مثل:
* ارسال ایمیل و نوتیفیکیشن
* پردازش داده‌های حجیم
* بک‌آپ‌گیری از دیتابیس
* و خیلی چیزای دیگه...
برای همین، یه پست جامع و حسابی در مورد Background Tasks توی NET. آماده کردم.
این پست رو توی ۲ یا ۳ بخش منتشر می‌کنم که از مقدماتی‌ترین مفاهیم شروع می‌کنیم و قدم به قدم تا پیشرفته‌ترین تکنیک‌ها و بهترین روش‌ها پیش می‌ریم.
مطمئن باشید این مبحث به شدت توی پروژه‌های واقعی به دردتون می‌خوره!
اجرای تسک‌های پس‌زمینه (Background Tasks) در ASP.NET Core ⚙️


در این مقاله در مورد اجرای تسک‌های پس‌زمینه در ASP.NET Core صحبت خواهیم کرد. پس از خواندن این مقاله، شما قادر خواهید بود یک تسک پس‌زمینه را راه‌اندازی کرده و در عرض چند دقیقه آن را اجرا کنید.

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

در این پست به شما نشان خواهیم داد که چگونه یک تسک پس‌زمینه ساده ایجاد کنید که یک بار اجرا شده و به پایان می‌رسد.
و همچنین خواهید دید که چگونه یک تسک پس‌زمینه مداوم را پیکربندی کنید، که پس از یک دوره زمانی مشخص تکرار می‌شود.

تسک‌های پس‌زمینه با IHostedService 🔌

شما می‌توانید یک تسک پس‌زمینه را با پیاده‌سازی اینترفیس IHostedService تعریف کنید. این اینترفیس فقط دو متد دارد.
public interface IHostedService
{
Task StartAsync(CancellationToken cancellationToken);
Task StopAsync(CancellationToken cancellationToken);
}


تمام کاری که باید انجام دهید پیاده‌سازی متدهای StartAsync و StopAsync است.
در داخل StartAsync شما معمولاً پردازش پس‌زمینه را انجام می‌دهید. و در داخل StopAsync هرگونه پاک‌سازی لازم، مانند آزاد کردن منابع، را انجام می‌دهید.

برای پیکربندی تسک پس‌زمینه باید متد AddHostedService را فراخوانی کنید:
builder.Services.AddHostedService<MyBackgroundTask>();

فراخوانی AddHostedService، تسک پس‌زمینه را به عنوان یک سرویس singleton پیکربندی می‌کند.

پس آیا تزریق وابستگی در پیاده‌سازی‌های IHostedService هنوز کار می‌کند؟
بله، اما شما فقط می‌توانید سرویس‌های transient یا singleton را تزریق کنید.

با این حال، من دوست ندارم خودم اینترفیس IHostedService را پیاده‌سازی کنم. در عوض، ترجیح می‌دهم از کلاس BackgroundService استفاده کنم.

تسک‌های پس‌زمینه با BackgroundService 👍


کلاس BackgroundService از قبل اینترفیس IHostedService را پیاده‌سازی کرده است و یک متد abstract دارد که شما باید آن را override کنید - ExecuteAsync. وقتی از کلاس BackgroundService استفاده می‌کنید، فقط باید به عملیاتی که می‌خواهید پیاده‌سازی کنید فکر کنید.

در اینجا یک مثال از تسک پس‌زمینه برای اجرای مایگریشن‌های EF آمده است:
public class RunEfMigrationsBackgroundTask : BackgroundService
{
private readonly IServiceProvider _serviceProvider;

public RunEfMigrationsBackgroundTask(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
using IServiceScope scope = _serviceProvider.CreateScope();

await using AppDbContext dbContext =
scope.ServiceProvider.GetRequiredService<AppDbContext>();

await dbContext.Database.MigrateAsync(stoppingToken);
}
}

DbContext
در EF یک سرویس scoped است، که ما نمی‌توانیم آن را مستقیماً داخل RunEfMigrationsBackgroundTask تزریق کنیم. ما باید یک نمونه از IServiceProvider را تزریق کنیم که می‌توانیم از آن برای ایجاد یک service scope سفارشی استفاده کنیم، تا بتوانیم AppDbContext scoped را resolve کنیم.

من توصیه نمی‌کنم RunEfMigrationsBackgroundTask را در پروداکشن اجرا کنید. مایگریشن‌های EF به راحتی می‌توانند شکست بخورند و شما با مشکل مواجه خواهید شد. با این حال، فکر می‌کنم برای توسعه محلی کاملاً مناسب است.
تسک‌های پس‌زمینه دوره‌ای (Periodic)

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

در اینجا یک مثال از PeriodicBackgroundTask برای شروع آمده است:
public class PeriodicBackgroundTask : BackgroundService
{
private readonly TimeSpan _period = TimeSpan.FromSeconds(5);
private readonly ILogger<PeriodicBackgroundTask> _logger;

public PeriodicBackgroundTask(ILogger<PeriodicBackgroundTask> logger)
{
_logger = logger;
}

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
using PeriodicTimer timer = new PeriodicTimer(_period);
while (!stoppingToken.IsCancellationRequested &&
await timer.WaitForNextTickAsync(stoppingToken))
{
_logger.LogInformation("Executing PeriodicBackgroundTask");
}
}
}


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

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

چه می‌شود اگر بخواهید یک تسک پس‌زمینه زمان‌بندی‌شده داشته باشید که هر روز ساعت ۲ بامداد اجرا شود؟
شما احتمالاً می‌توانید چیزی شبیه این را خودتان بسازید، اما راه‌حل‌های موجودی وجود دارد که باید ابتدا آن‌ها را در نظر بگیرید.

در اینجا دو راه‌حل محبوب برای اجرای تسک‌های پس‌زمینه آمده است:

Quartz 🔥

Hangfire 🔥

📌ادامه دارد...

🔖 هشتگ‌ها:
#CSharp #DotNet #BackgroundTask #HostedService #WorkerService #SystemDesign #TaskScheduling
زمان‌بندی Background Jobها با Quartz.NET ⏱️


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

در اینجا چند مثال از آن آمده است:

📧 ارسال نوتیفیکیشن‌های ایمیل
📊 تولید گزارش‌ها
🔄 به‌روزرسانی یک کش
🖼 پردازش تصویر

🤔چگونه می‌توانید یک background job تکرارشونده در NET. ایجاد کنید؟


Quartz.NET
یک سیستم زمان‌بندی job کامل و متن‌باز است که می‌تواند از کوچکترین اپلیکیشن‌ها تا سیستم‌های بزرگ سازمانی استفاده شود.

سه مفهوم وجود دارد که باید در Quartz.NET درک کنید:

Job 📝
تسک پس‌زمینه‌ای واقعی که می‌خواهید اجرا کنید.

Trigger
تریگری که زمان اجرای یک job را کنترل می‌کند.

Scheduler 🧠
مسئول هماهنگی jobها و تریگرها.

بیایید ببینیم چگونه می‌توانیم از Quartz.NET برای ایجاد و زمان‌بندی background jobها استفاده کنیم.

افزودن سرویس میزبانی شده Quartz.NET 🔧

اولین کاری که باید انجام دهیم، نصب پکیج NuGet Quartz.NET است.

Install-Package Quartz.Extensions.Hosting

دلیل استفاده ما از این کتابخانه این است که به خوبی با NET. با استفاده از یک نمونه IHostedService یکپارچه می‌شود.
برای راه‌اندازی سرویس میزبانی شده Quartz.NET، به دو چیز نیاز داریم:

افزودن سرویس‌های مورد نیاز به کانتینر DI.
services.AddQuartz(configure =>
{
configure.UseMicrosoftDependencyInjectionJobFactory();
});

services.AddQuartzHostedService(options =>
{
options.WaitForJobsToComplete = true;
});

Quartz.NET
با واکشی jobها از کانتینر DI، آن‌ها را ایجاد می‌کند. این همچنین به این معنی است که شما می‌توانید از سرویس‌های scoped در jobهای خود استفاده کنید، نه فقط سرویس‌های singleton یا transient.
تنظیم گزینه WaitForJobsToComplete روی true تضمین می‌کند که Quartz.NET قبل از خروج، منتظر بماند تا jobها به آرامی تکمیل شوند.

ایجاد Background Job با IJob 👨‍💻

برای ایجاد یک background job با Quartz.NET باید اینترفیس IJob را پیاده‌سازی کنید.
این اینترفیس فقط یک متد واحد - Execute - را ارائه می‌دهد که می‌توانید کد background job خود را در آن قرار دهید.

💡چند نکته قابل توجه در اینجا:


ما از DI برای تزریق سرویس‌های ApplicationDbContext و IPublisher استفاده می‌کنیم.

Job
با DisallowConcurrentExecution دکوریت شده تا از اجرای همزمان همان job جلوگیری شود.
[DisallowConcurrentExecution]
public class ProcessOutboxMessagesJob : IJob
{
private readonly ApplicationDbContext _dbContext;
private readonly IPublisher _publisher;

public ProcessOutboxMessagesJob(
ApplicationDbContext dbContext,
IPublisher publisher)
{
_dbContext = dbContext;
_publisher = publisher;
}

public async Task Execute(IJobExecutionContext context)
{
List<OutboxMessage> messages = await _dbContext
.Set<OutboxMessage>()
.Where(m => m.ProcessedOnUtc == null)
.Take(20)
.ToListAsync(context.CancellationToken);

foreach (OutboxMessage outboxMessage in messages)
{
IDomainEvent? domainEvent = JsonConvert
.DeserializeObject<IDomainEvent>(
outboxMessage.Content,
new JsonSerializerSettings
{
TypeNameHandling = TypeNameHandling.All
});

if (domainEvent is null)
{
continue;
}

await _publisher.Publish(domainEvent, context.CancellationToken);

outboxMessage.ProcessedOnUtc = DateTime.UtcNow;
await _dbContext.SaveChangesAsync();
}
}
}


حالا که background job آماده است، باید آن را در کانتینر DI ثبت کرده و یک تریگر اضافه کنیم که job را اجرا کند.
پیکربندی Job ⚙️

ما قبلاً ProcessOutboxMessagesJob را پیاده‌سازی کردیم.
کتابخانه Quartz.NET مسئولیت scheduler را بر عهده خواهد گرفت.
و این ما را با پیکربندی trigger برای ProcessOutboxMessagesJob تنها می‌گذارد.
services.AddQuartz(configure =>
{
var jobKey = new JobKey(nameof(ProcessOutboxMessagesJob));
configure
.AddJob<ProcessOutboxMessagesJob>(jobKey)
.AddTrigger(
trigger => trigger.ForJob(jobKey).WithSimpleSchedule(
schedule => schedule.WithIntervalInSeconds(10).RepeatForever()));

configure.UseMicrosoftDependencyInjectionJobFactory();
});


ما باید background job خود را با یک JobKey به طور منحصر به فرد شناسایی کنیم.
فراخوانی AddJob، ProcessOutboxMessagesJob را با DI و همچنین با Quartz ثبت می‌کند.
پس از آن ما یک تریگر برای این job با فراخوانی AddTrigger پیکربندی می‌کنیم. در این مثال، من job را طوری زمان‌بندی می‌کنم که هر ده ثانیه اجرا شود و تا زمانی که سرویس میزبانی شده در حال اجراست، برای همیشه تکرار شود.
Quartz
همچنین از پیکربندی تریگرها با استفاده از عبارات cron پشتیبانی می‌کند.

نکات پایانی 💡

Quartz.NET
اجرای background jobها در NET. را آسان می‌کند و شما می‌توانید از تمام قدرت DI در background jobهای خود استفاده کنید. همچنین برای نیازمندی‌های مختلف زمان‌بندی با پیکربندی از طریق کد یا استفاده از عبارات cron انعطاف‌پذیر است.

چند مورد برای بهبود وجود دارد تا زمان‌بندی jobها آسان‌تر و boilerplate کمتر شود:

🔹 افزودن یک متد توسعه برای ساده‌سازی پیکربندی jobها با زمان‌بندی ساده.

🔹 افزودن یک متد توسعه برای ساده‌سازی پیکربندی jobها با عبارات cron از تنظیمات اپلیکیشن.

🔖 هشتگ‌ها:
#CSharp #DotNet #QuartzNet #BackgroundJobs #TaskScheduling #HostedService #SystemDesign
زمان‌بندی Background Jobها با Quartz در NET. (مفاهیم پیشرفته) ⏱️


اکثر اپلیکیشن‌های ASP.NET Core به پردازش پس‌زمینه (background processing) نیاز دارند - از ارسال ایمیل‌های یادآوری گرفته تا اجرای تسک‌های پاک‌سازی. با اینکه راه‌های زیادی برای پیاده‌سازی background jobها وجود دارد، Quartz.NET با قابلیت‌های زمان‌بندی قوی، گزینه‌های پایداری (persistence)، و ویژگی‌های آماده پروداکشن، متمایز می‌شود.

در این مقاله، به موارد زیر خواهیم پرداخت:

🔹 راه‌اندازی Quartz.NET با ASP.NET Core و observability مناسب

🔹 پیاده‌سازی jobهای on-demand و تکرارشونده (recurring)

🔹 پیکربندی ذخیره‌سازی پایدار با PostgreSQL

🔹 مدیریت داده‌های job و نظارت بر اجرا

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

راه‌اندازی Quartz با ASP.NET Core 🔧

• ابتدا، Quartz را با ابزار دقیق (instrumentation) مناسب راه‌اندازی می‌کنیم.
باید چند پکیج NuGet را نصب کنیم:
Install-Package Quartz.Extensions.Hosting
Install-Package Quartz.Serialization.Json
# This might be in prerelease
Install-Package OpenTelemetry.Instrumentation.Quartz

• سپس، سرویس‌های Quartz و ابزار دقیق OpenTelemetry را پیکربندی کرده و زمان‌بند را شروع می‌کنیم:
builder.Services.AddQuartz();

// Add Quartz.NET as a hosted service
builder.Services.AddQuartzHostedService(options =>
{
options.WaitForJobsToComplete = true;
});

builder.Services.AddOpenTelemetry()
.WithTracing(tracing =>
{
tracing
.AddHttpClientInstrumentation()
.AddAspNetCoreInstrumentation()
.AddQuartzInstrumentation();
})
.UseOtlpExporter();


تعریف و زمان‌بندی Jobها 📝

برای تعریف یک background job، باید اینترفیس IJob را پیاده‌سازی کنید. تمام پیاده‌سازی‌های job به عنوان سرویس‌های scoped اجرا می‌شوند، بنابراین می‌توانید وابستگی‌ها را در صورت نیاز تزریق کنید. Quartz به شما اجازه می‌دهد داده‌ها را با استفاده از دیکشنری JobDataMap به یک job پاس دهید. توصیه می‌شود فقط از انواع داده اولیه برای داده‌های job استفاده کنید تا از مشکلات سریال‌سازی جلوگیری شود.

• هنگام اجرای job، چند راه برای واکشی داده‌های job وجود دارد:

JobDataMap
یک دیکشنری از زوج‌های کلید-مقدار

JobExecutionContext.JobDetail.JobDataMap
داده‌های مخصوص job

JobExecutionContext.Trigger.JobDataMap
داده‌های مخصوص trigger

MergedJobDataMap
داده‌های job را با داده‌های trigger ترکیب می‌کند

بهترین شیوه، استفاده از MergedJobDataMap برای بازیابی داده‌های job است.
public class EmailReminderJob(
ILogger<EmailReminderJob> logger, IEmailService emailService) : IJob
{
public const string Name = nameof(EmailReminderJob);

public async Task Execute(IJobExecutionContext context)
{
// Best practice: Prefer using MergedJobDataMap
var data = context.MergedJobDataMap;
// Get job data - note that this isn't strongly typed
string? userId = data.GetString("userId");
string? message = data.GetString("message");
// ...
}
}


💡یک نکته: JobDataMap به صورت strongly-typed نیست. این محدودیتی است که باید با آن کنار بیاییم.

حالا، بیایید در مورد زمان‌بندی jobها صحبت کنیم.

زمان‌بندی یادآوری‌های یک‌باره: 🗓
app.MapPost("/api/reminders/schedule", async (
ISchedulerFactory schedulerFactory,
ScheduleReminderRequest request) =>
{
var scheduler = await schedulerFactory.GetScheduler();
var jobData = new JobDataMap { /* ... */ };

var job = JobBuilder.Create<EmailReminderJob>()
.WithIdentity($"reminder-{Guid.NewGuid()}", "email-reminders")
.SetJobData(jobData)
.Build();

var trigger = TriggerBuilder.Create()
.WithIdentity($"trigger-{Guid.NewGuid()}", "email-reminders")
.StartAt(request.ScheduleTime)
.Build();

await scheduler.ScheduleJob(job, trigger);

return Results.Ok();
});
زمان‌بندی Jobهای تکرارشونده 🔄


برای jobهای پس‌زمینه تکرارشونده، می‌توانید از زمان‌بندی cron استفاده کنید:
app.MapPost("/api/reminders/schedule/recurring", async (
ISchedulerFactory schedulerFactory,
RecurringReminderRequest request) =>
{
// ... (Job creation is the same) ...
var trigger = TriggerBuilder.Create()
.WithIdentity($"recurring-trigger-{Guid.NewGuid()}", "recurring-reminders")
.WithCronSchedule(request.CronExpression)
.Build();

await scheduler.ScheduleJob(job, trigger);

return Results.Ok();
});


تریگرهای Cron قدرتمندتر از تریگرهای ساده هستند. آن‌ها به شما اجازه می‌دهند زمان‌بندی‌های پیچیده‌ای مانند "هر روز کاری ساعت ۱۰ صبح" یا "هر ۱۵ دقیقه" را تعریف کنید.

راه‌اندازی پایداری Job (Job Persistence) 💾

به‌طور پیش‌فرض، Quartz از ذخیره‌سازی درون-حافظه‌ای استفاده می‌کند، که یعنی jobهای شما با ری‌استارت شدن اپلیکیشن از بین می‌روند. برای محیط‌های پروداکشن، شما به یک فروشگاه پایدار (persistent store) نیاز دارید.

بیایید ببینیم چگونه ذخیره‌سازی پایدار را با ایزوله‌سازی اسکیمای مناسب راه‌اندازی کنیم:
builder.Services.AddQuartz(options =>
{
options.AddJob<EmailReminderJob>(c => c
.StoreDurably()
.WithIdentity(EmailReminderJob.Name));

options.UsePersistentStore(persistenceOptions =>
{
persistenceOptions.UsePostgres(cfg =>
{
cfg.ConnectionString = connectionString;
cfg.TablePrefix = "scheduler.qrtz_";
});
persistenceOptions.UseNewtonsoftJsonSerializer();
});
});


تنظیم TablePrefix به سازماندهی جداول Quartz در دیتابیس شما کمک می‌کند - در این مورد، آن‌ها را در یک اسکیمای اختصاصی scheduler قرار می‌دهد.

جاب های بادوام (Durable Jobs) 📌

توجه کنید که ما EmailReminderJob را با StoreDurably پیکربندی می‌کنیم. این یک الگوی قدرتمند است که به شما اجازه می‌دهد jobهای خود را یک بار تعریف کرده و با تریگرهای مختلف از آن‌ها استفاده مجدد کنید.
public async Task ScheduleReminder(string userId, string message, DateTime scheduledTime)
{
var scheduler = await _schedulerFactory.GetScheduler();

// Reference the stored job by its identity
var jobKey = new JobKey(EmailReminderJob.Name);

var trigger = TriggerBuilder.Create()
.ForJob(jobKey) // Reference the durable job
.WithIdentity($"trigger-{Guid.NewGuid()}")
.UsingJobData("userId", userId)
.UsingJobData("message", message)
.StartAt(scheduledTime)
.Build();

await scheduler.ScheduleJob(trigger); // Note: just passing the trigger
}


خلاصه

راه‌اندازی صحیح Quartz در NET. شامل موارد بیشتری از صرفاً افزودن پکیج NuGet است.
به این موارد توجه کنید:

🔹 تعریف صحیح job و مدیریت داده با JobDataMap

🔹 راه‌اندازی زمان‌بندی jobهای یک‌باره و تکرارشونده

🔹 پیکربندی ذخیره‌سازی پایدار با ایزوله‌سازی اسکیمای مناسب

🔹 استفاده از jobهای بادوام برای حفظ تعاریف ثابت job

هر یک از این عناصر به یک سیستم پردازش پس‌زمینه قابل اعتماد کمک می‌کند که می‌تواند با نیازهای اپلیکیشن شما رشد کند.


🔖 هشتگ‌ها:
#CSharp #DotNet #ASPNETCore #QuartzNet #BackgroundJobs #TaskScheduling #Observability #SystemDesign
📖 سری آموزشی کتاب C# 12 in a Nutshell

تکنیک‌های حرفه‌ای پراپرتی در #C: از private set تا init


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

1️⃣ مقداردهی اولیه پراپرتی‌ها (Property Initializers)

در #C مدرن، شما می‌تونید مستقیماً به پراپرتی‌های خودکار (auto-properties) مقدار اولیه بدید، دقیقاً مثل فیلدها. این کار کد رو خیلی تمیزتر و خواناتر می‌کنه و دیگه نیازی نیست برای مقداردهی‌های ساده، حتماً سازنده (constructor) بنویسید.
public class ServerConfig
{
// مقداردهی اولیه یک پراپرتی خواندنی-نوشتنی
public string IpAddress { get; set; } = "127.0.0.1";
// مقداردهی اولیه یک پراپرتی فقط-خواندنی
public int Port { get; } = 8080;
public ServerConfig(int port)
{
// شما همچنان می‌تونید مقدار پراپرتی فقط-خواندنی رو در سازنده هم تغییر بدید
Port = port;
}
}


2️⃣ کنترل دسترسی با private set

گاهی وقتا می‌خواید یه پراپرتی از بیرون کلاس فقط-خواندنی باشه، ولی از داخل خود کلاس بتونید مقدارش رو تغییر بدید (مثلاً برای شمارنده‌ها یا تغییر وضعیت داخلی). اینجاست که private set وارد میشه.
public class Counter
{
public int Value { get; private set; } = 0;
public void Increment()
{
// از داخل کلاس قابل تغییر است
Value++;
}
}

// --- نحوه استفاده ---
var counter = new Counter();
counter.Increment(); // Value is now 1
// counter.Value = 10; // خطای زمان کامپایل! از بیرون قابل تغییر نیست


3️⃣ انقلاب تغییرناپذیری با init (از 9 #C) 🔒

این یکی از مهم‌ترین قابلیت‌های #C مدرن برای ساخت آبجکت‌های تغییرناپذیر (Immutable) هست.

پراپرتی init فقط موقع ساخت آبجکت (در سازنده یا با Object Initializer) قابل مقداردهیه و بعد از اون کاملاً قفل و فقط-خواندنی میشه.
public class Note
{
public int Pitch { get; init; } = 20;
public int Duration { get; init; } = 100;
}
// --- نحوه استفاده ---
// موقع ساخت، با Object Initializer قابل تغییره
var note = new Note { Pitch = 50 };

// بعد از ساخته شدن، دیگه نمیشه تغییرش داد
// note.Pitch = 200; // خطای زمان کامپایل!

نکته کلیدی برای کتابخانه‌نویس‌ها: ⚠️

اگه شما یه پراپرتی init جدید به کلاستون اضافه کنید، کدهای قدیمی که از کتابخونه شما استفاده می‌کنن، نمی‌شکنن\! ولی اگه یه پارامتر جدید (حتی اختیاری) به سازنده اضافه کنید، اون کدها دچار Binary Breaking Change میشن و باید دوباره کامپایل بشن. این باعث میشه init برای تکامل APIها خیلی امن‌تر باشه.

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

این تکنیک‌ها ابزارهای شما برای پیاده‌سازی اصل کپسوله‌سازی و ساختن کلاس‌های امن، تغییرناپذیر و قابل نگهداری هستن.

🔖 هشتگ‌ها:
#CSharp #DotNet #OOP #Properties #CleanCode #Immutability
بحث Pagination در ASP.NET Core: Offset در برابر Cursor 📄🆚🚀


صفحه بندی برای مدیریت کارآمد مجموعه داده‌های بزرگ، حیاتی است. با اینکه offset pagination به طور گسترده استفاده می‌شود و کار را راه می‌اندازد، cursor-based pagination مزایای جالبی برای سناریوهای خاص ارائه می‌دهد.

این روش به ویژه برای فیدهای real-time، اینترفیس‌های infinite scroll، و APIهایی که عملکرد در مقیاس بالا در آن‌ها اهمیت دارد، ارزشمند است - مانند تایم‌لاین‌های شبکه‌های اجتماعی، لاگ‌های فعالیت، یا جریان‌های رویداد که کاربران به طور مکرر در صفحات مجموعه داده‌های بزرگ جابجا می‌شوند.

بیایید هر دو رویکرد را با استفاده از یک جدول ساده UserNotes بررسی کرده و ببینیم چگونه با یک میلیون رکورد عمل می‌کنند.

اسکیمای دیتابیس 💾

من یک جدول ساده برای نمایش تکنیک‌های pagination ایجاد کردم. این جدول با ۱,۰۰۰,۰۰۰ رکورد برای اهداف تست پر شده است، که باید برای نشان دادن تفاوت عملکرد بین offset و cursor pagination کافی باشد.
CREATE TABLE user_notes (
id uuid NOT NULL,
user_id uuid NOT NULL,
note character varying(500),
date date NOT NULL,
CONSTRAINT pk_user_notes PRIMARY KEY (id)
);

و این هم کلاس #C که انتیتی UserNote را نشان می‌دهد:
public class UserNote
{
public Guid Id { get; set; }
public Guid UserId { get; set; }
public string? Note { get; set; }
public DateOnly Date { get; set; }
}


Offset Pagination: رویکرد سنتی 🐢

ما در Offset pagination از عملیات Skip و Take استفاده می‌کنیم. ما تعداد معینی از ردیف‌ها را رد می‌کنیم (skip) و تعداد ثابتی از ردیف‌ها را برمی‌داریم (take). این‌ها معمولاً به OFFSET و LIMIT در کوئری‌های SQL ترجمه می‌شوند.
app.MapGet("/offset", async (
AppDbContext dbContext,
int page = 1,
int pageSize = 10,
CancellationToken cancellationToken = default) =>
{
// ... (بررسی ورودی‌ها) ...
var query = dbContext.UserNotes
.OrderByDescending(x => x.Date)
.ThenByDescending(x => x.Id);

// Offset pagination معمولاً تعداد کل آیتم‌ها را می‌شمارد
var totalCount = await query.CountAsync(cancellationToken);
var totalPages = (int)Math.Ceiling(totalCount / (double)pageSize);

// رد کردن و برداشتن تعداد مورد نیاز از آیتم‌ها
var items = await query
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync(cancellationToken);

return Results.Ok(new { /* ... نتایج ... */ });
});


SQL تولید شده:
-- این کوئری اول ارسال می‌شود
SELECT count(*)::int FROM user_notes AS u;

-- و سپس کوئری اصلی داده‌ها
SELECT u.id, u.date, u.note, u.user_id
FROM user_notes AS u
ORDER BY u.date DESC, u.id DESC
LIMIT @pageSize OFFSET @offset;


محدودیت‌های Offset Pagination:


• عملکرد با افزایش آفست کاهش می‌یابد زیرا دیتابیس باید تمام ردیف‌های قبل از آفست را اسکن و دور بریزد.

• ریسک از دست دادن یا تکرار آیتم‌ها وقتی داده‌ها بین صفحات تغییر می‌کنند.

• نتایج متناقض با آپدیت‌های همزمان.

Cursor-Based Pagination: یک رویکرد سریع‌تر 🚀


و حالا Cursor pagination از یک نقطه مرجع (cursor) برای واکشی مجموعه بعدی نتایج استفاده می‌کند. این نقطه مرجع معمولاً یک شناسه منحصر به فرد یا ترکیبی از فیلدهاست که ترتیب مرتب‌سازی را تعریف می‌کند.
app.MapGet("/cursor", async (
AppDbContext dbContext,
DateOnly? date = null,
Guid? lastId = null,
int limit = 10,
CancellationToken cancellationToken = default) =>
{
// ... (بررسی ورودی‌ها) ...
var query = dbContext.UserNotes.AsQueryable();

if (date != null && lastId != null)
{
// از cursor برای واکشی مجموعه بعدی نتایج استفاده می‌کنیم
query = query.Where(x => x.Date < date || (x.Date == date && x.Id <= lastId));
}

// آیتم‌ها را واکشی کرده و مشخص می‌کنیم آیا آیتم‌های بیشتری وجود دارد یا نه
var items = await query
.OrderByDescending(x => x.Date)
.ThenByDescending(x => x.Id)
.Take(limit + 1)
.ToListAsync(cancellationToken);

// ... (منطق برای استخراج cursor بعدی) ...

return Results.Ok(new { /* ... نتایج ... */ });
});
SQL تولید شده:
SELECT u.id, u.date, u.note, u.user_id
FROM user_notes AS u
WHERE u.date < @date OR (u.date = @date AND u.id <= @lastId)
ORDER BY u.date DESC, u.id DESC
LIMIT @limit;


💡توجه کنید که هیچ OFFSET در کوئری وجود ندارد. ما مستقیماً بر اساس cursor به دنبال ردیف‌ها می‌گردیم که کارآمدتر است.

محدودیت‌های Cursor Pagination:


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

• کاربران نمی‌توانند به یک شماره صفحه خاص بپرند.

• پیاده‌سازی صحیح آن پیچیده‌تر است.

بررسی پلن‌های اجرای SQL 📊

من پلن‌های اجرا را برای هر دو مقایسه کردم. برای صفحه‌ای در عمق دیتابیس (آفست ۹۰۰,۰۰۰):

زمان اجرای Offset Pagination: 704.217 ms 🐢

زمان اجرای Cursor Pagination: 40.993 ms 🚀

یک بهبود عملکرد ۱۷ برابری با cursor pagination!

افزودن ایندکس برای Cursor Pagination 🔑

من همچنین تأثیر ایندکس‌ها را روی cursor pagination تست کردم. یک ایندکس ترکیبی روی فیلدهای Date و Id ایجاد کردم.
نتیجه اولیه کندتر بود! اما با استفاده از مقایسه تاپل (tuple comparison) در SQL:
WHERE (u.date, u.id) <= (@date, @lastId)

زمان اجرا به 0.668 ms کاهش یافت! ⚡️

برای ترجمه این به EF Core، می‌توانید از EF.Functions.LessThanOrEqual که یک ValueTuple را به عنوان آرگومان می‌پذیرد، استفاده کنید.

خلاصه 📝

با اینکه offset pagination ساده‌تر است، در مقیاس بالا دچار افت عملکرد شدید می‌شود. Cursor pagination عملکرد ثابتی را حفظ می‌کند و برای فیدهای real-time و infinite scroll عالی است.

🤔چه زمانی از کدام استفاده کنیم؟


🔹 برای APIهای حساس به عملکرد، فیدهای real-time یا infinite scroll از ⟵ Cursor pagination

🔹 برای اینترفیس‌های ادمین، مجموعه داده‌های کوچک، یا زمانی که به تعداد کل صفحات نیاز دارید ⟵ Offset pagination.

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

خداحافظی با کدهای تکراری: معرفی Primary Constructors در 12 #C
از نوشتن این همه کد تکراری تو سازنده‌ها خسته شدید؟
public MyClass(string name) { this.name = name; }

این الگو که توش فقط پارامترهای ورودی به فیلدها اختصاص داده میشن، خیلی رایجه. خبر خوب اینه که از 12 #C، با Primary Constructors (سازنده‌های اصلی) می‌تونیم این کار رو خیلی شیک و کوتاه انجام بدیم!

1️⃣ Primary Constructor چیست و چطور کار می‌کند؟

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

روش قدیمی: 👎
class Person
{
string firstName, lastName; // تعریف فیلدها
public Person(string firstName, string lastName) // تعریف سازنده
{
this.firstName = firstName; // اختصاص به فیلدها
this.lastName = lastName;
}
public void Print() => Console.WriteLine(firstName + " " + lastName);
}

روش مدرن با Primary Constructor: 👍
class Person(string firstName, string lastName)
{
public void Print() => Console.WriteLine(firstName + " " + lastName);
}
// استفاده ازش هم مثل قبل سادست
Person p = new Person("Alice", "Jones");
p.Print(); // Alice Jones


2️⃣ قوانین و نکات کلیدی ⚖️

پارامترها در کل کلاس قابل دسترسن: برخلاف سازنده معمولی که پارامترهاش فقط داخل همون بلوک زنده هستن، پارامترهای Primary Constructor در تمام بدنه کلاس قابل دسترسی هستن.

• سازنده اصلی: اگه سازنده‌های دیگه‌ای تو کلاس داشته باشید، باید با : this(...) سازنده اصلی (Primary) رو صدا بزنن.

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

3️⃣ کلاس با Primary Constructor در برابر record 🆚

رکوردها هم Primary Constructor دارن، ولی با یه تفاوت بزرگ: کامپایلر برای رکوردها، به صورت پیش‌فرض برای هر پارامتر، یه پراپرتی public init-only هم میسازه.

اما برای classها این اتفاق نمیفته و پارامترها به صورت private باقی می‌مونن (مگر اینکه خودتون به صورت دستی یک پراپرتی عمومی براشون بسازید).

4️⃣ محدودیت‌ها: کی ازش استفاده نکنیم؟ ⚠️

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

🔖 هشتگ‌ها:
#CSharp #DotNet #CSharp12 #Developer #OOP #CleanCode