کتابخانه جدید کشینگ 🆕
HybridCache در ASP.NET Core
کشینگ ⚡️ برای ساخت اپلیکیشنهای سریع و مقیاسپذیر ضروری است. ASP.NET Core به طور سنتی دو گزینه کشینگ ارائه میداد: کشینگ درون-حافظهای و کشینگ توزیعشده. هر کدام مزایا و معایب خود را داشتند. کشینگ درون-حافظهای با استفاده از IMemoryCache سریع است اما به یک سرور واحد محدود میشود. کشینگ توزیعشده با IDistributedCache با استفاده از یک backplane در چندین سرور کار میکند.
حالا NET 9. قابلیت HybridCache را معرفی میکند، یک کتابخانه جدید که بهترینهای هر دو رویکرد را ترکیب میکند. این قابلیت از مشکلات رایج کشینگ مانند cache stampede جلوگیری میکند. همچنین ویژگیهای مفیدی مانند نامعتبرسازی بر اساس تگ و نظارت بهتر بر عملکرد را اضافه میکند.
به شما نشان خواهم داد که چگونه از HybridCache در اپلیکیشنهای خود استفاده کنید.
HybridCache چیست؟ 🤔
گزینههای کشینگ سنتی در ASP.NET Core محدودیتهایی دارند. کشینگ درون-حافظهای سریع است اما به یک سرور محدود میشود. کشینگ توزیعشده در سرورهای مختلف کار میکند اما کندتر است.
HybridCache
هر دو رویکرد را ترکیب کرده و ویژگیهای مهمی را اضافه میکند:
1️⃣ کشینگ دو سطحی (L1/L2)
• L1: کش درون-حافظهای سریع
• L2: کش توزیعشده (Redis، SQL Server و غیره)
2️⃣ محافظت در برابر cache stampede
(زمانی که درخواستهای زیادی به یکباره به کش خالی برخورد میکنند)
3️⃣ نامعتبرسازی کش بر اساس تگ
4️⃣ سریالسازی قابل پیکربندی
5️⃣ معیارها و مانیتورینگ
کش L1 در حافظه اپلیکیشن شما اجرا میشود. کش L2 میتواند Redis، SQL Server یا هر کش توزیعشده دیگری باشد. اگر به کشینگ توزیعشده نیاز ندارید، میتوانید از HybridCache فقط با کش L1 استفاده کنید.
نصب HybridCache 📦
پکیج NuGet Microsoft.Extensions.Caching.Hybrid را نصب کنید:
Install-Package
Microsoft.Extensions.Caching.Hybrid
HybridCache را به سرویسهای خود اضافه کنید:
builder.Services.AddHybridCache(options =>
{
// حداکثر اندازه آیتمهای کش شده
options.MaximumPayloadBytes = 1024 * 1024 * 10; // 10MB
options.MaximumKeyLength = 512;
// تایماوتهای پیشفرض
options.DefaultEntryOptions = new HybridCacheEntryOptions
{
Expiration = TimeSpan.FromMinutes(30),
LocalCacheExpiration = TimeSpan.FromMinutes(30)
};
});
برای انواع داده سفارشی، میتوانید سریالایزر خود را اضافه کنید:
builder.Services.AddHybridCache()
.AddSerializer<CustomType, CustomSerializer>();
استفاده از HybridCache 👨💻
HybridCache
چندین متد برای کار با دادههای کش شده فراهم میکند. مهمترین آنها GetOrCreateAsync، SetAsync و متدهای مختلف remove هستند. بیایید ببینیم چگونه از هر کدام در سناریوهای دنیای واقعی استفاده کنیم.
گرفتن یا ایجاد ورودیهای کش 🔎
متد GetOrCreateAsync ابزار اصلی شما برای کار با دادههای کش شده است. این متد به طور خودکار هم cache hit و هم cache miss را مدیریت میکند. اگر داده در کش نباشد، متد factory شما را برای گرفتن داده فراخوانی کرده، آن را کش میکند و برمیگرداند.
در اینجا یک endpoint برای دریافت جزئیات محصول آمده است:
app.MapGet("/products/{id}", async (
int id,
HybridCache cache,
ProductDbContext db,
CancellationToken ct) =>
{
var product = await cache.GetOrCreateAsync(
$"product-{id}",
async token =>
{
return await db.Products
.Include(p => p.Category)
.FirstOrDefaultAsync(p => p.Id == id, token);
},
cancellationToken: ct
);
return product is null ? Results.NotFound() : Results.Ok(product);
});📌در این مثال:
• کلید کش برای هر محصول منحصر به فرد است.
• اگر محصول در کش باشد، بلافاصله برگردانده میشود.
• اگر نباشد، متد factory برای گرفتن داده اجرا میشود.
• درخواستهای همزمان دیگر برای همان محصول، منتظر پایان یافتن اولین درخواست میمانند.
گاهی اوقات نیاز دارید کش را مستقیماً بهروزرسانی کنید، مانند پس از تغییر دادهها. متد SetAsync این کار را انجام میدهد:
تگها برای مدیریت گروههایی از ورودیهای کش مرتبط، قدرتمند هستند. شما میتوانید چندین ورودی را به یکباره با استفاده از تگها نامعتبر کنید:
• نامعتبر کردن تمام محصولات در یک دستهبندی.
• پاک کردن تمام دادههای کش شده برای یک کاربر خاص.
• رفرش کردن تمام دادههای مرتبط هنگامی که چیزی تغییر میکند.
برای نامعتبرسازی مستقیم آیتمهای خاص، از RemoveAsync استفاده کنید:
برای استفاده از Redis به عنوان کش توزیعشده خود:
پکیج NuGet را نصب کنید:
Install-Package Microsoft.Extensions.Caching.StackExchangeRedis
Redis و HybridCache را پیکربندی کنید:
HybridCache
کشینگ را در اپلیکیشنهای NET. ساده میکند. این قابلیت، کشینگ سریع درون-حافظهای را با کشینگ توزیعشده ترکیب میکند، از مشکلات رایج مانند cache stampede جلوگیری میکند، و هم در سیستمهای تک-سروری و هم توزیعشده به خوبی کار میکند.
با تنظیمات پیشفرض و الگوهای استفاده اولیه شروع کنید - این کتابخانه طوری طراحی شده که استفاده از آن ساده باشد در حالی که مشکلات پیچیده کشینگ را حل میکند.
• کلید کش برای هر محصول منحصر به فرد است.
• اگر محصول در کش باشد، بلافاصله برگردانده میشود.
• اگر نباشد، متد factory برای گرفتن داده اجرا میشود.
• درخواستهای همزمان دیگر برای همان محصول، منتظر پایان یافتن اولین درخواست میمانند.
تنظیم مستقیم ورودیهای کش ✍️
گاهی اوقات نیاز دارید کش را مستقیماً بهروزرسانی کنید، مانند پس از تغییر دادهها. متد SetAsync این کار را انجام میدهد:
app.MapPut("/products/{id}", async (int id, Product product, HybridCache cache) =>
{
// ابتدا دیتابیس را بهروزرسانی کنید
await UpdateProductInDatabase(product);
// سپس کش را با انقضای سفارشی بهروزرسانی کنید
var options = new HybridCacheEntryOptions
{
Expiration = TimeSpan.FromHours(1),
LocalCacheExpiration = TimeSpan.FromMinutes(30)
};
await cache.SetAsync(
$"product-{id}",
product,
options
);
return Results.NoContent();
});استفاده از تگهای کش 🏷
تگها برای مدیریت گروههایی از ورودیهای کش مرتبط، قدرتمند هستند. شما میتوانید چندین ورودی را به یکباره با استفاده از تگها نامعتبر کنید:
app.MapGet("/categories/{id}/products", async (...) =>
{
var tags = [$"category-{id}", "products"];
var products = await cache.GetOrCreateAsync(
$"products-by-category-{id}",
async token => { /* ... fetch products ... */ },
tags: tags,
cancellationToken: ct
);
return Results.Ok(products);
});
// Endpoint برای نامعتبر کردن تمام محصولات در یک دستهبندی
app.MapPost("/categories/{id}/invalidate", async (id, cache, ct) =>
{
await cache.RemoveByTagAsync($"category-{id}", ct);
return Results.NoContent();
});📍تگها برای موارد زیر مفید هستند:
• نامعتبر کردن تمام محصولات در یک دستهبندی.
• پاک کردن تمام دادههای کش شده برای یک کاربر خاص.
• رفرش کردن تمام دادههای مرتبط هنگامی که چیزی تغییر میکند.
حذف ورودیهای تکی 🗑
برای نامعتبرسازی مستقیم آیتمهای خاص، از RemoveAsync استفاده کنید:
app.MapDelete("/products/{id}", async (int id, HybridCache cache) =>
{
// ابتدا از دیتابیس حذف کنید
await DeleteProductFromDatabase(id);
// سپس از کش حذف کنید
await cache.RemoveAsync($"product-{id}");
return Results.NoContent();
});افزودن Redis به عنوان کش L2 🔥
برای استفاده از Redis به عنوان کش توزیعشده خود:
پکیج NuGet را نصب کنید:
Install-Package Microsoft.Extensions.Caching.StackExchangeRedis
Redis و HybridCache را پیکربندی کنید:
// افزودن Redis
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = "your-redis-connection-string";
});
// افزودن HybridCache - به طور خودکار از Redis به عنوان L2 استفاده خواهد کرد
builder.Services.AddHybridCache();
خلاصه 📝
HybridCache
کشینگ را در اپلیکیشنهای NET. ساده میکند. این قابلیت، کشینگ سریع درون-حافظهای را با کشینگ توزیعشده ترکیب میکند، از مشکلات رایج مانند cache stampede جلوگیری میکند، و هم در سیستمهای تک-سروری و هم توزیعشده به خوبی کار میکند.
با تنظیمات پیشفرض و الگوهای استفاده اولیه شروع کنید - این کتابخانه طوری طراحی شده که استفاده از آن ساده باشد در حالی که مشکلات پیچیده کشینگ را حل میکند.
🔖 هشتگها:
#CSharp #Programming #Developer #DotNet #HybridCache
مدیریت خطای سراسری در ASP.NET Core: از Middleware تا Handlerهای مدرن 🚨
بیایید در مورد چیزی صحبت کنیم که همه ما با آن سر و کار داریم اما اغلب تا آخرین لحظه به تعویق میاندازیم - مدیریت خطا در اپلیکیشنهای ASP.NET Core ما.
وقتی چیزی در پروداکشن خراب میشود، آخرین چیزی که میخواهید یک خطای مبهم 500 بدون هیچ زمینهای است. مدیریت خطای مناسب فقط مربوط به لاگ کردن استثناها نیست. بلکه در مورد این است که مطمئن شوید اپلیکیشن شما به آرامی شکست میخورد و اطلاعات مفیدی به فراخواننده (و شما) میدهد.
در این مقاله، گزینههای اصلی برای مدیریت خطای سراسری در ASP.NET Core را بررسی خواهیم کرد.
مدیریت خطای مبتنی بر Middleware 📜
روش کلاسیک برای گرفتن استثناهای کنترلنشده، استفاده از Middleware سفارشی است. اینجاست که اکثر ما شروع میکنیم، و صادقانه بگویم، هنوز هم برای اکثر سناریوها عالی کار میکند.
internal sealed class GlobalExceptionHandlerMiddleware(
RequestDelegate next,
ILogger<GlobalExceptionHandlerMiddleware> logger)
{
public async Task InvokeAsync(HttpContext context)
{
try
{
await next(context);
}
catch (Exception ex)
{
logger.LogError(ex, "Unhandled exception occurred");
// حتماً قبل از نوشتن در بدنه پاسخ، کد وضعیت را تنظیم کنید
context.Response.StatusCode = ex switch
{
ApplicationException => StatusCodes.Status400BadRequest,
_ => StatusCodes.Status500InternalServerError
};
await context.Response.WriteAsJsonAsync(
new ProblemDetails
{
Type = ex.GetType().Name,
Title = "An error occured",
Detail = ex.Message
});
}
}
}
فراموش نکنید که middleware را به پایپلاین درخواست اضافه کنید:
app.UseMiddleware<GlobalExceptionHandlerMiddleware>();
این رویکرد محکم است و در همه جای پایپلاین شما کار میکند. زیبایی آن در سادگیاش است: همه چیز را در یک try-catch بپیچید، خطا را لاگ کنید و یک پاسخ یکپارچه برگردانید.
اما وقتی شروع به افزودن قوانین خاص برای انواع مختلف استثناها میکنید (مانند ValidationException, NotFoundException)، این به یک آشفتگی تبدیل میشود.
معرفی IProblemDetailsService 📄
مایکروسافت این نقطه ضعف را تشخیص داد و IProblemDetailsService را برای استانداردسازی پاسخهای خطا به ما داد. به جای سریالسازی دستی آبجکتهای خطای خودمان، میتوانیم از فرمت داخلی Problem Details استفاده کنیم.
// ...
catch (Exception ex)
{
logger.LogError(ex, "Unhandled exception occurred");
context.Response.StatusCode = ex switch
{
ApplicationException => StatusCodes.Status400BadRequest,
_ => StatusCodes.Status500InternalServerError
};
await problemDetailsService.TryWriteAsync(new ProblemDetailsContext
{
HttpContext = context,
Exception = ex,
ProblemDetails = new ProblemDetails
{
Type = ex.GetType().Name,
Title = "An error occured",
Detail = ex.Message
}
});
}
// ...
این خیلی تمیزتر است. ما اکنون از یک فرمت استاندارد استفاده میکنیم که مصرفکنندگان API انتظار دارند. اما هنوز با مشکل آن switch statement در حال رشد گیر کردهایم.
روش مدرن: 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
'خود' آبجکت کیست؟ در دنیای شیءگرایی، گاهی وقتا یه آبجکت نیاز داره به "خودش" اشاره کنه. ابزار #C برای این کار، کلمه کلیدی ساده ولی قدرتمند this هست. this در واقع ضمیر "من" برای یک آبجکته و به نمونه فعلی (current instance) خودش اشاره میکنه.
1️⃣ کاربرد اول: رفع ابهام (Disambiguation)
این رایجترین کاربرد this هست. وقتی اسم پارامتر سازنده، هماسم یکی از فیلدهای کلاس باشه، برای اینکه به کامپایلر بفهمونیم منظورمون فیلد کلاسه، از this استفاده میکنیم.
2️⃣ کاربرد دوم: پاس دادن خودِ آبجکت 🤝
گاهی وقتا لازمه یه آبجکت، رفرنس خودش رو به یه آبجکت یا متد دیگه بده.
this
فقط در اعضای غیر استاتیک (non-static) یک کلاس یا struct معتبره.
👇کلیدواژه 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
دروازههای هوشمند کلاس شما چرا تو کدنویسی حرفهای، تقریباً هیچوقت فیلدهای یه کلاس رو public نمیکنن؟ چون این کار یعنی از دست دادن کنترل! هر کسی از بیرون میتونه هر مقدار نامعتبری رو توش بریزه.
راه حل #C برای این مشکل، یه قابلیت فوقالعاده به اسم پراپرتی (Property) هست.
پراپرتیها از بیرون شبیه فیلدهای معمولی به نظر میرسن و به همون سادگی استفاده میشن، ولی در داخل، در واقع متدهای خاصی هستن که بهشون اکسسور (accessor) گفته میشه. این به ما کنترل کامل روی خوندن و نوشتن مقدار رو میده.
در رایجترین حالت، یک پراپرتی از دو بخش تشکیل شده:
• فیلد پشتیبان (Backing Field): یک فیلد private که داده واقعی رو نگه میداره.
• پراپرتی public: دروازهای که به دنیای بیرون اجازه دسترسی کنترلشده به اون فیلد رو میده.
✨️این پراپرتی، دو اکسسور داره:
🔹️get:
وقتی پراپرتی رو میخونیم، این بلوک اجرا میشه.
🔹️set:
وقتی مقداری رو به پراپرتی اختصاص میدیم، این بلوک اجرا میشه. کلمه کلیدی value در اینجا، به مقداری که داره ست میشه، اشاره داره.
جادوی پراپرتیها اینجاست که میتونید تو اکسسورهای get و set، منطق دلخواهتون رو پیاده کنید. مثلاً اعتبارسنجی (validation).
🤔 حرف حساب و قانون طلایی
قانون طلایی شیءگرایی: فیلدها رو همیشه private نگه دارید و با پراپرتیهای public اونها رو در معرض دید بذارید. این کار به شما کنترل کامل روی کلاسهاتون میده و اساس کپسولهسازیه.
🏦 پراپرتیها (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 مدرن، شما میتونید مستقیماً به پراپرتیهای خودکار (auto-properties) مقدار اولیه بدید، دقیقاً مثل فیلدها. این کار کد رو خیلی تمیزتر و خواناتر میکنه و دیگه نیازی نیست برای مقداردهیهای ساده، حتماً سازنده (constructor) بنویسید.
گاهی وقتا میخواید یه پراپرتی از بیرون کلاس فقط-خواندنی باشه، ولی از داخل خود کلاس بتونید مقدارش رو تغییر بدید (مثلاً برای شمارندهها یا تغییر وضعیت داخلی). اینجاست که private set وارد میشه.
این یکی از مهمترین قابلیتهای #C مدرن برای ساخت آبجکتهای تغییرناپذیر (Immutable) هست.
پراپرتی init فقط موقع ساخت آبجکت (در سازنده یا با Object Initializer) قابل مقداردهیه و بعد از اون کاملاً قفل و فقط-خواندنی میشه.
اگه شما یه پراپرتی
این تکنیکها ابزارهای شما برای پیادهسازی اصل کپسولهسازی و ساختن کلاسهای امن، تغییرناپذیر و قابل نگهداری هستن.
✨ تکنیکهای حرفهای پراپرتی در #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