عنوان:

‫Domain-Driven Design (DDD)؛ بخش آخر: یک پروژه


نویسنده: وحید نصیری
تاریخ: ۱۴۰۴/۰۶/۰۸ ۰۸:۱۰
آدرس: www.dntips.ir
فصل عملی: ساختن دنیای واقعی با DDD 💻

تا به حال تمام مفاهیم و الگوهای پیچیده Domain-Driven Design (DDD) را درک کرده‌اید. از Entities و Value Objects گرفته تا Aggregates و Repository ها. اما سوال اصلی اینجاست: چگونه این قطعات پازل را کنار هم بگذاریم تا یک تصویر کامل و زیبا خلق کنیم؟
این بخش، جایی است که نظریه به عمل تبدیل می‌شود. ما قرار است یک سناریوی واقعی را از ابتدا تا انتها با DDD پیاده‌سازی کنیم. این کار به شما کمک می‌کند تا نه تنها مفاهیم را بفهمید، بلکه از آن‌ها در دنیای واقعی استفاده کنید.

سناریو: یک فروشگاه آنلاین ساده 🛒

تصور کنید که می‌خواهیم یک سیستم ساده برای یک فروشگاه آنلاین بسازیم. در نگاه اول، ممکن است یک مدل واحد برای "محصول" و "سفارش" ایجاد کنیم. اما با دیدگاه DDD، ما به سرعت متوجه می‌شویم که این سیستم شامل چندین زیرمجموعه (Sub-Domain) است.
  • Catalog: این بخش مربوط به مدیریت محصولات است.
  • Ordering: این بخش مربوط به فرآیند ثبت و مدیریت سفارش است.
  • Payments: این بخش مسئول پردازش پرداخت‌ها است.
هر کدام از این زیردامنه‌ها، زبان و مفاهیم خاص خود را دارند. بنابراین، آن‌ها را به Bounded Context های جداگانه تقسیم می‌کنیم.

گام به گام در مسیر پیاده‌سازی:

1. Context Mapping: ترسیم نقشه راه
ابتدا، روابط بین Bounded Context ها را ترسیم می‌کنیم.
  • Ordering به Catalog وابسته است تا اطلاعات محصولات را دریافت کند.
  • Ordering با Payments برای پردازش پرداخت‌ها ارتباط برقرار می‌کند.

2. مدل‌سازی در هر Bounded Context
در این مرحله، به سراغ هر Bounded Context می‌رویم و مدل دامنه‌ی آن را طراحی می‌کنیم.
Bounded Context "Catalog":
  • Aggregate Root:Product
  • Value Objects:Price, SKU
  • Repository:IProductRepository
  • Application Service:CatalogService برای مدیریت نمایش محصولات.

Bounded Context "Ordering":
  • Aggregate Root:Order
  • Entities:OrderItem
  • Value Objects:ShippingAddress
  • Repository:IOrderRepository
  • Domain Service:OrderingService برای هماهنگی فرآیند ثبت سفارش.

3. پیاده‌سازی با ASP.NET Core و EF Core
حالا، با استفاده از آموخته‌هایمان، شروع به کدنویسی می‌کنیم.
  • ساختار پروژه: پروژه را به صورت لایه‌ای (Clean Architecture) سازماندهی می‌کنیم تا هر Bounded Context در یک پروژه جداگانه قرار بگیرد.
  • نگاشت با EF Core: با استفاده از Owned Entities، private setters و Unit of Work، مدل‌های دامنه‌مان را به دیتابیس نگاشت می‌کنیم.
  • ارتباط بین Context ها: از الگوهایی مانند Domain Events برای ارتباط بین Ordering و Payments استفاده می‌کنیم تا وابستگی‌های مستقیم را کاهش دهیم.

درس‌های این بخش:

این بخش عملی به شما کمک می‌کند تا:
  • ببینید که مفاهیم DDD چگونه به صورت عملی به یکدیگر متصل می‌شوند.
  • تفاوت بین Anemic Model و Rich Model را به صورت عملی لمس کنید.
  • با چالش‌های دنیای واقعی، مانند ارتباط بین سرویس‌ها، روبرو شوید و راه‌حل‌های DDD را برای آن‌ها پیاده‌سازی کنید.
در نهایت، این بخش یک تجربه کامل از طراحی و پیاده‌سازی یک سیستم واقعی با استفاده از بهترین الگوها را در اختیار شما قرار می‌دهد. آماده باشید که به یک معمار نرم‌افزار حرفه‌ای تبدیل شوید.


معماری پروژه: Clean Architecture + CQRS

در این ساختار، پروژه به چندین لایه (پوشه) اصلی تقسیم می‌شود که هر کدام مسئولیت مشخصی دارند. قانون اصلی این است که وابستگی‌ها همیشه از لایه‌های بیرونی به سمت لایه‌های داخلی حرکت می‌کنند:

Core Project (پروژه اصلی): این لایه هسته و قلب تپنده برنامه است.
Domain: شامل تمام مفاهیم و قوانین تجاری اصلی شماست. این پوشه به هیچ چیز دیگری وابسته نیست.
  • Aggregates: پوشه‌ای برای ریشه‌های تجمیع (Aggregate Roots).
  • Entities: پوشه‌ای برای موجودیت‌ها.
  • ValueObjects: پوشه‌ای برای اشیاء ارزشی.
  • DomainEvents: پوشه‌ای برای رویدادهای دامنه.
  • DomainServices: پوشه‌ای برای سرویس‌های دامنه‌ای که منطق مربوط به چندین Aggregate را مدیریت می‌کنند.
  • Interfaces: شامل واسط‌هایی مانند IAggregateRoot و IUnitOfWork.
Application: این لایه به لایه Domain وابسته است و مسئول هماهنگی و اجرای فرآیندهای کاربردی است.
  • Commands: پوشه‌ای برای فرمان‌ها. هر فرمان، یک عملیات نوشتن را تعریف می‌کند.
  • Queries: پوشه‌ای برای پرس‌وجوها. هر پرس‌وجو، یک عملیات خواندن را تعریف می‌کند.
  • Handlers: پوشه‌ای برای CommandHandlers و QueryHandlers که منطق مربوط به هر فرمان و پرس‌وجو را مدیریت می‌کنند.
  • Interfaces: واسط‌هایی که Application Services را تعریف می‌کنند.
Common: شامل کلاس‌ها و واسط‌های مشترک مانند Entity و ValueObject.

Infrastructure Project (پروژه زیرساخت): این لایه به لایه Core وابسته است و مسئول پیاده‌سازی جزئیات فنی است.
Persistence: شامل پیاده‌سازی مربوط به دیتابیس است.
  • DbContext: کلاس اصلی DbContext برای EF Core.
  • Repositories: پیاده‌سازی واسط‌های Repository که در لایه Domain تعریف شده‌اند.
  • Configurations: پیکربندی‌های نگاشت Entities و Value Objects به دیتابیس.
