implementing-ef-core

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Repository Pattern

仓储模式

EF implementations live in
src/Infrastructure/EntityFramework/Repositories/
. Each class implements the same interface as its Dapper counterpart. The EF repository uses
DbContext
and LINQ queries instead of stored procedures, but must produce identical behavior.
EF实现代码位于
src/Infrastructure/EntityFramework/Repositories/
目录下。每个类都实现了与其Dapper对应类相同的接口。EF仓储使用
DbContext
和LINQ查询而非存储过程,但必须产生完全一致的行为。

Why behavior must match stored procedures exactly

为什么行为必须与存储过程完全匹配

Bitwarden self-hosted runs on the customer's choice of database. If
CipherRepository.GetManyByUserId()
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.
The
[DatabaseData]
integration test attribute runs the same test against all configured databases. This is the primary safety net for parity.
Bitwarden自托管版本运行在客户选择的数据库上。如果
CipherRepository.GetManyByUserId()
在PostgreSQL上返回的结果顺序与MSSQL上存储过程返回的顺序不同,或者过滤逻辑、空值处理存在差异,这都属于Bug。切换数据库或跨环境对比行为的用户会看到不一致的结果。
[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
    Min()
    on booleans or implicit string/int conversions that MySQL allows will throw
  • 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
[DatabaseData]
tests, and fix provider-specific failures as they surface rather than trying to predict every edge case.
EF Core的LINQ到SQL转换因数据库提供商而异。在某一数据库上可行的模式在另一数据库上可能失败:
  • PostgreSQL对类型要求更严格——像对布尔值使用
    Min()
    操作,或MySQL允许的隐式字符串/整数转换,在PostgreSQL中会抛出异常
  • SQLite对ALTER TABLE支持有限——某些在其他数据库上可行的迁移在SQLite中会失败
  • 大小写敏感性取决于数据库的排序规则,而非C#代码——不要假设字符串比较是大小写不敏感的
务实的做法是:编写简洁的LINQ代码,运行
[DatabaseData]
测试,在出现提供商特定的错误时再进行修复,而非试图预判所有边缘情况。

Migration Generation

迁移生成

Workflow

工作流程

Run
pwsh ef_migrate.ps1 <MigrationName>
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)创建迁移文件。

Why the migration name matters

迁移名称的重要性

The EF migration class name must exactly match the MSSQL migration name portion (from the
YYYY-MM-DD_##_MigrationName.sql
filename). This convention keeps migration history aligned across ORMs and makes it easy to trace which EF migration corresponds to which SQL script.
EF迁移类的名称必须与MSSQL迁移文件名(来自
YYYY-MM-DD_##_MigrationName.sql
)中的迁移名称部分完全匹配。这一约定确保了不同ORM之间的迁移历史保持一致,便于追踪哪个EF迁移对应哪个SQL脚本。

Always 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
    Cipher
    ,
    OrganizationUser
    etc. without careful review)
Review the generated
Up()
and
Down()
methods to ensure they align with the stored procedure migration's intent.
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.,
public virtual Organization Organization { get; set; }
) 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.
EF导航属性(例如
public virtual Organization Organization { get; set; }
)会影响查询生成和延迟加载行为。只有当对应的存储过程也关联了这些表时,才添加导航属性。不必要的导航属性会导致N+1查询,与存储过程的行为不符。

DbContext configuration lives in
EntityTypeConfiguration
classes

DbContext配置位于
EntityTypeConfiguration
类中

Don't configure entities inline in
OnModelCreating
. 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的简洁性,并使每个实体的配置独立封装。

Respect the same GUID generation strategy

遵循相同的GUID生成策略

Entity IDs are generated in application code via
CoreHelpers.GenerateComb()
, not by the database. Don't configure
ValueGeneratedOnAdd()
or database-generated defaults for ID columns in EF configuration.
实体ID由应用代码通过
CoreHelpers.GenerateComb()
生成,而非由数据库生成。不要在EF配置中为ID列配置
ValueGeneratedOnAdd()
或数据库生成的默认值。

Critical Rules

关键规则

These are the most frequently violated conventions. Claude cannot fetch the linked docs at runtime, so these are inlined here:
  • One
    EntityTypeConfiguration<T>
    class per entity
    — never configure inline in
    OnModelCreating
  • Migration name must match MSSQL migration name from
    YYYY-MM-DD_##_MigrationName.sql
  • Run
    pwsh ef_migrate.ps1 <Name>
    to generate migrations for all providers simultaneously
  • Review
    Up()
    and
    Down()
    methods
    in every generated migration before committing
  • No
    ValueGeneratedOnAdd()
    on ID columns
    — IDs come from
    CoreHelpers.GenerateComb()
    in app code
这些是最常被违反的约定。Claude无法在运行时获取链接文档,因此在此内联说明:
  • 每个实体对应一个
    EntityTypeConfiguration<T>
    ——绝不要在
    OnModelCreating
    中内联配置
  • 迁移名称必须与MSSQL迁移名称匹配(来自
    YYYY-MM-DD_##_MigrationName.sql
  • **运行
    pwsh ef_migrate.ps1 <Name>
    **以同时为所有提供商生成迁移
  • 提交前务必审查每个生成迁移的
    Up()
    Down()
    方法
  • ID列不要使用
    ValueGeneratedOnAdd()
    ——ID由应用代码中的
    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; }
}

Further Reading

进一步阅读