| name | dotnet-efcore-guidelines |
| description | Apply project EF Core conventions for repositories, DbContext setup, query filters, migrations, and seeded lookup data. |
| type | pattern |
| enforcement | suggest |
| priority | high |
Purpose
Use this skill when changing repositories, ExploreDbContext, EF configurations, migrations, or PostgreSQL-specific persistence behavior. It keeps runtime tenant isolation and data-shaping rules aligned with Application-layer expectations.
When to Load
- Keywords: EF Core, DbContext, repository, migration, PostgreSQL, query filter, configuration, seed data.
- File patterns:
Explore.Persistence/**/*.cs, **/*DbContext.cs, **/Configurations/**/*.cs, Explore.Persistence/Migrations/**/*.cs.
- Intent IDs:
add-ef-migration, update-repository-query, add-cqrs-handler.
When NOT to Load
Must-Read Docs
Top 5 Invariants
- Repositories return entities rather than DTOs, and read-only paths should prefer
AsNoTracking().
- Named
SoftDelete and Tenant query filters belong in OnModelCreating, and deleted-row inclusion should prefer IgnoreQueryFilters([QueryFilterNames.SoftDelete]) so tenant isolation stays active.
- The pooled DbContext factory sets scoped services such as
TenantContext and CurrentUserService through property injection, and both can be null during migrations or seeding.
- Applied migrations are never edited or removed, and corrective migrations should stay small, focused, and PascalCase verb-noun named.
- Lookup enum IDs and stable codes stay in parity with idempotent
LookupTableSeeder missing-row repair, and any migration-dependent lookup row is inserted locally before its backfill; model HasData() stays forbidden while EF Core #36682 applies.
Top 5 Anti-Patterns
- A repository returns a DTO or
IQueryable, which leaks mapping or EF internals beyond Persistence and weakens Application ownership.
- Domain entities carry default values for persistence behavior, which hides business intent that belongs in handlers or EF configuration.
- Runtime request paths disable the
Tenant filter, which introduces tenant-isolation bugs that are hard to detect.
- A large unfocused migration mixes unrelated schema changes, which makes review, rollback, and diagnosis harder.
- Model
HasData() seeds lookup rows while EF Core #36682 applies, which can break migration generation and leaves dependent backfills ordered after unavailable runtime seed data.
Minimal Examples
public sealed class EventStatusConfiguration : IEntityTypeConfiguration<EventStatus>
{
public void Configure(EntityTypeBuilder<EventStatus> builder)
{
builder.HasQueryFilter("SoftDelete", x => !x.IsDeleted);
builder.HasQueryFilter("Tenant", x => x.TenantId == TenantContext.CurrentTenantId);
builder.Property(x => x.Id).ValueGeneratedNever();
builder.HasIndex(x => x.MasterCode).IsUnique();
}
}
migrationBuilder.InsertData(
table: "event_statuses",
columns: ["id", "master_code", "full_name"],
values: new object[,] { { 1, "DRAFT", "Draft" } });
migrationBuilder.Sql("UPDATE events SET event_status_id = 1 WHERE event_status_id IS NULL;");
public sealed class EventRepository(ExploreDbContext dbContext) : IEventRepository
{
public async Task<IReadOnlyList<Event>> ListAsync(
IQuerySpecification<Event> specification,
CancellationToken cancellationToken)
{
IQueryable<Event> query = dbContext.Events.AsNoTracking();
query = specification.Apply(query);
return await query.ToListAsync(cancellationToken);
}
}
Verification Hooks
dotnet test --project tests/Event.Architecture.Tests/Event.Architecture.Tests.csproj --configuration Release --verbosity quiet -- --treenode-filter "/*/*/CleanArchitectureTests/*" --minimum-expected-tests 1 --no-progress --maximum-parallel-tests 1
dotnet test --project tests/Event.Persistence.IntegrationTests/Event.Persistence.IntegrationTests.csproj --configuration Release --verbosity quiet
dotnet build --configuration Release --verbosity quiet
Related Skills