UnitOfWork: پیاده‌سازی الگوی Unit of Work که DbContext را مدیریت می‌کند.
EventBus: پیاده‌سازی برای انتشار و مدیریت Domain Events.

Presentation Project (پروژه ارائه): این لایه بیرونی‌ترین لایه است و مسئول ارائه API یا UI است.
  • Controllers: کنترلرها برای API. این کنترلرها فقط با Application Services (یا Command و Query ها) صحبت می‌کنند و هیچ اطلاعی از لایه‌های داخلی‌تر ندارند.
  • Dtos: کلاس‌های DTO (Data Transfer Objects) که برای انتقال داده بین لایه ارائه و Application Services استفاده می‌شوند.

مثال عملی: ساخت یک سفارش جدید

بیایید فرآیند ثبت یک سفارش جدید را با این ساختار بررسی کنیم:
Presentation Layer (Controller): کاربر درخواستی را به یک Controller در لایه ارائه می‌فرستد.
  • OrderController یک CreateOrderCommand را از درخواست دریافت می‌کند.
  • این Controller، CreateOrderCommand را به CommandHandler مربوطه در لایه Application ارسال می‌کند.

Application Layer (CommandHandler):
  • CreateOrderCommandHandler فرمان را دریافت می‌کند.
  • این CommandHandler با استفاده از IOrderRepository، یک نمونه جدید از Aggregate Root به نام Order را ایجاد می‌کند.
  • سپس، Order را از طریق Repository به دیتابیس اضافه می‌کند.
  • در نهایت، Unit of Work را ذخیره می‌کند تا تغییرات به صورت یکپارچه در دیتابیس اعمال شوند.

Domain Layer (Order Aggregate):
  • تمام قوانین تجاری مربوط به ایجاد سفارش در داخل Order Aggregate مدیریت می‌شوند. این کلاس تضمین می‌کند که سفارش در یک وضعیت سازگار ایجاد می‌شود.

Infrastructure Layer (Repository and DbContext):
  • OrderRepository (که یک پیاده‌سازی EF Core است) مسئول ذخیره Order Aggregate در دیتابیس است. این پیاده‌سازی در لایه زیرساخت قرار دارد و به لایه‌های بالاتر وابسته نیست.

این ساختار، یک جریان یک‌طرفه و شفاف از وابستگی‌ها ایجاد می‌کند که به هر بخش اجازه می‌دهد تا بر روی مسئولیت‌های خود تمرکز کند و از پیچیدگی غیرضروری جلوگیری نماید.


در ادامه، کد مربوط به سناریوی فروشگاه آنلاین با رویکرد DDD را ارائه می‌دهیم. این کدها بر اساس مفاهیمی که پیش از این توضیح داده شد، شامل Bounded Contexts، Entities، Value Objects، Aggregates و Repository Pattern هستند. ما بر روی دو Bounded Context اصلی، یعنی Catalog و Ordering تمرکز می‌کنیم.

۱. Bounded Context: Catalog
این بخش مسئول مدیریت محصولات است.

مدل دامنه
Product (Aggregate Root)
این کلاس نماینده یک محصول است. Price و ProductStatus به عنوان Value Objects در آن استفاده می‌شوند.
namespace OnlineStore.Catalog.Domain
{
    public enum ProductStatus
    {
        Available,
        Discontinued
    }
    
    public class Price : ValueObject
    {
        public decimal Value { get; }
        public string Currency { get; }

        public Price(decimal value, string currency)
        {
            if (value < 0) throw new ArgumentException("Price cannot be negative.");
            this.Value = value;
            this.Currency = currency;
        }

        protected override IEnumerable<object> GetEqualityComponents()
        {
            yield return Value;
            yield return Currency;
        }
    }
    
    public class Product : Entity, IAggregateRoot
    {
        public string Name { get; private set; }
        public Price Price { get; private set; }
        public ProductStatus Status { get; private set; }

        // Private constructor for EF Core
        private Product() { }

        public Product(Guid id, string name, Price price)
        {
            this.Id = id;
            this.Name = name;
            this.Price = price;
            this.Status = ProductStatus.Available;
        }

        public void ChangePrice(Price newPrice)
        {
            if (newPrice == null) throw new ArgumentNullException(nameof(newPrice));
            this.Price = newPrice;
            // You could add a Domain Event here: new ProductPriceChangedEvent(this.Id, newPrice);
        }
    }
}

لایه Repository و Persistence

IProductRepository (Interface)
واسط برای Repository که در لایه دامنه قرار دارد.
namespace OnlineStore.Catalog.Domain
{
    public interface IProductRepository
    {
        Task<Product> GetByIdAsync(Guid id);
        Task AddAsync(Product product);
        // We will not have a "Remove" method as a product is typically not removed.
    }
}

ProductRepository (EF Core Implementation)
پیاده‌سازی Repository با استفاده از EF Core.
using Microsoft.EntityFrameworkCore;
using OnlineStore.Catalog.Domain;

namespace OnlineStore.Catalog.Infrastructure
{
    public class ProductRepository : IProductRepository
    {
        private readonly CatalogDbContext _context;

        public ProductRepository(CatalogDbContext context)
        {
            _context = context;
        }

        public async Task<Product> GetByIdAsync(Guid id)
        {
            return await _context.Products.SingleOrDefaultAsync(p => p.Id == id);
        }

        public async Task AddAsync(Product product)
        {
            await _context.Products.AddAsync(product);
        }
    }
}

۲. Bounded Context: Ordering
این بخش مسئول فرآیند ثبت و مدیریت سفارش است.

مدل دامنه

Order (Aggregate Root)
کلاس Order به عنوان ریشه تجمیع عمل می‌کند و OrderItem را در خود مدیریت می‌کند.
namespace OnlineStore.Ordering.Domain
{
    public enum OrderStatus
    {
        Pending,
        Paid,
        Shipped,
        Cancelled
    }

    public class OrderItem : Entity
    {
        public Guid ProductId { get; private set; }
        public string ProductName { get; private set; }
        public decimal UnitPrice { get; private set; }
        public int Units { get; private set; }

        private OrderItem() { }

        public OrderItem(Guid productId, string productName, decimal unitPrice, int units)
        {
            this.ProductId = productId;
            this.ProductName = productName;
            this.UnitPrice = unitPrice;
            this.Units = units;
        }
    }

    public class Order : Entity, IAggregateRoot
    {
        public OrderStatus Status { get; private set; }
        public Guid CustomerId { get; private set; }
        public DateTime OrderDate { get; private set; }
        
        private readonly List<OrderItem> _orderItems = new List<OrderItem>();
        public IReadOnlyList<OrderItem> OrderItems => _orderItems.AsReadOnly();

        private Order() { }

        public Order(Guid customerId)
        {
            this.Id = Guid.NewGuid();
            this.CustomerId = customerId;
            this.OrderDate = DateTime.UtcNow;
            this.Status = OrderStatus.Pending;
        }

