# 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.

- Skill: `bitwarden/implementing-ef-core` (Agent Skill)
- Install (CLI): `npx skillmds@latest add bitwarden/implementing-ef-core`
- Raw SKILL.md: https://api.skillmd.com/api/skills/bitwarden/implementing-ef-core/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: bitwarden (https://skillmd.com/u/bitwarden)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/bitwarden/implementing-ef-core

---


## 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/)

