implementing-ef-core
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseRepository Pattern
仓储模式
EF implementations live in . Each class implements the same interface as its Dapper counterpart. The EF repository uses and LINQ queries instead of stored procedures, but must produce identical behavior.
src/Infrastructure/EntityFramework/Repositories/DbContextEF实现代码位于目录下。每个类都实现了与其Dapper对应类相同的接口。EF仓储使用和LINQ查询而非存储过程,但必须产生完全一致的行为。
src/Infrastructure/EntityFramework/Repositories/DbContextWhy behavior must match stored procedures exactly
为什么行为必须与存储过程完全匹配
Bitwarden self-hosted runs on the customer's choice of database. If returns results in a different order on PostgreSQL than the stored procedure returns on MSSQL, or filters differently, or handles nulls differently — that's a bug. Users switching databases or comparing behavior across environments will see inconsistencies.
CipherRepository.GetManyByUserId()The integration test attribute runs the same test against all configured databases. This is the primary safety net for parity.
[DatabaseData]Bitwarden自托管版本运行在客户选择的数据库上。如果在PostgreSQL上返回的结果顺序与MSSQL上存储过程返回的顺序不同,或者过滤逻辑、空值处理存在差异,这都属于Bug。切换数据库或跨环境对比行为的用户会看到不一致的结果。
CipherRepository.GetManyByUserId()[DatabaseData]Cross-database considerations
跨数据库注意事项
EF Core's LINQ-to-SQL translation varies by provider. Patterns that work on one database may fail on another:
- PostgreSQL is stricter about types — operations like on booleans or implicit string/int conversions that MySQL allows will throw
Min() - SQLite has limited ALTER TABLE support — some migrations that work elsewhere fail on SQLite
- Case sensitivity depends on database collation, not on C# code — don't assume case-insensitive string comparison
The pragmatic approach: write clean LINQ, run tests, and fix provider-specific failures as they surface rather than trying to predict every edge case.
[DatabaseData]EF Core的LINQ到SQL转换因数据库提供商而异。在某一数据库上可行的模式在另一数据库上可能失败:
- PostgreSQL对类型要求更严格——像对布尔值使用操作,或MySQL允许的隐式字符串/整数转换,在PostgreSQL中会抛出异常
Min() - SQLite对ALTER TABLE支持有限——某些在其他数据库上可行的迁移在SQLite中会失败
- 大小写敏感性取决于数据库的排序规则,而非C#代码——不要假设字符串比较是大小写不敏感的
务实的做法是:编写简洁的LINQ代码,运行测试,在出现提供商特定的错误时再进行修复,而非试图预判所有边缘情况。
[DatabaseData]Migration Generation
迁移生成
Workflow
工作流程
Run to generate migrations for all EF targets simultaneously. This creates migration files for each provider (PostgreSQL, MySQL, SQLite).
pwsh ef_migrate.ps1 <MigrationName>运行可同时为所有EF目标生成迁移。这会为每个数据库提供商(PostgreSQL、MySQL、SQLite)创建迁移文件。
pwsh ef_migrate.ps1 <MigrationName>Why the migration name matters
迁移名称的重要性
The EF migration class name must exactly match the MSSQL migration name portion (from the filename). This convention keeps migration history aligned across ORMs and makes it easy to trace which EF migration corresponds to which SQL script.
YYYY-MM-DD_##_MigrationName.sqlEF迁移类的名称必须与MSSQL迁移文件名(来自)中的迁移名称部分完全匹配。这一约定确保了不同ORM之间的迁移历史保持一致,便于追踪哪个EF迁移对应哪个SQL脚本。
YYYY-MM-DD_##_MigrationName.sqlAlways review generated migrations
务必审查生成的迁移
EF's migration generator makes mechanical decisions that aren't always optimal:
- It may drop and recreate indexes instead of renaming them
- It may generate unnecessary column modifications when model annotations change
- It doesn't know about Bitwarden's large table concerns (never add indexes to ,
Cipheretc. without careful review)OrganizationUser
Review the generated and methods to ensure they align with the stored procedure migration's intent.
Up()Down()EF的迁移生成器做出的机械性决策并非总是最优的:
- 它可能会删除并重新创建索引,而非重命名索引
- 当模型注解变更时,它可能会生成不必要的列修改
- 它不了解Bitwarden中大型表的相关注意事项(在未仔细审查的情况下,绝不要向、
Cipher等表添加索引)OrganizationUser
审查生成的和方法,确保它们与存储过程迁移的意图一致。
Up()Down()Key Decisions That Trip Up AI Assistants
容易误导AI助手的关键决策
Don't add navigation properties casually
不要随意添加导航属性
EF navigation properties (e.g., ) affect query generation and lazy loading behavior. Only add them when the stored procedure equivalent also joins those tables. Unnecessary navigation properties cause N+1 queries that don't match the stored procedure's behavior.
public virtual Organization Organization { get; set; }EF导航属性(例如)会影响查询生成和延迟加载行为。只有当对应的存储过程也关联了这些表时,才添加导航属性。不必要的导航属性会导致N+1查询,与存储过程的行为不符。
public virtual Organization Organization { get; set; }DbContext configuration lives in EntityTypeConfiguration
classes
EntityTypeConfigurationDbContext配置位于EntityTypeConfiguration
类中
EntityTypeConfigurationDon't configure entities inline in . Each entity has a configuration class that defines table mapping, relationships, and constraints. This keeps the DbContext clean and each entity's configuration self-contained.
OnModelCreating不要在中内联配置实体。每个实体都有一个配置类,用于定义表映射、关系和约束。这能保持DbContext的简洁性,并使每个实体的配置独立封装。
OnModelCreatingRespect the same GUID generation strategy
遵循相同的GUID生成策略
Entity IDs are generated in application code via , not by the database. Don't configure or database-generated defaults for ID columns in EF configuration.
CoreHelpers.GenerateComb()ValueGeneratedOnAdd()实体ID由应用代码通过生成,而非由数据库生成。不要在EF配置中为ID列配置或数据库生成的默认值。
CoreHelpers.GenerateComb()ValueGeneratedOnAdd()Critical Rules
关键规则
These are the most frequently violated conventions. Claude cannot fetch the linked docs at runtime, so these are inlined here:
- One class per entity — never configure inline in
EntityTypeConfiguration<T>OnModelCreating - Migration name must match MSSQL migration name from
YYYY-MM-DD_##_MigrationName.sql - Run to generate migrations for all providers simultaneously
pwsh ef_migrate.ps1 <Name> - Review and
Up()methods in every generated migration before committingDown() - No on ID columns — IDs come from
ValueGeneratedOnAdd()in app codeCoreHelpers.GenerateComb()
这些是最常被违反的约定。Claude无法在运行时获取链接文档,因此在此内联说明:
- 每个实体对应一个类——绝不要在
EntityTypeConfiguration<T>中内联配置OnModelCreating - 迁移名称必须与MSSQL迁移名称匹配(来自)
YYYY-MM-DD_##_MigrationName.sql - **运行**以同时为所有提供商生成迁移
pwsh ef_migrate.ps1 <Name> - 提交前务必审查每个生成迁移的和
Up()方法Down() - ID列不要使用——ID由应用代码中的
ValueGeneratedOnAdd()生成CoreHelpers.GenerateComb()
Examples
示例
GUID configuration
GUID配置
csharp
// CORRECT — ID generated in application code
public void Configure(EntityTypeBuilder<Cipher> builder)
{
builder.HasKey(c => c.Id);
// No ValueGeneratedOnAdd — CoreHelpers.GenerateComb() handles this
}
// WRONG — lets database generate IDs, breaks MSSQL parity
public void Configure(EntityTypeBuilder<Cipher> builder)
{
builder.HasKey(c => c.Id);
builder.Property(c => c.Id).ValueGeneratedOnAdd();
}csharp
// CORRECT — ID generated in application code
public void Configure(EntityTypeBuilder<Cipher> builder)
{
builder.HasKey(c => c.Id);
// No ValueGeneratedOnAdd — CoreHelpers.GenerateComb() handles this
}
// WRONG — lets database generate IDs, breaks MSSQL parity
public void Configure(EntityTypeBuilder<Cipher> builder)
{
builder.HasKey(c => c.Id);
builder.Property(c => c.Id).ValueGeneratedOnAdd();
}Navigation properties
导航属性
csharp
// CORRECT — only add when the SP also joins this table
public class Cipher
{
public Guid Id { get; set; }
public Guid OrganizationId { get; set; }
// No navigation property — the SP doesn't JOIN Organization
}
// WRONG — causes N+1 queries that don't match SP behavior
public class Cipher
{
public Guid Id { get; set; }
public Guid OrganizationId { get; set; }
public virtual Organization Organization { get; set; }
}csharp
// CORRECT — only add when the SP also joins this table
public class Cipher
{
public Guid Id { get; set; }
public Guid OrganizationId { get; set; }
// No navigation property — the SP doesn't JOIN Organization
}
// WRONG — causes N+1 queries that don't match SP behavior
public class Cipher
{
public Guid Id { get; set; }
public Guid OrganizationId { get; set; }
public virtual Organization Organization { get; set; }
}