Domain-Driven Design (DDD)؛ بخش آخر: یک پروژه
نویسنده: وحید نصیری
تاریخ: ۱۴۰۴/۰۶/۰۸ ۰۸:۱۰
آدرس: www.dntips.ir
| مطالب | ۳۶۹۴ |
| نویسندگان | ۲۷۶ |
| گروههای مطالب | ۱۰۲۴ |
| نقشههای راه | ۱۱۹ |
| دورهها | ۱۴ |
| اشتراکها | ۱۷۹۱۴ |
ProductPrice, SKUIProductRepositoryCatalogService برای مدیریت نمایش محصولات.OrderOrderItemShippingAddressIOrderRepositoryOrderingService برای هماهنگی فرآیند ثبت سفارش.Owned Entities، private setters و Unit of Work، مدلهای دامنهمان را به دیتابیس نگاشت میکنیم.IAggregateRoot و IUnitOfWork.Domain وابسته است و مسئول هماهنگی و اجرای فرآیندهای کاربردی است.CommandHandlers و QueryHandlers که منطق مربوط به هر فرمان و پرسوجو را مدیریت میکنند.Entity و ValueObject.Core وابسته است و مسئول پیادهسازی جزئیات فنی است.DbContext برای EF Core.Domain تعریف شدهاند.DbContext را مدیریت میکند.Domain Events.Application Services (یا Command و Query ها) صحبت میکنند و هیچ اطلاعی از لایههای داخلیتر ندارند.Controller در لایه ارائه میفرستد.OrderController یک CreateOrderCommand را از درخواست دریافت میکند.Controller، CreateOrderCommand را به CommandHandler مربوطه در لایه Application ارسال میکند.CreateOrderCommandHandler فرمان را دریافت میکند.CommandHandler با استفاده از IOrderRepository، یک نمونه جدید از Aggregate Root به نام Order را ایجاد میکند.Order را از طریق Repository به دیتابیس اضافه میکند.Unit of Work را ذخیره میکند تا تغییرات به صورت یکپارچه در دیتابیس اعمال شوند.Order Aggregate مدیریت میشوند. این کلاس تضمین میکند که سفارش در یک وضعیت سازگار ایجاد میشود.OrderRepository (که یک پیادهسازی EF Core است) مسئول ذخیره Order Aggregate در دیتابیس است. این پیادهسازی در لایه زیرساخت قرار دارد و به لایههای بالاتر وابسته نیست.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);
}
}
}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.
}
}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);
}
}
}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));
}
}
}namespace OnlineStore.Ordering.Domain
{
public interface IOrderRepository
{
Task<Order> GetByIdAsync(Guid id);
Task AddAsync(Order order);
}
}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);
}
}
}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
}
}
}CommandHandler در لایه Application پردازش میشود تا منطق دامنه را فراخوانی کند.Commands) و کنترلکنندههای آنها (Handlers) است.// 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; }
}
}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;
}
}
}IUnitOfWork است.// 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);
}
}
}IRequestSender (که در MediatR با نام IMediator شناخته میشود) صحبت میکنند.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 });
}
}
}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();ProductName) را در OrderItem در Aggregate Order ذخیره نکنیم.ProductReadModel ایجاد کنیم.ProductReadModel را با استفاده از رویدادهای دامنه (Domain Events) که از Bounded Context Catalog منتشر میشوند، بهروزرسانی کنیم (الگوی Eventual Consistency).OrderReportQuery)، با پیوند زدن به ProductReadModel، نام فعلی محصول را بازیابی کنیم.Ordering مستقیماً از ProductReadModel در کانتکست Catalog برای یک تکه اطلاعات ساده مانند نام محصول کوئری بزند، باعث ایجاد وابستگی شدید (tight coupling) بین دو کانتکست میشود. این کار اصل استقلال کانتکستها را نقض میکند.Order باید تمام دادههای لازم برای برآورده کردن نیازمندیهای کسبوکار در کانتکست Ordering را شامل شود، از جمله یک snapshot از اطلاعات محصول در زمان سفارش. این تضمین میکند که سفارش به عنوان یک رکورد تاریخی کامل و منسجم باقی بماند، حتی اگر نام محصول در آینده در کانتکست Catalog تغییر کند.Ordering میتواند یک API اختصاصی در کانتکست Catalog را فراخوانی کند تا اطلاعات محصول مورد نیاز برای سفارش را دریافت کند. این کار همچنان تفکیک دغدغهها (separation of concerns) را حفظ میکند، اما یک وابستگی همزمان (synchronous dependency) اضافه میکند که اغلب در سیستمهای توزیعشده مطلوب نیست.// 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; }
// لیست آیتمها و منطق دامنهای
}IdentityUser). در موجودیتهای دیگر دامنهی شما (مانند UserFollow و UserList)، به جای استفاده از نوع IdentityUser، فقط از شناسه (ID) کاربر (معمولاً Guid یا string بسته به پیکربندی Identity) برای ارجاع به آن استفاده کنید. این ارجاع، یک ارجاع خارجی ضعیف (Soft Foreign Key) در DDD است که از وابستگی به Aggregate Root خارجی (که در اینجا Identity User است) جلوگیری میکند.User دامنه شما (اگر تعریف کردهاید) به جدول IdentityUser نگاشت شود (معمولاً با استفاده از ToTable و پیکربندی خواص مشترک).UserFollow و UserList از شناسههایی مانند FollowerId و UserId برای ایجاد رابطه واقعی خارجی (Actual Foreign Key) در پایگاه داده با جدول AspNetUsers استفاده کنند.// در لایه 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/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
}
}builder.Services.AddDbContext<AppDbContext>(options => options.UseSqlServer(connectionString));
builder.Services.AddIdentity<AppUser, IdentityRole<Guid>>()
.AddEntityFrameworkStores<AppDbContext>()
.AddDefaultTokenProviders();// 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/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);
}
}// Domain/Common/UserReference.cs public record UserReference(Guid Id, string DisplayName);
Post, Comment, UserFollow از همین استفاده میشود.AspNetUsers)UserId را میشناسد).├── 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
└── DTOsUserReference بهعنوان Value Object؛ فقط مشخصات پایه کاربر که در منطق دامنه لازم است:// Domain/Common/UserReference.cs namespace MyApp.Domain.Common; public record UserReference(Guid Id, string DisplayName);
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/Identity/ApplicationUser.cs
using Microsoft.AspNetCore.Identity;
namespace MyApp.Infrastructure.Identity;
public class ApplicationUser : IdentityUser<Guid>
{
public string DisplayName { get; set; } = default!;
}// 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/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);
}
}// 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).UserFollow، از UserReference (Value Object) استفاده میکنید.IdentityDbContext برای ApplicationUser؛ فقط برای احراز هویت و مدیریت کاربر (IdentityUser)AppDbContext برای موجودیتهای دامنه (مثل UserFollow, Post, …)AspNetUsers وصلاند، EF بدون تداخل کار میکند؛ با این معماری:┌──────────────────────┐
│ IdentityDbContext │
│ └── ApplicationUser │
│ (IdentityUser) │
└──────────┬───────────┘
│ AspNetUsers (shared physical table)
┌──────────┴───────────┐
│ AppDbContext │
│ └── User (Domain) │
│ └── UserFollow │
└──────────────────────┘// Infrastructure/Identity/ApplicationUser.cs
using Microsoft.AspNetCore.Identity;
namespace MyApp.Infrastructure.Identity;
public class ApplicationUser : IdentityUser<Guid>
{
public string DisplayName { get; set; } = default!;
}// 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!;
}// 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");
}
}// 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");
});
});
}
}builder.Services.AddDbContext<AppIdentityDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));AspNetUsers فقط یکبار در دیتابیس ساخته میشود (توسط Identity migration).AppDbContext فقط به همان جدول map میشود، ولی خودش در migration جدید ایجادش نمیکند.Infrastructure.Persistence باید migrations را فقط برای AppDbContext بسازی بدون آنکه جدول User را دوباره تعریف کند.dotnet ef migrations add Initial --context AppDbContext --ignore-changes
| مزیت | توضیح |
| Clean Architecture | Identity جدا از Domain |
| فقط یک جدول User | داده تکراری یا sync لازم نیست |
| استقلال migrationها | هر Context migration خودش را دارد |
| انعطافپذیر | میتوانید دامنه را بدون دستزدن به Identity توسعه دهید |
| سادگی تست | میتوانید در تستها فقط AppDbContext را mock کنید |
User یک Aggregate Root است.UserFollow هم Aggregate Root جداگانه محسوب میشود.User = Aggregate RootUserFollow, UserList = Entities متعلق به آن است.UserFollow و UserList فقط از طریق User ساخته و مدیریت میشوند. اما اگر روابط کاربری (مثلاً دنبالکردن) حجم یا پیچیدگی زیادی داشته باشند، بار زیادی روی Aggregate User میافتد.User معمولاً Aggregate Root اصلی است.UserFollow یا UserListدر ابتدا بهصورت Entity داخلی پیاده میشوند.