C# Academy · 课时

领域驱动设计中的记录

将值对象、DTO 和领域事件建模为记录,以确保不可变性并提升代码清晰度。

第 4 / 4 课12 个步骤

领域驱动设计中的记录 是 CoddyKit 上的免费 C# Academy 课时。 这是第 4 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 C# Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 C# Academy 课程共包含 4 节课。

DDD 与值对象

在领域驱动设计(DDD)中,值对象由其属性而非标识定义。两个 Money(100, "USD") 值可以互换。记录是用 C# 表示值对象的理想方式。

将值对象表示为记录

将领域值对象建模为记录。记录的结构相等性、不可变性和简洁语法与 DDD 原则完美契合。

// Value objects — defined by their values, not identity
public record Money(decimal Amount, string Currency);
public record Email(string Value);
public record PhoneNumber(string CountryCode, string Number);
public record Address(string Street, string City, string Country, string PostalCode);

// Two Money objects with same values are equal:
var price = new Money(99.99m, "USD");
var same  = new Money(99.99m, "USD");
Console.WriteLine(price == same); // True

在值对象构造函数中进行验证

添加紧凑构造函数来验证不变量。值对象只能在有效状态下创建——无效状态应在构造时抛出异常。

public record Email(string Value)
{
    // Compact constructor for validation
    public Email : this(Value)
    {
        if (string.IsNullOrWhiteSpace(Value) || !Value.Contains('@'))
            throw new ArgumentException("Invalid email address", nameof(Value));
        Value = Value.Trim().ToLowerInvariant();
    }
}

// Usage:
var email = new Email("  Alice@Example.COM  "); // normalized to alice@example.com
new Email("not-an-email"); // throws ArgumentException

实体与值对象

实体具有标识(其 ID 会随时间被追踪);值对象没有标识,只要值相同就可以互换。在 C# 中应以不同方式对它们建模。

// ENTITY: has identity, mutable state
public class Customer
{
    public Guid Id { get; } = Guid.NewGuid();
    public string Name { get; set; } = "";
    public Email Email { get; set; } = default!; // value object
}

// VALUE OBJECT: no identity, immutable
public record Email(string Value);
public record Money(decimal Amount, string Currency);

// Two customers with same name are DIFFERENT entities
// Two Money(100, "USD") are the SAME value

包含领域方法的丰富值对象

值对象可以通过方法包含领域逻辑。操作会返回新的值对象,而不是修改现有对象。

public record Money(decimal Amount, string Currency)
{
    public Money Add(Money other)
    {
        if (Currency != other.Currency)
            throw new DomainException("Cannot add different currencies");
        return this with { Amount = Amount + other.Amount };
    }

    public Money Subtract(Money other) =>
        Amount >= other.Amount
            ? this with { Amount = Amount - other.Amount }
            : throw new DomainException("Insufficient funds");

    public Money ApplyDiscount(decimal percent) =>
        this with { Amount = Amount * (1 - percent / 100) };

    public static Money Zero(string currency) => new(0, currency);
}

将领域事件表示为记录

领域事件描述领域中已经发生的事情。记录非常适合表示领域事件:它们不可变、描述性强,并使用结构相等性,从而简化测试。

// Domain events as records
public abstract record DomainEvent(DateTime OccurredAt);

public record OrderPlaced(
    Guid OrderId,
    Guid CustomerId,
    Money Total,
    DateTime OccurredAt) : DomainEvent(OccurredAt);

public record OrderShipped(
    Guid OrderId,
    string TrackingNumber,
    DateTime OccurredAt) : DomainEvent(OccurredAt);

public record PaymentReceived(
    Guid OrderId,
    Money Amount,
    DateTime OccurredAt) : DomainEvent(OccurredAt);

将 DTO 和 API 契约表示为记录

请求/响应 DTO 本质上是值对象——它们携带数据但没有标识。记录可以使它们简洁且不可变。

// Request DTOs
public record CreateOrderCommand(
    Guid CustomerId,
    IReadOnlyList<OrderLineDto> Lines);

public record OrderLineDto(Guid ProductId, int Quantity);

