领域建模

在任何领域驱动的应用程序的核心,是表示系统核心知识 和业务规则的模型。良好的领域建模意味着将现实世界的概念转化为 具有表现力内聚性一致性的软件结构。

实体

实体是一个主要由其身份定义的对象,而不仅仅是其属性。即使属性随时间变化,实体的身份保持不变。

主要特征:

  • 具有唯一身份(通常是一个Id)。
  • 重要的是实体的是谁,而不仅仅是它包含的什么
  • 其属性可以随着时间变化。

使用 Lino 创建实体

要使用 Lino 创建新实体,请执行:

lino entity new

命令行助手将询问:

  • 服务 – 实体将被创建的服务。
  • 模块 – 实体将被创建的模块(仅限模块化服务)。
  • 实体名称 – 域和数据库表中使用的名称。

然后,您将定义构成实体的字段,并配置每个字段。

可用字段类型

类型 描述 范围 / 备注
short16位整数-32,768 → 32,767
int32位整数-2,147,483,648 → 2,147,483,647
long64位整数-9,223,372,036,854,775,808 → 9,223,372,036,854,775,807
string文本最多约20亿字符
bool布尔值truefalse
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 名称

然后定义构成该对象的字段。

可用字段类型

类型描述备注
short16 位整数-32,768 → 32,767
int32 位整数-2,147,483,648 → 2,147,483,647
long64 位整数-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 仅限于整数或字符串值。
  • 将枚举建模为类可以提供更高的灵活性和表达力。

主要特点:

  • 它们是继承自共同基类的类,封装了 IdName
  • 允许添加验证、辅助方法和行为。

使用 Lino 创建枚举

执行:

lino enumeration new

助手会询问:

  • 服务
  • 模块(如果适用)。
  • 位置 — 域或聚合的根。
  • 枚举名称
  • 类型 — 传统的 enum 或智能枚举(class)。
  • 存储方式 — 数据库中的 intstring

示例

枚举 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 之间的一致性也是领域变更的一部分。

发生了未处理的错误。 重新加载 🗙