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 实现值对象的核心原则
- 不可变性:值对象应设计为不可变(如用
record
或readonly struct
),修改时需创建新对象。 - 无独立标识:值对象不单独存在,必须依赖实体,因此不需要
Id
属性。 - 映射策略:
- 简单值对象:用
HasConversion
映射为单个列。 - 复合值对象:用
OwnsOne
映射为实体表的多个列。 - 集合值对象:用
OwnsMany
映射为关联表。
- 简单值对象:用
- 等价性判断:值对象的相等性基于属性值(而非引用),
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);
}
}
- 聚合边界与访问控制
- 聚合根(
Order
)是外部访问的唯一入口,子实体(OrderItem
)不能被直接访问 - 通过
IReadOnlyList<OrderItem>
暴露订单项,确保外部只能通过聚合根的方法(如AddItem
)修改集合 - 数据库上下文(
AppDbContext
)只包含聚合根(Order
)的DbSet
,子实体通过聚合根间接管理
- 聚合根(
- 领域规则保护
- 所有修改聚合状态的操作(如添加商品、提交订单)都封装在聚合根内部,确保业务规则被遵守
- 例如:只有草稿状态的订单可以添加商品,订单提交前必须包含至少一个商品
- EF Core 映射策略
- 聚合根和子实体分别映射到独立表(
Orders
和OrderItems
),通过外键关联 - 值对象(
Address
、Money
)通过OwnsOne
拆分到主表的列中,不创建独立表 - 枚举(
OrderStatus
)映射为字符串,提高数据库存储的可读性
- 聚合根和子实体分别映射到独立表(
- 数据一致性保障
- 子实体依赖聚合根存在,删除订单时通过级联删除自动清理订单项
- 聚合内的所有变更通过事务统一提交,确保数据一致性