Serena MCP is mandatory for C# code. First call
mcp__serena__initial_instructionsto load the Serena tool manual, then use the Serena tools for ALL.csreading / searching / navigation / creation / editing — prefer symbol navigation (get_symbols_overview/find_symbol/find_referencing_symbols) over whole-file reads. NativeEdit/Writeon.csis hook-blocked (the TS/React frontend uses the native tools).
EF Core on PostgreSQL (Npgsql) — persistence patterns
Source of truth (read it; do not duplicate): .claude/rules/backend/persistence.md and
docs/projectStandards/backend-architecture.md §7–§8. We use EF Core with the Npgsql provider — yes
EF Core, just not SQL Server.
Steps
- Classify — entity + EF config / repository (write side, per aggregate root) / query (read side, project to DTO) / migration.
- Locate the feature slice — entity in
{{ProjectName}}.Domain/<Feature>; EF config + repo in{{ProjectName}}.Infrastructure.Persistence/<Feature>; repo interface in{{ProjectName}}.Application/<Feature>. - Generate (C# via Serena
create_text_file/ symbol edits — native.csedits are blocked):- Entity — rich mutable class; private ctor + static factory minting
Guid.CreateVersion7(); private setters; invariants throw;tenant_id. Domain stays EF-free (no persistence leakage). - EF config —
IEntityTypeConfiguration<T>, Fluent API;timestamptzfor timestamps (allDateTimeKind=Utc);xmin/uintconcurrency via.IsRowVersion(); EF 10 named query filters (TenantFilter+SoftDeleteFilter, fail-closed); snake_case naming (hand-rolled convention). - Repository (write side, per aggregate) — interface in Application, impl here; change-tracking
writes, no
SaveChangesin the repo (the command interceptor commits). Reads bypass repos and project to DTOs withAsNoTracking+ explicitInclude(avoid N+1).
- Entity — rich mutable class; private ctor + static factory minting
- Migration —
dotnet ef migrations add <Name>; review the generated Postgres DDL; never edit an applied migration. - Validate —
dotnet buildzero warnings; tests via the testing-expert per the plan.
Hard rules (non-negotiable — restated)
- Thin
IUnitOfWork, implemented by theDbContext— it carries the transaction verbs only (ExecuteInTransactionAsync<T>(Func<Task<T>>, ct)+SaveChangesAsync), no repository properties (not a god object). TheDbContextowns the execution strategy / retry / begin-commit. The Kommand interceptor (Application) callsExecuteInTransactionAsync(() => next())and commits only on a successfulResult; handlers orchestrate, repos/handlers never callSaveChanges. - UUIDv7 minted in the domain factory — never
NEWSEQUENTIALID(),Guid.NewGuid(), or DB-generated keys. - Tenancy — named query filters + PostgreSQL RLS backstop;
IgnoreQueryFilters()is privileged/audited; enforcetenant_idon writes. ON CONFLICTfor idempotent upserts;PostgresExceptionSQLSTATE23505for dup detection.- No new NuGet package without Dan's approval (the Npgsql provider is already pinned).