        public void AddItem(Guid productId, string productName, decimal unitPrice, int units)
        {
            var existingItem = _orderItems.SingleOrDefault(o => o.ProductId == productId);
            if (existingItem != null)
            {
                // Logic to update existing item
            }
            else
            {
                var orderItem = new OrderItem(productId, productName, unitPrice, units);
                _orderItems.Add(orderItem);
            }
        }
        
        public void MarkAsPaid()
        {
            if (this.Status != OrderStatus.Pending)
            {
                throw new InvalidOperationException("Order cannot be marked as paid.");
            }
            this.Status = OrderStatus.Paid;
            // This is a crucial point for a Domain Event
            // AddDomainEvent(new OrderPaidEvent(this.Id, this.OrderItems));
        }
    }
}

لایه Repository و Persistence

IOrderRepository (Interface)
namespace OnlineStore.Ordering.Domain
{
    public interface IOrderRepository
    {
        Task<Order> GetByIdAsync(Guid id);
        Task AddAsync(Order order);
    }
}

OrderRepository (EF Core Implementation)
using Microsoft.EntityFrameworkCore;
using OnlineStore.Ordering.Domain;

namespace OnlineStore.Ordering.Infrastructure
{
    public class OrderRepository : IOrderRepository
    {
        private readonly OrderingDbContext _context;

        public OrderRepository(OrderingDbContext context)
        {
            _context = context;
        }

        public async Task<Order> GetByIdAsync(Guid id)
        {
            return await _context.Orders.SingleOrDefaultAsync(o => o.Id == id);
        }

        public async Task AddAsync(Order order)
        {
            await _context.Orders.AddAsync(order);
        }
    }
}

Application Service

این سرویس، پل ارتباطی با دنیای بیرون است و منطق دامنه را فراخوانی می‌کند.
using OnlineStore.Ordering.Domain;
using OnlineStore.Ordering.Infrastructure;

namespace OnlineStore.Ordering.Application
{
    public class OrderingService
    {
        private readonly IOrderRepository _orderRepository;
        // Other repositories or domain services might be injected here

        public OrderingService(IOrderRepository orderRepository)
        {
            _orderRepository = orderRepository;
        }

        public async Task CreateOrder(Guid customerId, List<OrderItemDto> items)
        {
            var order = new Order(customerId);
            foreach (var item in items)
            {
                order.AddItem(item.ProductId, item.ProductName, item.UnitPrice, item.Units);
            }
            
            await _orderRepository.AddAsync(order);
            // The DbContext SaveChanges would be handled by a Unit of Work
        }
    }
}

این کدها به شما یک دید جامع از نحوه پیاده‌سازی DDD در یک پروژه واقعی با ASP.NET Core و EF Core می‌دهد. این ساختار، جداسازی مسئولیت‌ها، تست‌پذیری و نگهداری آسان را تضمین می‌کند.

در ادامه با توجه به کدهای قبلی که برای Bounded Contextهای Catalog و Ordering ذکر شدند، کدهای مربوط به بخش CQRS را پیاده‌سازی می‌کنیم. این کدها نشان می‌دهند که چگونه یک فرمان (Command) از لایه Presentation دریافت می‌شود و توسط یک CommandHandler در لایه Application پردازش می‌شود تا منطق دامنه را فراخوانی کند.
این کدها تکمیل‌کننده ساختار Clean Architecture + CQRS است که پیش‌تر توضیح داده شد.

۱. Application Layer (Application Project)

این لایه شامل فرمان‌ها (Commands) و کنترل‌کننده‌های آن‌ها (Handlers) است.

Command
این کلاس، اطلاعات مورد نیاز برای ایجاد یک سفارش را در خود نگه می‌دارد.
// File: Application/Commands/CreateOrderCommand.cs

using OnlineStore.Ordering.Application.Dtos;
using MediatR; // We use MediatR for clean command handling

namespace OnlineStore.Ordering.Application.Commands
{
    public class CreateOrderCommand : IRequest<Guid>
    {
        public Guid CustomerId { get; set; }
        public List<OrderItemDto> Items { get; set; }
    }

    public class OrderItemDto
    {
        public Guid ProductId { get; set; }
        public string ProductName { get; set; }
        public decimal UnitPrice { get; set; }
        public int Units { get; set; }
    }
}

CommandHandler

این کلاس، منطق مربوط به پردازش فرمان CreateOrderCommand را در خود دارد. این Handler هیچ منطق تجاری‌ای ندارد و فقط Aggregates را هماهنگ می‌کند.
// File: Application/Handlers/CreateOrderCommandHandler.cs

using MediatR;
using OnlineStore.Ordering.Application.Commands;
using OnlineStore.Ordering.Domain;
using OnlineStore.Core.Domain; // Assume IUnitOfWork is here

namespace OnlineStore.Ordering.Application.Handlers
{
    public class CreateOrderCommandHandler : IRequestHandler<CreateOrderCommand, Guid>
    {
        private readonly IOrderRepository _orderRepository;
        private readonly IUnitOfWork _unitOfWork;

        public CreateOrderCommandHandler(IOrderRepository orderRepository, IUnitOfWork unitOfWork)
        {
            _orderRepository = orderRepository;
            _unitOfWork = unitOfWork;
        }

        public async Task<Guid> Handle(CreateOrderCommand request, CancellationToken cancellationToken)
        {
            // 1. Create the Order Aggregate
            var order = new Order(request.CustomerId);

            foreach (var itemDto in request.Items)
            {
                order.AddItem(
                    itemDto.ProductId,
                    itemDto.ProductName,
                    itemDto.UnitPrice,
                    itemDto.Units);
            }

            // 2. Add the Aggregate to the Repository
            await _orderRepository.AddAsync(order);

            // 3. Save changes using Unit of Work
            await _unitOfWork.SaveAsync(cancellationToken);

            return order.Id;
        }
    }
}

۲. Infrastructure Layer (Infrastructure Project)

این لایه مسئول پیاده‌سازی IUnitOfWork است.

Unit of Work
این کلاس، وظیفه مدیریت تراکنش و ذخیره‌سازی تمام تغییرات در یک عملیات واحد را بر عهده دارد.
// File: Infrastructure/UnitOfWork.cs

using OnlineStore.Ordering.Infrastructure.Context;
using OnlineStore.Core.Domain;
using System.Threading.Tasks;

namespace OnlineStore.Ordering.Infrastructure
{
    public class UnitOfWork : IUnitOfWork
    {
        private readonly OrderingDbContext _context;

        public UnitOfWork(OrderingDbContext context)
        {
            _context = context;
        }

        public async Task SaveAsync(CancellationToken cancellationToken = default)
        {
            await _context.SaveChangesAsync(cancellationToken);
        }
    }
}

۳. Presentation Layer (ASP.NET Core Web API Project)

این لایه شامل کنترلرها است که با IRequestSender (که در MediatR با نام IMediator شناخته می‌شود) صحبت می‌کنند.

API Controller
این کنترلر، مسئول دریافت درخواست HTTP و ارسال آن به CommandHandler مربوطه است.
// File: WebApi/Controllers/OrdersController.cs

