آموزش DDD با پروژه Order
در این آموزش یاد میگیرید Domain-Driven Design و CQRS در سرویس سفارش چگونه کار میکنند.
DDD چیست؟
Domain-Driven Design (طراحی مبتنی بر دامنه) رویکردی برای ساخت نرمافزار است که در آن، منطق واقعی کسبوکار در مرکز طراحی قرار میگیرد — نه دیتابیس، نه فریمورک، نه UI. «دامنه» (Domain) همان حوزهی مسئلهای است که نرمافزار برای آن ساخته شده؛ مثلاً در یک فروشگاه آنلاین: سفارش، کالا، مشتری، موجودی انبار و قوانینی مثل «سفارش باید حداقل یک قلم داشته باشد» یا «سفارش لغوشده قابل ویرایش نیست».
این ایده را Eric Evans در کتاب «Domain-Driven Design» (۲۰۰۳) مطرح کرد. مشکل رایج در پروژههای بزرگ این است که کد بهمرور به یک مجموعهی Controller/Service/Repository تبدیل میشود که منطق کسبوکار پراکنده و تکراری است و فهم آن برای تیم سخت میشود. DDD میگوید: اول دامنه را بفهمید، بعد کد را حول آن بسازید.
چند اصل مهم DDD
- زبان مشترک (Ubiquitous Language): تیم فنی و کسبوکار از یک واژگان یکسان استفاده میکنند. اگر در جلسه میگوییم «سفارش لغو شد»، در کد هم
Cancel()داریم — نهUpdateStatus(3). - لایهبندی (Layered Architecture): Domain (قوانین خالص)، Application (هماهنگی سناریوها)، Infrastructure (دیتابیس و API) و Api (ورودی HTTP) از هم جدا میشوند.
- Entity و Value Object: Entity با Id شناخته میشود (مثل خط سفارش). Value Object با مقدارش (مثل پول یا آدرس).
- Aggregate و Aggregate Root: گروهی از اشیاء مرتبط که با هم یک واحد هستند. فقط از طریق «ریشه» (Root) قابل تغییرند — مثل سفارش و خطوط داخلش.
- Domain Event: وقتی اتفاق مهمی در کسبوکار میافتد (مثلاً «سفارش ثبت شد») آن را بهصورت رویداد ثبت میکنیم تا بخشهای دیگر بدون وابستگی مستقیم واکنش نشان دهند.
OrderAggregate هستند و Controller فقط میگوید «یک سفارش بسازید» — کد نزدیکتر به زبان کسبوکار و قابلتستتر میشود.
در این آموزش همین مفاهیم را قدمبهقدم با یک سرویس واقعی Order در .NET 8 میبینید: چهار لایه، Aggregate، CQRS، MediatR و ارتباط با میکروسرویسهای User و Product. اگر تازهکار هستید، از بخش «سرویس Order چه کاری میکند؟» شروع کنید.
۱. سرویس Order چه کاری میکند؟
سرویس Order مسئول مدیریت سفارشهاست. کارهای اصلیاش:
- ساخت سفارش جدید (با چند قلم کالا)
- دیدن یک سفارش یا همه سفارشهای یک کاربر
- لغو سفارش
اما نکتهی مهم این است: Order بهتنهایی کار نمیکند. قبل از ساخت سفارش باید بپرسد:
- آیا این کاربر وجود دارد و فعال است؟ (از سرویس User میپرسد)
- آیا این کالا موجود است و موجودی کافی دارد؟ (از سرویس Product میپرسد)
۲. چهار لایه پروژه
پروژه Order به چهار پروژه (لایه) تقسیم شده. هر لایه فقط یک مسئولیت دارد:
🟢 Order.Domain — دامنه
قوانین کسبوکار اینجاست. هیچ وابستگی به دیتابیس یا فریمورک ندارد.
شامل: OrderAggregate، OrderLine، OrderStatus، Eventها، کلاسهای پایه (Entity، AggregateRoot)
🔵 Order.Application — اپلیکیشن
سناریوها را هماهنگ میکند: «وقتی سفارش میآید، چه اتفاقاتی به چه ترتیبی بیفتد؟»
شامل: Commandها، Queryها، Handlerها، Validatorها، DTOها، Interfaceها
🟣 Order.Infrastructure — زیرساخت
پیادهسازی واقعی دیتابیس و ارتباط با سرویسهای دیگر.
شامل: OrderDbContext، Repository، Configurationها، HttpClientها
🟡 Order.Api — رابط HTTP
درخواست HTTP میگیرد و به Application تحویل میدهد.
شامل: OrdersController، Request DTOها، Program.cs
همه فلشها به سمت Domain اشاره میکنند. قلب پروژه نباید بداند دیتابیس یا API چیست.
۳. مفاهیم DDD با مثال واقعی
۳.۱ Entity (موجودیت)
Entity چیزی است که با یک شناسه (Id) شناخته میشود، نه با محتوایش. حتی اگر تمام مشخصاتش عوض شود، تا وقتی Id یکی باشد، همان موجودیت است.
مثال: یک «خط سفارش» (OrderLine). دو خط سفارش حتی اگر هر دو «۲ عدد لپتاپ» باشند، باز هم دو موجودیت متفاوتاند چون Id جدا دارند.
// کلاس پایه: هر موجودیت یک Id دارد
public abstract class Entity
{
public Guid Id { get; protected set; }
protected Entity() { }
}
// OrderLine یک Entity است
public sealed class OrderLine : Entity
{
public Guid ProductId { get; private set; }
public string ProductName { get; private set; }
public int Quantity { get; private set; }
public decimal UnitPrice { get; private set; }
public decimal LineTotal => UnitPrice * Quantity; // محاسباتی
// فقط از داخل همین assembly قابل ساخت است (internal)
internal static OrderLine Create(Guid productId, string name, int qty, decimal price)
{
if (qty <= 0) throw new ArgumentException("Quantity must be > 0");
if (price < 0) throw new ArgumentException("Price cannot be negative");
return new OrderLine { Id = Guid.NewGuid(), /* ... */ };
}
}
private هستند. یعنی از بیرون نمیتوان مقادیر را عوض کرد — فقط از طریق متدهای کنترلشده. این یعنی کپسولهسازی (Encapsulation).
۳.۲ Value Object (شیء ارزشی)
Value Object برعکس Entity است: با محتوایش شناخته میشود، نه با Id. دو Value Object با مقادیر یکسان، کاملاً برابرند.
مثال کلاسیک: پول. «۱۰۰۰ تومان» همیشه برابر «۱۰۰۰ تومان» است. در سرویس Product این را داریم:
public sealed class Money : ValueObject
{
public decimal Amount { get; }
public string Currency { get; }
// دو Money با Amount و Currency یکسان، برابرند
}
decimal ساده نگه داشته شده. اما در Product مفهوم Money بهعنوان Value Object پیاده شده تا تفاوت را ببینید.
| ویژگی | Entity | Value Object |
|---|---|---|
| شناسایی با | Id | محتوا |
| قابل تغییر؟ | بله (کنترلشده) | خیر (تغییرناپذیر) |
| مثال | OrderLine، OrderAggregate | Money، Email |
۳.۳ Aggregate و Aggregate Root
Aggregate یک گروه از اشیاء مرتبط است که با هم یک واحد را میسازند. در سرویس ما:
OrderAggregate + چند OrderLine داخلش
Aggregate Root «رئیس» این گروه است. قانون مهم:
OrderLine را دستکاری کند. باید از OrderAggregate بخواهد.
پیادهسازی را مشاهده کنید:
public sealed class OrderAggregate : AggregateRoot
{
// لیست خطوط: private است! از بیرون دیده نمیشود
private readonly List<OrderLine> _lines = [];
public Guid UserId { get; private set; }
public OrderStatus Status { get; private set; }
public decimal TotalAmount { get; private set; }
// بیرون فقط یک کپی فقط-خواندنی میبیند
public IReadOnlyCollection<OrderLine> Lines => _lines.AsReadOnly();
private OrderAggregate() { } // سازنده خصوصی
// تنها راه ساخت سفارش: این متد کارخانهای (Factory Method)
public static OrderAggregate Create(Guid userId, IEnumerable<...> items)
{
var lineItems = items.ToList();
if (lineItems.Count == 0)
throw new ArgumentException("Order must contain at least one line item.");
var order = new OrderAggregate
{
Id = Guid.NewGuid(),
UserId = userId,
Status = OrderStatus.Confirmed,
CreatedAt = DateTime.UtcNow
};
// خطوط فقط از داخل ریشه ساخته میشوند
foreach (var item in lineItems)
order._lines.Add(OrderLine.Create(item.ProductId, item.ProductName,
item.Quantity, item.UnitPrice));
order.TotalAmount = order._lines.Sum(l => l.LineTotal); // محاسبه جمع
order.RaiseDomainEvent(new OrderPlacedEvent(order.Id, userId, order.TotalAmount));
return order;
}
public void Cancel()
{
if (Status == OrderStatus.Cancelled)
throw new InvalidOperationException("Order is already cancelled.");
Status = OrderStatus.Cancelled;
UpdatedAt = DateTime.UtcNow;
RaiseDomainEvent(new OrderCancelledEvent(Id));
}
}
TotalAmount با مجموع خطوط نخواند و دیتای ناسالم بهوجود بیاید. ریشه تضمین میکند سفارش همیشه در وضعیت معتبر بماند.
کلاس پایه AggregateRoot
public abstract class AggregateRoot : Entity
{
private readonly List<IDomainEvent> _domainEvents = [];
public IReadOnlyCollection<IDomainEvent> DomainEvents => _domainEvents.AsReadOnly();
protected void RaiseDomainEvent(IDomainEvent e) => _domainEvents.Add(e);
public void ClearDomainEvents() => _domainEvents.Clear();
}
تفاوت AggregateRoot با Entity: علاوه بر Id، میتواند رویداد دامنه (Domain Event) ثبت کند. به همین خاطر OrderAggregate از AggregateRoot ارث میبرد ولی OrderLine فقط از Entity.
۳.۴ Domain Event (رویداد دامنه)
Domain Event یعنی «یک اتفاق مهم در کسبوکار افتاد». مثلاً «یک سفارش ثبت شد» یا «یک سفارش لغو شد». این رویدادها واقعیتهای گذشتهاند، پس اسمشان همیشه ماضی است.
public interface IDomainEvent
{
DateTime OccurredOn { get; } // چه زمانی اتفاق افتاد
}
public sealed class OrderPlacedEvent : IDomainEvent
{
public Guid OrderId { get; }
public Guid UserId { get; }
public decimal TotalAmount { get; }
public DateTime OccurredOn { get; }
// ...
}
وقتی سفارش ساخته میشود، داخل Create این خط اجرا میشود:
order.RaiseDomainEvent(new OrderPlacedEvent(order.Id, userId, order.TotalAmount));
_domainEvents). در پروژههای واقعی میتوان بعد از ذخیرهسازی، این رویدادها را منتشر کرد تا مثلاً ایمیل تأیید بفرستد یا به سرویس انبار خبر بدهد. این کار اتصال سست (Loose Coupling) بین بخشها میسازد.
۴. CQRS با مثال واقعی
CQRS یعنی جدا کردن نوشتن از خواندن:
| Command (فرمان) | Query (پرسوجو) | |
|---|---|---|
| کار | تغییر میدهد (نوشتن) | فقط میخواند |
| مثال | ساخت سفارش، لغو سفارش | دیدن سفارش |
| خروجی | معمولاً فقط Id یا هیچ | داده (DTO) |
برای پیادهسازی، از کتابخانهی MediatR استفاده میکنیم. کنترلر مستقیماً Handler را صدا نمیزند؛ یک پیام (Command/Query) به MediatR میدهد و MediatR خودش Handler مناسب را پیدا میکند.
۴.۱ Command (فرمان)
هر Command سه بخش در یک فایل دارد: خود فرمان، قوانین اعتبارسنجی، و پردازشگر.
// ۱) خود فرمان: فقط دادهها (یک record ساده)
public sealed record CreateOrderCommand(
Guid UserId,
IReadOnlyList<CreateOrderLineRequest> Lines) : IRequest<Guid>;
// ^^^^^^^^^^^^^ خروجی: Id سفارش
// ۲) قوانین اعتبارسنجی
public sealed class CreateOrderCommandValidator : AbstractValidator<CreateOrderCommand>
{
public CreateOrderCommandValidator()
{
RuleFor(x => x.UserId).NotEmpty();
RuleFor(x => x.Lines).NotEmpty();
RuleForEach(x => x.Lines).ChildRules(line =>
{
line.RuleFor(l => l.ProductId).NotEmpty();
line.RuleFor(l => l.Quantity).GreaterThan(0);
});
}
}
۴.۲ Handler (پردازشگر)
Handler جایی است که منطق سناریو اجرا میشود. این مهمترین قسمت سرویس Order است:
public sealed class CreateOrderCommandHandler : IRequestHandler<CreateOrderCommand, Guid>
{
private readonly IOrderRepository _orderRepository;
private readonly IUserServiceClient _userServiceClient;
private readonly IProductServiceClient _productServiceClient;
// (تزریق وابستگی از طریق سازنده)
public async Task<Guid> Handle(CreateOrderCommand request, CancellationToken ct)
{
// ۱) آیا کاربر وجود دارد؟ (تماس با سرویس User)
var user = await _userServiceClient.GetUserAsync(request.UserId, ct)
?? throw new KeyNotFoundException($"User '{request.UserId}' not found.");
// ۲) آیا کاربر فعال است؟
if (!user.IsActive)
throw new InvalidOperationException("Cannot create order for inactive user.");
// ۳) برای هر قلم، کالا را از سرویس Product دریافت کنید و موجودی را بررسی کنید
var orderItems = new List<(Guid, string, int, decimal)>();
foreach (var line in request.Lines)
{
var product = await _productServiceClient.GetProductAsync(line.ProductId, ct)
?? throw new KeyNotFoundException($"Product '{line.ProductId}' not found.");
if (product.StockQuantity < line.Quantity)
throw new InvalidOperationException(
$"Insufficient stock for '{product.Name}'.");
orderItems.Add((product.Id, product.Name, line.Quantity, product.Price));
}
// ۴) حالا که همهچیز معتبر است، سفارش را بساز (منطق در خود aggregate)
var order = OrderAggregate.Create(request.UserId, orderItems);
// ۵) ذخیره در دیتابیس
await _orderRepository.AddAsync(order, ct);
return order.Id;
}
}
OrderAggregate هستند. Handler منطق دامنه را تکرار نمیکند.
۴.۳ Query (پرسوجو)
Query فقط میخواند و یک DTO برمیگرداند (نه خود aggregate):
public sealed record GetOrderByIdQuery(Guid OrderId) : IRequest<OrderDto?>;
public sealed class GetOrderByIdQueryHandler
: IRequestHandler<GetOrderByIdQuery, OrderDto?>
{
private readonly IOrderRepository _repository;
public async Task<OrderDto?> Handle(GetOrderByIdQuery request, CancellationToken ct)
{
var order = await _repository.GetByIdAsync(request.OrderId, ct);
return order?.ToDto(); // تبدیل aggregate به DTO
}
}
public sealed record OrderDto(
Guid Id, Guid UserId, OrderStatus Status, decimal TotalAmount,
DateTime CreatedAt, DateTime? UpdatedAt, IReadOnlyList<OrderLineDto> Lines);
۴.۴ FluentValidation و ValidationBehavior
سؤال: Validator را نوشتیم، اما چه کسی آن را اجرا میکند؟ جواب: ValidationBehavior.
این یک «حلقه میانی» (Pipeline Behavior) در MediatR است. هر Command/Query قبل از رسیدن به Handler، از این حلقه عبور میکند:
public sealed class ValidationBehavior<TRequest, TResponse>
: IPipelineBehavior<TRequest, TResponse> where TRequest : notnull
{
private readonly IEnumerable<IValidator<TRequest>> _validators;
public async Task<TResponse> Handle(TRequest request,
RequestHandlerDelegate<TResponse> next, CancellationToken ct)
{
if (!_validators.Any()) return await next(); // ولیدیتوری نیست؟ رد شو
var context = new ValidationContext<TRequest>(request);
var results = await Task.WhenAll(
_validators.Select(v => v.ValidateAsync(context, ct)));
var failures = results.SelectMany(r => r.Errors).ToList();
if (failures.Count != 0)
throw new ValidationException(failures); // خطا؟ به Handler نرو
return await next(); // همهچیز اوکی؟ برو به Handler بعدی
}
}
این رفتار در DI ثبت میشود تا برای همه درخواستها فعال باشد:
services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(assembly));
services.AddValidatorsFromAssembly(assembly);
services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));
۵. جریان کامل ساخت یک سفارش
حالا همه را کنار هم بگذاریم. وقتی کلاینت یک POST /api/orders میفرستد:
CreateOrderCommand تبدیل میکند و _mediator.Send(...) صدا میزندOrderPlacedEvent ثبت میشود201 Created با Id سفارش برمیگرداندو کنترلر اینطور خطاها را به کد HTTP درست تبدیل میکند:
[HttpPost]
public async Task<IActionResult> Create([FromBody] CreateOrderRequest request, CancellationToken ct)
{
try
{
var id = await _mediator.Send(new CreateOrderCommand(
request.UserId,
request.Lines.Select(l => new CreateOrderLineRequest(l.ProductId, l.Quantity)).ToList()), ct);
return CreatedAtAction(nameof(GetById), new { id }, new { id });
}
catch (KeyNotFoundException ex) { return NotFound(new { title = ex.Message }); } // 404
catch (InvalidOperationException ex) { return Conflict(new { title = ex.Message }); } // 409
}
۶. ذخیرهسازی در دیتابیس
در OrderDbContext فقط یک DbSet داریم — برای ریشه:
public sealed class OrderDbContext : DbContext
{
public DbSet<OrderAggregate> Orders => Set<OrderAggregate>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfigurationsFromAssembly(typeof(OrderDbContext).Assembly);
base.OnModelCreating(modelBuilder);
}
}
۶.۱ EF Core چطور کلاس Configuration را پیدا میکند؟
شاید بپرسی: ما کلاس OrderAggregateConfiguration را نوشتیم، اما هیچجا new نکردیم و در DI هم ثبتش نکردیم. پس EF Core از کجا آن را میشناسد؟ جواب همین یک خط است:
modelBuilder.ApplyConfigurationsFromAssembly(typeof(OrderDbContext).Assembly);
این خط به EF Core میگوید:
Order.Infrastructure) بگردید و هر کلاسی که IEntityTypeConfiguration<T> را پیاده کرده پیدا کنید، یک نمونه از آن بسازید و متد Configure را فراخوانی کنید.»
قرارداد یک کلاس Configuration
برای اینکه EF Core یک کلاس را بهصورت خودکار پیدا کند، فقط سه شرط لازم است:
- کلاس
publicباشد - اینترفیس
IEntityTypeConfiguration<T>را پیاده کند - در همان Assembly باشد که
ApplyConfigurationsFromAssemblyروی آن صدا زده شده
// نام دلخواه قرارداد ↓ برای کدام entity ↓
public sealed class OrderAggregateConfiguration : IEntityTypeConfiguration<OrderAggregate>
{
public void Configure(EntityTypeBuilder<OrderAggregate> builder)
{
// EF Core این متد را خودش صدا میزند
}
}
EF Core پشت پرده چه میکند؟
بهصورت سادهشده، چیزی شبیه این اجرا میشود:
// شبهکد داخل EF Core
foreach (var type in assembly.GetTypes())
{
if (type implements IEntityTypeConfiguration<SomeEntity>)
{
var instance = Activator.CreateInstance(type); // ساخت نمونه
instance.Configure(modelBuilder.Entity<SomeEntity>()); // اجرای Configure
}
}
در پروژه ما دو کلاس پیدا و اجرا میشوند:
| کلاس Configuration | برای کدام entity | جدول |
|---|---|---|
| OrderAggregateConfiguration | OrderAggregate | Orders |
| OrderLineConfiguration | OrderLine | OrderLines |
کل جریان از ابتدا
OrderDbContext استفاده میشودOnModelCreating را صدا میزندApplyConfigurationsFromAssembly همه کلاسهای IEntityTypeConfiguration را اسکن میکندConfigure اجرا میشود (نگاشت جدولها، کلیدها، رابطهها)IEntityTypeConfiguration بسازی، خودکار شناسایی میشود. به همین خاطر هم OrderLineConfiguration بدون اینکه DbSet داشته باشد، باز هم اعمال میشود.
_lines درست map نمیشد، و DomainEvents و Lines ممکن بود اشتباهاً ستون دیتابیس در نظر گرفته شوند.
OrderLine یک DbSet نگذاشتیم؟
چون OrderLine یک aggregate مستقل نیست — بخشی از داخل aggregate سفارش است. در DDD فقط ریشهها DbSet میگیرند. این کار جلوی دستکاری مستقیم خطوط را میگیرد و مرز aggregate را حفظ میکند.
اما OrderLine همچنان جدول خودش (OrderLines) را دارد. EF Core از طریق رابطه آن را میشناسد:
public void Configure(EntityTypeBuilder<OrderAggregate> builder)
{
builder.ToTable("Orders");
builder.HasKey(o => o.Id);
builder.Property(o => o.TotalAmount).HasPrecision(18, 2);
// رابطه یک-به-چند با خطوط؛ از فیلد خصوصی _lines استفاده کنید
builder.HasMany<OrderLine>("_lines")
.WithOne()
.HasForeignKey("OrderId")
.OnDelete(DeleteBehavior.Cascade); // سفارش حذف شد؟ خطوطش هم برود
builder.Navigation("_lines").UsePropertyAccessMode(PropertyAccessMode.Field);
builder.Ignore(o => o.Lines); // پراپرتی فقط-خواندنی، نگاشت نمیشود
builder.Ignore(o => o.DomainEvents); // رویدادها در دیتابیس ذخیره نمیشوند
}
پس بدون DbSet چطور ذخیره میشود؟ وقتی order را اضافه میکنید، EF Core کل گراف متصل را دنبال میکند:
await _context.Orders.AddAsync(order, ct); // فقط ریشه را اضافه کردیم
await _context.SaveChangesAsync(ct); // EF خودش خطوط داخلش را هم INSERT میکند
نتیجه SQL تولیدشده:
INSERT INTO Orders (Id, UserId, Status, TotalAmount, CreatedAt) VALUES (...);
INSERT INTO OrderLines (Id, OrderId, ProductId, ProductName, Quantity, UnitPrice) VALUES (...);
INSERT INTO OrderLines (...) VALUES (...);
و موقع خواندن، با Include خطوط را هم میآوریم:
return await _context.Orders
.Include("_lines") // خطوط را هم بارگذاری کنید
.FirstOrDefaultAsync(o => o.Id == id, ct);
۷. ارتباط با سرویسهای دیگر
سرویس Order برای گرفتن اطلاعات User و Product، از طریق HTTP با آنها حرف میزند. اما لایه Application نباید بداند HTTP چیست — پس یک Interface تعریف میکنیم:
// در لایه Application: فقط قرارداد
public interface IUserServiceClient
{
Task<UserInfo?> GetUserAsync(Guid userId, CancellationToken ct = default);
}
public sealed record UserInfo(Guid Id, string Email, string FullName, bool IsActive);
و پیادهسازی واقعی HTTP در لایه Infrastructure است:
// در لایه Infrastructure: پیادهسازی واقعی
public sealed class UserServiceClient : IUserServiceClient
{
private readonly HttpClient _httpClient;
public async Task<UserInfo?> GetUserAsync(Guid userId, CancellationToken ct = default)
{
var response = await _httpClient.GetAsync($"/api/users/{userId}", ct);
if (response.StatusCode == HttpStatusCode.NotFound) return null;
response.EnsureSuccessStatusCode();
var user = await response.Content.ReadFromJsonAsync<UserApiResponse>(ct);
return user is null ? null : new UserInfo(user.Id, user.Email, user.FullName, user.IsActive);
}
}
و در DI به هم وصل میشوند:
services.AddHttpClient<IUserServiceClient, UserServiceClient>(client =>
{
client.BaseAddress = new Uri(userServiceUrl);
});
IUserServiceClient را میشناسد. اینکه پشتش HTTP باشد یا gRPC یا حتی یک Mock برای تست، مهم نیست. این باعث میشود کد قابل تست و قابل تغییر بماند.
۸. اجرا و تست
چون Order به User و Product وابسته است، بهتر است هر سه را اجرا کنی.
راه ۱: با Docker Compose (سادهتر)
docker compose up --build
راه ۲: دستی
dotnet run --project src/User/User.Api
dotnet run --project src/Product/Product.Api
dotnet run --project src/Order/Order.Api
سپس Swagger سرویس Order را باز کنید و یک سفارش بسازید:
POST /api/orders
{
"userId": "GUID یک کاربر فعال",
"lines": [
{ "productId": "GUID یک کالای موجود", "quantity": 2 }
]
}
| متد | مسیر | کار |
|---|---|---|
| POST | /api/orders | ساخت سفارش |
| GET | /api/orders/{id} | دیدن یک سفارش |
| GET | /api/orders/user/{userId} | سفارشهای یک کاربر |
| PUT | /api/orders/{id}/cancel | لغو سفارش |
جمعبندی نهایی
- Domain = قلب و قوانین (OrderAggregate، OrderLine، Eventها) — مستقل از همهچیز
- Application = هماهنگی سناریوها (Command/Query/Handler) با MediatR و FluentValidation
- Infrastructure = دیتابیس و ارتباط HTTP با سرویسهای دیگر
- Api = درِ ورودی HTTP
- Aggregate Root تضمین میکند سفارش همیشه معتبر بماند
- CQRS نوشتن و خواندن را جدا میکند
- فقط ریشه
DbSetمیگیرد؛ خطوط از طریق رابطه ذخیره میشوند
این فایل برای یادگیری DDD و CQRS با پروژه Order ساخته شده — موفق باشی! 🚀