نوشتن Middleware سفارشی در ASP.NET Core 🧩
ءMiddleware نرمافزاری است که در قالب یک pipeline داخل برنامه کنار هم قرار میگیرد تا درخواستها (requests) و پاسخها (responses) را مدیریت کند.
ءASP.NET Core مجموعهی غنیای از middlewareهای داخلی (built-in) را ارائه میدهد، اما در بعضی سناریوها ممکن است بخواهید یک middleware سفارشی (custom middleware) بنویسید.
کلاس Middleware
ءMiddleware معمولاً داخل یک کلاس کپسوله میشود و از طریق یک extension method در دسترس قرار میگیرد. مثال زیر یک middleware درونخطی (inline) را نشان میدهد که culture مربوط به درخواست جاری را از روی query string تنظیم میکند:
using System.Globalization;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.UseHttpsRedirection();
app.Use(async (context, next) =>
{
var cultureQuery = context.Request.Query["culture"];
if (!string.IsNullOrWhiteSpace(cultureQuery))
{
var culture = new CultureInfo(cultureQuery);
CultureInfo.CurrentCulture = culture;
CultureInfo.CurrentUICulture = culture;
}
// Call the next delegate/middleware in the pipeline.
await next(context);
});
app.Run(async (context) =>
{
await context.Response.WriteAsync(
$"CurrentCulture.DisplayName: {CultureInfo.CurrentCulture.DisplayName}");
});
app.Run();
ءMiddleware درونخطی بالا برای نمایش نحوهی ساخت یک middleware با استفاده از متد Microsoft.AspNetCore.Builder.UseExtensions.Use استفاده شده است. این متد، یک delegate تعریفشده بهصورت درونخطی را به pipeline درخواستهای برنامه اضافه میکند.
متدهای Use
دو overload برای متد Use وجود دارد:
یکی که HttpContext و <Func<Task میگیرد. در این حالت <Func<Task بدون پارامتر فراخوانی میشود.
دیگری که HttpContext و RequestDelegate میگیرد. در این حالت RequestDelegate با ارسال HttpContext فراخوانی میشود.
استفاده از overload دوم ترجیح داده میشود چون باعث صرفهجویی در دو allocation داخلی در هر درخواست میشود. ⚡️
تست Middleware
میتوانید middleware را با ارسال culture تست کنید. مثلاً:
https://localhost:5001/?culture=es-es
انتقال Middleware به یک کلاس
کد زیر همان middleware قبلی را داخل یک کلاس قرار میدهد:
using System.Globalization;
namespace Middleware.Example;
public class RequestCultureMiddleware
{
private readonly RequestDelegate _next;
public RequestCultureMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
var cultureQuery = context.Request.Query["culture"];
if (!string.IsNullOrWhiteSpace(cultureQuery))
{
var culture = new CultureInfo(cultureQuery);
CultureInfo.CurrentCulture = culture;
CultureInfo.CurrentUICulture = culture;
}
// Call the next delegate/middleware in the pipeline.
await _next(context);
}
}
الزامات کلاس Middleware
کلاس middleware باید شامل موارد زیر باشد:
• یک constructor عمومی با پارامتر از نوع RequestDelegate
• یک متد عمومی به نام Invoke یا InvokeAsync که:
• مقدار بازگشتی آن Task باشد
• اولین پارامتر آن از نوع HttpContext باشد
پارامترهای اضافی در constructor و متد Invoke/InvokeAsync از طریق Dependency Injection (DI) مقداردهی میشوند. 💉
Extension Method برای Middleware
معمولاً یک extension method ساخته میشود تا middleware از طریق IApplicationBuilder قابل استفاده باشد:
using System.Globalization;
namespace Middleware.Example;
public class RequestCultureMiddleware
{
private readonly RequestDelegate _next;
public RequestCultureMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
var cultureQuery = context.Request.Query["culture"];
if (!string.IsNullOrWhiteSpace(cultureQuery))
{
var culture = new CultureInfo(cultureQuery);
CultureInfo.CurrentCulture = culture;
CultureInfo.CurrentUICulture = culture;
}
await _next(context);
}
}
public static class RequestCultureMiddlewareExtensions
{
public static IApplicationBuilder UseRequestCulture(
this IApplicationBuilder builder)
{
return builder.UseMiddleware<RequestCultureMiddleware>();
}
}
استفاده از Middleware در Program.cs
using Middleware.Example;
using System.Globalization;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.UseHttpsRedirection();
app.UseRequestCulture();
app.Run(async (context) =>
{
await context.Response.WriteAsync(
$"CurrentCulture.DisplayName: {CultureInfo.CurrentCulture.DisplayName}");
});
app.Run();
وابستگیهای Middleware
ءMiddleware باید از اصل Explicit Dependencies Principle پیروی کند؛ یعنی وابستگیهای خود را در constructor مشخص کند.
🔹️ءMiddleware فقط یک بار در طول عمر برنامه ساخته میشود. 🕒
🔹️ءMiddleware میتواند وابستگیهای خود را از طریق DI در constructor دریافت کند. همچنین متد UseMiddleware میتواند پارامترهای اضافی هم بگیرد.
وابستگیهای سطح درخواست (Per-request)
از آنجا که middleware در زمان startup ساخته میشود، طول عمر آن برابر طول عمر برنامه است. بنابراین سرویسهای با Scoped lifetime که در constructor تزریق شوند، با بقیه سرویسهای هر درخواست به اشتراک گذاشته نمیشوند.
برای اشتراک سرویسهای scoped بین middleware و سایر بخشها، باید آنها را در متد InvokeAsync تزریق کنیم:
namespace Middleware.Example;
public class MyCustomMiddleware
{
private readonly RequestDelegate _next;
public MyCustomMiddleware(RequestDelegate next)
{
_next = next;
}
// IMessageWriter is injected into InvokeAsync
public async Task InvokeAsync(HttpContext httpContext, IMessageWriter svc)
{
svc.Write(DateTime.Now.Ticks.ToString());
await _next(httpContext);
}
}
public static class MyCustomMiddlewareExtensions
{
public static IApplicationBuilder UseMyCustomMiddleware(
this IApplicationBuilder builder)
{
return builder.UseMiddleware<MyCustomMiddleware>();
}
}
تست Middleware سفارشی
using Middleware.Example;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddScoped<IMessageWriter, LoggingMessageWriter>();
var app = builder.Build();
app.UseHttpsRedirection();
app.UseMyCustomMiddleware();
app.MapGet("/", () => "Hello World!");
app.Run();
تعریف Interface و Implementation
namespace Middleware.Example;
public interface IMessageWriter
{
void Write(string message);
}
public class LoggingMessageWriter : IMessageWriter
{
private readonly ILogger<LoggingMessageWriter> _logger;
public LoggingMessageWriter(ILogger<LoggingMessageWriter> logger) =>
_logger = logger;
public void Write(string message) =>
_logger.LogInformation(message);
}
جمعبندی ذهنی 🧠
ءMiddleware قلب pipeline در ASP.NET Core است.
🔹️میتواند درونخطی یا در قالب کلاس پیادهسازی شود
🔸️باید RequestDelegate و متد Invoke/InvokeAsync داشته باشد
🔹️برای وابستگیهای scoped، تزریق باید در InvokeAsync انجام شود
🔸️بهترین ابزار برای پیادهسازی cross-cutting concernها مثل:
• Logging
• Authentication
• Localization
• Exception Handling
🔖هشتگها:
#ASPNETCore #Middleware #DotNet #Backend
میخوایم یکسری مطالب درمورد یکی از معروفترین کتابهای حوزهی نرمافزار داشته باشیم 📘
مهمترین سرفصلهاش رو با هم برسی میکنیم، و هر نکتهای که به چشمم جالب و کاربردی بود رو باهاتون شیر میکنم😄
اگه دوست دارید خلاصهی مفیدِ یه کتاب قطور رو بدون دردِ کمر و سردرد ،مفهومی یاد بگیرید، این پستها احتمالاً به کارتون میان 😉
مهمترین سرفصلهاش رو با هم برسی میکنیم، و هر نکتهای که به چشمم جالب و کاربردی بود رو باهاتون شیر میکنم😄
اگه دوست دارید خلاصهی مفیدِ یه کتاب قطور رو بدون دردِ کمر و سردرد ،مفهومی یاد بگیرید، این پستها احتمالاً به کارتون میان 😉
"Domain Driven Design"
📌سرفصل : ایزوله کردن دامنه
📌سرفصل : ایزوله کردن دامنه
Telegraph
Isolating the Domain
بخشی از نرمافزار که بهطور مشخص مسائل مربوط به domain را حل میکند، معمولاً فقط بخش کوچکی از کل سیستم نرمافزاری را تشکیل میدهد، هرچند اهمیت آن بهمراتب بیشتر از اندازهٔ آن است. برای بهکارگیری بهترین تفکر خود، لازم است بتوانیم به عناصر model نگاه کنیم…
Heap و Stack Allocation در C# 🧠💾(بخش اول)
درک نحوهی تخصیص (allocation) و مدیریت حافظه در #C برای بهینهسازی عملکرد (performance) برنامه و اطمینان از استفادهی کارآمد از منابع بسیار حیاتی است.
در این راهنمای جامع، مفاهیم heap و stack allocation در #C را بررسی میکنیم؛ اینکه چگونه کار میکنند، چه زمانی استفاده میشوند، و چگونه بر عملکرد کد شما تأثیر میگذارند.
مقدمهای بر Memory Allocation
مدیریت حافظه در #C اغلب توسط runtime مربوط به NET. انجام میشود؛ به این معنا که توسعهدهندگان در بیشتر مواقع نیازی به تخصیص و آزادسازی دستی حافظه ندارند.
با این حال، درک اینکه دادههای شما چگونه و در کجا تخصیص داده میشوند — در stack یا در heap — میتواند به شما کمک کند کدی کارآمدتر و با performance بهتر بنویسید. ⚙️
در سیشارپ ، stack و heap دو ناحیهی حافظه هستند که برای ذخیرهسازی دادهها استفاده میشوند:
• ءstack برای static memory allocation استفاده میشود
• ءheap برای dynamic memory allocation استفاده میشود
ءStack Allocation چیست؟
ءstack ناحیهای از حافظه است که value typeها و pointerهای reference type را ذخیره میکند.
این ساختار از مدل Last-In, First-Out (LIFO) پیروی میکند؛ به این معنی که آخرین دادهی تخصیص دادهشده، اولین دادهای است که آزاد میشود.
به دلیل این ساختار، stack allocation بسیار سریع و کارآمد است. 🚀
ویژگیهای Stack Allocation:
🔹️Automatic Memory Management:
دادهها بهصورت خودکار هنگام خروج از scope آزاد میشوند
🔹️LIFO Structure:
استفاده از مدل LIFO باعث استفادهی بهینه از حافظه میشود
🔹️Limited Size:
اندازهی stack محدود است و برای objectهای بزرگ مناسب نیست
ءHeap Allocation چیست؟
ءheap برای dynamic memory allocation استفاده میشود.
اینجاست که reference typeها (مانند objectها، stringها و ...) تخصیص داده میشوند.
ءheap توسط Garbage Collector (GC) مدیریت میشود که بهصورت دورهای حافظهای که دیگر استفاده نمیشود را آزاد میکند. ♻️
ویژگیهای Heap Allocation:
🔹️Managed by Garbage Collector:
ء GC فرآیند آزادسازی حافظه را بهصورت خودکار مدیریت میکند
🔹️Dynamic Allocation:
ءobjectها میتوانند حتی پس از اتمام متدی که آنها را ایجاد کرده، باقی بمانند
🔹️Larger Memory Pool:
ءheap میتواند نسبت به stack objectهای بسیار بزرگتری را ذخیره کند
تفاوت بین Stack و Heap
تفاوتهای کلیدی بین stack و heap allocation در #C حول محور نحوه و زمان تخصیص و آزادسازی حافظه میچرخند.
حافظهی stack کارآمد است و برای دادههای کوتاهعمر (short-lived) که scope آنها محدود به function یا method است مناسب میباشد.
در مقابل، heap برای دادههایی استفاده میشود که نیاز دارند فراتر از scope یک method باقی بمانند؛ مانند instanceهای reference type.
ءstack مدیریت حافظهی خودکار و بسیار سریعی را فراهم میکند، اما از نظر اندازه محدود است و نمیتواند ساختارهای دادهای بزرگ و پیچیده را مدیریت کند.
ءheap در حالی که انعطافپذیری بیشتر و تخصیص حافظهی بزرگتری ارائه میدهد، نیازمند garbage collection است که مقداری overhead در performance ایجاد میکند. 📉
نحوهی عملکرد Stack در #C: مثال
برای درک بهتر stack allocation، به مثال زیر توجه کنید:
public void StackExample()
{
int a = 5;
int b = 10;
int result = a + b;
}
در مثال بالا:
اعداد صحیح a، b و result روی stack تخصیص داده میشوند زیرا از نوع value type هستند
زمانی که متد StackExample به پایان میرسد، تمام حافظهی استفادهشده توسط این متغیرها بهصورت خودکار آزاد میشود
Local Variables و Stack Allocation
متغیرهای محلی (Local variables)، شامل value typeها و reference به objectها، روی stack ذخیره میشوند.
با این حال، referenceهای مربوط به objectها به heap اشاره میکنند؛ جایی که دادهی واقعی object در آن قرار دارد.
زمانی که یک value type بخشی از یک reference type باشد (برای مثال، یک فیلد داخل یک class)، آن value type بهعنوان بخشی از object مربوط به reference type روی heap ذخیره میشود.
نحوهی عملکرد Heap در #C: مثال
کد زیر را در نظر بگیرید:
public class Person
{
public string Name;
public int Age;
}
public void HeapExample()
{
Person person = new Person();
person.Name = "Alice";
person.Age = 30;
}
در این مثال: object از نوع Person روی heap ایجاد میشود زیرا یک reference type است.reference با نام person روی stack ذخیره میشود که به object روی heap اشاره میکند.
فیلدهای Name و Age نیز بهعنوان بخشی از object مربوط به Person روی heap ذخیره میشوند
Value Typeها در مقابل Reference Typeها
درک value typeها و reference typeها به شفافسازی نحوهی استفاده از حافظه کمک میکند.
🔹️Value Types:
بهصورت مستقیم در حافظهی stack ذخیره میشوند. مانند int، float، bool و structهای سفارشی
🔹️Reference Types:
روی heap ذخیره میشوند، اما reference مربوط به آنها روی stack قرار میگیرد. مانند classها، arrayها و stringها
مثال: Value Type در مقابل Reference Type
// Value type, allocated on the stack
int x = 10;
// Reference type, reference on stack, object on heap
Person p1 = new Person();
در مثال بالا، x روی stack ذخیره میشود زیرا یک value type است، در حالی که p1 روی heap ذخیره میشود زیرا یک reference type است.
Memory Management در .NET
ءruntime مربوط به NET. شامل یک Garbage Collector (GC) است که به مدیریت تخصیص و آزادسازی حافظه برای heap کمک میکند.
ءGC بهصورت خودکار objectهایی که همچنان در حال استفاده هستند را ردیابی کرده و حافظهای که دیگر مورد نیاز نیست را آزاد میکند. ♻️
Heap و Stack Allocation در C# 🧠💾(بخش دوم)ءGC در NET. برای بهینهسازی پرفورمنس، objectها را به سه نسل تقسیم میکند:
Generational Garbage Collection
🔹️Generation 0:
ءobjectهای کوتاهعمر که معمولاً در methodها تخصیص داده میشوند
🔸️Generation 1:
ءobjectهای با طول عمر متوسط که از collection مربوط به Gen 0 عبور کردهاند
🔹️Generation 2:
ءobjectهای بلندعمر که معمولاً برای دادههای سطح application استفاده میشوند
مثال: نحوهی عملکرد GC
public void GarbageCollectionExample()
{
// Allocated on the heap (Generation 0)
Person p1 = new Person();
}
مثال Generation 0 (GarbageCollectionExample):
ءinstance مربوط به Person با نام p1 روی heap تخصیص داده شده و در Generation 0 آغاز میشود که برای objectهای کوتاهعمر در نظر گرفته شده است.
اگر پس از اتمام method هیچ reference دیگری به p1 وجود نداشته باشد، garbage collector ممکن است آن را در طی collection مربوط به Generation 0 آزاد کند.
public void Generation1Example()
{
List<int> numbers = new List<int>();
for (int i = 0; i < 1000; i++)
{
numbers.Add(i);
}
// 'numbers' will likely survive Gen 0 collection and move to Gen 1.
}
مثال Generation 1 (Generation1Example):
ء<List<int با نام numbers روی heap تخصیص داده میشود. از آنجا که شامل چندین مقدار بوده و برای مدت نسبتاً طولانیتری باقی میماند (برای مثال، تا پایان method)، ممکن است از garbage collection مربوط به Generation 0 عبور کرده و به Generation 1 ارتقاء یابد.
ءGeneration 1 برای objectهایی است که به اندازهی objectهای Generation 0 کوتاهعمر نیستند اما همچنان نیازی به باقیماندن در طول کل عمر application ندارند.
public class CacheManager
{
private static Dictionary<string, string> _cache = new Dictionary<string, string>();
public static void AddItem(string key, string value)
{
_cache[key] = value;
}
}
public void Generation2Example()
{
CacheManager.AddItem("user1", "data1");
CacheManager.AddItem("user2", "data2");
// '_cache' is a static reference and is long-lived, likely residing in Generation 2.
}
مثال Generation 2 (Generation2Example):
کلاس CacheManager از یک Dictionary بهصورت static استفاده میکند، که به این معناست دادههای آن در طول چرخهی عمر application باقی میمانند.
از آنجا که این dictionary (_cache) بلندعمر است، احتمالاً به Generation 2 ارتقاء مییابد که objectهایی را نگهداری میکند که انتظار میرود برای مدت طولانی باقی بمانند؛ مانند cacheها، دادههای static یا objectهای singleton.
مشکلات رایج و Best Practiceها
🔹️Stack Overflow:
زمانی رخ میدهد که تعداد زیادی فراخوانی method تودرتو یا recursion بیش از حد وجود داشته باشد که منجر به اتمام حافظهی stack میشود
🔸️Memory Leaks:
با اینکه NET. دارای GC است، memory leak همچنان میتواند رخ دهد اگر referenceها بهصورت ناخواسته زنده نگه داشته شوند و مانع از جمعآوری آنها توسط GC شوند
Best Practices
🔹️Minimize Long-Lived Objects:
از نگهداشتن referenceها بیش از حد لازم خودداری کنید
🔸️Use Structs Wisely:
تنها زمانی از struct استفاده کنید که type کوچک و کوتاهعمر باشد تا از تخصیص غیرضروری روی heap جلوگیری شود
🔹️Avoid Recursive Calls:
برای recursionهای عمیق، از روشهای iterative استفاده کنید تا از stack overflow جلوگیری شود
نتیجهگیری
🔹️درک تفاوتهای بین heap و stack allocation در #C میتواند به شما کمک کند applicationهایی کارآمدتر و با performance بالاتر بنویسید.
🔸️با دانستن اینکه دادههای شما کجا و چگونه ذخیره میشوند، میتوانید تصمیمات بهتری دربارهی مدیریت حافظه اتخاذ کنید، از pitfalls جلوگیری کنید و از مزایای هر دو نوع تخصیص حافظه بهره ببرید. ⚡️
🔹️برای درک کامل پتانسیل memory management، مهم است بدانید که هر نوع allocation دارای سناریوهای استفادهی بهینهی خاص خود است.
🔸️ء stack برای سناریوهایی ایدهآل است که دادهها دارای طول عمر کوتاه و مشخص هستند؛ مانند محاسبات سطح method و value typeهای ساده.
🔹️دسترسی سریع و آزادسازی خودکار آن تضمین میکند که دادههای کوچک و موقتی با حداقل overhead مدیریت شوند که منجر به performance بهینه میشود.
🔸️از سوی دیگر، heap انعطافپذیری لازم برای dynamic memory allocation و objectهای بلندعمر را فراهم میکند، اما این امر با هزینهی زمان دسترسی کندتر و نیاز به garbage collection همراه است.
🔹️برای ساختارهای دادهای پیچیده، منابع اشتراکی یا objectهایی که فراتر از method ایجادکنندهی خود باقی میمانند، heap ضروری است.
🔸️با این حال، توسعهدهندگان باید مراقب باشند referenceها را بیش از حد لازم نگه ندارند تا از memory leak و افزایش GC pressure جلوگیری شود.
🔹️ءGenerational garbage collection با دستهبندی objectها بر اساس طول عمر آنها، به ایجاد تعادل میان performance و کارایی memory management کمک میکند.
🔸️درک نحوهی ارتقاء objectها میان نسلها میتواند به توسعهدهندگان کمک کند کدی بنویسند که توقفهای GC را به حداقل رسانده و throughput را به حداکثر برساند.
🔹️در عمل، آگاهی از محل و نحوهی تخصیص حافظه میتواند به شما کمک کند کدی بنویسید که عملکرد بهتری داشته باشد، از مشکلات رایجی مانند stack overflow یا memory leak جلوگیری کند و از منابع سیستم بهصورت مؤثرتری استفاده نماید.
🔸️برای مثال، با کاهش تخصیص objectهای غیرضروری روی heap و استفاده از تخصیصهای مبتنی بر stack در صورت امکان، میتوانید overhead ناشی از garbage collection را کاهش داده و responsiveness applicationهای خود را بهبود دهید.
🔹️درک عمیق این مفاهیم حافظه همچنین شما را در انتخاب ساختارهای دادهای و design patternهای مناسب راهنمایی میکند.
🔸️برای مثال، استفاده از struct بهجای class برای objectهای کوچک و immutable میتواند به کاهش تخصیص روی heap کمک کند، در حالی که استفادهی هوشمندانه از مکانیزمهای caching میتواند تضمین کند objectهای بلندعمر در Generation 2 بهدرستی مدیریت شوند بدون آنکه استفاده از حافظه بیش از حد افزایش یابد.
🔹️در نهایت، memory management در #C ابزاری قدرتمند است اگر با رویکردی استراتژیک مورد استفاده قرار گیرد.
🔸️استفادهی مؤثر از heap و stack در کنار best practiceهای مربوط به garbage collection به توسعهدهندگان کمک میکند applicationهایی مقاوم و با performance بالا ایجاد کنند.
🔹️با افزایش پیچیدگی پروژههای شما، در نظر داشتن این مفاهیم تضمین میکند که applicationهای شما همچنان scalable، maintainable و کارآمد باقی بمانند.
🔸️به تفاوتهای میان value type و reference type توجه داشته باشید و از garbage collector مربوط به runtime در NET. بهره ببرید، در حالی که نسبت به مشکلات رایج حافظه مانند leakها و stack overflow نیز هوشیار باشید.
🔹️درک مناسب این مفاهیم در پروژههای پیچیدهتر #C به شما کمک شایانی خواهد کرد. 🚀
🔖هشتگها:
#CSharp #DotNet #MemoryManagement #GarbageCollector #Heap #Stack #Performance
📌عنوان : Factory Method
Telegraph
Factory Method
🎯Intent ءFactory Method یک creational design pattern است که یک interface برای ایجاد اشیاء در کلاس والد فراهم میکند، ولی به subclassها اجازه میدهد نوع اشیاء ایجادشده را تغییر دهند. ❗Problem تصور کنید که در حال توسعه یک logistics management application هستید.…
📌Domain Driven Design : A Model Expressed in Software
در تصور معمول ما، یک شخص دارای هویتی است که از تولد تا مرگ و حتی فراتر از آن امتداد مییابد. ویژگیهای فیزیکی آن شخص تغییر میکنند و در نهایت از بین میروند. نام ممکن است تغییر کند. روابط مالی به وجود میآیند و از بین میروند. حتی یک ویژگی هم در انسان وجود ندارد که تغییرناپذیر باشد؛ با این حال، هویت باقی میماند. آیا من همان فردی هستم که در پنجسالگی بودم؟ این نوع پرسش متافیزیکی در جستجوی مدلهای دامنه (domain models) مؤثر اهمیت دارد.
با بیانی کمی متفاوت: آیا کاربرِ اپلیکیشن اهمیتی میدهد که من همان فردی هستم که در پنجسالگی بودم؟
دو واریز (deposit) با مبلغ یکسان، به یک حساب یکسان، در یک روز یکسان، همچنان دو transaction متمایز محسوب میشوند.بنابراین دارای هویت هستند و یک ENTITY به شمار میآیند.
از سوی دیگر، attribute مبلغ این دو transaction احتمالاً instanceهایی از یک Object از نوع money هستند. این مقادیر هیچ هویتی ندارند، زیرا تمایز میان آنها هیچ سودمندیای ندارد.
در واقع، ممکن است دو Object دارای هویت یکسان باشند، بدون آنکه attributeهای یکسانی داشته باشند یا حتی الزاماً از یک class یکسان باشند.
در واقع، ممکن است یک چیز واحد در دنیای واقعی در یک domain model بهصورت یک ENTITY نمایش داده شود یا اصلاً چنین نباشد.
برای مثال، یک application جهت رزرو صندلی در یک استادیوم ممکن است صندلیها (seat) و شرکتکنندگان (attendee) را بهعنوان ENTITY در نظر بگیرد.
در حالت assigned seating که روی هر بلیت شماره صندلی درج شده است، صندلی یک ENTITY محسوب میشود. شناسه (identifier) آن شماره صندلی است که در محدودهی استادیوم یکتا (unique) است.
صندلی ممکن است attributeهای دیگری نیز داشته باشد، مانند:
• موقعیت مکانی
• داشتن یا نداشتن دیدِ مسدود
• قیمت
اما تنها شماره صندلی یا ترکیب یکتای ردیف (row) و موقعیت (position) برای شناسایی و تمایز صندلیها استفاده میشود.
از سوی دیگر، اگر رویداد از نوع general admission باشد به این معنا که دارندگان بلیت در هر صندلی خالی که پیدا کنند مینشینند نیازی به تمایز میان صندلیهای منفرد وجود ندارد.
در این حالت، تنها تعداد کل صندلیها اهمیت دارد. با اینکه شماره صندلیها همچنان بهصورت فیزیکی روی صندلیها حک شدهاند، هیچ نیازی نیست که software آنها را ردیابی کند.
در واقع، اگر مدل شماره صندلی خاصی را به بلیتها نسبت دهد، این یک خطای مدلسازی خواهد بود؛ زیرا در یک رویداد با general admission چنین محدودیتی وجود ندارد.
در چنین شرایطی، صندلیها دیگر ENTITY نیستند و هیچ identifierای نیز موردنیاز نخواهد بود.
🔖هشتگها :
#DDD #DomainModel
#Entity
Forwarded from tech-afternoon (Amin Mesbahi)
🪞روز مهندس، و داستان جناب هانسکریستیناندرسون که هنوز تمام نشده!
قرن ۱۹ میلادی، نویسنده سرشناس دانمارکی، در کنار داستانهای به ظاهر سادهای مثل دختر کبریتفروش یا جوجهاردک زشت، داستان لباس جدید پادشاه رو نوشت که احتمالا اکثر ما یا کتابش رو در کودکی خوندیم، یا کارتونش رو تماشا کردیم.
فکر میکنم بد نباشه جامعه مهندسی نرمافزار، گاهی جلو آیینه بایسته و ببینه که چقدر لُخت است! توی قصه «پادشاه لخت»، مشکل فقط یک پادشاه سادهلوح نبود. مسئله، یک اکوسیستم کامل بود:
- خیاطهای دروغین
- درباریان تأییدکننده
- جمعیتی که جرئت پرسیدن نداشت
و فقط یک کودک بود که گفت: چیزی تنش نیست!
اگر بخوایم صادق باشیم، ما هم در اکوسیستم نرمافزار، کم از اون دربار نداریم. و چقدر برای مناسب ظاهر شدن، نیاز به یادگیری و تلاش مضاعف داریم. خیاطهای دروغین امروز همیشه شیادهای بیرونی نیستن.
گاهی در لباس لیدر فنی ظاهر میشن که بدون threat model و بدون طراحی امنیتی، سیستم رو «production-ready» اعلام میکنن.
گاهی در قامت تیم محصول که زمان تحویل را بالاتر از امنیت و کیفیت مینشونن.
گاهی در نقش مدیر که بودجه آموزش، معماری، یا امنیت رو هزینه اضافی میدونن.
و گاهی هم در قامت مهندس باتجربهای که سالهاست چیز جدیدی یاد نگرفته ولی همچنان با اعتمادبهنفس حرف میزنه.
و درباریان؟
ما وقتی بدون خوندن دقیق PR رو تأیید میکنیم.
وقتی postmortem واقعی نمینویسیم.
وقتی ضعف امنیتی رو میدونیم ولی میگیم “بعداً درستش میکنیم”.
وقتی outage رو «اتفاق طبیعی» جا میزنیم.
مسئله این نیست که هک شدیم یا نشدیم. کند یا ناپایدار هستیم یا نه!
مسئله اینه که آیا اصول مهندسی رو جدی گرفتهایم یا نه.
اما سؤال جدی اینه:
چند تیم واقعاً اینها رو در عمل اجرا میکنند، نه فقط در رزومه؟
اگر بخواهیم جلوی آینه بایستیم، شاید بهتر باشد به جای شعار، این چکلیست رو از خودمون بپرسیم:
آیا ما اینها رو داریم؟
- اصول Security by Design، نه Security after Incident
- مفاهیم Threat Modeling مستند
- ساختار Secure SDLC واقعی، نه اسلاید پاورپوینت
- مکانیزمهای observability جدی
- ساختار Data Governance مشخص
- طبقهبندی داده و سیاست retention
- مدیریت دسترسی مبتنی بر اصل Least Privilege یا زیروتراست
- اصول Software Composition Analysis و مدیریت dependency
- ساختار Incident response plan تمرینشده
و...
اگر اینها نیست، ما فقط امیدواریم، مهندسی نمیکنیم.
روز مهندس شاید بیشتر از اونکه تبریک بخواد، احتیاج به صداقت داره.
صداقت با خودمون.
شاید وقتش باشه به جای تبریکهای شاعرانه، هرکدوممون یک ضعف مهندسی رو در تیممون جدی اصلاح کنیم. (جلو آینه بایستیم و لخت بودنمون رو ببینیم!)
لباس، با آرزو دوخته نمیشه.
با استاندارد، تمرین و مسئولیتپذیری دوخته میشه.
قرن ۱۹ میلادی، نویسنده سرشناس دانمارکی، در کنار داستانهای به ظاهر سادهای مثل دختر کبریتفروش یا جوجهاردک زشت، داستان لباس جدید پادشاه رو نوشت که احتمالا اکثر ما یا کتابش رو در کودکی خوندیم، یا کارتونش رو تماشا کردیم.
فکر میکنم بد نباشه جامعه مهندسی نرمافزار، گاهی جلو آیینه بایسته و ببینه که چقدر لُخت است! توی قصه «پادشاه لخت»، مشکل فقط یک پادشاه سادهلوح نبود. مسئله، یک اکوسیستم کامل بود:
- خیاطهای دروغین
- درباریان تأییدکننده
- جمعیتی که جرئت پرسیدن نداشت
و فقط یک کودک بود که گفت: چیزی تنش نیست!
اگر بخوایم صادق باشیم، ما هم در اکوسیستم نرمافزار، کم از اون دربار نداریم. و چقدر برای مناسب ظاهر شدن، نیاز به یادگیری و تلاش مضاعف داریم. خیاطهای دروغین امروز همیشه شیادهای بیرونی نیستن.
گاهی در لباس لیدر فنی ظاهر میشن که بدون threat model و بدون طراحی امنیتی، سیستم رو «production-ready» اعلام میکنن.
گاهی در قامت تیم محصول که زمان تحویل را بالاتر از امنیت و کیفیت مینشونن.
گاهی در نقش مدیر که بودجه آموزش، معماری، یا امنیت رو هزینه اضافی میدونن.
و گاهی هم در قامت مهندس باتجربهای که سالهاست چیز جدیدی یاد نگرفته ولی همچنان با اعتمادبهنفس حرف میزنه.
و درباریان؟
ما وقتی بدون خوندن دقیق PR رو تأیید میکنیم.
وقتی postmortem واقعی نمینویسیم.
وقتی ضعف امنیتی رو میدونیم ولی میگیم “بعداً درستش میکنیم”.
وقتی outage رو «اتفاق طبیعی» جا میزنیم.
مسئله این نیست که هک شدیم یا نشدیم. کند یا ناپایدار هستیم یا نه!
مسئله اینه که آیا اصول مهندسی رو جدی گرفتهایم یا نه.
من قبلاً دو اپیزود درباره تولید امن نرمافزار ساختم. درباره: SSDLC, STRIDE, Shift-left, SAST, DAST, IAST, RASP, SCA
در مورد بدهی فنی هم یه ویدیو کوتاه
اما سؤال جدی اینه:
چند تیم واقعاً اینها رو در عمل اجرا میکنند، نه فقط در رزومه؟
اگر بخواهیم جلوی آینه بایستیم، شاید بهتر باشد به جای شعار، این چکلیست رو از خودمون بپرسیم:
آیا ما اینها رو داریم؟
- اصول Security by Design، نه Security after Incident
- مفاهیم Threat Modeling مستند
- ساختار Secure SDLC واقعی، نه اسلاید پاورپوینت
- مکانیزمهای observability جدی
- ساختار Data Governance مشخص
- طبقهبندی داده و سیاست retention
- مدیریت دسترسی مبتنی بر اصل Least Privilege یا زیروتراست
- اصول Software Composition Analysis و مدیریت dependency
- ساختار Incident response plan تمرینشده
و...
اگر اینها نیست، ما فقط امیدواریم، مهندسی نمیکنیم.
روز مهندس شاید بیشتر از اونکه تبریک بخواد، احتیاج به صداقت داره.
صداقت با خودمون.
شاید وقتش باشه به جای تبریکهای شاعرانه، هرکدوممون یک ضعف مهندسی رو در تیممون جدی اصلاح کنیم. (جلو آینه بایستیم و لخت بودنمون رو ببینیم!)
لباس، با آرزو دوخته نمیشه.
با استاندارد، تمرین و مسئولیتپذیری دوخته میشه.
منظورم از اکوسیستم، فقط گروه خاصی نیست که ربطش بدیم به وقایع سیاسی و اجتماعی کنونی و بگیم اونا که از ما نیستن یا رفتنیاند و بعدش درستش میشه و از خودمون صلب مسئولیت کنیم.
از استارتاپهای متعدد خصوصی که هک شدن، تا بانک و سازمانی که با هک شدن دادههاش نابود شد و از روی پیامکها موجودی مردم رو بازسازی! کردن! تا نرمافزار دانشگاه تا... همه و همه گواهی بر لُختی جامعه نرم افزاریه!
ءVertical Slices دقیقاً کجای Modular Monolith قرار میگیرند؟ 🤔
بیشتر تیمها معماری در سطح کلان (macro architecture) را بهدرستی پیادهسازی میکنند. آنها یک Modular Monolith با مرزهای مشخص بین ماژولها، APIهای عمومی و ایزولاسیون مناسب دادهها میسازند.
اما بعد از آن، دیگر به معماری فکر نمیکنند! ❌
هر ماژول دقیقاً همان ساختار داخلی را دریافت میکند. معمولاً نوعی از Layered Architecture.
موضوع اینجاست که Clean Architecture و Vertical Slice Architecture آنقدرها هم که فکر میکنید از هم دور نیستند. هر دو روی Use Caseها تمرکز دارند و هدفشان حداکثرسازی Cohesion است.
ءClean Architecture فقط یک قانون اضافه میکند:
جهت وابستگیها (Direction of Dependencies) باید کنترل شود. که این معمولاً منجر به Abstractionها و Ceremonyهای بیشتری میشود.
رویکرد Pragmatic Clean Architecture یک حد وسط را انتخاب میکند و از نظر ماهیت، شباهت زیادی به VSA دارد.
سوال اصلی این نیست که کدامیک "بهتر" است.
سوال این است که هر کدام در کجای Modular Monolith شما میدرخشند ✨
و بخش جذاب ماجرا اینجاست:
میتوانید آنها را با هم Mix & Match کنید.
🧠 دو سطح از معماری
وقتی یک Modular Monolith میسازید، باید دو تصمیم معماری مهم بگیرید:
1️⃣ Macro Architecture
سیستم را چطور به ماژولها تقسیم میکنید؟
این شامل موارد زیر است:
🔹️مرزهای ماژولها
🔹️الگوهای ارتباطی بین ماژولها
🔹️ایزولاسیون دادهها
🔹️طراحی Public API
🔹️نحوه Deploy شدن ماژولها
2️⃣ Micro Architecture
کد داخل هر ماژول چطور سازماندهی میشود؟
این شامل موارد زیر است:
🔸️ساختار پوشهها
🔸️جهت وابستگیها
🔸️نحوه پیادهسازی Use Caseها
🔸️محل قرارگیری Validation
ُ🔸️نحوه دسترسی به دیتابیس
بیشتر مقالات درباره Modular Monolith کاملاً روی سطح Macro تمرکز میکنند.
و دلیل خوبی هم دارد — اشتباه گرفتن مرزهای ماژولها، هزینهی اصلاح بسیار بالایی دارد 💸
اما سطح Micro هم به همان اندازه مهم است.
چرا؟ چون تعیین میکند:
🔹️اضافه کردن Feature جدید چقدر آسان است
🔹️ءNavigation در کد چقدر ساده است
🔹️ءOnboard کردن توسعهدهنده جدید چقدر زمان میبرد
این همان معماریای است که تیم شما هر روز با آن تعامل دارد.
🔑 نکته کلیدی:
ءMacro Architecture مشخص میکند که ماژولها چگونه با یکدیگر تعامل داشته باشند.
اما Micro Architecture یک تصمیم محلی است که هر ماژول میتواند بهصورت مستقل بگیرد.
ماژول Ticketing شما مجبور نیست همان ساختار داخلی ماژول Notifications را دنبال کند.
مرز ماژولار این آزادی را به شما میدهد 🚀
ءVertical Sliceها ماژول نیستند ❗️
من دیدهام که بعضیها Vertical Sliceها را با ماژولها یکی میدانند.
در سطح Macro، یک ماژول میتواند شبیه یک "vertical slice" از دامنهی کسبوکار به نظر برسد.
اما این تشبیه در سطح Application از بین میرود.
یک ماژول یک Bounded Context است.
مالک دادههای خودش است، یک Public API ارائه میدهد، و یک قابلیت کسبوکاری را کپسوله میکند.
یک Vertical Slice یک الگوی پیادهسازی Feature است.
درخواست (Request)، Handler، Validation، و Data Access را برای یک Use Case در کنار هم گروهبندی میکند.
ماژولها و Vertical Sliceها در سطوح متفاوتی عمل میکنند.ماژولها مرزهای سیستم را تعریف میکنند.Vertical Sliceها کد داخل این مرزها را سازماندهی میکنند.
📦 ءVertical Sliceها داخل یک ماژول
ءVertical Slice Architecture کد را بر اساس Feature سازماندهی میکند، نه بر اساس لایههای فنی.
هر Feature یک واحد Self-contained است:
🔹️ءRequest، Handler، Validation، Data Access — همه در یک مکان.
🔸️در داخل یک ماژول از Modular Monolith، این یک انتخاب طبیعی است. مرز ماژول از قبل جداسازی از بقیه سیستم را اعمال میکند. شما به Layerها برای محافظت نیازی ندارید.Public API ماژول این کار را انجام میدهد.
🧭 انتخاب معماری داخلی
یک سوءبرداشت رایج این است که VSA فقط برای ماژولهای ساده است و Clean Architecture برای ماژولهای پیچیده. اینطور نیست.
ءVertical Sliceها با Domain Modelهای غنی هم عالی کار میکنند. میتوانید Domain Entityها، Value Objectها، و Domain Eventها را داخل یک Vertical Slice داشته باشید.
ءSlice نقطه ورود و Orchestration را سازماندهی میکند.Domain Model قوانین کسبوکار را مدیریت میکند. با رشد پیچیدگی Slice، منطق را به Domain منتقل میکنید دقیقاً همانطور که در Clean Architecture انجام میدهید.
بهطور مشابه، Pragmatic Clean Architecture برای ماژولهای ساده هم به خوبی کار میکند. پس تصمیمگیری درباره پیچیدگی نیست. بلکه درباره راحتی تیم شما و میزان شفافیتی است که به دست میآورید.
📌 چند موردی که باید در نظر بگیرید:
✨️آشنایی تیم
اگر تیم شما با Layer و Direction of Dependencies فکر میکند، Clean Architecture طبیعیتر خواهد بود. اگر ترجیح میدهند حول Featureها سازماندهی شوند، VSA اصطکاک را کاهش میدهد.
✨️منطق Domain مشترک
وقتی Use Caseهای زیادی از Domain Entityهای مشابه استفاده میکنند، داشتن یک Domain Layer اختصاصی میتواند این اشتراک را واضحتر کند. در VSA میتوانید منطق مشترک را به یک پوشه جداگانه استخراج کنید.
✨️استقلال Featureها
وقتی Featureها مستقل هستند و به ندرت رفتار مشترک دارند، Vertical Sliceها همه چیز را ساده نگه میدارند.
مرز ماژول، صرفنظر از انتخاب شما در داخل آن،از بقیه سیستم محافظت میکند.
پس چیزی را انتخاب کنید که تیم شما را بیشترین بهرهور میکند و آماده تغییر باشید وقتی ماژول تکامل پیدا میکند.
🎯 جمعبندی
ءModular Monolith به سوال Macro پاسخ میدهد:
چگونه سیستم را به ماژولهایی با مرزهای مشخص تقسیم کنیم؟
ءVertical Slice Architecture به سوال Micro پاسخ میدهد:چگونه کد را بر اساس Feature داخل این ماژولها سازماندهی کنیم؟
آنها در سطوح متفاوتی عمل میکنند.
ءModular Monolith Cohesion بالا در سطح ماژول فراهم میکند و Coupling بین ماژولها را مدیریت میکند.
ءVertical Slice Architecture Cohesion بالا در سطح Feature داخل ماژول فراهم میکند.
شما مجبور نیستید یک معماری داخلی را برای کل سیستم انتخاب کنید. هر ماژول میتواند گزینه مناسب با Context خودش را انتخاب کند.برخی ماژولها از Pragmatic Clean Architecture استفاده خواهند کرد.
برخی دیگر از Vertical Sliceها.
مرز ماژول که بهخوبی تعریف شده باشد،
این انتخاب را ایمن میکند.
🔖هشتگها:
#DotNet #SoftwareArchitecture #ModularMonolith #VerticalSliceArchitecture #CleanArchitecture
📌Domain Driven Design : Value Object
مفهوم Value Object یکی از مهمترین Building Blockهایی است که توسط Eric Evans در کتاب معروفش Domain-Driven Design: Tackling Complexity in the Heart of Software معرفی شد؛ مفهومی که اگر درست درک شود، میتواند ساختار Domain Model شما را بهشدت سادهتر، Maintainableتر و Expressiveتر کند.
💡ءValue Object چیست؟
در DDD، همه چیز در Domain یا Identity دارد یا فقط Value است.
یک Value Object:
🔹 هویتی (Identity) ندارد
🔹 فقط با مقدارش (Value) تعریف میشود
🔹 ءImmutable است
🔹 قابل جایگزینی است بدون اینکه معنی Domain تغییر کند
به بیان ساده:
اگر دو Object از نظر مقدار برابر باشند، در Domain یکی محسوب میشوند.
مثال:
📍 Address
💰 Money
📏 Distance
در Domain، مهم نیست کدام Address instance را داریم؛ اگر مقدارشان برابر باشد، از نظر Business یکی هستند.
🆚 Entity vs Value Object
📌Entity :
• دارای Identity
• ءMutable
• ءLifecycle دارد
• مقایسه با Id
• قابل Track شدن
📌 Value Object :
• بدون Identity
• ءImmutable
• ءLifecycle ندارد
• مقایسه با Value
• ءReplaceable
مثال:
Customer → Entity
CustomerEmail → Value Object
در دامین، Email هویت ندارد؛ فقط مقدار آن مهم است.
🔒 Immutability در Value Object
طبق تعریف Evans:
"Value Object should be immutable"
چرا؟چون اگر مقدار تغییر کند، دیگر همان مفهوم قبلی نیست.
مثال:
public sealed class Email
{
public string Value { get; }
public Email(string value)
{
if(string.IsNullOrWhiteSpace(value))
throw new ArgumentException("Email is required");
Value = value;
}
}
اگر Email تغییر کند:
یعنی یک Email جدید ساختهایم،
نه اینکه Email قبلی تغییر کرده باشد.
⚖️ Equality بر اساس Value
ءValue Objectها باید بر اساس مقدارشان مقایسه شوند نه Reference.
public override bool Equals(object? obj)
{
if (obj is not Email other)
return false;
return Value == other.Value;
}
public override int GetHashCode()
{
return Value.GetHashCode();
}
در Domain:
new Email("a@gmail.com") == new Email("a@gmail.com")باید True باشد حتی اگر دو Instance متفاوت باشند.
🎯 چه زمانی از Value Object استفاده کنیم؟
وقتی:
✅ ءObject مفهوم Domain دارد
✅ ءIdentity ندارد
✅ فقط با مقدار تعریف میشود
✅ قابل تغییر نیست
✅ ءLifecycle مستقل ندارد
🚫 ءAnti-Pattern رایج
بسیاری از پروژهها این اشتباه را انجام میدهند:
public string Email { get; set; }در حالی که:
ءEmail یک Primitive نیست
بلکه یک Concept در Domain است.
این همان Primitive Obsession است که DDD سعی دارد از آن جلوگیری کند.
🧠 مزایای استفاده از Value Object
✔️ جلوگیری از Invalid State
✔️ افزایش Expressiveness در Domain Model
✔️ کاهش Coupling
✔️ ءEncapsulation بهتر Business Ruleها
✔️ جلوگیری از Primitive Obsession
✔️ تستپذیری بیشتر
✅ جمعبندی
ءValue Objectها کمک میکنند:
🔹️ءDomain شما Richتر شود
🔸️ءBusiness Ruleها نزدیک به Data تعریف شوند
🔹️ءModel شما از Primitive Typeها جدا شود
🔸️و مهمتر از همه، زبان Ubiquitous Language در کد منعکس شود
اگر در Domain چیزی Identity ندارد،
احتمالاً باید یک Value Object باشد.
🔖هشتگها:
#DDD #ValueObject #DomainModel
📌نکته های مفیدی که در رابطه با رزومه فرستادن وجود داره و بهمون کمک میکنه که بهتر دیده بشیم رو از زبان مهندس کرمپور بشنویم.
https://t.me/thisisnabi_dev/1521
https://t.me/thisisnabi_dev/1521
Forwarded from iCodeNext
Audio
آیا داریم کد نوشتن رو فراموش میکنیم ؟
03:00 دنیال چند طبقه ی قبلی
10:00 تحلیل رفتن ذهن
14:00 یادگیری واقعی
16:00 توهم قطعیت
21:00 بعد از فراموشی کد چطوری سیستم رو نجات بدم.
🧶 تمام صحبت ها نظرات شخصی خودم هست، و هیچ دلیلی بر درست بودنش نیست.
مثل همیشه مشتی هستید. ❤️
امیدوارم که بزودی با محتوی تصویری هم این مسیر رو جلو ببریم.
03:00 دنیال چند طبقه ی قبلی
10:00 تحلیل رفتن ذهن
14:00 یادگیری واقعی
16:00 توهم قطعیت
21:00 بعد از فراموشی کد چطوری سیستم رو نجات بدم.
🧶 تمام صحبت ها نظرات شخصی خودم هست، و هیچ دلیلی بر درست بودنش نیست.
مثل همیشه مشتی هستید. ❤️
امیدوارم که بزودی با محتوی تصویری هم این مسیر رو جلو ببریم.
🚀 چرا برای Dependency Injection در #C به Primary Constructor مهاجرت کردم؟صادقانه بگویم، مدتها در برابر Primary Constructorها مقاومت میکردم.
زمانی که در C# 12 این قابلیت از Recordها به Classها و Structهای معمولی گسترش پیدا کرد، اولین واکنش من تردید بود.
استفاده از یک mutable capture ضمنی بهجای فیلدهای صریح
readonly، در نگاه اول شبیه این بود که امنیت و صراحت کد را فدای راحتی کنیم.اما بعد از استفاده از آن در چندین پروژه مختلف، نظرم کاملاً تغییر کرد.
مقدار Boilerplateی که در کلاسهای مبتنی بر Dependency Injection حذف میشود قابل توجه است و مشکلی که در ابتدا بابت آن نگران بودم، در عمل کاملاً قابل مدیریت است؛ البته به شرطی که از آن آگاه باشید.
در این مطلب توضیح میدهم چه چیزی باعث شد به Primary Constructorها مهاجرت کنم و مهمترین نکتهای که باید هنگام استفاده از آنها بدانید چیست.
💡 چه چیزی نظرم را تغییر داد؟
کلاسهای سرویس من قبلاً معمولاً به این شکل بودند:
public class OrderService
{
private readonly IOrderRepository _orderRepository;
private readonly ILogger<OrderService> _logger;
public OrderService(
IOrderRepository orderRepository,
ILogger<OrderService> logger)
{
_orderRepository = orderRepository;
_logger = logger;
}
public async Task<Order?> GetOrderAsync(Guid id)
{
_logger.LogInformation("Fetching order {OrderId}", id);
return await _orderRepository.GetByIdAsync(id);
}
}
و حالا همان کلاس به این شکل نوشته میشود:
public class OrderService(
IOrderRepository orderRepository,
ILogger<OrderService> logger)
{
public async Task<Order?> GetOrderAsync(Guid id)
{
logger.LogInformation("Fetching order {OrderId}", id);
return await orderRepository.GetByIdAsync(id);
}
}
تعریف فیلدها، بدنه Constructor و Assignmentها همگی حذف شدهاند.
پارامترهای Constructor بهصورت خودکار Capture میشوند و در تمام بدنه کلاس قابل استفاده هستند.
این دقیقاً رایجترین سناریوی استفاده از Primary Constructorها است:Dependency Injection در کلاسهای سرویس.
وابستگیها را تعریف میکنید و مستقیماً از آنها استفاده میکنید.
🎯 جایی که بیشترین استفاده را از آن دارم: کلاسهای سرویس مبتنی بر DI
بیشترین جایی که Primary Constructorها مرا متقاعد کردند، کلاسهای سرویس در ASP.NET Core بود.
بخش عمده زمان من صرف توسعه همین نوع کلاسها میشود و حذف Boilerplate در اینجا بهسرعت خودش را نشان میدهد.
یک نمونه واقعیتر از فرآیند Checkout:
public class CheckoutService(
IPaymentProcessor paymentProcessor,
IOrderRepository orderRepository,
ILogger<CheckoutService> logger,
IOptions<CheckoutOptions> options)
{
public async Task<CheckoutResult> ProcessAsync(
Cart cart,
CancellationToken ct = default)
{
var settings = options.Value;
if (cart.Total < settings.MinimumOrderAmount)
{
logger.LogWarning("Order below minimum: {Total}", cart.Total);
return CheckoutResult.BelowMinimum;
}
var order = Order.Create(cart);
await paymentProcessor.ChargeAsync(order, ct);
await orderRepository.SaveAsync(order, ct);
logger.LogInformation("Checkout complete for order {OrderId}", order.Id);
return CheckoutResult.Success;
}
}
چهار Dependency، بدون حتی یک خط Boilerplate.
کلاس از ابتدا تا انتها فقط منطق کسبوکار را نمایش میدهد و هیچ کد اضافهای بین آن دیده نمیشود.
این الگو بسیار خوب عمل میکند زیرا کلاسهای سرویس معمولاً نیازی به اعتبارسنجی یا تبدیل Dependencyها ندارند.Container آنها را ایجاد میکند و شما صرفاً از آنها استفاده میکنید.
به همین دلیل Primary Constructorها انتخابی بسیار مناسب برای این سناریو هستند.
🏗 استفاده در Entityها و Value Objectها (با یک نکته مهم)
کمکم استفاده از Primary Constructorها را در Entityها و Value Objectها نیز آغاز کردم؛ مخصوصاً در جاهایی که میخواهید وجود برخی پارامترها را هنگام ساخت شیء اجباری کنید.
public class Order(Guid customerId, Money total)
{
public Guid Id { get; } = Guid.NewGuid();
public Guid CustomerId { get; } = customerId;
public Money Total { get; } = total;
public OrderStatus Status { get; private set; } = OrderStatus.Pending;
public DateTime CreatedAt { get; } = DateTime.UtcNow;
public void Confirm()
{
if (Status != OrderStatus.Pending)
{
throw new InvalidOperationException(
$"Cannot confirm order in {Status} status.");
}
Status = OrderStatus.Confirmed;
}
}
در این طراحی، هیچ راهی برای ایجاد یک Order بدون
customerId یا total وجود ندارد.ءPrimary Constructor این محدودیت را مستقیماً در سطح تعریف Type نمایش میدهد.
اما یک تفاوت مهم با مثال سرویسها وجود دارد:
در اینجا پارامترهای Primary Constructor را به Propertyها اختصاص دادهایم:
public Guid CustomerId { get; } = customerId;این نکته اهمیت زیادی دارد و مستقیماً به بزرگترین چالش Primary Constructorها منتهی میشود.
⚠️ مشکلی که نزدیک بود باعث شود هرگز از آن استفاده نکنم
دلیل اصلی مقاومت اولیه من همین موضوع بود.
پارامترهای Primary Constructor در واقع فیلدهای readonly نیستند.
زمانی که مستقیماً از پارامترهای Primary Constructor در بدنه کلاس استفاده میکنید، کامپایلر آنها را بهعنوان یک متغیر قابل تغییر (mutable) Capture میکند.
هیچ فیلد readonly مخفیای در پشت صحنه ایجاد نمیشود.
به همین دلیل میتوانید بهاشتباه مقدار آنها را تغییر دهید:
public class OrderService(
IOrderRepository orderRepository,
ILogger<OrderService> logger)
{
public async Task<Order?> GetOrderAsync(Guid id)
{
logger.LogInformation("Fetching order {OrderId}", id);
return await orderRepository.GetByIdAsync(id);
}
public void SomeOtherMethod()
{
// This compiles. No warning. No error.
orderRepository = null!;
logger = null!;
}
}
این کد بدون هیچ Warning یا Error کامپایل میشود.
در صورتی که اگر از Constructor سنتی و فیلدهای
private readonly استفاده میکردید، کامپایلر بلافاصله جلوی این کار را میگرفت.اما در Primary Constructorها سکوت میکند.
🔒 اگر به Immutability نیاز دارید
در صورتی که تضمین Immutable بودن برای شما اهمیت دارد، میتوانید پارامترها را به فیلدهای readonly اختصاص دهید:
public class OrderService(
IOrderRepository orderRepository,
ILogger<OrderService> logger)
{
private readonly IOrderRepository _orderRepository = orderRepository;
private readonly ILogger<OrderService> _logger = logger;
public async Task<Order?> GetOrderAsync(Guid id)
{
_logger.LogInformation("Fetching order {OrderId}", id);
return await _orderRepository.GetByIdAsync(id);
}
}
اما در این حالت بخش زیادی از مزیت Primary Constructor از بین میرود.
دوباره به تعریف فیلدها و Assignmentها بازمیگردید؛ فقط با سینتکسی متفاوت.
در عمل، تاکنون هرگز با این مشکل در یک کلاس سرویس مبتنی بر DI مواجه نشدهام.
احتمال اینکه در میانه اجرای یک متد، بهاشتباه Logger یا Repository را Reassign کنید بسیار کم است.
اما در Entityها و Value Objectها که Immutability اهمیت بیشتری دارد، این موضوع میتواند دردسرساز شود.
به همین دلیل هنوز در این بخش با احتیاط بیشتری عمل میکنم.
❌ چه زمانی هنوز از Constructorهای سنتی استفاده میکنم؟
با وجود تمام مزایا، هنوز همه چیز را به Primary Constructor تبدیل نکردهام.
برخی سناریوها همچنان برای Constructorهای کلاسیک مناسبتر هستند.
1️⃣ اعتبارسنجی پیچیده
اگر لازم باشد قبل از مقداردهی، پارامترها اعتبارسنجی شوند، به بدنه Constructor نیاز خواهید داشت.
public class EmailAddress
{
private readonly string _value;
public EmailAddress(string value)
{
if (string.IsNullOrWhiteSpace(value) || !value.Contains('@'))
{
throw new ArgumentException(
"Invalid email address.", nameof(value));
}
_value = value;
}
}