using MediatR;
using Microsoft.AspNetCore.Mvc;
using OnlineStore.Ordering.Application.Commands;

namespace OnlineStore.WebApi.Controllers
{
    [ApiController]
    [Route("api/[controller]")]
    public class OrdersController : ControllerBase
    {
        private readonly IMediator _mediator;

        public OrdersController(IMediator mediator)
        {
            _mediator = mediator;
        }

        [HttpPost("create")]
        public async Task<IActionResult> CreateOrder([FromBody] CreateOrderCommand command)
        {
            var orderId = await _mediator.Send(command);
            return Ok(new { OrderId = orderId });
        }
    }
}

۴. Dependency Injection Setup

در نهایت، باید وابستگی‌ها را در فایل Program.cs یا Startup.cs پیکربندی کنیم تا MediatR و سایر سرویس‌ها به درستی کار کنند.
// File: WebApi/Program.cs (or Startup.cs)

using OnlineStore.Ordering.Application.Handlers;
using OnlineStore.Ordering.Domain;
using OnlineStore.Ordering.Infrastructure;
using OnlineStore.Ordering.Infrastructure.Context;
using MediatR;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddControllers();

// Configure MediatR
builder.Services.AddMediatR(typeof(CreateOrderCommandHandler).Assembly);

// Add DbContext
builder.Services.AddDbContext<OrderingDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("OrderingDb")));

// Register our Repositories and Unit of Work
builder.Services.AddScoped<IOrderRepository, OrderRepository>();
builder.Services.AddScoped<IUnitOfWork, UnitOfWork>();

var app = builder.Build();

// Configure the HTTP request pipeline.
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
با این کدها، یک جریان کامل از دریافت درخواست تا پردازش آن در مدل دامنه، به صورت شفاف و بر اساس اصول DDD و CQRS پیاده‌سازی شده است. این ساختار، یک نقطه شروع عالی برای پروژه‌های بزرگ و پیچیده است.