// Response DTOs
public record OrderCreatedResponse(
    Guid OrderId,
    string Status,
    decimal Total,
    DateTime CreatedAt);

// These can be compared in tests by value:
var expected = new OrderCreatedResponse(id, "Pending", 99m, now);
Assert.Equal(expected, actual);

在 EF Core 中存储值对象

使用拥有实体在 EF Core 中持久化值对象。每个值对象的属性都会存储在所有者的表中(或单独的表中),且不会有单独的 PK。

public class Order
{
    public int Id { get; set; }
    public Money Total { get; set; } = new(0, "USD");
    public Address ShipTo { get; set; } = default!;
}

// EF Core config:
modelBuilder.Entity<Order>(e =>
{
    e.OwnsOne(o => o.Total, money =>
    {
        money.Property(m => m.Amount).HasColumnType("decimal(18,2)");
        money.Property(m => m.Currency).HasMaxLength(3);
    });
    e.OwnsOne(o => o.ShipTo);
});

包含值对象的聚合根

将这些内容结合起来:使用记录表示值对象和领域事件的 Order 聚合根,并在方法中强制执行所有业务规则。

public class Order
{
    private readonly List<DomainEvent> _events = new();
    public IReadOnlyList<DomainEvent> DomainEvents => _events;

    public Guid Id { get; } = Guid.NewGuid();
    public Address ShipTo { get; private set; } = default!; // value obj
    public Money Total { get; private set; } = Money.Zero("USD");

    public void PlaceOrder(Address shipTo, IEnumerable<OrderLine> lines)
    {
        ShipTo = shipTo;
        Total  = lines.Aggregate(Money.Zero("USD"),
            (acc, l) => acc.Add(l.Price));
        _events.Add(new OrderPlaced(Id, CustomerId, Total, DateTime.UtcNow));
    }
}

实际案例:类型安全的 ID

使用记录包装基元 ID,防止意外混用不同实体的 ID——这是一种常见的 DDD 技术,称为“强类型 ID”。

// Strongly typed IDs — can't confuse CustomerId with OrderId
public record CustomerId(Guid Value)
{
    public static CustomerId New() => new(Guid.NewGuid());
    public static implicit operator Guid(CustomerId id) => id.Value;
}

public record OrderId(Guid Value)
{
    public static OrderId New() => new(Guid.NewGuid());
}

// Type safety:
void ShipOrder(OrderId orderId, CustomerId customerId) { }

ShipOrder(OrderId.New(), CustomerId.New()); // OK
ShipOrder(CustomerId.New(), OrderId.New()); // COMPILE ERROR!

快速检查

DDD 值对象由什么定义?为什么记录非常适合表示值对象?

回顾:领域驱动设计中的记录

要点:

  • 值对象:由值定义,没有标识——使用记录
  • 实体:具有标识(ID)和可变状态——使用类
  • 在紧凑构造函数中验证不变量——无效状态无法表示
  • 将领域事件表示为记录:不可变、可比较且具有自描述性
  • 使用 EF Core 拥有实体持久化值对象
  • 使用记录实现强类型 ID,防止混用实体 ID
免费开始

用 AI 导师学习 C# — 免费

在浏览器中编写并运行真实代码,获得全天候 AI 导师的即时帮助,并在网页或应用中继续学习。

课程
93
课程
346

常见问题解答

「领域驱动设计中的记录」课时是免费的吗?

是的 — 「领域驱动设计中的记录」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 C# Academy 课程的其余内容,请升级到 CoddyKit PRO。 C# Academy 课程共包含 4 节课。

「领域驱动设计中的记录」这节课中我会学到什么?

将值对象、DTO 和领域事件建模为记录,以确保不可变性并提升代码清晰度。 你通过在浏览器中直接运行的动手代码来练习 C# Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 C# Academy 需要有经验吗?

无需任何先前经验。CoddyKit 上的 C# Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 4 节课,共 4 节。

「领域驱动设计中的记录」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 C# Academy 课中编写并运行代码吗?

能。每节 C# Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

  1. 记录类型:基础与语法
  2. 使用 init 与 with 实现不可变性
  3. 值相等性与解构
  4. 领域驱动设计中的记录
← 返回 C# Academy