📖 سری آموزشی کتاب 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
ساخت یک توزیعکننده (Dispatcher) سفارشی Domain Events در NET. 🧱
Domain events
یک راه قدرتمند برای جداسازی (decouple) بخشهای مختلف سیستم شما هستند. به جای اتصال سفت و سخت منطق خود، میتوانید رویدادها را منتشر کرده و بخشهای دیگر کدتان را به آن رویدادها مشترک (subscribe) کنید. این الگو به ویژه در طراحی دامنه محور (Domain-Driven Design - DDD) ارزشمند است، جایی که منطق بیزینس باید متمرکز و منسجم باقی بماند.
در این مقاله، ما نحوه پیادهسازی یک توزیعکننده رویداد دامنه سبک و سفارشی را در NET. بررسی خواهیم کرد. منطق اصلی توزیع نباید به کتابخانههای شخص ثالث وابسته باشد.
📌ما پوشش خواهیم داد:
🔹 چرا ممکن است بخواهید از publish-subscribe در اپلیکیشن خود استفاده کنید.
🔹 نحوه تعریف انتزاعهای پایه رویداد دامنه.
🔹 نحوه پیادهسازی و ثبت handlerها.
🔹 نحوه ساخت یک توزیعکننده رویدادهای دامنه.
🔹 مزایا و معایب و زمان در نظر گرفتن گزینههای دیگر.
چرا Domain Events اهمیت دارند؟ 🤔
قبل از شیرجه زدن در پیادهسازی، بیایید مشکلی را که رویدادهای دامنه حل میکنند، درک کنیم. این کد با اتصال سفت و سخت را در نظر بگیرید:
public class UserService
{
public async Task RegisterUser(string email, string password)
{
var user = new User(email, password);
await _userRepository.SaveAsync(user);
// 🔗 مستقیماً به سرویس ایمیل متصل است
await _emailService.SendWelcomeEmail(user.Email);
// 🔗 مستقیماً به سرویس آنالیتیکس متصل است
await _analyticsService.TrackUserRegistration(user.Id);
// اگر بخواهیم ویژگیهای بیشتری اضافه کنیم چه؟
// این متد به رشد خود ادامه خواهد داد...
}
}
با رویدادهای دامنه، میتوانیم این را جدا کنیم: ✨
public class UserService
{
public async Task RegisterUser(string email, string password)
{
var user = new User(email, password);
await _userRepository.SaveAsync(user);
// رویداد را منتشر کن - بگذار بخشهای دیگر سیستم واکنش نشان دهند
await _domainEventsDispatcher.DispatchAsync(
[new UserRegisteredDomainEvent(user.Id, user.Email)]);
}
}
اکنون UserService فقط بر روی ثبت نام کاربر تمرکز دارد، در حالی که دغدغههای دیگر از طریق event handlerها مدیریت میشوند.
انتزاعهای پایه 🧬
بیایید با تعریف دو اینترفیس ساده که پایه و اساس سیستم رویداد ما را تشکیل میدهند، شروع کنیم:
// اینترفیس نشانگر برای تمام رویدادهای دامنه.
public interface IDomainEvent { }
// اینترفیس جنریک برای مدیریت رویدادهای دامنه.
public interface IDomainEventHandler<in T> where T : IDomainEvent
{
Task Handle(T domainEvent, CancellationToken cancellationToken = default);
}
پیادهسازی Handlerهای نمونه 📧📊
بیایید چند handler نمونه اضافه کنیم که نشان میدهند چگونه بخشهای مختلف سیستم شما میتوانند به یک رویداد یکسان واکنش نشان دهند:
// ارسال ایمیل خوشآمدگویی هنگام ثبت نام کاربر
internal sealed class SendWelcomeEmailHandler(...)
: IDomainEventHandler<UserRegisteredDomainEvent>
{
// ...
}
// ردیابی ثبت نام کاربر جدید در آنالیتیکس
internal sealed class TrackUserRegistrationHandler(...)
: IDomainEventHandler<UserRegisteredDomainEvent>
{
// ...
}
برای اینکه این کار کند، باید handlerهای خود را در کانتینر DI ثبت کنیم. میتوانید این ثبت را با اسکن اسمبلی با Scrutor خودکار کنید. نکته مهم این است که چندین handler میتوانند به یک رویداد یکسان واکنش نشان دهند.
توزیعکننده (Dispatcher) (Strongly Typed) 🧠
حالا به چیزی نیاز داریم که فراخوانی handlerها را هماهنگ کند.
public interface IDomainEventsDispatcher
{
Task DispatchAsync(IEnumerable<IDomainEvent> domainEvents, CancellationToken cancellationToken = default);
}
internal sealed class DomainEventsDispatcher(IServiceProvider serviceProvider)
: IDomainEventsDispatcher
{
// ... (پیادهسازی پیچیده با Wrapper برای جلوگیری از reflection در زمان اجرا)
}
این توزیعکننده از یک wrapper برای حذف reflection در حین اجرای handler استفاده میکند در حالی که ایمنی نوع را حفظ میکند. این به ما مزایای عملکردی اجتناب از reflection در مسیر اصلی (اجرای handler) را میدهد. فراموش نکنید که توزیعکننده را با DI ثبت کنید.
مثال استفاده 🎯
در اینجا نحوه استفاده از توزیعکننده رویدادهای دامنه در اپلیکیشن شما آمده است:
public class UserController(...) : ControllerBase
{
[HttpPost("register")]
public async Task<IActionResult> Register([FromBody] RegisterUserRequest request)
{
// کاربر را ایجاد کن
var user = await userService.CreateUserAsync(request.Email, request.Password);
// رویداد دامنه را منتشر کن
var userRegisteredEvent = new UserRegisteredDomainEvent(user.Id, user.Email);
await domainEventsDispatcher.DispatchAsync([userRegisteredEvent]);
return Ok(...);
}
}
محدودیتها و مزایا و معایب ⚠️
این پیادهسازی کاملاً درون-فرآیندی (in-process) اجرا میشود، که پیامدهای مهمی دارد:
• بازخورد فوری: اگر هر handler شکست بخورد، استثنا بلافاصله به فراخواننده برمیگردد.
• کنترل فراخواننده: کدی که رویدادها را توزیع میکند، تصمیم میگیرد چگونه با شکستها برخورد کند.
• نگرانیهای قابلیت اطمینان: اگر فرآیند پس از موفقیت برخی handlerها اما قبل از تکمیل دیگران کرش کند، بازیابی خودکار وجود ندارد.
برای عوارض جانبی حیاتی که نباید از دست بروند، الگوی Outbox 📬 را در نظر بگیرید.
جمعبندی 📝
Domain events
یک الگوی قدرتمند برای جداسازی منطق بیزینس هستند و شما برای استفاده مؤثر از آنها به یک فریمورک سنگین نیاز ندارید. پیادهسازیای که ما در اینجا ساختیم، یک پایه محکم فراهم میکند.
زیبایی ساختن راهحل خودتان این است که شما هر قطعه را درک میکنید، که دیباگ و سفارشیسازی را ساده میکند. این الگو به طور عالی در سیستمهای Domain-Driven Design و Clean Architecture که در آنها جداسازی منطق بیزینس حیاتی است، جای میگیرد.
بینش کلیدی، درک مزایا و معایب خود از پیش است، نه کشف آنها در پروداکشن. ساده شروع کنید، آنچه مهم است را اندازهگیری کنید و بر اساس نیازمندیهای واقعی تکامل پیدا کنید.
🔖 هشتگها:
#CSharp #SoftwareArchitecture #DomainDrivenDesign #DDD #DomainEvents #CleanArchitecture #CQRS
📖 سری آموزشی کتاب C# 12 in a Nutshell
تا حالا شده یه فایل کد اونقدر بزرگ بشه که پیدا کردن یه متد توش عذابآور باشه؟ یا بخواید کدی که خودتون نوشتید رو از کدی که به صورت خودکار توسط یه ابزار (مثل دیزاینر) تولید شده، جدا کنید؟
سیشارپ برای این کار یه راه حل عالی و تمیز داره: کلمه کلیدی partial.
یک کلاس در چند فایل کلمه کلیدی partial به شما اجازه میده تعریف یک کلاس، struct یا اینترفیس رو در چند فایل مختلف تقسیم کنید. کامپایلر موقع کامپایل، تمام این تیکهها رو به هم میچسبونه و به عنوان یک کلاس واحد در نظر میگیره.
مهمترین کاربرد: جداسازی کدهای دستنویس شما از کدهای اتوماتیک.
فایل ۱ (اتوماتیک): PaymentForm.g.cs
فایل ۲ (دستنویس): PaymentForm.cs
این قابلیت به شما اجازه میده در یک بخش از کلاس (معمولاً کد اتوماتیک)، یک "قلاب" یا تعریف متد بدون بدنه بذارید. بعداً در بخش دیگه کلاس (کد دستنویس)، میتونید اون متد رو پیادهسازی کنید.
💡نکته جادویی: اگه شما هیچ پیادهسازیای براش ارائه ندید، کامپایلر هم تعریف و هم تمام فراخوانیهای اون متد رو از کد نهایی حذف میکنه! این یعنی هیچ هزینه پرفورمنسی نداره.
فایل ۱ (اتوماتیک):
فایل ۲ (دستنویس):
این نسخه جدیدتر، برای کار با Source Generatorها طراحی شده. در این حالت، شما تعریف متد رو مینویسید و انتظار دارید که Source Generator بدنه اون رو تولید کنه. این متدها دیگه اختیاری نیستن و باید پیادهسازی بشن و میتونن خروجی و پارامتر out هم داشته باشن.
🏗 تقسیم کدهای بزرگ با partial در #C: کلاسها و متدهای چندتکه
تا حالا شده یه فایل کد اونقدر بزرگ بشه که پیدا کردن یه متد توش عذابآور باشه؟ یا بخواید کدی که خودتون نوشتید رو از کدی که به صورت خودکار توسط یه ابزار (مثل دیزاینر) تولید شده، جدا کنید؟
سیشارپ برای این کار یه راه حل عالی و تمیز داره: کلمه کلیدی partial.
1️⃣ کلاسهای Partial:
یک کلاس در چند فایل کلمه کلیدی partial به شما اجازه میده تعریف یک کلاس، struct یا اینترفیس رو در چند فایل مختلف تقسیم کنید. کامپایلر موقع کامپایل، تمام این تیکهها رو به هم میچسبونه و به عنوان یک کلاس واحد در نظر میگیره.
مهمترین کاربرد: جداسازی کدهای دستنویس شما از کدهای اتوماتیک.
فایل ۱ (اتوماتیک): PaymentForm.g.cs
// این بخش توسط یک ابزار ساخته شده
public partial class PaymentForm
{
// ... کنترلهای دیزاینر و کدهای اتوماتیک
}
فایل ۲ (دستنویس): PaymentForm.cs
// این بخش رو شما مینویسید
public partial class PaymentForm
{
// ... منطق و ایونتهای مربوط به فرم
}
2️⃣ متدهای Partial: قلابهای جادویی که ناپدید میشن! 🎩
این قابلیت به شما اجازه میده در یک بخش از کلاس (معمولاً کد اتوماتیک)، یک "قلاب" یا تعریف متد بدون بدنه بذارید. بعداً در بخش دیگه کلاس (کد دستنویس)، میتونید اون متد رو پیادهسازی کنید.
💡نکته جادویی: اگه شما هیچ پیادهسازیای براش ارائه ندید، کامپایلر هم تعریف و هم تمام فراخوانیهای اون متد رو از کد نهایی حذف میکنه! این یعنی هیچ هزینه پرفورمنسی نداره.
فایل ۱ (اتوماتیک):
public partial class PaymentForm
{
public void Submit()
{
// یه قلاب برای اعتبارسنجی قبل از ارسال
ValidatePayment(this.Amount);
// ...
}
// تعریف متد partial (بدون بدنه)
partial void ValidatePayment(decimal amount);
}
فایل ۲ (دستنویس):
public partial class PaymentForm
{
// پیادهسازی متد partial
partial void ValidatePayment(decimal amount)
{
if (amount > 1000)
{
// ... منطق اعتبارسنجی ...
}
}
}
3️⃣ متدهای Partial توسعهیافته (از 9 #C) 🚀
این نسخه جدیدتر، برای کار با Source Generatorها طراحی شده. در این حالت، شما تعریف متد رو مینویسید و انتظار دارید که Source Generator بدنه اون رو تولید کنه. این متدها دیگه اختیاری نیستن و باید پیادهسازی بشن و میتونن خروجی و پارامتر out هم داشته باشن.
🔖 هشتگها:
#CSharp #Programming #DotNet #CleanCode #Partial
5 شیوه برتر (Best Practice) برای لاگینگ ساختاریافته بهتر با Serilog 📝
سریلاگ یک کتابخانه لاگینگ ساختاریافته (structured logging) برای NET. است.Serilog از مقصدهای لاگینگ زیادی به نام Sinks پشتیبانی میکند. مقصدهای لاگ از سینکهای کنسول و فایل گرفته تا سرویسهای مدیریت شده لاگینگ مانند Application Insights را شامل میشود.
امروز، میخواهم ۵ نکته عملی برای لاگینگ ساختاریافته بهتر با Serilog را به اشتراک بگذارم.
1️⃣ از سیستم پیکربندی (Configuration System) استفاده کنید ⚙️
شما میتوانید Serilog را در ASP.NET Core به دو روش پیکربندی کنید:
سیستم پیکربندی _ Fluent API
Fluent API
به شما امکان میدهد با کد، Serilog را به راحتی پیکربندی کنید. عیب آن این است که شما پیکربندی خود را هاردکد میکنید. هرگونه تغییر در پیکربندی نیازمند دیپلوی یک نسخه جدید است.
من ترجیح میدهم از سیستم پیکربندی ASP.NET برای راهاندازی Serilog استفاده کنم. مزیت آن این است که میتوانید پیکربندی لاگینگ را بدون دیپلوی مجدد اپلیکیشن خود تغییر دهید.
شما باید کتابخانه Serilog.Settings.Configuration را نصب کنید.
این به شما امکان میدهد Serilog را با استفاده از سیستم پیکربندی، کانفیگ کنید:
builder.Host.UseSerilog((context, loggerConfig) =>
loggerConfig.ReadFrom.Configuration(context.Configuration));
در اینجا یک پیکربندی Serilog با سینکهای Console و Seq آمده است. ما همچنین چند Serilog enricher را برای غنیسازی لاگهای اپلیکیشن با اطلاعات اضافی پیکربندی میکنیم.
{
"Serilog": {
"Using": ["Serilog.Sinks.Console", "Serilog.Sinks.Seq"],
"MinimumLevel": {
"Default": "Information",
"Override": { "Microsoft": "Information" }
},
"WriteTo": [
{ "Name": "Console" },
{
"Name": "Seq",
"Args": { "serverUrl": "http://localhost:5341" }
}
],
"Enrich": ["FromLogContext", "WithMachineName", "WithThreadId"]
}
}2️⃣ از لاگینگ درخواست Serilog استفاده کنید 🌐
شما میتوانید کتابخانه Serilog.AspNetCore را نصب کنید تا لاگینگ Serilog را برای پایپلاین درخواست ASP.NET Core اضافه کنید. این قابلیت، عملیات داخلی ASP.NET را به همان سینکهای Serilog که رویدادهای اپلیکیشن شما ثبت میشوند، اضافه میکند.
تنها کاری که باید انجام دهید، فراخوانی متد UseSerilogRequestLogging است:
app.UseSerilogRequestLogging();
3️⃣ لاگهای خود را با CorrelationId غنیسازی کنید 🔗
چگونه میتوانید تمام لاگهای مربوط به یک درخواست واحد را ردیابی کنید؟
شما میتوانید یک پراپرتی CorrelationId به لاگهای ساختاریافته خود اضافه کنید.
این همچنین در چندین اپلیکیشن کار میکند. شما باید CorrelationId را با استفاده از یک هدر HTTP پاس دهید. برای مثال، میتوانید از یک هدر سفارشی X-Correlation-Id استفاده کنید.
در RequestContextLoggingMiddleware، من CorrelationId را به LogContext Serilog اضافه میکنم. این کار آن را برای تمام لاگهای ایجاد شده در طول این درخواست اپلیکیشن در دسترس قرار میدهد.
public class RequestContextLoggingMiddleware
{
private const string CorrelationIdHeaderName = "X-Correlation-Id";
// ...
public Task Invoke(HttpContext context)
{
string correlationId = GetCorrelationId(context);
using (LogContext.PushProperty("CorrelationId", correlationId))
{
return _next.Invoke(context);
}
}
// ...
}
4️⃣ رویدادهای مهم اپلیکیشن را لاگ کنید 📌
به طور کلی، من سعی میکنم رویدادهای مهم را در اپلیکیشن خود لاگ کنم. این شامل اطلاعات درخواست فعلی، خطاها، شکستها، مقادیر غیرمنتظره، نقاط انشعاب و غیره است.
اگر از الگوی CQRS با MediatR استفاده میکنید، میتوانید به راحتی لاگینگ را برای تمام درخواستهای اپلیکیشن اضافه کنید.
در RequestLoggingPipelineBehavior من پراپرتی Error را به LogContext پوش میکنم.
internal sealed class RequestLoggingPipelineBehavior<TRequest, TResponse>
: IPipelineBehavior<TRequest, TResponse>
where TRequest : class
where TResponse : Result
{
// ...
public async Task<TResponse> Handle(...)
{
// ...
TResponse result = await next();
if (result.IsSuccess) { /* ... */ }
else
{
using (LogContext.PushProperty("Error", result.Error, true))
{
_logger.LogError(
"Completed request {RequestName} with error", requestName);
}
}
return result;
}
}
5️⃣ از Seq برای توسعه محلی استفاده کنید 🔍
Seq
یک سرور جستجو، تحلیل و هشدار self-hosted است که برای دادههای لاگ ساختاریافته ساخته شده است. استفاده از آن برای توسعه محلی رایگان است. این ابزار قابلیتهای جستجو و فیلترینگ پیشرفتهای را روی دادههای لاگ ساختاریافته ارائه میدهد.
میتوانید یک نمونه Seq را در یک کانتینر Docker بالا بیاورید:
version: '3.4'
services:
seq:
image: datalust/seq:latest
container_name: seq
environment:
- ACCEPT_EULA=Y
ports:
- 5341:5341
- 8081:80
`
خلاصه ✅
لاگهای ساختاریافته از یک ساختار یکسان پیروی میکنند و چون قابل خواندن توسط ماشین هستند، میتوانید آنها را برای اطلاعات خاص جستجو کنید. آنها زمینه و جزئیات بیشتری در مورد خطاهای اپلیکیشن ارائه میدهند و شناسایی و رفع مشکلات را آسانتر میکنند.
شما میتوانید از LogContext قدرتمند Serilog برای غنیسازی لاگهای خود با یک CorrelationId استفاده کنید.
🔖 هشتگها:
#CSharp #DotNet #Logging #StructuredLogging #Observability #Serilog
📖 سری آموزشی کتاب C# 12 in a Nutshell
تا حالا شده اسم یه پراپرتی رو تو یه رشته بنویسید (مثلاً برای Exceptionها یا INotifyPropertyChanged) و بعداً که اسم اون پراپرتی رو عوض میکنید، یادتون بره اون رشته رو هم آپدیت کنید و کل برنامه به باگ بخوره؟
به این رشتهها میگن "رشتههای جادویی" (Magic Strings) و یکی از منابع اصلی باگهای پنهان هستن. #C برای حل این مشکل، یه اپراتور خیلی ساده و قدرتمند داره.
اپراتور nameof اسم هرچیزی (متغیر، متد، کلاس، پراپرتی و...) رو در زمان کامپایل به یه رشته تبدیل میکنه.
مزیت اصلیش چیه؟ چک شدن در زمان کامپایل! ✅
اگه شما اسم اون متغیر یا پراپرتی رو عوض کنید (Refactor)، ویژوال استودیو به صورت خودکار تمام nameofهای مربوط به اون رو هم آپدیت میکنه و دیگه هیچوقت کد شما به خاطر یه رشته قدیمی، به باگ نمیخوره.
2️⃣ نحوه استفاده
برای متغیرهای محلی:
برای اعضای یک تایپ:
🪄 خداحافظی با رشتههای جادویی: قدرت nameof در #C
تا حالا شده اسم یه پراپرتی رو تو یه رشته بنویسید (مثلاً برای Exceptionها یا INotifyPropertyChanged) و بعداً که اسم اون پراپرتی رو عوض میکنید، یادتون بره اون رشته رو هم آپدیت کنید و کل برنامه به باگ بخوره؟
به این رشتهها میگن "رشتههای جادویی" (Magic Strings) و یکی از منابع اصلی باگهای پنهان هستن. #C برای حل این مشکل، یه اپراتور خیلی ساده و قدرتمند داره.
1️⃣ راه حل: اپراتور nameof
اپراتور nameof اسم هرچیزی (متغیر، متد، کلاس، پراپرتی و...) رو در زمان کامپایل به یه رشته تبدیل میکنه.
مزیت اصلیش چیه؟ چک شدن در زمان کامپایل! ✅
اگه شما اسم اون متغیر یا پراپرتی رو عوض کنید (Refactor)، ویژوال استودیو به صورت خودکار تمام nameofهای مربوط به اون رو هم آپدیت میکنه و دیگه هیچوقت کد شما به خاطر یه رشته قدیمی، به باگ نمیخوره.
2️⃣ نحوه استفاده
برای متغیرهای محلی:
int userCount = 123;
string variableName = nameof(userCount); // مقدار: "userCount"
برای اعضای یک تایپ:
// برای اعضای استاتیک و غیراستاتیک کار میکنه
string lengthPropName = nameof(StringBuilder.Length); // مقدار: "Length"
// برای گرفتن اسم کامل
string fullName = nameof(StringBuilder) + "." + nameof(StringBuilder.Length);
// "StringBuilder.Length"
🔖 هشتگها:
#CleanCode #Refactoring
📖 سری آموزشی کتاب C# 12 in a Nutshell
آمادهاید که به قلب برنامهنویسی شیءگرا (OOP) سفر کنیم؟ امروز میخوایم سه تا از مهمترین و به هم پیوستهترین مفاهیم #C رو کالبدشکافی کنیم: وراثت، چندریختی و کستینگ.
تسلط بر این سه مفهوم، پایه و اساس ساختن سیستمهای انعطافپذیر و قدرتمنده.
1️⃣ وراثت (Inheritance): "یک نوع از..." 👨👩👧
وراثت به یک کلاس اجازه میده که ویژگیها و رفتارهای یک کلاس دیگه رو به ارث ببره. به کلاس اصلی میگیم کلاس پایه (Base Class) و به کلاس جدید میگیم کلاس مشتقشده (Derived Class). این یعنی "کلاس مشتقشده، یک نوع از کلاس پایه است".
حالا Stock و House هم پراپرتی Name رو دارن و هم پراپرتیهای مخصوص خودشون.
چندریختی یعنی شما میتونید یه متغیر از نوع کلاس پایه داشته باشید، ولی در زمان اجرا، به اون یک نمونه از هر کدوم از کلاسهای فرزندش رو اختصاص بدید. این قابلیت به شما اجازه میده کدهای خیلی انعطافپذیری بنویسید.
مثال: این متد یک Asset به عنوان ورودی میگیره، ولی شما میتونید هم Stock و هم House رو بهش پاس بدید!
کستینگ یعنی تغییر نوع یک رفرنس برای دسترسی به اعضای مختلف. دو نوع اصلی داریم:
این کار همیشه امنه و به صورت ضمنی (implicit) انجام میشه. شما یک نمونه از کلاس فرزند رو در یک متغیر از نوع پدر قرار میدید. بعد از این کار، شما به آبجکت از یک زاویه دید "محدودتر" نگاه میکنید و فقط به اعضایی که در کلاس پدر تعریف شدن، دسترسی دارید.
این کار میتونه خطرناک باشه و باید به صورت صریح (explicit) با (type) انجام بشه. شما به کامپایلر میگید: "من مطمئنم این آبجکت که از نوع پدره، در واقع یک نمونه از نوع فرزند هست". اگه اشتباه کرده باشید، در زمان اجرا با خطای InvalidCastException مواجه میشید.
درک درست Upcasting و Downcasting، کلید استفاده امن و قدرتمند از چندریختی در کدهای شماست. (در آینده با روشهای امنتر Downcasting مثل as و is آشنا میشیم).
بهترین مثالی که از قدرت چندریختی تو پروژههاتون دیدید چی بوده؟ آیا تا حالا به خطای InvalidCastException به خاطر یه Downcast اشتباه برخورد کردید؟
🏛 ستونهای OOP در #C: وراثت، چندریختی و کستینگ
آمادهاید که به قلب برنامهنویسی شیءگرا (OOP) سفر کنیم؟ امروز میخوایم سه تا از مهمترین و به هم پیوستهترین مفاهیم #C رو کالبدشکافی کنیم: وراثت، چندریختی و کستینگ.
تسلط بر این سه مفهوم، پایه و اساس ساختن سیستمهای انعطافپذیر و قدرتمنده.
1️⃣ وراثت (Inheritance): "یک نوع از..." 👨👩👧
وراثت به یک کلاس اجازه میده که ویژگیها و رفتارهای یک کلاس دیگه رو به ارث ببره. به کلاس اصلی میگیم کلاس پایه (Base Class) و به کلاس جدید میگیم کلاس مشتقشده (Derived Class). این یعنی "کلاس مشتقشده، یک نوع از کلاس پایه است".
// این کلاس پایه ماست
public class Asset
{
public string Name;
}
// این دو کلاس، از Asset ارثبری میکنند
public class Stock : Asset
{
public long SharesOwned;
}
public class House : Asset
{
public decimal Mortgage;
}
حالا Stock و House هم پراپرتی Name رو دارن و هم پراپرتیهای مخصوص خودشون.
🎭 جادوی چندریختی (Polymorphism) در
C# : Upcasting و Downcasting
2️⃣ چندریختی (Polymorphism): یک متغیر، چند چهره
چندریختی یعنی شما میتونید یه متغیر از نوع کلاس پایه داشته باشید، ولی در زمان اجرا، به اون یک نمونه از هر کدوم از کلاسهای فرزندش رو اختصاص بدید. این قابلیت به شما اجازه میده کدهای خیلی انعطافپذیری بنویسید.
مثال: این متد یک Asset به عنوان ورودی میگیره، ولی شما میتونید هم Stock و هم House رو بهش پاس بدید!
// کلاسهای پست قبلی
public class Asset { public string Name; }
public class Stock : Asset { public long SharesOwned; }
public class House : Asset { public decimal Mortgage; }
// این متد به لطف چندریختی، با هر نوع Asset کار میکنه
public static void Display(Asset asset)
{
Console.WriteLine(asset.Name);
}
// --- نحوه استفاده ---
Stock msft = new Stock { Name = "MSFT" };
House mansion = new House { Name = "Mansion" };
Display(msft); // خروجی: MSFT
Display(mansion); // خروجی: Mansion
3️⃣ کستینگ (Casting): تغییر زاویه دید به آبجکت 🔄
کستینگ یعنی تغییر نوع یک رفرنس برای دسترسی به اعضای مختلف. دو نوع اصلی داریم:
Upcasting (تبدیل فرزند به پدر) ⬆️
این کار همیشه امنه و به صورت ضمنی (implicit) انجام میشه. شما یک نمونه از کلاس فرزند رو در یک متغیر از نوع پدر قرار میدید. بعد از این کار، شما به آبجکت از یک زاویه دید "محدودتر" نگاه میکنید و فقط به اعضایی که در کلاس پدر تعریف شدن، دسترسی دارید.
Stock msft = new Stock { Name = "MSFT", SharesOwned = 1000 };
Asset a = msft; // Upcast (ضمنی و همیشه امن)
Console.WriteLine(a.Name); // ✅ درسته، چون Name در Asset هست
// Console.WriteLine(a.SharesOwned); // ❌ خطای زمان کامپایل! a از نوع Asset است و SharesOwned را نمیشناسد.Downcasting (تبدیل پدر به فرزند) ⬇️
این کار میتونه خطرناک باشه و باید به صورت صریح (explicit) با (type) انجام بشه. شما به کامپایلر میگید: "من مطمئنم این آبجکت که از نوع پدره، در واقع یک نمونه از نوع فرزند هست". اگه اشتباه کرده باشید، در زمان اجرا با خطای InvalidCastException مواجه میشید.
Stock msft = new Stock();
Asset a = msft; // Upcast
// Downcast موفقیتآمیز
Stock s = (Stock)a;
Console.WriteLine(s.SharesOwned); // <بدون خطا>
// --- مثال خطا ---
House h = new House();
Asset a2 = h; // Upcast
// ❌ Downcast ناموفق! a2 یک Stock نیست.
// Stock s2 = (Stock)a2; // در زمان اجرا خطای InvalidCastException میدهد
🤔 حرف حساب و تجربه شما
درک درست Upcasting و Downcasting، کلید استفاده امن و قدرتمند از چندریختی در کدهای شماست. (در آینده با روشهای امنتر Downcasting مثل as و is آشنا میشیم).
بهترین مثالی که از قدرت چندریختی تو پروژههاتون دیدید چی بوده؟ آیا تا حالا به خطای InvalidCastException به خاطر یه Downcast اشتباه برخورد کردید؟
🔖 هشتگها:
#OOP #Polymorphism #Casting
معماری برش عمودی (Vertical Slice): ساختاربندی برشهای عمودی 🔪
از سازماندهی پروژه خود در لایههای مختلف خسته شدهاید؟ 😫
معماری برش عمودی (VSA) یک جایگزین قانعکننده برای معماریهای لایهای سنتی است. VSA فیلمنامه را در مورد نحوه ساختاردهی کد ما برعکس میکند.
به جای لایههای افقی (Presentation, Application, Domain)، VSA کد را بر اساس ویژگی (feature) سازماندهی میکند. هر ویژگی همه چیز مورد نیاز خود را، از endpointهای API گرفته تا دسترسی به داده، در بر میگیرد.
در این مقاله، ما بررسی خواهیم کرد که چگونه میتوانید برشهای عمودی را در VSA ساختاربندی کنید.
درک برشهای عمودی 🧩
در هسته خود، یک برش عمودی یک واحد مستقل از عملکرد را نشان میدهد. این یک برش از طریق کل پشته اپلیکیشن است. این برش، تمام کدها و کامپوننتهای لازم برای تحقق یک ویژگی خاص را کپسوله میکند.
در معماریهای لایهای سنتی، کد به صورت افقی در لایههای مختلف سازماندهی میشود. پیادهسازی یک ویژگی میتواند در چندین لایه پراکنده شود. تغییر یک ویژگی نیازمند اصلاح کد در چندین لایه است.
VSA
با گروهبندی تمام کدها برای یک ویژگی در یک برش واحد، به این مشکل رسیدگی میکند.
📌این تغییر دیدگاه چندین مزیت به همراه دارد:
✅ انسجام بهبود یافته: کدهای مربوط به یک ویژگی خاص در کنار هم قرار میگیرند، که درک، اصلاح و تست آن را آسانتر میکند.
✅ پیچیدگی کاهش یافته: VSA با اجتناب از نیاز به پیمایش چندین لایه، مدل ذهنی اپلیکیشن شما را ساده میکند.
✅ تمرکز بر منطق بیزینس: ساختار به طور طبیعی بر روی مورد استفاده بیزینس تأکید میکند تا جزئیات پیادهسازی فنی.
✅ نگهداری آسانتر: تغییرات در یک ویژگی، در داخل برش آن محلیسازی میشوند و ریسک عوارض جانبی ناخواسته را کاهش میدهند.
پیادهسازی معماری برش عمودی 👨💻
در اینجا یک مثال از برش عمودی که ویژگی CreateProduct را نشان میدهد، آمده است. ما از یک کلاس استاتیک برای نمایش ویژگی و گروهبندی انواع مرتبط استفاده میکنیم.
public static class CreateProduct
{
public record Request(string Name, decimal Price);
public record Response(int Id, string Name, decimal Price);
public class Endpoint : IEndpoint
{
public void MapEndpoint(IEndpointRouteBuilder app)
app.MapPost("products", Handler).WithTags("Products");
}
public static IResult Handler(Request request, AppDbContext context)
{
var product = new Product
{
Name = request.Name,
Price = request.Price
};
context.Products.Add(product);
context.SaveChanges();
return Results.Ok(
new Response(product.Id, product.Name, product.Price));
}
}
}
کد برای کل ویژگی CreateProduct به طور فشرده در یک فایل واحد گروهبندی شده است. این کار مکانیابی، درک و اصلاح همه چیز مربوط به این عملکرد را بسیار آسان میکند. ما نیازی به پیمایش چندین لایه (مانند کنترلرها، سرویسها، ریپازیتوریها و غیره) نداریم.
معرفی اعتبارسنجی (Validation) در برشهای عمودی 🛡
برشهای عمودی معمولاً نیاز به حل برخی دغدغههای مشترک (cross-cutting concerns) دارند که یکی از آنها اعتبارسنجی است. ما میتوانیم به راحتی اعتبارسنجی را با کتابخانه FluentValidation پیادهسازی کنیم.
public static class CreateProduct
{
public record Request(string Name, decimal Price);
public record Response(int Id, string Name, decimal Price);
public class Validator : AbstractValidator<Request> { /* ... */ }
public class Endpoint : IEndpoint
{
public void MapEndpoint(IEndpointRouteBuilder app)
{
app.MapPost("products", Handler).WithTags("Products");
}
public static async Task<IResult> Handler(
Request request,
IValidator<Request> validator,
AppDbContext context)
{
var validationResult = await validator.ValidateAsync(request);
if (!validationResult.IsValid)
{
return Results.BadRequest(validationResult.Errors);
}
// ... (ایجاد محصول و بازگرداندن پاسخ)
}
}
}
مدیریت ویژگیهای پیچیده و منطق مشترک 🧠
VSA
در مدیریت ویژگیهای مستقل برتری دارد. با این حال، اپلیکیشنهای دنیای واقعی اغلب شامل تعاملات پیچیده و منطق مشترک هستند.
در اینجا چند استراتژی وجود دارد که میتوانید در نظر بگیرید:
🔹️ تجزیه (Decomposition): ویژگیهای پیچیده را به برشهای عمودی کوچکتر و قابل مدیریتتر تقسیم کنید.
🔹️ بازآرایی (Refactoring): وقتی یک برش عمودی نگهداریاش دشوار میشود، از تکنیکهای بازآرایی مانند Extract method و Extract class استفاده کنید.
🔹️ استخراج منطق مشترک: منطق مشترکی که در چندین ویژگی استفاده میشود را شناسایی کنید. یک کلاس جداگانه (یا متد توسعه) برای ارجاع به آن از برشهای عمودی خود ایجاد کنید.
🔹️ انتقال منطق به پایین: برشهای عمودی را با استفاده از کد رویهای، مانند یک Transaction Script بنویسید. سپس، میتوانید بخشهایی از منطق بیزینس را که به طور طبیعی به انتیتیهای دامین تعلق دارند، شناسایی کنید.
خلاصه 📝
معماری برش عمودی بیش از یک راه برای ساختاردهی کد شماست. با تمرکز بر ویژگیها، VSA به شما اجازه میدهد اپلیکیشنهای منسجم و قابل نگهداری ایجاد کنید. برشهای عمودی مستقل هستند و تست واحد و یکپارچهسازی را سادهتر میکنند.
تغییرات محلیسازی میشوند، ریسک رگرسیونها را کاهش داده و امکان تکرارهای سریعتر را فراهم میکنند.
معماری برش عمودی را در پروژه بعدی خود در نظر بگیرید. این یک تغییر ذهنی بزرگ از معماری تمیز است. با این حال، هر دو جایگاه خود را دارند و حتی ایدههای مشابهی را به اشتراک میگذارند.
🔖 هشتگها:
#SoftwareArchitecture #VerticalSliceArchitecture #CQRS
📌5 books that made me a better software engineer:
1️⃣ Clean Architecture, Robert Martin
2️⃣ Domain-Driven Design, Eric Evans
3️⃣ Building Microservices, Sam Newman
4️⃣ Designing Data-Intensive Applications, Martin Kleppmann
5️⃣ Patterns of Enterprise Application Architecture, Martin Fowler
Which book made you a better software engineer?🤔
1️⃣ Clean Architecture, Robert Martin
2️⃣ Domain-Driven Design, Eric Evans
3️⃣ Building Microservices, Sam Newman
4️⃣ Designing Data-Intensive Applications, Martin Kleppmann
5️⃣ Patterns of Enterprise Application Architecture, Martin Fowler
Which book made you a better software engineer?🤔