管理数据持久性
Lino 中的数据持久化围绕 Clean Architecture 边界生成:领域模型保持独立于 Entity Framework Core,而数据库相关关注点位于 Infrastructure.Persistence。这让实体专注于业务规则,并把映射、provider 配置、migrations、repositories、事务和初始化放在基础设施层。
使用 lino service new 创建服务时,Lino 会记录重要的持久化决策,例如服务架构和数据 provider。当前 provider 选项是 SqlServer 和 PostgreSql。在传统服务中,持久化直接生成在服务下方。在模块化服务中,每个模块都会获得自己的 Infrastructure.Persistence 项目和自己的 ApplicationDbContext,而数据库仍由该服务共享。
无论传统服务还是模块化服务,每个服务只有一个数据库。在模块化服务中,每个模块都会映射到同一数据库内不同的 schema。这让每个 bounded context 保持独立的逻辑 namespace,具备功能隔离、更清晰的版本演进和更有组织的 migrations。
本页说明 Lino 如何组织 Entity Framework Core 配置、DbContext 注册、repositories、IUnitOfWork、事务、Transactional Outbox 流程,以及 CLI 暴露的 migrations 生命周期。
重要:生成的持久化代码是面向生产的 scaffolding。在把变更应用到共享数据库或生产数据库之前,始终复查映射、constraints、索引、删除行为、事务和 migration scripts。
Entity Type Configurations
Lino 遵循 Persistence-Ignorant 原则:领域实体不了解基础设施细节。所有 ORM 映射都放在实现 IEntityTypeConfiguration<TEntity> 的类中,位置为 Infrastructure.Persistence/Configurations。这样,实体表达业务行为,而基础设施定义它们如何被存储。
配置文件会根据 CLI 中做出的决定生成:primary keys、strongly typed IDs、必填字段、字符串长度、Value Objects、关系、枚举、审计字段、tenant fields 和子实体。生成结果应视为一个扎实的起点,而不是永远不需要复查的内容。
配置通常定义什么
- 表和 schema:当前服务或模块的表名和 schema。
- Primary keys:实体标识符、strongly typed IDs 转换以及 key 生成行为。
- 属性:必填字段、最大长度、列类型、enum 持久化、Value Objects 的 owned structures,以及文件 metadata 列。
- 关系:one-to-one、one-to-many、many-to-many、child collections、foreign keys 和删除行为。
- 索引和 constraints:唯一性、lookup 性能、tenant-aware 唯一性以及数据库层面的约束一致性。
全局约定
重复约定可以集中在 ModelConfiguration 或等效 helper 中,例如 decimal precision、collation、DateTime 转换、naming convention、global filters、可审计属性以及 multi-tenant 字段的通用规则。
配置如何应用
生成的 ApplicationDbContext 会在 OnModelCreating 中应用配置。默认情况下,Lino 使用 Source Generators 注册配置,以获得更好的性能并避免使用 Reflection。使用 assembly scanning 时,配置可能如下所示:
modelBuilder.ApplyConfigurationsFromAssembly(typeof(ApplicationDbContext).Assembly);
启用消息传递时,Lino 还会应用 OutboxMessage 配置,使 integration events 在 worker 异步发布之前,能够和数据库操作持久化在同一个事务中。
复查清单
- 确认表名、schemas 和约定符合模块边界。
- 在生成 migrations 之前,复查字符串长度、decimal precision、nullable 列以及 enum 持久化。
- 仔细复查关系和 delete behavior,尤其是 aggregate roots 和子实体。
- 为 grids、filters、integrations 和 background jobs 使用的 queries 添加或调整索引。
- 创建 migration 之前先运行 build,确保 EF Core 看到的是一致的模型。
DbContexts
生成的 ApplicationDbContext 是服务或模块持久化边界的 EF Core context。它为已映射实体公开 DbSet<TEntity> 属性,并实现 IApplicationDbContext,使 query handlers 和 application services 可以依赖应用层抽象,而不是依赖基础设施中的具体类。
传统服务和模块化服务
- 传统服务:持久化生成在
src/Services/<ServiceName>/Infrastructure.Persistence,服务 API 用作 migrations 的 startup project。 - 模块化服务:每个模块都有自己的
src/Services/<ServiceName>/Modules/<ModuleName>/Infrastructure.Persistence项目和自己的ApplicationDbContext。服务 host 用作 migrations 的 startup project。
在模块化服务中,这种分离让每个 bounded context 在代码中显式可见。即使按所选架构运行在同一个服务数据库中,各模块也可以独立演进自己的持久化模型。
Provider 注册和配置
Lino 通过 IHostApplicationBuilder 扩展生成持久化注册。生成的代码会注册 Unit of Work、repositories、domain services、ApplicationDbContext 的 pooled factory、scoped ApplicationDbContext 以及 IApplicationDbContext 抽象。
provider 的具体配置会根据服务选择的数据库生成:
- SQL Server 服务使用
UseSqlServer(...)。 - PostgreSQL 服务使用
UseNpgsql(...).UseSnakeCaseNamingConvention()。 - 使用
MigrationsHistoryTable(Constants.Database.MigrationsHistoryTable, Constants.Database.Schema)将 migrations history 存储在预期的表和 schema 中。 - 会注册 provider-specific constraint validators,用于将数据库约束违规转换为一致的应用错误。
Tenant-aware contexts
当模块包含 tenant-aware 实体时,生成的 context 可以包含 tenant 状态和全局 query filters。在这种场景下,scoped factory 会为当前 scope 创建已配置的 context,避免 handlers 在每个读写位置手动重复 tenant filters。
Repositories
Lino 在领域层生成 repository interfaces,并在 Infrastructure.Persistence/Repositories 中生成具体实现。这保留了依赖方向:领域定义自己需要的持久化 contract,基础设施用 Entity Framework Core 提供实现。
根据服务结构,具体 repositories 也可能出现在 <ModuleName>/Infrastructure.Persistence.Repositories,并实现 <ModuleName>/Domain.Repositories 中的接口。重点是 application 和 domain 不直接依赖 EF Core 的具体细节。
Repositories 主要由 command handlers 和需要持久化 aggregates 的 domain-oriented flows 使用。当需要优化读取 projection、filters、pagination 和 DTO 格式结果时,Query Handlers 可以直接使用 IApplicationDbContext。
Repository responsibilities
- 加载 command handlers 所需的 aggregate roots 和子数据。
- 根据持久化模型添加、更新和移除 aggregates。
- 封装属于 aggregate 行为或写入编排的持久化查询。
- 让 EF Core 特定操作留在 domain project 之外。
- 当复杂查询属于写入流程时封装它们,包括 LINQ、
FromSql和辅助 projections。
实践指南
- 对于需要保持 aggregate 一致性的写入 use cases,使用 repositories。
- 读取时使用 query projections,而不是仅为组装 DTO 加载完整 aggregates。
- 保持 repository 方法显式;避免暴露任意持久化操作给 application 的泛型方法。
- 当 handler 修改 aggregate、child collection 或 many-to-many relationship 时,复查 includes 和 tracking。
- 只暴露 domain 和 application 需要的方法,让 repository 保持 aggregate-root-centric。
Unit of Work
IUnitOfWork 是 Lino 生成的事务边界。它协调 EF Core 持久化、事务控制、domain events 发布,以及 integration events 的 Outbox 持久化。Handlers 使用这个抽象显式提交变更,而不是在 application 的每个位置直接调用 DbContext.SaveChangesAsync。
在简单场景中,DbContext 本身通常已经足够作为 Unit of Work。即便如此,Lino 仍会生成专用的 UnitOfWork 实现,以便对事务、事件和 Outbox 集成提供一致控制。
生成的操作
SaveChangesAsync(cancellationToken):保存变更,并在配置后发布 domain events。SaveChangesAsync(publishDomainEvents, cancellationToken):允许在发布或不发布 domain events 的情况下保存。SaveChangesInTransactionAsync(cancellationToken):开启事务、保存变更并提交。BeginTransactionAsync、CommitAsync和RollbackAsync:允许显式事务控制。CommitOrRollbackAsync:根据Result提交或回滚事务。
Domain events 和 Outbox
当存在 domain events 时,Lino 要求在发布它们之前已经开启事务。这是有意设计的。如果 domain event 产生 integration events,这些 events 会记录到 Outbox,并在同一个事务中持久化。之后,worker 可以处理 Outbox records,并通过已配置的消息基础设施发布消息。
这个流程保护一致性:当 command 创建会引发事件的实体且项目使用 Outbox 时,handler 应通过事务保存,例如使用 SaveChangesInTransactionAsync,或显式开启并提交事务。没有该事务时,基础设施应失败,而不是允许不可靠的事件流程继续。
何时使用每种 save 风格
- 当直接持久化且不要求 events 与 Outbox 之间保持一致性时,使用
SaveChangesAsync。 - 当 domain events 和 Outbox 持久化必须属于同一个 commit 时,使用
SaveChangesInTransactionAsync。 - 当 handler 有多个步骤,并且需要根据
Result决定最终结果时,使用BeginTransactionAsync、CommitAsync和RollbackAsync。
经验规则:如果写入操作发布的 domain events 可能产生 integration events,请把数据库变更和 Outbox records 保持在同一个事务中。
Managing Migrations
Lino 使用 CLI commands 封装 Entity Framework Core 的 migrations 生命周期,这些命令了解 service、module、architecture、provider、DbContext、startup project、版本文件以及 scripts 输出位置。这减少了手写命令出错,并让 migration metadata 由 Lino 项目模型跟踪。
Migrations 会根据 entities、Value Objects、关系、索引和 Entity Framework 配置的变化记录数据库演进。创建 migration 之前,请编译 solution,或至少编译受影响的服务,以便 EF Core 加载当前模型。
创建 migration
lino database migrations add --service <ServiceName> --module <ModuleName>
该命令会请求 service、module、src/Services/<ServiceName>/version.txt 中的当前服务版本,以及 migration 描述。Lino 会使用版本和序列生成名称,然后为正确的持久化项目和 startup project 执行 dotnet ef migrations add。
Lino 还会使用 dotnet ef migrations script 生成 migration 的 SQL script。该 script 会写入受影响持久化项目下的版本化文件夹,遵循以下模式:
Infrastructure.Persistence/Scripts/<Version>/<Sequence>_<Description>.sql Infrastructure.Persistence/Scripts/v1.2.3/001_AddCustomerIsActive.sql
注意:除了 Entity Framework 生成的 .cs 文件外,Lino 还会生成包含 DDL 指令的对应 .sql script。这便于基础设施团队、DBA 和受控 deploy 流程审查。
应用、列出、回滚和移除
lino database migrations add --service <ServiceName> --module <ModuleName> lino database migrations list --service <ServiceName> --module <ModuleName> lino database migrations apply --service <ServiceName> --module <ModuleName> lino database migrations revert --service <ServiceName> --module <ModuleName> lino database migrations remove --service <ServiceName> --module <ModuleName>
add:为模型中的待处理变更创建 migration。list:列出所选 context 已知的 migrations。apply:执行dotnet ef database update,把 migrations 应用到已配置数据库。revert:在流程允许时,回退最后一次成功的 migration 路径。remove:在适用时移除最后创建的 migration 记录和生成的 EF 文件。
canonical command group 是 database migrations。db 和 migration 等 aliases 可能为了效率而存在,但文档应优先使用完整形式。
Lino 为你选择什么
- 在传统服务中,EF project 是
src/Services/<ServiceName>/Infrastructure.Persistence,startup project 是src/Services/<ServiceName>/Api。 - 在模块化服务中,EF project 是
src/Services/<ServiceName>/Modules/<ModuleName>/Infrastructure.Persistence,startup project 是src/Services/<ServiceName>/Host。 - context 是为所选 service 或 module 生成的
ApplicationDbContext。 - EF migrations history table 使用
Constants.Database.MigrationsHistoryTable和Constants.Database.Schema配置。
推荐 workflow
- 建模或修改 entities、Value Objects、关系、索引或持久化配置。
- 生成 migration 之前运行 build 并修复编译错误。
- 为正确的 service 和 module 执行
lino database migrations add。 - 应用之前复查生成的 C# migration 和 SQL script。
- 在本地或开发环境应用 migration,并检查数据库 schema。
- 将 migration 文件和 SQL scripts 与需要该变更的 domain 或 application 修改一起提交。
Checklist:发布前复查 migration diff,在本地数据库应用,运行 build,并测试受影响流程。