نظرات

  • رضا اسماعیلی در ۱۴۰۴/۰۶/۰۸ ۱۰:۲۷
    الف. با فرض اینکه حفظ تاریخچه نام محصول در گزارشات و اسناد سفارش ضروری نیست و تنها نمایش آخرین نام محصول کافی است، آیا رویکرد زیر یک الگوی طراحی صحیح و توصیه‌شده در معماری‌های مبتنی بر DDD و CQRS است؟
    رویکرد پیشنهادی:
    1. نام محصول (ProductName) را در OrderItem در Aggregate Order ذخیره نکنیم.
    2. در Bounded Context Ordering، یک مدل خواندن (Read Model) به نام ProductReadModel ایجاد کنیم.
    3. این ProductReadModel را با استفاده از رویدادهای دامنه (Domain Events) که از Bounded Context Catalog منتشر می‌شوند، به‌روزرسانی کنیم (الگوی Eventual Consistency).
    4. هنگام اجرای پرس‌وجوهای گزارش‌گیری (مانند OrderReportQuery)، با پیوند زدن به ProductReadModel، نام فعلی محصول را بازیابی کنیم.


    ب. با توجه به نیاز به شروع سریع پروژه و سادگی در فاز اولیه، اغلب این رویکرد مطرح می‌شود که به جای پیاده‌سازی کامل معماری توزیع‌شده از ابتدا، به‌صورت تکاملی پیش برویم. نظرتتان در مورد رویکرد زیر چیست؟
    رویکرد پیشنهادی:
    1. در فاز اول، تمام Bounded Contextها (مانند Catalog و Ordering) از یک دیتابیس مشترک استفاده کنند.
    2. به جای پیاده‌سازی رویدادهای دامنه (Domain Events)، مدل‌های خواندن (Read Model) در هر کانتکست به صورت یک نمای منطقی (Logical View) و با کوئری مستقیم از داده‌های کانتکست دیگر در دیتابیس مشترک ساخته و استفاده شوند.
    3. در آینده، با رشد و پیچیدگی پروژه، به‌تدریج رویدادهای دامنه و Handlers مربوطه را پیاده‌سازی کرده و دیتابیس‌ها را از هم جدا کنیم.

    • وحید نصیری در ۱۴۰۴/۰۶/۰۸ ۱۰:۴۵
      الف. خیر، این رویکرد یک الگوی طراحی صحیح و توصیه‌شده در معماری‌های مبتنی بر DDD و CQRS نیست. یکی از اصول اساسی در این معماری‌ها این است که هر Bounded Context باید مستقل باشد و داده‌های خود را داشته باشد. اینکه کانتکست Ordering مستقیماً از ProductReadModel در کانتکست Catalog برای یک تکه اطلاعات ساده مانند نام محصول کوئری بزند، باعث ایجاد وابستگی شدید (tight coupling) بین دو کانتکست می‌شود. این کار اصل استقلال کانتکست‌ها را نقض می‌کند.
      رویکرد صحیح در یک سیستم توزیع‌شده با DDD و CQRS یکی از این دو حالت است:
      • ذخیره نام محصول در زمان ثبت سفارش در Aggregat-ی به نام Order. این رایج‌ترین و قوی‌ترین راه‌حل است. Aggregat-ی به نام Order باید تمام داده‌های لازم برای برآورده کردن نیازمندی‌های کسب‌وکار در کانتکست Ordering را شامل شود، از جمله یک snapshot از اطلاعات محصول در زمان سفارش. این تضمین می‌کند که سفارش به عنوان یک رکورد تاریخی کامل و منسجم باقی بماند، حتی اگر نام محصول در آینده در کانتکست Catalog تغییر کند.
      • پیاده‌سازی یک میکروسرویس جداگانه برای بازیابی اطلاعات محصول در زمان سفارش. این گزینه کمتر رایج است اما هنوز هم قابل قبول است. کانتکست Ordering می‌تواند یک API اختصاصی در کانتکست Catalog را فراخوانی کند تا اطلاعات محصول مورد نیاز برای سفارش را دریافت کند. این کار همچنان تفکیک دغدغه‌ها (separation of concerns) را حفظ می‌کند، اما یک وابستگی همزمان (synchronous dependency) اضافه می‌کند که اغلب در سیستم‌های توزیع‌شده مطلوب نیست.

      ب. این رویکرد یک استراتژی معتبر و اغلب مورد استفاده است که به آن رویکرد تکاملی "Big Ball of Mud" یا "Monolithic" گفته می‌شود. این یک راه عملی برای شروع سریع یک پروژه است، در حالی که راه برای تبدیل آینده به معماری توزیع‌شده و میکروسرویس‌ها باز نگه داشته می‌شود.
      مزایا:
      • توسعه اولیه سریع‌تر: از سربار راه‌اندازی یک سیستم توزیع‌شده، Message Broker-ها و Event Handler-ها در شروع کار اجتناب می‌کنید.
      • سادگی: یک دیتابیس مشترک و کوئری‌های مستقیم، مدیریت را در مراحل اولیه پروژه و با یک تیم کوچک بسیار آسان‌تر می‌کند.
      • مسیر تکاملی: برنامه برای ریفکتورینگ در آینده، بخش کلیدی این رویکرد است. این رویکرد به شما امکان می‌دهد بپذیرید که طراحی اولیه موقت است و تکامل خواهد یافت.
      معایب:
      • پیچیدگی دیتابیس مشترک: با رشد پروژه، دیتابیس مشترک به یک گلوگاه و منبعی برای وابستگی شدید مدل‌های داده تبدیل می‌شود. تغییرات در یک کانتکست می‌تواند به طور غیرمنتظره‌ای کانتکست دیگری را مختل کند.
      • بدهی فنی (Technical Debt): شما به طور عمدی با به تعویق انداختن پیاده‌سازی Domain Events و جداسازی دیتابیس‌ها، بدهی فنی ایجاد می‌کنید. این بدهی باید بعداً پرداخت شود که می‌تواند یک کار بزرگ و دشوار باشد.
      • مقیاس‌پذیری محدود: یک دیتابیس واحد می‌تواند با مقیاس سیستم به یک گلوگاه عملکردی تبدیل شود و مقیاس‌پذیری آن دشوار است.
  • وحید نصیری در ۱۴۰۴/۰۶/۲۷ ۱۹:۴۳
    یک نمونه مثال WPF پیاده سازی شده بر اساس الگوهای MVVM، DDD، معماری تمیز و ...
  • محمد حسین کرمی در ۱۴۰۴/۰۸/۰۴ ۱۵:۵۱
    در صورتی که از Identity user خود Microsoft استفاده کرده باشیم و در سیستم مواردی مثل UserFollow یا UserList داشته باشیم، روش درست داشتن موجودیت کاربر در سیستم بدون نیاز به اینکه کتابخونه Microsoft.AspNetCore.Identity در domain نصب بشه به چه صورت هست؟
    • وحید نصیری در ۱۴۰۴/۰۸/۰۴ ۱۸:۳۹
      در طراحی دامنه‌محور (DDD) با .NET و EF Core، وقتی از ASP.NET Core Identity برای مدیریت کاربران (مانند احراز هویت، نقش‌ها و غیره) استفاده می‌کنید، هدف اصلی حفظ استقلال لایه Domain از وابستگی‌های زیرساختی مانند کتابخانه Microsoft.AspNetCore.Identity است تا لایه Domain فقط بر منطق کسب‌وکار تمرکز کند و وابستگی به فریم‌ورک‌های خارجی نداشته باشد. در ادامه، روش درست برای مدیریت موجودیت کاربر در سیستم (با مواردی مثل UserFollow یا UserList) بدون نصب کتابخانه Identity در لایه Domain بیان شده:

      - جداسازی موجودیت‌ها: دو مدل کاربر جداگانه
      • موجودیت دامنه (Domain User): در لایه Domain، یک موجودیت ساده برای کاربر تعریف کنید که فقط شامل ویژگی‌های مرتبط با منطق کسب‌وکار باشد (مانند Id، نام، و ویژگی‌های دامنه‌ای مانند لیست دنبال‌کنندگان یا لیست‌های کاربر). این موجودیت نباید به Identity وابسته باشد و می‌تواند یک Entity یا Aggregate Root باشد.
      • موجودیت احراز هویت (Identity User): در لایه Infrastructure، از کلاس IdentityUser استفاده کنید (یا آن را گسترش دهید) برای مدیریت احراز هویت، نقش‌ها و ذخیره‌سازی در دیتابیس.
      • دلیل: این جداسازی اجازه می‌دهد لایه Domain “تمیز” بماند (Separation of Concerns) و تست‌پذیر باشد، بدون وابستگی به EF Core یا Identity.

      مثال کد در لایه Domain (بدون وابستگی به Identity):
      // Domain/Entities/User.cs
      public class User : Entity<Guid> // یا AggregateRoot<Guid> اگر از Aggregate استفاده می‌کنید
      {
          public string FirstName { get; private set; }
          public string LastName { get; private set; }
          // ویژگی‌های دامنه‌ای دیگر، مانند لیست دنبال‌کنندگان
          private readonly List<UserFollow> _follows = new();
          public IReadOnlyCollection<UserFollow> Follows => _follows.AsReadOnly();
      
          // Constructor و متدهای دامنه‌ای
          private User() { } // برای EF Core
          public User(Guid id, string firstName, string lastName)
          {
              Id = id;
              FirstName = firstName;
              LastName = lastName;
          }
      
          // متد برای افزودن دنبال‌کننده (مثال)
          public void AddFollow(UserFollow follow)
          {
              // منطق دامنه‌ای، مانند چک کردن تکراری نبودن
              _follows.Add(follow);
          }
      }
      
      // Domain/Entities/UserFollow.cs (مثال برای روابط)
      public class UserFollow : Entity<Guid>
      {
          public Guid FromUserId { get; private set; }
          public Guid ToUserId { get; private set; }
          // ویژگی‌های دیگر مانند تاریخ دنبال کردن
      
          private UserFollow() { }
          public UserFollow(Guid fromUserId, Guid toUserId)
          {
              FromUserId = fromUserId;
              ToUserId = toUserId;
          }
      }
      
      // Domain/Entities/UserList.cs (مثال مشابه)
      public class UserList : Entity<Guid>
      {
          public Guid UserId { get; private set; }
          // لیست آیتم‌ها و منطق دامنه‌ای
      }
      توجه: اینجا از Guid برای Id استفاده شده چون IdentityUser پیش‌فرض از string استفاده می‌کند، اما می‌توانید Identity را برای استفاده از Guid پیکربندی کنید (با IdentityUser). در موجودیت‌های دیگر دامنه‌ی شما (مانند UserFollow و UserList)، به جای استفاده از نوع IdentityUser، فقط از شناسه (ID) کاربر (معمولاً Guid یا string بسته به پیکربندی Identity) برای ارجاع به آن استفاده کنید. این ارجاع، یک ارجاع خارجی ضعیف (Soft Foreign Key) در DDD است که از وابستگی به Aggregate Root خارجی (که در اینجا Identity User است) جلوگیری می‌کند.

      در لایه Infrastructure که EF Core و ASP.NET Core Identity در آن قرار دارند، شما نگاشت (Mapping) را به گونه‌ای تعریف می‌کنید که:
      • User دامنه شما (اگر تعریف کرده‌اید) به جدول IdentityUser نگاشت شود (معمولاً با استفاده از ToTable و پیکربندی خواص مشترک).
      • UserFollow و UserList از شناسه‌هایی مانند FollowerId و UserId برای ایجاد رابطه واقعی خارجی (Actual Foreign Key) در پایگاه داده با جدول AspNetUsers استفاده کنند.
      مثال پیکربندی EF Core (در لایه Infrastructure):
      // در لایه Infrastructure/Persistence
      public class UserFollowConfiguration : IEntityTypeConfiguration<UserFollow>
      {
          public void Configure(EntityTypeBuilder<UserFollow> builder)
          {
              // ... پیکربندی اولیه
              
              // ایجاد رابطه واقعی با AspNetUsers با استفاده از شناسه
              // فرض می‌کنیم AppUser : IdentityUser<Guid> است
              builder.HasOne<AppUser>() 
                     .WithMany()
                     .HasForeignKey(uf => uf.FromUserId) 
                     .OnDelete(DeleteBehavior.Restrict);
      
              builder.HasOne<AppUser>()
                     .WithMany()
                     .HasForeignKey(uf => uf.ToUserId)
                     .OnDelete(DeleteBehavior.Restrict);
              
              // ...
          }
      }
      در این روش، موجودیت AppUser (که از IdentityUser ارث می‌برد) در لایه Infrastructure شناخته شده است و برای پیکربندی رابطه پایگاه داده استفاده می‌شود، اما لایه Domain تنها با ID کار می‌کند و مستقل باقی می‌ماند.

      - مدیریت در لایه Infrastructure
      در این لایه، کلاس IdentityUser را گسترش دهید و آن را به دیتابیس (با EF Core) متصل کنید. DbContext را اینجا تعریف کنید تا موجودیت‌های Identity و دامنه را مدیریت کند.
      // Infrastructure/Identity/AppUser.cs
      using Microsoft.AspNetCore.Identity;
      
      public class AppUser : IdentityUser<Guid> // گسترش IdentityUser
      {
          public string FirstName { get; set; } // فیلدهای اضافی اگر لازم
          public string LastName { get; set; }
      
          // می‌توانید فیلدهای دامنه‌ای را اینجا تکرار کنید اگر برای ذخیره‌سازی نیاز باشد
      }
      
      // Infrastructure/Persistence/AppDbContext.cs
      using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
      using Microsoft.EntityFrameworkCore;
      using Domain.Entities; // وابستگی مجاز، چون Infrastructure به Domain وابسته است
      
      public class AppDbContext : IdentityDbContext<AppUser, IdentityRole<Guid>, Guid>
      {
          public DbSet<User> Users { get; set; } // موجودیت دامنه
          public DbSet<UserFollow> UserFollows { get; set; }
          public DbSet<UserList> UserLists { get; set; }
      
          public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
      
          protected override void OnModelCreating(ModelBuilder builder)
          {
              base.OnModelCreating(builder);
              // مپینگ موجودیت‌های دامنه
              builder.Entity<User>().ToTable("Users"); // مثال
              // مپینگ روابط، مانند UserFollow با کلیدهای خارجی FromUserId و ToUserId
          }
      }

      پیکربندی Identity در Program.cs یا Startup.cs (لایه Presentation):
      builder.Services.AddDbContext<AppDbContext>(options => options.UseSqlServer(connectionString));
      builder.Services.AddIdentity<AppUser, IdentityRole<Guid>>()
          .AddEntityFrameworkStores<AppDbContext>()
          .AddDefaultTokenProviders();

      - مپینگ بین موجودیت‌ها (در لایه Application)
      برای همگام‌سازی بین Domain User و AppUser، از ابزارهایی مانند AutoMapper استفاده کنید یا متدهای دستی بنویسید. این کار در لایه Application انجام می‌شود، جایی که سرویس‌ها (مانند UserService) عملیات را هماهنگ می‌کنند. مثال با AutoMapper:
      // Application/Common/Mappings/UserMappingProfile.cs
      public class UserMappingProfile : Profile
      {
          public UserMappingProfile()
          {
              CreateMap<User, AppUser>()
                  .ForMember(dest => dest.UserName, opt => opt.MapFrom(src => src.FirstName + src.LastName)); // مثال
              CreateMap<AppUser, User>(); // جهت معکوس
          }
      }
      مثال سرویس در Application:
      // Application/Users/UserService.cs
      public class UserService : IUserService
      {
          private readonly IMapper _mapper;
          private readonly UserManager<AppUser> _userManager; // تزریق از Infrastructure
          private readonly IRepository<User> _userRepository; // ریپازیتوری دامنه
      
          public async Task CreateUserAsync(CreateUserCommand command)
          {
              var domainUser = new User(Guid.NewGuid(), command.FirstName, command.LastName);
              // ذخیره در دامنه
              await _userRepository.AddAsync(domainUser);
      
              var appUser = _mapper.Map<AppUser>(domainUser);
              await _userManager.CreateAsync(appUser, command.Password);
          }
      }

      مدیریت روابط مانند UserFollow و UserList
      این موجودیت‌ها در لایه Domain تعریف می‌شوند و فقط از UserId (Guid یا string) برای روابط استفاده می‌کنند، بدون نیاز به کلاس کامل User. در کوئری‌ها، از EF Core برای جوین کردن استفاده کنید (در ریپازیتوری Infrastructure). مثال: در UserFollow، FromUserId و ToUserId کلیدهای خارجی به User.Id هستند، اما بدون وابستگی مستقیم به Identity.
      • محمد حسین کرمی در ۱۴۰۴/۰۸/۰۵ ۰۸:۳۲
        اگه درست متوجه شده باشم با این روش سمت دیتابیس یک جدول به اسم AppUser و یک جدول User ایجاد شده و بطور مثال برای افزودن کاربر جدید نیاز هست در هر دوتا یوزر جدید اضافه بشه. این مورد مشکلی نداره؟ امکان یکی کردن این دو مورد نیست؟
        • وحید نصیری در ۱۴۰۴/۰۸/۰۵ ۰۹:۲۲
          یکی از الگوهای مواجه شدن با این مساله، تعریف User به‌صورت Value Object یا Reference در دامنه‌ است. در این حالت، موجودیت کامل User در دامنه وجود ندارد. بلکه فقط به‌صورت یک Value Object ساده (مثلاً شامل UserId, DisplayName) نگه داشته می‌شود:
          // Domain/Common/UserReference.cs
          public record UserReference(Guid Id, string DisplayName);
          سپس در سایر Aggregateها مثل Post, Comment, UserFollow از همین استفاده می‌شود.
          مزیت‌ها:
          • در این حالت فقط یک جدول IdentityUser داریم (مثلاً AspNetUsers)
          • دامنه وابسته به آن نیست (چون فقط UserId را می‌شناسد).
          • نیازی به sync بین دو جدول نیست.
          • معماری کاملاً پاک (Clean) باقی می‌ماند.

          یک نمونه پیاده سازی آن:

          الگوی UserReference از نظر معماری تمیز (Clean Architecture / DDD) بهترین گزینه برای اکثر سیستم‌هاست، مخصوصاً وقتی احراز هویت فقط برای login و authorization است و دامنه فقط نیاز دارد بداند کاربر کیست (نه اینکه خودش احراز هویت را مدیریت کند).

          ساختار کلی این الگو:
          ├── Domain
          │   ├── Common
          │   │   └── UserReference.cs
          │   ├── Follows
          │   │   └── UserFollow.cs
          │   └── Posts
          │       └── Post.cs
          │
          ├── Application
          │   └── UseCases (یا Services)
          │       └── FollowUserHandler.cs
          │
          ├── Infrastructure
          │   ├── Identity
          │   │   └── ApplicationUser.cs
          │   ├── Persistence
          │   │   └── ApplicationDbContext.cs
          │   └── Repositories
          │       └── UserFollowRepository.cs
          │
          └── API
              ├── Controllers
              └── DTOs

          Domain Layer
          تعریف UserReference به‌عنوان Value Object؛ فقط مشخصات پایه کاربر که در منطق دامنه لازم است:
          // Domain/Common/UserReference.cs
          namespace MyApp.Domain.Common;
          
          public record UserReference(Guid Id, string DisplayName);

          نمونه استفاده از آن در یک Aggregate مثل UserFollow
          // Domain/Follows/UserFollow.cs
          using MyApp.Domain.Common;
          
          namespace MyApp.Domain.Follows;
          
          public class UserFollow
          {
              public Guid Id { get; private set; }
              public UserReference Follower { get; private set; }
              public UserReference Followee { get; private set; }
              public DateTime CreatedAt { get; private set; }
          
              protected UserFollow() { } // for EF
          
              public UserFollow(UserReference follower, UserReference followee)
              {
                  if (follower.Id == followee.Id)
                      throw new InvalidOperationException("Cannot follow yourself.");
          
                  Id = Guid.NewGuid();
                  Follower = follower;
                  Followee = followee;
                  CreatedAt = DateTime.UtcNow;
              }
          }

          Infrastructure Layer
          تعریف مدل Identity کاربر:
          // Infrastructure/Identity/ApplicationUser.cs
          using Microsoft.AspNetCore.Identity;
          
          namespace MyApp.Infrastructure.Identity;
          
          public class ApplicationUser : IdentityUser<Guid>
          {
              public string DisplayName { get; set; } = default!;
          }

          نگاشت به Value Object دامنه
          (در لایه Application یا Infrastructure انجام می‌شود)
          // Infrastructure/Mappers/UserMapper.cs
          using MyApp.Domain.Common;
          
          namespace MyApp.Infrastructure.Mappers;
          
          public static class UserMapper
          {
              public static UserReference ToReference(this ApplicationUser user)
                  => new(user.Id, user.DisplayName);
          }

          Application Layer (Use Case Example)
          مثلاً سناریو «دنبال کردن کاربر دیگر»:
          // Application/UseCases/FollowUserHandler.cs
          using MyApp.Domain.Common;
          using MyApp.Domain.Follows;
          using MyApp.Infrastructure.Identity;
          using MyApp.Infrastructure.Mappers;
          using Microsoft.AspNetCore.Identity;
          
          public class FollowUserHandler
          {
              private readonly UserManager<ApplicationUser> _userManager;
              private readonly IFollowRepository _followRepository;
          
              public FollowUserHandler(UserManager<ApplicationUser> userManager, IFollowRepository followRepository)
              {
                  _userManager = userManager;
                  _followRepository = followRepository;
              }
          
              public async Task HandleAsync(Guid followerId, Guid followeeId)
              {
                  var follower = await _userManager.FindByIdAsync(followerId.ToString());
                  var followee = await _userManager.FindByIdAsync(followeeId.ToString());
          
                  if (follower == null || followee == null)
                      throw new InvalidOperationException("User not found.");
          
                  var follow = new UserFollow(
                      follower.ToReference(),
                      followee.ToReference()
                  );
          
                  await _followRepository.AddAsync(follow);
              }
          }

          Persistence Layer (EF Core)
          // Infrastructure/Persistence/ApplicationDbContext.cs
          using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
          using Microsoft.EntityFrameworkCore;
          using MyApp.Infrastructure.Identity;
          using MyApp.Domain.Follows;
          
          public class ApplicationDbContext 
              : IdentityDbContext<ApplicationUser, IdentityRole<Guid>, Guid>
          {
              public DbSet<UserFollow> UserFollows { get; set; }
          
              public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
                  : base(options) { }
          
              protected override void OnModelCreating(ModelBuilder builder)
              {
                  base.OnModelCreating(builder);
          
                  builder.Entity<UserFollow>(entity =>
                  {
                      entity.HasKey(x => x.Id);
          
                      entity.OwnsOne(x => x.Follower, f =>
                      {
                          f.Property(p => p.Id).HasColumnName("FollowerId");
                          f.Property(p => p.DisplayName).HasColumnName("FollowerName");
                      });
          
                      entity.OwnsOne(x => x.Followee, f =>
                      {
                          f.Property(p => p.Id).HasColumnName("FolloweeId");
                          f.Property(p => p.DisplayName).HasColumnName("FolloweeName");
                      });
                  });
              }
          }

          نتیجه:
          • فقط یک جدول AspNetUsers وجود دارد (از Identity).
          • جدول UserFollows فقط مقادیر FollowerId و FolloweeId را نگه می‌دارد (نه FK به Domain User).
          • Domain هیچ وابستگی‌ای به Identity ندارد.
          • و برای ساختن UserFollow، از UserReference (Value Object) استفاده می‌کنید.

          مزیت‌های کلیدی:
          • استقلال دامنه: Domain هیچ وابستگی به Identity ندارد.
          • فقط یک جدول User: داده تکراری ندارید.
          • تست‌پذیر: UserReference را راحت می‌توان در تست‌ها mock کرد.
          • انعطاف: اگر احراز هویت را از Identity به JWT یا سرویس خارجی منتقل کنی، دامنه بدون تغییر می‌ماند.

        • وحید نصیری در ۱۴۰۴/۰۸/۰۵ ۱۰:۴۳
          روش دوم: پروژه‌های DDD واقعی، معمولاً دو DbContext جدا دارند:
          • IdentityDbContext برای ApplicationUser؛ فقط برای احراز هویت و مدیریت کاربر (IdentityUser)
          • AppDbContext برای موجودیت‌های دامنه (مثل UserFollow, Post, …)
          و چون هر دو به یک دیتابیس و حتی یک جدول AspNetUsers وصل‌اند، EF بدون تداخل کار می‌کند؛ با این معماری:
          ┌──────────────────────┐
          │   IdentityDbContext  │
          │  └── ApplicationUser │
          │      (IdentityUser)  │
          └──────────┬───────────┘
                     │  AspNetUsers (shared physical table)
          ┌──────────┴───────────┐
          │     AppDbContext     │
          │  └── User (Domain)   │
          │  └── UserFollow      │
          └──────────────────────┘


          یک نمونه پیاده سازی الگوی استاندارد DDD با دو DbContext جدا

          تعریف موجودیت‌ها
          ApplicationUser (در لایه Infrastructure.Identity)
          // Infrastructure/Identity/ApplicationUser.cs
          using Microsoft.AspNetCore.Identity;
          
          namespace MyApp.Infrastructure.Identity;
          
          public class ApplicationUser : IdentityUser<Guid>
          {
              public string DisplayName { get; set; } = default!;
          }

          User (در لایه Domain یا Infrastructure.Persistence)
          این مدل فقط برای دامنه است (و ممکن است فقط خواندنی باشد):
          // Domain/Users/User.cs
          namespace MyApp.Domain.Users;
          
          public class User
          {
              public Guid Id { get; private set; }
              public string DisplayName { get; private set; } = default!;
              public string Email { get; private set; } = default!;
          }

          IdentityDbContext
          // Infrastructure/Identity/AppIdentityDbContext.cs
          using Microsoft.AspNetCore.Identity;
          using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
          using Microsoft.EntityFrameworkCore;
          
          namespace MyApp.Infrastructure.Identity;
          
          public class AppIdentityDbContext : IdentityDbContext<ApplicationUser, IdentityRole<Guid>, Guid>
          {
              public AppIdentityDbContext(DbContextOptions<AppIdentityDbContext> options)
                  : base(options)
              {
              }
          
              protected override void OnModelCreating(ModelBuilder builder)
              {
                  base.OnModelCreating(builder);
          
                  // تغییر نام جدول‌ها برای تمیزی
                  builder.Entity<ApplicationUser>().ToTable("AspNetUsers");
                  builder.Entity<IdentityRole<Guid>>().ToTable("AspNetRoles");
                  builder.Entity<IdentityUserRole<Guid>>().ToTable("AspNetUserRoles");
                  builder.Entity<IdentityUserLogin<Guid>>().ToTable("AspNetUserLogins");
                  builder.Entity<IdentityUserClaim<Guid>>().ToTable("AspNetUserClaims");
                  builder.Entity<IdentityRoleClaim<Guid>>().ToTable("AspNetRoleClaims");
                  builder.Entity<IdentityUserToken<Guid>>().ToTable("AspNetUserTokens");
              }
          }

          AppDbContext (برای دامنه)
          // Infrastructure/Persistence/AppDbContext.cs
          using Microsoft.EntityFrameworkCore;
          using MyApp.Domain.Users;
          using MyApp.Domain.Follows;
          
          namespace MyApp.Infrastructure.Persistence;
          
          public class AppDbContext : DbContext
          {
              public DbSet<User> Users { get; set; } = default!;
              public DbSet<UserFollow> UserFollows { get; set; } = default!;
          
              public AppDbContext(DbContextOptions<AppDbContext> options)
                  : base(options)
              {
              }
          
              protected override void OnModelCreating(ModelBuilder builder)
              {
                  base.OnModelCreating(builder);
          
                  // نگاشت User دامنه به جدول Identity
                  builder.Entity<User>(b =>
                  {
                      b.ToTable("AspNetUsers"); // همان جدول Identity
                      b.HasKey(u => u.Id);
                      b.Property(u => u.DisplayName);
                      b.Property(u => u.Email);
                  });
          
                  builder.Entity<UserFollow>(b =>
                  {
                      b.HasKey(f => f.Id);
                      b.OwnsOne(f => f.Follower, owned =>
                      {
                          owned.Property(p => p.Id).HasColumnName("FollowerId");
                          owned.Property(p => p.DisplayName).HasColumnName("FollowerName");
                      });
          
                      b.OwnsOne(f => f.Followee, owned =>
                      {
                          owned.Property(p => p.Id).HasColumnName("FolloweeId");
                          owned.Property(p => p.DisplayName).HasColumnName("FolloweeName");
                      });
                  });
              }
          }

          Dependency Injection در Startup (یا Program.cs)
          builder.Services.AddDbContext<AppIdentityDbContext>(options =>
              options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
          
          builder.Services.AddDbContext<AppDbContext>(options =>
              options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
          هر دو Context به همان connection string وصل می‌شوند؛ اما هرکدام View مخصوص خود از جدول‌ها را دارند.

          نکته کلیدی
          • جدول AspNetUsers فقط یک‌بار در دیتابیس ساخته می‌شود (توسط Identity migration).
          • AppDbContext فقط به همان جدول map می‌شود، ولی خودش در migration جدید ایجادش نمی‌کند.
          • برای جلوگیری از تداخل، در پروژه‌ی Infrastructure.Persistence باید migrations را فقط برای AppDbContext بسازی بدون آنکه جدول User را دوباره تعریف کند.
          می‌توانی به EF بگویی که آن جدول را موجود فرض کند:
          dotnet ef migrations add Initial --context AppDbContext --ignore-changes

          مزایای این ساختار
          مزیتتوضیح
          Clean ArchitectureIdentity جدا از Domain
          فقط یک جدول Userداده تکراری یا sync لازم نیست
          استقلال migrationهاهر Context migration خودش را دارد
          انعطاف‌پذیرمی‌توانید دامنه را بدون دست‌زدن به Identity توسعه دهید
          سادگی تستمی‌توانید در تست‌ها فقط AppDbContext را mock کنید
          • محمد حسین کرمی در ۱۴۰۴/۰۸/۰۵ ۱۱:۰۱
            با توجه به مطالب قبلی دوره داشتن ریپازیتوری جدا برای مواردی که AggrigateRoot نیستن مثل UserFollow یا UserList درست هست؟ بجای record یک کلاس User در domain که متودهایی مثل AddList , AddFollower داشته باشه بهتر نیست؟
            • وحید نصیری در ۱۴۰۴/۰۸/۰۵ ۱۱:۲۴
              بستگی دارد:

              آیا Follow یا List، خودش مفهوم مستقل کسب‌وکاری دارد؟ (در سؤال تک‌سطری ابتدایی، این را مشخص نکردید)
              یعنی این‌ها باید بدون User هم وجود داشته باشند یا منطق خاصی دارند؛ سیستم بزرگ با روابط پیچیده (follow، group، like...)، در این حالت:
              • User یک Aggregate Root است.
              • UserFollow هم Aggregate Root جداگانه محسوب می‌شود.
              پس ریپازیتوری جدا برایش کاملاً درست و لازم است.

              و یا، User، مالک روابط و فهرست‌هاست.
              در این حالت:
              • User = Aggregate Root
              • UserFollow, UserList = Entities متعلق به آن است.
              پس فقط یک UserRepository داریم و رفتارها در متدهای خودش پیاده می‌شوند. UserFollow و UserList فقط از طریق User ساخته و مدیریت می‌شوند. اما اگر روابط کاربری (مثلاً دنبال‌کردن) حجم یا پیچیدگی زیادی داشته باشند، بار زیادی روی Aggregate User می‌افتد.

              در اغلب سیستم‌های واقعی:
              • User معمولاً Aggregate Root اصلی است.
              • UserFollow یا UserListدر ابتدا به‌صورت Entity داخلی پیاده می‌شوند.
              • اما اگر رشد کردند یا نیاز به query زیاد و مستقل داشتند، می‌توان آن‌ها را در آینده به Aggregate مستقل ارتقا داد (Refactoring ساده در DDD).