Skip to main content

implementing-ef-core

Implementing Entity Framework Core repositories and migrations for PostgreSQL, MySQL, and SQLite at Bitwarden. Use when creating or modifying EF repositories, generating EF migrations, or working with non-MSSQL data access in the server repo. Also use when editing `EntityTypeConfiguration<T>` classes or debugging provider-specific LINQ translation issues.

Informations de source

Dépôt
bitwarden/server
Dernière activité de la source
21 avril 2026 à 13:42
Langue détectée de SKILL.md
anglais
Étoiles
20 508
Forks
1 820

Options d'installation

Le prompt qui vérifie d'abord la source est sélectionné par défaut. Vous pouvez passer à une commande directe ou télécharger une copie locale.

Vérifiez les fichiers source

Lisez SKILL.md et les fichiers associés affichés par SkillsMP avant de décider de l'installer.

Affichage de SKILL.md

SKILL.md
Instructions source · Aperçu en lecture seule
name
implementing-ef-core
description
Implementing Entity Framework Core repositories and migrations for PostgreSQL, MySQL, and SQLite at Bitwarden. Use when creating or modifying EF repositories, generating EF migrations, or working with non-MSSQL data access in the server repo. Also use when editing `EntityTypeConfiguration<T>` classes or debugging provider-specific LINQ translation issues.
## 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. ### 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. ### 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. ## 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). ### 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. ### 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. ## Key Decisions That Trip Up AI Assistants ### 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. ### DbContext configuration lives in `EntityTypeConfiguration` classes 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. ### Respect the same GUID generation strategy 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. ## 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 ## Examples ### GUID configuration ```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; } } ``` ## Further Reading - [Database migrations (EF)](https://contributing.bitwarden.com/contributing/database-migrations/ef) - [SQL code style](https://contributing.bitwarden.com/contributing/code-style/sql/)
Voir sur GitHub