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.
npx skills add https://github.com/bitwarden/server --skill implementing-ef-core
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.
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.
EF Core's LINQ-to-SQL translation varies by provider. Patterns that work on one database may fail on another:
Min() on booleans or implicit string/int conversions that MySQL allows will throwThe pragmatic approach: write clean LINQ, run [DatabaseData] tests, and fix provider-specific failures as they surface rather than trying to predict every edge case.
Run pwsh ef_migrate.ps1 <MigrationName> to generate migrations for all EF targets simultaneously. This creates migration files for each provider (PostgreSQL, MySQL, SQLite).
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's migration generator makes mechanical decisions that aren't always optimal:
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 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.
EntityTypeConfiguration classesDon'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.
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.
These are the most frequently violated conventions. Claude cannot fetch the linked docs at runtime, so these are inlined here:
EntityTypeConfiguration<T> class per entity — never configure inline in OnModelCreatingYYYY-MM-DD_##_MigrationName.sqlpwsh ef_migrate.ps1 <Name> to generate migrations for all providers simultaneouslyUp() and Down() methods in every generated migration before committingValueGeneratedOnAdd() on ID columns — IDs come from CoreHelpers.GenerateComb() in app code// 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();
}
// 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; }
}
Efficient database search tool for bioRxiv preprint server. Use this skill when searching for life sciences preprints by keywords, authors, date ranges, or categories, retrieving paper metadata, downloading PDFs, or conducting literature reviews.
Access BRENDA enzyme database via SOAP API. Retrieve kinetic parameters (Km, kcat), reaction equations, organism data, and substrate-specific enzyme information for biochemical research and metabolic pathway analysis.
Access ClinPGx pharmacogenomics data (successor to PharmGKB). Query gene-drug interactions, CPIC guidelines, allele functions, for precision medicine and genotype-guided dosing decisions.
Query NCBI ClinVar for variant clinical significance. Search by gene/position, interpret pathogenicity classifications, access via E-utilities API or FTP, annotate VCFs, for genomic medicine.
Access COSMIC cancer mutation database. Query somatic mutations, Cancer Gene Census, mutational signatures, gene fusions, for cancer research and precision oncology. Requires authentication.
Query Ensembl genome database REST API for 250+ species. Gene lookups, sequence retrieval, variant analysis, comparative genomics, orthologs, VEP predictions, for genomic research.
Query openFDA API for drugs, devices, adverse events, recalls, regulatory submissions (510k, PMA), substance identification (UNII), for FDA regulatory data analysis and safety research.
Query NCBI Gene via E-utilities/Datasets API. Search by symbol/ID, retrieve gene info (RefSeqs, GO, locations, phenotypes), batch lookups, for gene annotation and functional analysis.
Take bitwarden/implementing-ef-core from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.