در توسعه نرمافزارهای تجاری، بهخصوص پروژههایی که از معماری Domain-Driven Design (DDD) و Entity Framework Core استفاده میکنند، یکی از چالشهای رایج این است:
«آیا باید Navigation Property داشته باشیم یا فقط ID ذخیره کنیم؟»
این سؤال ساده در ظاهر، اما در واقع تأثیر عمیقی روی طراحی دامنه، عملکرد، استقلال ماژولها و مرزبندی Aggregateها دارد.
در این مقاله با یک مثال واقعی (سیستم تخفیف در ماژول Booking) بررسی میکنیم که چرا و چه زمانی باید Navigation Property را نگه داریم یا حذف کنیم.
در معماری DDD، موجودیتها در Aggregateها سازماندهی میشوند.
هر Aggregate شامل:
یک Aggregate Root
موجودیتها یا Value Objectهای داخلی
Aggregateها نباید بهصورت مستقیم به Aggregateهای دیگر ارجاع شیء–به–شیء داشته باشند.
یعنی موجودیتهای یک Aggregate نباید Navigation Property به موجودیتهای Aggregate دیگر داشته باشند.
این قانون باعث:
استقلال دامنهها
کاهش coupling
جلوگیری از بارگذاریهای غیرضروری
شفافیت قوانین تجاری
میشود.
EF Core اعتقاد دارد روابط را بهتر بفهمد، بنابراین معمولاً وجود Navigation Property مثل:
public Service Service { get; set; }
به آن کمک میکند:
Lazy/Eager Loading
Include()
Mapping آسانتر به DTO
Tracking بهتر
اما در DDD، این دقیقاً چیزی است که نباید برای Aggregateهای مختلف استفاده شود.
فرض کنیم در یک سیستم رزرو خدمات (Booking System) ساختاری شبیه زیر داریم:
DiscountCard (Aggregate Root)
DiscountRule (Entity)
Service (Aggregate Root مستقل)
public class DiscountRule : BaseEntity { public Guid DiscountCardId { get; set; } public Guid? ServiceId { get; set; } // فقط شناسه public DateTime? FromDate { get; set; } public DateTime? ToDate { get; set; } public decimal? Price { get; set; } public decimal? Percent { get; set; } public DiscountCard DiscountCard { get; set; } // داخل Aggregate }
DiscountRule و DiscountCard متعلّق به یک Aggregate هستند، اما Service نه.
بنابراین Navigation زیر حذف میشود:
// public Service Service { get; set; } ❌
DiscountCard (Root) └── DiscountRule
تميیز:
DiscountRule → DiscountCard ✔ Navigation Allowed
DiscountRule ----X----> Service
چرا؟
چون:
Aggregateها باید مستقل باشند
اعمال قوانین دامنه نباید مرز Aggregateها را بشکند
EF Core نباید این دو را در یک درخت آبجکت لود کند
DiscountRule فقط شناسه Service را نگه میدارد:
public Guid? ServiceId { get; set; }
اگر Service لازم شد، در لایه Application بازیابی میشود:
var rule = await _ruleRepo.GetByIdAsync(id); var service = await _serviceRepo.GetByIdAsync(rule.ServiceId); return new DiscountRuleDto(rule, service);
Aggregate DiscountCard هیچ وابستگی به Service پیدا نمیکند.
خیر!
نوع رابطهNavigationتوضیحEntity → Root (در یک Aggregate)✔ مجازمثل DiscountRule → DiscountCardAggregate → Aggregate❌ ممنوعمثل DiscountRule → ServiceValue Object → Entity✔ مجازمشکلی نداردچند به چند میان Aggregateها❌ ممنوعباید به صورت ID طراحی شود
Aggregate رزرو (Booking) به Aggregate Service وابسته نمیشود.
Includeهای سنگین حذف میشوند.
هر Aggregate قوانین خودش را کنترل میکند.
سرویسها یا قوانین تخفیف میتوانند بدون شکستن دیگری تغییر کنند.
Aggregateها مستقلتر میشوند.
تمایز بین Navigation Property و Reference by ID یک تصمیم ساده نیست، بلکه قلب معماری DDD است.
بهصورت خلاصه:
اگر دو موجودیت داخل یک Aggregate هستند → Navigation بگذارید.
اگر دو موجودیت از دو Aggregate متفاوت هستند → Navigation را حذف کنید و فقط ID نگه دارید.
با رعایت این اصل، سیستم شما:
مقیاسپذیرتر
تمیزتر
مستقلتر
با قابلیت توسعه بیشتر
خواهد بود.