领域建模
在任何领域驱动的应用程序的核心,是表示系统核心知识 和业务规则的模型。良好的领域建模意味着将现实世界的概念转化为 具有表现力、内聚性和一致性的软件结构。
实体
实体是一个主要由其身份定义的对象,而不仅仅是其属性。即使属性随时间变化,实体的身份保持不变。
主要特征:
- 具有唯一身份(通常是一个
Id)。 - 重要的是实体的是谁,而不仅仅是它包含的什么。
- 其属性可以随着时间变化。
使用 Lino 创建实体
要使用 Lino 创建新实体,请执行:
lino entity new
命令行助手将询问:
- 服务 – 实体将被创建的服务。
- 模块 – 实体将被创建的模块(仅限模块化服务)。
- 实体名称 – 域和数据库表中使用的名称。
然后,您将定义构成实体的字段,并配置每个字段。
可用字段类型
| 类型 | 描述 | 范围 / 备注 |
|---|---|---|
short | 16位整数 | -32,768 → 32,767 |
int | 32位整数 | -2,147,483,648 → 2,147,483,647 |
long | 64位整数 | -9,223,372,036,854,775,808 → 9,223,372,036,854,775,807 |
string | 文本 | 最多约20亿字符 |
bool | 布尔值 | true 或 false |
Guid | 全局唯一标识符 | 分布式唯一性 |
decimal | 高精度小数 | 适合货币值 |
float | 浮点数(32位) | 约6-9位精度 |
double | 浮点数(64位) | 约15-17位精度 |
DateTime | 日期和时间 | 包含时区 |
DateOnly | 仅日期 (C# 10+) | – |
TimeOnly | 仅时间 (C# 10+) | – |
Entity | 引用另一实体 | 1:1 或 1:N |
Value Object | 不可变的 Value Object | 例如:地址,CPF |
Enum | 枚举 | 固定值集合 |
List<Entity> | 实体列表 | 1:N |
ManyToMany | 多对多 | 需要中间表 |
示例
创建实体 Person:
┌────┬────┬───────────────┬────────┬────────┬───────────┬────────────────┐ │ PK │ FK │ Property name │ Type │ Length │ Required │ Auto-increment │ ├────┼────┼───────────────┼────────┼────────┼───────────┼────────────────┤ │ x │ │ Id │ int │ │ x │ x │ ├────┼────┼───────────────┼────────┼────────┼───────────┼────────────────┤ │ │ │ Name │ string │ 100 │ x │ │ └────┴────┴───────────────┴────────┴────────┴───────────┴────────────────┘
Lino 生成的结构:
<ProjectName>/
└── src/
└── Services/
└── <ServiceName>/
└── Domain/
├── <ProjectName>.<ServiceName>.Domain.csproj
└── Aggregates/
└── People/
├── Person.cs
├── Errors/
│ └── PersonErrors.cs
├── Repositories/
│ └── IPersonRepository.cs
└── Resources/
└── Person/
├── PersonResources.resx
├── PersonResources.en.resx
└── PersonResources.pt-BR.resx
定义完实体后,使用 Lino 管理迁移,保持数据库同步。 该过程将在持久层章节中详细说明。
当前演进工作流
除了 lino entity new 的交互式流程外,当服务、模块和名称已经明确时,CLI 也允许在命令中显式表达意图:
lino entity new --name <EntityName> --service <ServiceName> --module <ModuleName> lino entity edit --service <ServiceName> --module <ModuleName> --entity <EntityName> lino entity list --service <ServiceName> --module <ModuleName>
在大型模块中创建新概念之前,请使用 entity list。编辑期间,请检查标识符、属性、必填性、长度、关系、索引、ownership、tenant 以及对 migrations 的影响。
Strongly Typed IDs、ownership 与 invariants
启用 Strongly Typed IDs 后,标识符会变成专用类型,例如 ProductId,从而减少不同实体的 IDs 被意外混用。关系应反映真实的领域 ownership:并非每个引用都需要变成直接导航;很多情况下,标识符、shadow entity、integration event 或显式查询能更好地保护模块边界。
请将 invariants 放在领域中,而不只是放在 UI、数据库或 validators 中。修改实体后,请运行 build、检查 diff,并生成或更新 migrations。
Value Object
一个 Value Object 表示一个仅由其属性定义的领域概念 — 它没有自身的身份标识。如果两个 Value Object 的所有属性值都相同,则认为它们相等。
主要特征:
- 创建后不可变。
- 没有
Id。
使用 Lino 创建 Value Object
执行:
lino value-object new
CLI 会询问:
- 服务 – 创建对象的服务。
- 模块 – 创建对象的模块(仅限模块化服务)。
- 位置 – 域根或特定聚合根。
- Value Object 名称。
然后定义构成该对象的字段。
可用字段类型
| 类型 | 描述 | 备注 |
|---|---|---|
short | 16 位整数 | -32,768 → 32,767 |
int | 32 位整数 | -2,147,483,648 → 2,147,483,647 |
long | 64 位整数 | -9,223,372,036,854,775,808 → 9,223,372,036,854,775,807 |
string | 文本 | 最多约 20 亿字符 |
bool | 布尔值 | true/false |
decimal | 精确小数 | 货币值 |
float | 浮点数(32 位) | 约 6–9 位数字 |
double | 浮点数(64 位) | 约 15–17 位数字 |
DateTime | 日期/时间 | 包含时区 |
DateOnly | 仅日期 | C# 10 及以上 |
TimeOnly | 仅时间 | C# 10 及以上 |
示例
Value Object Address:
┌───────────────┬────────┬────────┬───────────┐ │ Property name │ Type │ Length │ Required │ ├───────────────┼────────┼────────┼───────────┤ │ Street │ string │ 100 │ x │ ├───────────────┼────────┼────────┼───────────┤ │ Number │ string │ 10 │ x │ ├───────────────┼────────┼────────┼───────────┤ │ Neighborhood │ string │ 50 │ │ ├───────────────┼────────┼────────┼───────────┤ │ City │ string │ 100 │ x │ ├───────────────┼────────┼────────┼───────────┤ │ State │ string │ 2 │ x │ ├───────────────┼────────┼────────┼───────────┤ │ PostalCode │ string │ 20 │ x │ ├───────────────┼────────┼────────┼───────────┤ │ Country │ string │ 100 │ x │ └───────────────┴────────┴────────┴───────────┘
生成的文件结构(聚合 Person):
<ProjectName>/
└── src/
└── Services/
└── <ServiceName>/
└── Domain/
├── <ProjectName>.<ServiceName>.Domain.csproj
└── Aggregates/
└── People/
├── Person.cs
├── ValueObjects/
│ └── Address.cs
├── Errors/
│ ├── AddressErrors.cs
│ └── PersonErrors.cs
├── Repositories/
│ └── IPersonRepository.cs
└── Resources/
├── Address/
│ ├── AddressResources.resx
│ ├── AddressResources.en.resx
│ └── AddressResources.pt-BR.resx
└── Person/
├── PersonResources.resx
├── PersonResources.en.resx
└── PersonResources.pt-BR.resx
与实体一样,Migrations 可以由 Lino 管理,以保持数据模型同步。
当前演进工作流
除了交互式命令外,当你已经知道该概念应在哪里创建时,请使用参数:
lino value-object new --name <ValueObjectName> --service <ServiceName> --module <ModuleName> lino value-object edit --service <ServiceName> --module <ModuleName> --value-object <ValueObjectName> lino value-object list --service <ServiceName> --module <ModuleName>
当实体属性声明为 ValueObject 时,Lino 可以在实体建模期间复用现有 Value Object,或创建新的 Value Object。当一组字段表示同一个概念时,例如金额、地址、尺寸、期间或文档,应优先使用此选项。
持久化、UI 与本地化
Lino 会通过模块的持久化层映射 Value Objects 的属性,并将显示元数据带入 resources。这允许为嵌套值提供本地化 labels 和消息。Value Object 属于生成它的模块;没有显式 integration 时,它不会成为模块之间的共享契约。
请在类型本身中保护 invariants:负值、无效货币、颠倒的期间或格式错误的文档都不应被创建为有效对象。
枚举
在领域驱动设计(DDD)中,枚举可以超越C#传统的enum。它们可以是丰富的对象,
表示固定状态,包含验证、辅助方法甚至行为。
动机:
- C# 的
enum仅限于整数或字符串值。 - 将枚举建模为类可以提供更高的灵活性和表达力。
主要特点:
- 它们是继承自共同基类的类,封装了
Id和Name。 - 允许添加验证、辅助方法和行为。
使用 Lino 创建枚举
执行:
lino enumeration new
助手会询问:
- 服务。
- 模块(如果适用)。
- 位置 — 域或聚合的根。
- 枚举名称。
- 类型 — 传统的
enum或智能枚举(class)。 - 存储方式 — 数据库中的
int或string。
示例
枚举 PersonStatus:
┌───────┬───────────┬──────────────┐ │ Value │ Name │ Display Name │ ├───────┼───────────┼──────────────┤ │ 1 │ Active │ Active │ ├───────┼───────────┼──────────────┤ │ 2 │ Inactive │ Inactive │ ├───────┼───────────┼──────────────┤ │ 3 │ Suspended │ Suspended │ ├───────┼───────────┼──────────────┤ │ 4 │ Deleted │ Deleted │ └───────┴───────────┴──────────────┘
生成的结构:
<ProjectName>/
└── src/
└── Services/
└── <ServiceName>/
└── Domain/
├── <ProjectName>.<ServiceName>.Domain.csproj
└── Aggregates/
└── People/
├── Person.cs
├── Enums/
│ └── PersonStatus.cs
├── ValueObjects/
│ └── Address.cs
├── Errors/
│ ├── AddressErrors.cs
│ └── PersonErrors.cs
├── Repositories/
│ └── IPersonRepository.cs
└── Resources/
├── Address/
│ ├── AddressResources.resx
│ ├── AddressResources.en.resx
│ └── AddressResources.pt-BR.resx
├── Person/
│ ├── PersonResources.resx
│ ├── PersonResources.en.resx
│ └── PersonResources.pt-BR.resx
└── PersonStatus/
├── PersonStatusResources.resx
├── PersonStatusResources.en.resx
└── PersonStatusResources.pt-BR.resx
将枚举值存储为 string 是可行的,且能提升可读性,但通常在性能和存储方面效率较低。
因此建议使用 int 存储,并为枚举创建辅助实体(表),其主键对应枚举值,以保证引用完整性并简化维护。
定义枚举后,使用 Lino 生成并应用 Migrations,确保数据库反映领域模型。 详细内容见持久层章节。
当前演进工作流
除了交互式命令外,当位置和名称已经定义时,请使用参数:
lino enumeration new --name <EnumerationName> --service <ServiceName> --module <ModuleName> lino enumeration edit --service <ServiceName> --module <ModuleName> --enumeration <EnumerationName> lino enumeration list --service <ServiceName> --module <ModuleName>
Enumeration、实体或配置
| 使用 enumeration | 使用实体或配置 |
|---|---|
| 订单状态、发布状态、技术集成类型 | 用户可管理的类别、tenant 可配置的原因、在 backoffice 中管理的供应商 |
| 随代码一起版本化的值 | 在 runtime 中更改或由权限控制的值 |
| 每个值对应的简单规则或小型行为 | 生命周期、audit、动态翻译或自身关系 |
如果 enumeration 暴露显示名称,Lino 可以为本地化 labels 生成 resources。如果它被持久化或用作 seed data,请在添加、删除或重命名值之后检查 migrations。代码、数据库和 UI 之间的一致性也是领域变更的一部分。
