DDD - .net core 技术实现

贫血模型和充血模型

贫血模型:

  • 特点:领域对象只包含数据属性(字段或属性),而对这些数据的操作(业务逻辑)则放置在领域对象之外,通常在服务层或其他层中实现。可以将其理解为 “数据 + 数据访问方法” 的简单组合,领域对象本身缺乏行为,就像贫血一样,没有足够的 “活力” 。
  • 举例:以一个简单的用户管理场景为例,贫血模型下的 User 类可能如下:
class User
{
    public string UserName { get; set; }     // 用户名
    public string PasswordHash { get; set; } // 密码的哈希值
    public int Credit { get; set; }          // 积分
}

充血模型

  • 特点:领域对象不仅包含数据属性,还将对这些数据的相关业务逻辑封装在对象内部,使领域对象更加 “丰满” 和具有行为能力,符合面向对象设计中 “对象是数据和行为的统一体” 的理念。

EF CORE 充血模型实现的要求

一:属性是只读的或者是只能被类内部的代码修改。 (不需要配置EF CORE映射)

  • 使用private set确保属性只能被类内部修改(如Username { get; private set; }
  • 完全私有成员(如_passwordHash)通过类内部方法操作,外部无法直接访问
  • 领域行为(如ChangeEmail)封装修改逻辑,保证数据一致性

二:定义有参数的构造方法。(不需要配置EF CORE映射)

  • 重点:提供私有private无参数构造方法,供 EF Core 通过反射实例化对象(查询时使用)

    // 规则二:私有无参数构造方法(供EF Core反射使用,从数据库查询)
    private User() { }
    
  • 公共构造方法用于创建新实体,包含必要的业务校验(如用户名非空), 参数名和属性名一样

三:有的成员变量没有对应属性,但是这些成员变量需要映射为数据表中的列,也就是我们需要把私有成员变量映射到数据表中的列。

  • 使用HasField,完整的builder.Property<string>("PasswordHash").HasField("_passwordHash");
  • 示例中_passwordHash映射到数据库PasswordHash列,_credit映射到Credit
  • 映射配置放在实体内部的Configure方法,符合封装原则

四:有的属性是只读的,也就是它的值是从数据库中读取出来的,但是我们不能修改属性值。(不需要配置EF CORE映射)

  • Id属性使用private set,确保创建后不可修改(仅 EF Core 查询时赋值)

    public class Order
    {
        public DateTime CreateTime { get; } // 特征四:从数据库读取,外部无法修改
    
        // EF Core 会通过构造函数或反射为 CreateTime 赋值(查询时从数据库读取)
        private Order() { } 
    }
    
  • 从数据库加载后,外部无法修改这些属性,只能通过领域方法间接影响

五:有的属性不需要映射到数据列,仅在运行时被使用。

  • 既可以使用 [NotMapped] 特性,也可以使用 Ignore() fluent API 配置
  • 使用[NotMapped]特性标记不需要持久化的属性(如IsTemporaryUser
  • 这类属性通常用于计算或临时状态判断,不与数据库交互

例子:

AppDbContext.cs

using Microsoft.EntityFrameworkCore;

public class AppDbContext : DbContext
{
    public DbSet<User> Users { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlServer("Your_Connection_String");
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        // 应用User实体的配置
        User.Configure(modelBuilder);
    }
}

User.cs

using System;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;

// 充血模型实体类
public class User
{
    // 规则四:只读属性(从数据库读取,不可修改)
    public Guid Id { get; private set; }
    
    // 规则一:只读属性(仅类内部可修改)
    public string Username { get; private set; }
    
    // 规则一:只读属性(仅类内部可修改)
    public string Email { get; private set; }
    
    // 规则三:私有成员变量(需映射到数据库列,但无公开属性)
    private string _passwordHash;
    
    // 规则一:私有成员变量(仅类内部修改)
    private int _credit;
    
    // 规则五:运行时属性(不映射到数据库)
    [NotMapped]
    public bool IsTemporaryUser => Id == Guid.Empty;

    // 规则二:有参数的构造方法(用于创建新实体)
    public User(string username, string email, string password)
    {
        if (string.IsNullOrEmpty(username))
            throw new ArgumentException("用户名不能为空");
            
        Username = username;
        Email = email;
        _passwordHash = HashPassword(password); // 内部处理密码哈希
        _credit = 0; // 初始积分
    }

    // 规则二:私有无参数构造方法(供EF Core反射使用,从数据库查询)
    private User() { }

    // 领域行为:修改邮箱
    public void ChangeEmail(string newEmail)
    {
        if (string.IsNullOrEmpty(newEmail) || !newEmail.Contains("@"))
            throw new ArgumentException("无效的邮箱地址");
            
        Email = newEmail;
    }

    // 领域行为:增加积分
    public void AddCredit(int amount)
    {
        if (amount <= 0)
            throw new ArgumentException("积分必须为正数");
            
        _credit += amount;
    }

    // 领域行为:验证密码
    public bool VerifyPassword(string password)
    {
        return _passwordHash == HashPassword(password);
    }

    // 密码哈希处理(内部行为)
    private string HashPassword(string password)
    {
        // 实际项目中使用BCrypt等安全算法
        return Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(password));
    }

    // EF Core配置(映射私有成员和实体关系)
    internal static void Configure(ModelBuilder modelBuilder)
    {
        var entity = modelBuilder.Entity<User>();
        
        // 配置主键
        entity.HasKey(u => u.Id);
        
        // 规则三:映射私有成员变量到数据库列
        entity.Property<string>("PasswordHash")  // 第一个参数指定数据库列名(与数据库字段一致)
              .HasField("_passwordHash");       // 关联到类中的私有字段 _passwordHash
               
        // 映射私有成员变量_credit到数据库列Credit
        entity.Property<int>("Credit")
            .HasField("_credit"); 
        
        // 配置其他属性
        entity.Property(u => u.Username)
              .IsRequired()
              .HasMaxLength(50);
              
        entity.Property(u => u.Email)
              .IsRequired()
              .HasMaxLength(100);
        
        //规则五:运行时属性(不映射到数据库)
        //entity.Ignore(u=>u.IsTemporaryUser); //既可以使用 `[NotMapped]` 特性,也可以使用 `Ignore()` fluent API 配置
    }
}

EF CORE 配置值对象

EF Core 实现值对象的核心原则

  1. 不可变性:值对象应设计为不可变(如用 recordreadonly struct),修改时需创建新对象。
  2. 无独立标识:值对象不单独存在,必须依赖实体,因此不需要 Id 属性。
  3. 映射策略:
    • 简单值对象:用 HasConversion 映射为单个列。
    • 复合值对象:用 OwnsOne 映射为实体表的多个列。
    • 集合值对象:用 OwnsMany 映射为关联表。
  4. 等价性判断:值对象的相等性基于属性值(而非引用),record 类型自动实现这一点。

例1.简单值对象:用 HasConversion 映射为单个列。

Email.cs

public record Email
{
    public string Value { get; }

    // 私有构造函数:防止外部直接创建
    private Email(string value)
    {
        Value = value;
    }

    // 工厂方法:集中验证逻辑
    public static Email Create(string value)
    {
        // 验证邮箱格式(简单示例)
        if (string.IsNullOrEmpty(value) || !value.Contains("@"))
        {
            throw new ArgumentException("无效的邮箱格式");
        }
        return new Email(value);
    }
}

AppDbContext.cs

using Microsoft.EntityFrameworkCore;

public class AppDbContext : DbContext
{
    public DbSet<User> Users { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        // 配置数据库连接字符串(实际项目中建议放在配置文件)
        optionsBuilder.UseSqlServer("Your_Connection_String");
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        // 配置User实体中的Email值对象映射
        modelBuilder.Entity<User>(entity =>
        {
            // 将Email值对象映射到数据库的Email列
            entity.Property(u => u.Email)
                  .HasConversion(
                      // 保存到数据库时:从Email对象提取Value属性
                      email => email.Value,
                      // 从数据库读取时:通过工厂方法创建Email对象(确保验证)
                      value => Email.Create(value)
                  )
                  .HasColumnName("Email"); // 数据库列名(可省略,默认与属性名一致)
        });
    }
}

例2:复合值对象:用 OwnsOne 映射为实体表的多个列。

// 值对象:地址(多属性组合)
public record Address(
    string Province, 
    string City, 
    string Street, 
    string ZipCode)
{
    // 验证逻辑
    public Address
    {
        if (string.IsNullOrEmpty(City))
            throw new ArgumentException("城市不能为空");
        if (string.IsNullOrEmpty(ZipCode) || ZipCode.Length != 6)
            throw new ArgumentException("无效的邮编");
    }
}
// EF Core 配置:将复合值对象的属性映射到多个数据库列
public class AppDbContext : DbContext
{
    public DbSet<Customer> Customers { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Customer>(entity =>
        {
            // 配置 Address 值对象的每个属性映射到独立列
            entity.OwnsOne(
                c => c.ShippingAddress, // 指定值对象属性
                address =>
                {
                    address.Property(a => a.Province).HasColumnName("ShippingProvince");
                    address.Property(a => a.City).HasColumnName("ShippingCity");
                    address.Property(a => a.Street).HasColumnName("ShippingStreet");
                    address.Property(a => a.ZipCode).HasColumnName("ShippingZipCode");
                }
            );
        });
    }
}

例3:集合值对象:用 OwnsMany 映射为关联表。

适用于实体包含多个相同类型的值对象(如订单的多个标签、用户的多个联系方式)。

// 实体:包含多个 Tag 值对象
public class Article
{
    public Guid Id { get; private set; }
    public string Title { get; private set; }
    public List<Tag> Tags { get; private set; } = new(); // 标签列表

    public Article(Guid id, string title, IEnumerable<Tag> tags)
    {
        Id = id;
        Title = title;
        Tags.AddRange(tags);
    }

    private Article() { } // EF Core 用
}
// 值对象:标签
public record Tag(string Name)
{
    public Tag
    {
        if (string.IsNullOrEmpty(Name) || Name.Length > 20)
            throw new ArgumentException("标签无效");
    }
}
// EF Core 配置:将值对象列表映射到关联表
public class AppDbContext : DbContext
{
    public DbSet<Article> Articles { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        // 配置 Tag 列表:创建关联表 ArticleTags
        modelBuilder.Entity<Article>(entity =>
        {
            entity.OwnsMany(
                a => a.Tags, // 集合值对象属性
                tag =>
                {
                    tag.ToTable("ArticleTags"); // 关联表名
                    tag.Property(t => t.Name).HasColumnName("TagName"); // 列名
                    tag.WithOwner().HasForeignKey("ArticleId"); // 外键列
                    tag.HasKey("ArticleId", "TagName"); // 复合主键(避免重复标签)
                }
            );
        });
    }
}

EF CORE 实现聚合

在 DDD 中,聚合(Aggregate)是一组相关实体和值对象的集合,由聚合根(Aggregate Root)统一管理,确保领域规则的一致性。以订单为例,订单聚合通常包含:

  • 聚合根Order(订单),数据库上下文(AppDbContext)只包含聚合根(Order)的 DbSet,子实体通过聚合根间接管理
  • 子实体OrderItem(订单项,依赖订单存在),数据库上下文(AppDbContext)不包含子实体(OrderItem)的 DbSet,子实体通过聚合根间接管理
  • 值对象Address(地址)、Money(金额)、OrderStatus(状态枚举)

以下是 EF Core 实现订单聚合的完整示例:

Order.cs

// 订单聚合根
public class Order: IAggregateRoot
{
    // 聚合根ID
    public Guid Id { get; private set; }
    
    // 订单编号(业务唯一标识)
    public string OrderNumber { get; private set; }
    
    // 订单状态(值对象/枚举)
    public OrderStatus Status { get; private set; }
    
    // 收货地址(值对象)
    public Address ShippingAddress { get; private set; }
    
    // 订单项集合(子实体)
    private readonly List<OrderItem> _items = new();
    public IReadOnlyList<OrderItem> Items => _items.AsReadOnly();
    
    // 计算属性:订单总金额(不映射到数据库)
    public Money TotalAmount => new Money(
        _items.Sum(item => item.UnitPrice.Amount * item.Quantity),
        _items.FirstOrDefault()?.UnitPrice.Currency ?? "CNY"
    );

    // 私有构造函数(禁止外部直接创建,确保通过工厂方法)
    private Order(Guid id, string orderNumber, Address shippingAddress)
    {
        Id = id;
        OrderNumber = orderNumber;
        Status = OrderStatus.Draft;
        ShippingAddress = shippingAddress;
    }

    // 工厂方法:创建订单(确保聚合完整性)
    public static Order Create(string orderNumber, Address shippingAddress)
    {
        if (string.IsNullOrEmpty(orderNumber))
            throw new ArgumentException("订单编号不能为空");
        
        return new Order(Guid.NewGuid(), orderNumber, shippingAddress);
    }

    // 聚合内的业务行为:添加订单项
    public void AddItem(Product product, int quantity, Money unitPrice)
    {
        if (Status != OrderStatus.Draft)
            throw new InvalidOperationException("只有草稿状态的订单可以添加商品");
        
        if (quantity <= 0)
            throw new ArgumentException("数量必须大于0");
        
        _items.Add(new OrderItem(
            Guid.NewGuid(),
            product.Id,
            product.Name,
            quantity,
            unitPrice
        ));
    }

    // 聚合内的业务行为:提交订单
    public void Submit()
    {
        if (_items.Count == 0)
            throw new InvalidOperationException("订单必须包含至少一个商品");
        
        if (Status != OrderStatus.Draft)
            throw new InvalidOperationException("只有草稿状态的订单可以提交");
        
        Status = OrderStatus.Submitted;
    }

    // 聚合内的业务行为:支付订单
    public void Pay()
    {
        if (Status != OrderStatus.Submitted)
            throw new InvalidOperationException("只有已提交的订单可以支付");
        
        Status = OrderStatus.Paid;
    }

    // EF Core 映射用的无参构造函数
    private Order() { }
}

// 子实体:订单项
public class OrderItem
{
    public Guid Id { get; private set; }
    public Guid ProductId { get; private set; }
    public string ProductName { get; private set; }
    public int Quantity { get; private set; }
    public Money UnitPrice { get; private set; }

    public OrderItem(Guid id, Guid productId, string productName, int quantity, Money unitPrice)
    {
        Id = id;
        ProductId = productId;
        ProductName = productName;
        Quantity = quantity;
        UnitPrice = unitPrice;
    }

    // EF Core 用的无参构造函数
    private OrderItem() { }
}

// 值对象:地址
public record Address(
    string Province,
    string City,
    string Street,
    string ZipCode)
{
    // 验证逻辑
    public Address
    {
        if (string.IsNullOrEmpty(City))
            throw new ArgumentException("城市不能为空");
        if (string.IsNullOrEmpty(ZipCode) || ZipCode.Length != 6)
            throw new ArgumentException("无效的邮编");
    }
}

// 值对象:金额
public record Money(decimal Amount, string Currency)
{
    // 验证逻辑
    public Money
    {
        if (Amount < 0)
            throw new ArgumentException("金额不能为负数");
        if (string.IsNullOrEmpty(Currency) || Currency.Length != 3)
            throw new ArgumentException("货币代码必须是3个字符");
    }
}

// 枚举:订单状态
public enum OrderStatus
{
    Draft,       // 草稿
    Submitted,   // 已提交
    Paid,        // 已支付
    Shipped,     // 已发货
    Completed,   // 已完成
    Cancelled    // 已取消
}

// 外部实体:产品(不属于订单聚合,通过ID引用)
public class Product
{
    public Guid Id { get; set; }
    public string Name { get; set; }
}

AppDbContext.cs

using Microsoft.EntityFrameworkCore;

public class AppDbContext : DbContext
{
    // 只暴露聚合根的DbSet,子实体通过聚合根访问
    public DbSet<Order> Orders { get; set; }
    public DbSet<Product> Products { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlServer("Server=(localdb)\\mssqllocaldb;Database=OrderAggregateDb;Trusted_Connection=True;");
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        // 应用所有配置
        modelBuilder.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly);
    }
}

  1. 聚合边界与访问控制
    • 聚合根(Order)是外部访问的唯一入口,子实体(OrderItem)不能被直接访问
    • 通过 IReadOnlyList<OrderItem> 暴露订单项,确保外部只能通过聚合根的方法(如 AddItem)修改集合
    • 数据库上下文(AppDbContext)只包含聚合根(Order)的 DbSet,子实体通过聚合根间接管理
  2. 领域规则保护
    • 所有修改聚合状态的操作(如添加商品、提交订单)都封装在聚合根内部,确保业务规则被遵守
    • 例如:只有草稿状态的订单可以添加商品,订单提交前必须包含至少一个商品
  3. EF Core 映射策略
    • 聚合根和子实体分别映射到独立表(OrdersOrderItems),通过外键关联
    • 值对象(AddressMoney)通过 OwnsOne 拆分到主表的列中,不创建独立表
    • 枚举(OrderStatus)映射为字符串,提高数据库存储的可读性
  4. 数据一致性保障
    • 子实体依赖聚合根存在,删除订单时通过级联删除自动清理订单项
    • 聚合内的所有变更通过事务统一提交,确保数据一致性
posted @ 2025-09-21 14:28  【唐】三三  阅读(16)  评论(0)    收藏  举报