⚡️ پستهای سریالی جدید: 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
بحث 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 تولید شده:
💡توجه کنید که هیچ OFFSET در کوئری وجود ندارد. ما مستقیماً بر اساس cursor به دنبال ردیفها میگردیم که کارآمدتر است.
• اگر کاربران نیاز به تغییر داینامیک فیلدهای مرتبسازی داشته باشند، پیادهسازی به شدت پیچیده میشود.
• کاربران نمیتوانند به یک شماره صفحه خاص بپرند.
• پیادهسازی صحیح آن پیچیدهتر است.
من پلنهای اجرا را برای هر دو مقایسه کردم. برای صفحهای در عمق دیتابیس (آفست ۹۰۰,۰۰۰):
زمان اجرای Offset Pagination: 704.217 ms 🐢
زمان اجرای Cursor Pagination: 40.993 ms 🚀
یک بهبود عملکرد ۱۷ برابری با cursor pagination!
من همچنین تأثیر ایندکسها را روی cursor pagination تست کردم. یک ایندکس ترکیبی روی فیلدهای Date و Id ایجاد کردم.
نتیجه اولیه کندتر بود! اما با استفاده از مقایسه تاپل (tuple comparison) در SQL:
زمان اجرا به 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.
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
این الگو که توش فقط پارامترهای ورودی به فیلدها اختصاص داده میشن، خیلی رایجه. خبر خوب اینه که از 12 #C، با Primary Constructors (سازندههای اصلی) میتونیم این کار رو خیلی شیک و کوتاه انجام بدیم!
این یه سینتکس جدیده که به شما اجازه میده پارامترهای سازنده اصلی رو مستقیماً بعد از اسم کلاس و داخل پرانتز تعریف کنید. کامپایلر به صورت خودکار یک سازنده بر اساس این پارامترها میسازه.
روش قدیمی: 👎
روش مدرن با Primary Constructor: 👍
پارامترها در کل کلاس قابل دسترسن: برخلاف سازنده معمولی که پارامترهاش فقط داخل همون بلوک زنده هستن، پارامترهای Primary Constructor در تمام بدنه کلاس قابل دسترسی هستن.
• سازنده اصلی: اگه سازندههای دیگهای تو کلاس داشته باشید، باید با : this(...) سازنده اصلی (Primary) رو صدا بزنن.
• حذف سازنده پیشفرض: با تعریف سازنده اصلی، دیگه سازنده پیشفرض بدون پارامتر به صورت خودکار ساخته نمیشه.
رکوردها هم Primary Constructor دارن، ولی با یه تفاوت بزرگ: کامپایلر برای رکوردها، به صورت پیشفرض برای هر پارامتر، یه پراپرتی public init-only هم میسازه.
اما برای classها این اتفاق نمیفته و پارامترها به صورت private باقی میمونن (مگر اینکه خودتون به صورت دستی یک پراپرتی عمومی براشون بسازید).
این قابلیت برای سناریوهای ساده عالیه، ولی محدودیتهایی هم داره: شما نمیتونید منطق اضافهای (مثل اعتبارسنجی) رو مستقیم به خود سازنده اصلی اضافه کنید و برای این کارها باید از سازندههای سنتی استفاده کنید.
✨ خداحافظی با کدهای تکراری: معرفی 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
عنوان: Problem Details برای APIهای ASP.NET Core 🚨
هنگام توسعه APIهای HTTP، ارائه پاسخهای خطای یکپارچه و آموزنده برای یک تجربه توسعهدهنده روان، حیاتی است. Problem Details در ASP.NET Core یک راهحل استاندارد برای این چالش ارائه میدهد و تضمین میکند که APIهای شما خطاها را به طور مؤثر و یکنواخت مخابره کنند.
در این مقاله، ما آخرین تحولات در Problem Details را بررسی خواهیم کرد، از جمله:
🔹 قالب RFC 9457 جدید که استاندارد Problem Details را بهبود میبخشد.
🔹 استفاده از IExceptionHandler در Net 8. برای مدیریت خطای سراسری.
🔹 استفاده از IProblemDetailsService برای سفارشیسازی Problem Details.
بیایید به این قابلیتها شیرجه بزنیم و ببینیم چگونه میتوانند مدیریت خطای API شما را بهبود بخشند.
درک Problem Details 📄
این Problem Details یک فرمت قابل خواندن توسط ماشین برای مشخص کردن خطاها در پاسخهای HTTP API است. کدهای وضعیت HTTP همیشه جزئیات کافی در مورد خطاها را ندارند. مشخصات Problem Details یک فرمت سند JSON (و XML) را برای توصیف مشکلات تعریف میکند.
Problem Details شامل موارد زیر است:
• نوع (Type) : یک ارجاع URI که نوع مشکل را مشخص میکند.
• عنوان (Title) : یک خلاصه کوتاه و قابل خواندن توسط انسان از نوع مشکل.
• وضعیت (Status) : کد وضعیت HTTP.
• جزئیات (Detail) : یک توضیح قابل خواندن توسط انسان مختص این رخداد از مشکل.
• نمونه (Instance) : یک ارجاع URI که رخداد خاص مشکل را مشخص میکند.
و RFC 9457،که جایگزین RFC 7807 میشود، بهبودهایی مانند شفافسازی استفاده از فیلد type و ارائه راهنمایی برای توسعه Problem Details را معرفی میکند.
در اینجا یک مثال از پاسخ Problem Details آمده است:
Content-Type: application/problem+json
{
"type": "https://tools.ietf.org/html/rfc9110#section-15.5.5",
"noscript": "Not Found",
"status": 404,
"detail": "The habit with the specified identifier was not found",
"instance": "PUT /api/habits/aadcad3f-8dc8-443d-be44-3d99893ba18a"
}
پیادهسازی Problem Details 🔧
بیایید ببینیم چگونه Problem Details را در ASP.NET Core پیادهسازی کنیم. با فراخوانی AddProblemDetails، ما اپلیکیشن را برای استفاده از فرمت Problem Details برای درخواستهای ناموفق پیکربندی میکنیم. با UseExceptionHandler، ما یک middleware مدیریت استثنا را به پایپلاین درخواست معرفی میکنیم. با افزودن UseStatusCodePages، ما یک middleware معرفی میکنیم که پاسخهای خطا با بدنه خالی را به یک پاسخ Problem Details تبدیل میکند.
var builder = WebApplication.CreateBuilder(args);
// Adds services for using Problem Details format
builder.Services.AddProblemDetails();
var app = builder.Build();
// Converts unhandled exceptions into Problem Details responses
app.UseExceptionHandler();
// Returns the Problem Details response for (empty) non-successful responses
app.UseStatusCodePages();
app.Run();
هنگامی که با یک استثنای کنترلنشده مواجه شویم، به یک پاسخ Problem Details ترجمه خواهد شد.
روش مدرن: IExceptionHandler ✨
با 8 Net. ، ما میتوانیم از IExceptionHandler که در middleware داخلی مدیریت استثنا اجرا میشود، استفاده کنیم. این handler به شما اجازه میدهد تا پاسخ Problem Details را برای استثناهای خاص تنظیم کنید. برگرداندن true از متد TryHandleAsync پایپلاین را short-circuit کرده و پاسخ API را برمیگرداند.
در اینجا یک پیادهسازی از CustomExceptionHandler آمده است:
internal sealed class CustomExceptionHandler : IExceptionHandler
{
public async ValueTask<bool> TryHandleAsync(
HttpContext httpContext,
Exception exception,
CancellationToken cancellationToken)
{
int status = exception switch
{
ArgumentException => StatusCodes.Status400BadRequest,
_ => StatusCodes.Status500InternalServerError
};
httpContext.Response.StatusCode = status;
var problemDetails = new ProblemDetails { /* ... */ };
await httpContext.Response.WriteAsJsonAsync(problemDetails, cancellationToken);
return true;
}
}
// In Program.cs
builder.Services.AddExceptionHandler<CustomExceptionHandler>();
استفاده از ProblemDetailsService 🛠
فراخوانی AddProblemDetails یک پیادهسازی پیشفرض از IProblemDetailsService را ثبت میکند. این سرویس میتواند پاسخ را برای ما بنویسد.
در اینجا نحوه استفاده از آن در CustomExceptionHandler آمده است:
public class CustomExceptionHandler(IProblemDetailsService problemDetailsService) : IExceptionHandler
{
public async ValueTask<bool> TryHandleAsync(
HttpContext httpContext,
Exception exception,
CancellationToken cancellationToken)
{
var problemDetails = new ProblemDetails
{
Status = exception switch { /* ... */ },
// ...
};
return await problemDetailsService.TryWriteAsync(new ProblemDetailsContext
{
Exception = exception,
HttpContext = httpContext,
ProblemDetails = problemDetails
});
}
}
سفارشیسازی Problem Details 🎨
ما میتوانیم یک delegate به متد AddProblemDetails پاس دهیم تا CustomizeProblemDetails را تنظیم کنیم. شما میتوانید از این برای افزودن اطلاعات اضافی به تمام پاسخهای Problem Details استفاده کنید.
builder.Services.AddProblemDetails(options =>
{
options.CustomizeProblemDetails = context =>
{
context.ProblemDetails.Instance =
$"{context.HttpContext.Request.Method} {context.HttpContext.Request.Path}";
context.ProblemDetails.Extensions.TryAdd("requestId", context.HttpContext.TraceIdentifier);
Activity? activity = context.HttpContext.Features.Get<IHttpActivityFeature>()?.Activity;
context.ProblemDetails.Extensions.TryAdd("traceId", activity?.Id);
};
});
این سفارشیسازی مسیر درخواست، یک requestId و یک traceId را به هر پاسخ Problem Details اضافه میکند که قابلیت دیباگ و ردیابی خطاها را افزایش میدهد.
مدیریت استثناهای خاص (کدهای وضعیت) 🆕
حالا 9 Net. یک راه سادهتر برای مپ کردن استثناها به کدهای وضعیت معرفی میکند. شما میتوانید از StatusCodeSelector برای تعریف این مپینگها استفاده کنید.
app.UseExceptionHandler(new ExceptionHandlerOptions
{
StatusCodeSelector = ex => ex switch
{
ArgumentException => StatusCodes.Status400BadRequest,
NotFoundException => StatusCodes.Status404NotFound,
_ => StatusCodes.Status500InternalServerError
}
});
نکات پایانی ✅
پیادهسازی Problem Details در APIهای ASP.NET Core شما بیش از یک رویه بهتر است - این یک استاندارد برای بهبود تجربه توسعهدهنده مصرفکنندگان API شماست. با ارائه پاسخهای خطای یکپارچه، دقیق و با ساختار مناسب، شما درک و مدیریت سناریوهای خطا را برای کلاینتها آسانتر میکنید.
🔖 هشتگها:
#CSharp #DotNet #ASPNETCore #ErrorHandling #ExceptionHandling #WebAPI #ProblemDetails
📖 سری آموزشی کتاب C# 12 in a Nutshell
تو پست قبلی با سینتکس ساده و جذاب Primary Constructors آشنا شدیم. اما قدرت واقعی این قابلیت، در تکنیکهای پیشرفتهتریه که به شما اجازه میده کدهای خیلی تمیزتر و هوشمندتری بنویسید.
امروز میخوایم با این ترفندها و البته محدودیتهاشون آشنا بشیم.
1️⃣ مقداردهی مستقیم فیلدها و پراپرتیها
شما میتونید پارامترهای سازنده اصلی رو مستقیماً برای مقداردهی اولیه فیلدها و پراپرتیهای عمومی استفاده کنید. این کار نیاز به نوشتن کد انتساب تکراری در یک سازنده سنتی رو حذف میکنه.
2️⃣ ماسک کردن (Masking) و تغییرناپذیری
یه تکنیک جالب، تعریف یه فیلد readonly با همون اسم پارامتره. در این حالت، فیلد، پارامتر اصلی رو "ماسک" میکنه. این کار یه راه ساده برای اطمینان از اینه که مقدار اولیه، بعد از ساخت آبجکت دیگه تغییر نمیکنه و به صورت داخلی readonly باقی میمونه.
3️⃣ اعتبارسنجی (Validation) در لحظه تولد! 🛡
قدرت اصلی اینجاست! شما میتونید منطق اعتبارسنجی رو مستقیماً در مقداردهی اولیه فیلدها پیاده کنید. با استفاده از "throw expressions"، میتونیم کد خیلی تمیزی برای چک کردن null یا مقادیر نامعتبر بنویسیم.
4️⃣ محدودیت مهم: پراپرتیهای خواندنی/نوشتنی ⚠️
با تمام این قدرت، Primary Constructors یه محدودیت مهم دارن. اگه بخواید یه پراپرتی خواندنی/نوشتنی ({ get; set; }) با منطق اعتبارسنجی داشته باشید، کار پیچیده میشه. چون باید اون منطق رو هم در مقداردهی اولیه و هم در اکسسور set تکرار کنید.
در این سناریو، برگشتن به روش سازنده سنتی و صریح معمولاً راه حل تمیزتریه.
Primary Constructors
یه ابزار عالی برای سناریوهای ساده و کلاسهای تغییرناپذیره، ولی مثل هر ابزار دیگهای، باید بدونیم کجا ازش استفاده کنیم و محدودیتهاش رو بشناسیم.
🚀 تکنیکهای حرفهای با Primary Constructors در 12 #C
تو پست قبلی با سینتکس ساده و جذاب Primary Constructors آشنا شدیم. اما قدرت واقعی این قابلیت، در تکنیکهای پیشرفتهتریه که به شما اجازه میده کدهای خیلی تمیزتر و هوشمندتری بنویسید.
امروز میخوایم با این ترفندها و البته محدودیتهاشون آشنا بشیم.
1️⃣ مقداردهی مستقیم فیلدها و پراپرتیها
شما میتونید پارامترهای سازنده اصلی رو مستقیماً برای مقداردهی اولیه فیلدها و پراپرتیهای عمومی استفاده کنید. این کار نیاز به نوشتن کد انتساب تکراری در یک سازنده سنتی رو حذف میکنه.
class Person(string firstName, string lastName)
{
// پارامتر firstName مستقیماً به فیلد عمومی FirstName اختصاص داده میشه
public readonly string FirstName = firstName;
// پارامتر lastName مستقیماً به پراپرتی عمومی LastName اختصاص داده میشه
public string LastName { get; } = lastName;
}
2️⃣ ماسک کردن (Masking) و تغییرناپذیری
یه تکنیک جالب، تعریف یه فیلد readonly با همون اسم پارامتره. در این حالت، فیلد، پارامتر اصلی رو "ماسک" میکنه. این کار یه راه ساده برای اطمینان از اینه که مقدار اولیه، بعد از ساخت آبجکت دیگه تغییر نمیکنه و به صورت داخلی readonly باقی میمونه.
class Person(string firstName, string lastName)
{
// این فیلدها، پارامترهای ورودی رو ماسک میکنن
readonly string firstName = firstName;
readonly string lastName = lastName.ToUpper(); // حتی میتونیم روشون عملیات هم انجام بدیم!
public void Print() => Console.WriteLine(firstName + " " + lastName);
}
3️⃣ اعتبارسنجی (Validation) در لحظه تولد! 🛡
قدرت اصلی اینجاست! شما میتونید منطق اعتبارسنجی رو مستقیماً در مقداردهی اولیه فیلدها پیاده کنید. با استفاده از "throw expressions"، میتونیم کد خیلی تمیزی برای چک کردن null یا مقادیر نامعتبر بنویسیم.
class Person(string firstName, string lastName)
{
// اگه lastName نال باشه، همینجا یه Exception پرتاب میشه
readonly string lastName = lastName ?? throw new ArgumentNullException(nameof(lastName));
public void Print() => Console.WriteLine(firstName + " " + lastName);
}
// new Person("Alice", null); // throws ArgumentNullException
4️⃣ محدودیت مهم: پراپرتیهای خواندنی/نوشتنی ⚠️
با تمام این قدرت، Primary Constructors یه محدودیت مهم دارن. اگه بخواید یه پراپرتی خواندنی/نوشتنی ({ get; set; }) با منطق اعتبارسنجی داشته باشید، کار پیچیده میشه. چون باید اون منطق رو هم در مقداردهی اولیه و هم در اکسسور set تکرار کنید.
در این سناریو، برگشتن به روش سازنده سنتی و صریح معمولاً راه حل تمیزتریه.
🤔 حرف حساب و تجربه شما
Primary Constructors
یه ابزار عالی برای سناریوهای ساده و کلاسهای تغییرناپذیره، ولی مثل هر ابزار دیگهای، باید بدونیم کجا ازش استفاده کنیم و محدودیتهاش رو بشناسیم.
🔖 هشتگها:
#CSharp #DotNet #CSharp12 #Programming #Developer #OOP
لاگینگ در NET. - بهترین شیوهها 📝
لاگها باید به شما در رفع باگها کمک کنند و توضیح دهند که اپلیکیشن در حال حاضر چه کاری انجام میدهد. آنها نباید شما را در متن غرق کنند یا آن یک خطی را که نیاز دارید، پنهان کنند. این راهنما نشان میدهد که چگونه لاگینگ را در NET. مدرن راهاندازی کنید، پیامهای ساختاریافته (structured) بنویسید، سطح مناسب را انتخاب کنید و request correlation را اضافه کنید. همچنین، لاگها را با متریکها اشتباه نگیرید، اشتباه است که برای بررسی تعداد فراخوانیهای API در هر بازه زمانی X، لاگ اضافه کنید.
شروع سریع (Minimal API) 🚀
// Program.cs
var builder = WebApplication.CreateBuilder(args);
// لاگینگ در کنسول به صورت پیشفرض فعال است؛ میتوانید آن را از طریق appsettings.json تغییر دهید
var app = builder.Build();
app.MapGet("/", (ILogger<Program> log) =>
{
log.LogInformation("Health probe hit at {UtcNow}", DateTime.UtcNow);
return "OK";
});
app.Run();
این کد از قبل در کنسول با timestamp، سطح لاگ، دستهبندی و پیام مینویسد. پیکربندی
در appsettings.json قرار دارد.
سطوح لاگ که منطقی هستند
🔬 Trace :
بسیار پرحرف، جریان گامبهگام. در پروداکشن خاموش است.
🐞 Debug :
در طول توسعه مفید است؛ بعداً میتوان آن را با خیال راحت غیرفعال کرد.
ℹ️ Information :
رویدادهای سطح بالا: شروع برنامه، تکمیل درخواست، رویدادهای بیزینسی.
⚠️ Warning :
شرایط غیرعادی: تلاشهای مجدد (retries)، تایماوتهایی که بازیابی میشوند.
❌ Error :
شکستهایی که شما catch و مدیریت میکنید (شامل exception).
🔥 Critical :
اپلیکیشن سالم نیست.
پایینترین سطحی را انتخاب کنید که هنوز فوریت مناسب را منتقل میکند. اگر همه چیز Information باشد، هیچ چیز Information نیست.
لاگینگ ساختاریافته بهتر از الحاق رشته است
قالبهای پیام (Message templates)، دادهها را برای کوئری زدن در آینده در فیلدها نگه میدارند. در اینجا از درونیابی رشته (string interpolation) خودداری کنید.
// ✅ خوب
log.LogInformation("User {UserId} logged in from {Ip}", userId, ip);
// ❌ بد (کوئری زدن سخت است)
log.LogInformation($"User {userId} logged in from {ip}");
با قالبها، ابزارها میتوانند بر روی UserId یا Ip بدون تجزیه متن، فیلتر کنند.
استثناها را به عنوان اولین آرگومان وارد کنید:
try
{
await service.ProcessAsync(orderId);
}
catch (Exception ex)
{
log.LogError(ex, "Failed to process order {OrderId}", orderId);
}
پیکربندی لاگینگ از طریق appsettings.json ⚙️
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"Microsoft.EntityFrameworkCore.Database.Command": "Warning"
},
"Console": {
"FormatterName": "json",
"FormatterOptions": {
"IncludeScopes": true,
"UseUtcTimestamp": true
}
}
}
}دستهبندیهای پرسروصدا (مانند جزئیات داخلی فریمورک) را به Warning کاهش دهید.
فرمتدهنده JSON با جمعآوریکنندههای لاگ به خوبی کار میکند و فیلدها را ساختاریافته نگه میدارد.
دستهبندیها و DI🗃
دستهبندی لاگر، نوعی است که شما درخواست میکنید. این به فیلتر کردن و گروهبندی لاگها کمک میکند.
public sealed class BillingService(ILogger<BillingService> log)
{
public void Charge(Guid orderId, decimal amount)
=> log.LogInformation("Charging {OrderId} {Amount}", orderId, amount);
}
Scopeها و Correlation IDها 🔗
اسکوپ ها پراپرتیهای اضافی را به هر خط لاگ داخل یک بلوک متصل میکنند. یک Correlation ID برای هر درخواست اضافه کنید تا بتوانید یک مسیر را در سراسر ماژولها ردیابی کنید.
app.Use(async (ctx, next) =>
{
var cid = ... // Get Correlation ID from header or create a new one
using (loggerFactory.CreateLogger("Correlation").BeginScope(new Dictionary<string, object?>
{
["CorrelationId"] = cid
}))
{
// ...
await next();
}
});
حالا هر خط لاگ در آن درخواست شامل CorrelationId است.
Middleware لاگینگ درخواست (داخلی) 🌐
using Microsoft.AspNetCore.HttpLogging;
builder.Services.AddHttpLogging(o =>
{
o.LoggingFields = HttpLoggingFields.RequestMethod
| HttpLoggingFields.RequestPath
| HttpLoggingFields.ResponseStatusCode
| HttpLoggingFields.Duration;
});
var app = builder.Build();
app.UseHttpLogging();
این متد، مسیر، وضعیت و مدت زمان را ثبت میکند. از لاگ کردن bodyها خودداری کنید مگر اینکه دلیل محکمی داشته باشید.
مسیرهای پرترافیک: LoggerMessage.Define ⚡️
با پیشکامپایل کردن قالبهای پیام، از تخصیص حافظه برای فرمتدهی رشته جلوگیری کنید. سورس جنریتور کد لاگینگ بهینهای ایجاد میکند.
static class Logs
{
private static readonly Action<ILogger, string, Exception?> _cacheMiss =
LoggerMessage.Define<string>(LogLevel.Debug, new EventId(1001, "CacheMiss"),
"Cache miss for key {Key}");
public static void CacheMiss(this ILogger log, string key) => _cacheMiss(log, key, null);
}
// استفاده
log.CacheMiss(key);
لاگهای فایلی (اختیاری، با Serilog) 📂
ارائهدهندگان داخلی در فایل نمینویسند. اگر میخواهید فایلهای چرخشی (rolling files) به صورت محلی داشته باشید، Serilog را اضافه کنید:
dotnet add package Serilog.AspNetCore
dotnet add package Serilog.Sinks.File
در برنامه:
using Serilog;
var logger = new LoggerConfiguration()
.WriteTo.Console()
.WriteTo.File("logs/app-.log", rollingInterval: RollingInterval.Day)
.CreateLogger();
builder.Host.UseSerilog(logger);
✅️چه چیزی را لاگ کنیم (و چه چیزی را نه)❌️
👍 لاگ کنید: رویدادهای شروع/پایان، فراخوانیهای خارجی (هدف + مدت زمان)، رویدادهای بیزینسی، هشدارها با زمینه، خطاهای مدیریت شده با stack traces.
👎 لاگ نکنید: اسرار (secrets)، body کامل درخواست/پاسخ با اطلاعات شخصی، حلقههای پرحرف، اسپم heartbeat.
اشتباهات رایج 🤦♂️
استفاده از درونیابی رشته در پیامهای لاگ. از قالبها با placeholderهای نامدار استفاده کنید.
• لاگ کردن استثناها بدون پاس دادن خود آبجکت exception.
• نداشتن ارتباط (correlation) بین لاگهای یک درخواست.
• تبدیل همه چیز به Information یا Debug و هرگز کوتاه نکردن آن.
• نوشتن تصادفی اسرار در لاگها (توکنها، پسوردها).
🔖 هشتگها:
#CSharp #DotNet #ASPNETCore #Logging #StructuredLogging #Observability #Serilog #Debugging
📖 سری آموزشی کتاب C# 12 in a Nutshell
ما معمولاً نگران از بین بردن آبجکتها در #C نیستیم، چون Garbage Collector (GC) این کار رو به صورت خودکار برامون انجام میده. اما گاهی وقتا یه آبجکت، منابعی خارج از کنترل داتنت (unmanaged resources) مثل دستگیرههای فایل یا اتصالات شبکه رو مدیریت میکنه.
اینجا پای فاینالایزر (Finalizer) به میدون باز میشه.
فاینالایزر، یه متد خاص در کلاسه که درست قبل از اینکه Garbage Collector حافظهی اون آبجکت رو پاک کنه، به صورت خودکار صدا زده میشه. این آخرین شانس آبجکته که منابع مدیریتنشدهی خودش رو آزاد کنه.
سینتکس فاینالایزر، اسم کلاسه که قبلش یه علامت مد (~) اومده:
💡نکته فنی: این سینتکس در واقع یه میانبر شیک در #C برای override کردن متد Finalize از کلاس Object هست.
در ۹۹.۹٪ مواقع، شما به عنوان یک توسعهدهنده #C نباید مستقیماً فاینالایزر بنویسید. دلیلش اینه:
• ضربه به پرفورمنس: آبجکتهایی که فاینالایزر دارن، فرآیند پاکسازی حافظه توسط GC رو پیچیده و کند میکنن.
• غیرقطعی بودن: شما هیچ کنترلی روی اینکه فاینالایزر دقیقاً چه زمانی اجرا میشه، ندارید. ممکنه خیلی دیرتر از چیزی که انتظار دارید، اجرا بشه.
راه حل صحیح و مدرن برای مدیریت منابع، پیادهسازی اینترفیس IDisposable و استفاده از دستور using هست که قبلاً در موردش صحبت کردیم.
تنها کاربرد منطقی فاینالایزر، به عنوان یه مکانیسم پشتیبان یا Safety Net هست.
یعنی شما کلاس خودتون رو IDisposable میکنید و انتظار دارید که کاربر همیشه متد Dispose() رو (معمولاً با using) صدا بزنه. اما برای محکمکاری، یه فاینالایزر هم مینویسید که اگه کاربر یادش رفت Dispose() رو صدا بزنه، فاینالایزر به عنوان آخرین امید، اون منابع رو آزاد کنه تا از نشت منابع (resource leaks) جلوگیری بشه.
🤔 حرف حساب و تجربه شما
فاینالایزرها مثل دکمه Eject صندلی خلبان هستن؛ امیدوارید هیچوقت لازم نشه ازش استفاده کنید، ولی خوبه که بدونید وجود داره.
👻 مبحث پیشرفته #C: فاینالایزرها (~Finalizers) و آخرین کلمات یک آبجکت
ما معمولاً نگران از بین بردن آبجکتها در #C نیستیم، چون Garbage Collector (GC) این کار رو به صورت خودکار برامون انجام میده. اما گاهی وقتا یه آبجکت، منابعی خارج از کنترل داتنت (unmanaged resources) مثل دستگیرههای فایل یا اتصالات شبکه رو مدیریت میکنه.
اینجا پای فاینالایزر (Finalizer) به میدون باز میشه.
1️⃣ فاینالایزر چیست؟
فاینالایزر، یه متد خاص در کلاسه که درست قبل از اینکه Garbage Collector حافظهی اون آبجکت رو پاک کنه، به صورت خودکار صدا زده میشه. این آخرین شانس آبجکته که منابع مدیریتنشدهی خودش رو آزاد کنه.
سینتکس فاینالایزر، اسم کلاسه که قبلش یه علامت مد (~) اومده:
class MyClass
{
~MyClass()
{
// کد پاکسازی منابع در اینجا قرار میگیرد
Console.WriteLine("Finalizing object!");
}
}
💡نکته فنی: این سینتکس در واقع یه میانبر شیک در #C برای override کردن متد Finalize از کلاس Object هست.
2️⃣ چرا تقریباً هیچوقت نباید فاینالایزر بنویسید؟ (مهم!) ⚠️
در ۹۹.۹٪ مواقع، شما به عنوان یک توسعهدهنده #C نباید مستقیماً فاینالایزر بنویسید. دلیلش اینه:
• ضربه به پرفورمنس: آبجکتهایی که فاینالایزر دارن، فرآیند پاکسازی حافظه توسط GC رو پیچیده و کند میکنن.
• غیرقطعی بودن: شما هیچ کنترلی روی اینکه فاینالایزر دقیقاً چه زمانی اجرا میشه، ندارید. ممکنه خیلی دیرتر از چیزی که انتظار دارید، اجرا بشه.
راه حل صحیح و مدرن برای مدیریت منابع، پیادهسازی اینترفیس IDisposable و استفاده از دستور using هست که قبلاً در موردش صحبت کردیم.
3️⃣ پس کی به درد میخوره؟ (تنها کاربرد منطقی) 💡
تنها کاربرد منطقی فاینالایزر، به عنوان یه مکانیسم پشتیبان یا Safety Net هست.
یعنی شما کلاس خودتون رو IDisposable میکنید و انتظار دارید که کاربر همیشه متد Dispose() رو (معمولاً با using) صدا بزنه. اما برای محکمکاری، یه فاینالایزر هم مینویسید که اگه کاربر یادش رفت Dispose() رو صدا بزنه، فاینالایزر به عنوان آخرین امید، اون منابع رو آزاد کنه تا از نشت منابع (resource leaks) جلوگیری بشه.
🤔 حرف حساب و تجربه شما
فاینالایزرها مثل دکمه Eject صندلی خلبان هستن؛ امیدوارید هیچوقت لازم نشه ازش استفاده کنید، ولی خوبه که بدونید وجود داره.
🔖 هشتگها:
#CSharp #DotNet #MemoryManagement #GarbageCollector