dotnet-efcore-architecture
Designing data layer architecture. Read/write split, aggregate boundaries, N+1 governance.
What this skill does
# dotnet-efcore-architecture
Strategic architectural patterns for EF Core data layers. Covers read/write model separation, aggregate boundary design, repository vs direct DbContext policy, N+1 query governance, row limit enforcement, and projection patterns. These patterns guide how to structure a data layer -- not how to write individual queries (see [skill:dotnet-efcore-patterns] for tactical usage).
**Out of scope:** Tactical EF Core usage (DbContext lifecycle, AsNoTracking, migrations, interceptors, compiled queries) is covered in [skill:dotnet-efcore-patterns]. Choosing between EF Core, Dapper, and ADO.NET is covered in [skill:dotnet-data-access-strategy]. DI container mechanics -- see [skill:dotnet-csharp-dependency-injection]. Async patterns -- see [skill:dotnet-csharp-async-patterns]. Integration testing data layers -- see [skill:dotnet-integration-testing] for database fixture and Testcontainers patterns. CI/CD pipelines -- see [skill:dotnet-gha-patterns] and [skill:dotnet-ado-patterns].
Cross-references: [skill:dotnet-efcore-patterns] for tactical DbContext usage and migrations, [skill:dotnet-data-access-strategy] for technology selection, [skill:dotnet-csharp-dependency-injection] for service registration, [skill:dotnet-csharp-async-patterns] for async query patterns.
---
## Package Prerequisites
Examples in this skill use PostgreSQL (`UseNpgsql`). Substitute the provider package for your database:
| Database | Provider Package |
|----------|-----------------|
| PostgreSQL | `Npgsql.EntityFrameworkCore.PostgreSQL` |
| SQL Server | `Microsoft.EntityFrameworkCore.SqlServer` |
| SQLite | `Microsoft.EntityFrameworkCore.Sqlite` |
All examples also require the core `Microsoft.EntityFrameworkCore` package (pulled in transitively by provider packages).
---
## Read/Write Model Separation
Separate read models (queries) from write models (commands) to optimize each path independently. This is not full CQRS -- it is a practical separation using EF Core features.
### Approach: Separate DbContext Types
```csharp
// Write context: full change tracking, navigation properties, interceptors
public sealed class WriteDbContext : DbContext
{
public DbSet<Order> Orders => Set<Order>();
public DbSet<Product> Products => Set<Product>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfigurationsFromAssembly(typeof(WriteDbContext).Assembly);
}
}
// Read context: no-tracking by default, optimized for projections
public sealed class ReadDbContext : DbContext
{
public DbSet<Order> Orders => Set<Order>();
public DbSet<Product> Products => Set<Product>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfigurationsFromAssembly(typeof(ReadDbContext).Assembly);
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
// Note: this is supplemental -- primary config is in DI registration
}
}
```
### Registration
```csharp
// Write context: standard tracking, connection resiliency
builder.Services.AddDbContext<WriteDbContext>(options =>
options.UseNpgsql(connectionString, npgsql =>
npgsql.EnableRetryOnFailure(maxRetryCount: 3)));
// Read context: no-tracking, optionally pointed at a read replica
builder.Services.AddDbContext<ReadDbContext>(options =>
options.UseNpgsql(readReplicaConnectionString ?? connectionString)
.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking));
```
### When to Separate
| Scenario | Recommendation |
|----------|---------------|
| Simple CRUD app | Single `DbContext` with per-query `AsNoTracking()` |
| Read-heavy API with complex queries | Separate read/write contexts |
| Read replica database | Separate contexts with different connection strings |
| CQRS architecture | Separate contexts, possibly separate models |
**Start simple.** Use a single `DbContext` and per-query `AsNoTracking()` until you have a concrete reason to split (different connection strings, divergent model shapes, or query complexity that justifies dedicated read models).
---
## Aggregate Boundaries
An aggregate is a cluster of entities that are always loaded and saved together as a consistency boundary. EF Core maps well to aggregate-oriented design when navigation properties follow aggregate boundaries.
### Defining Aggregates
```csharp
// Order is the aggregate root -- it owns OrderItems
public sealed class Order
{
public int Id { get; private set; }
public string CustomerId { get; private set; } = default!;
public OrderStatus Status { get; private set; }
public DateTimeOffset CreatedAt { get; private set; }
// Owned collection -- part of the Order aggregate
private readonly List<OrderItem> _items = [];
public IReadOnlyList<OrderItem> Items => _items.AsReadOnly();
public void AddItem(int productId, int quantity, decimal unitPrice)
{
if (Status != OrderStatus.Draft)
throw new InvalidOperationException("Cannot add items to a non-draft order.");
_items.Add(new OrderItem(productId, quantity, unitPrice));
}
}
// OrderItem belongs to the Order aggregate -- no independent access
public sealed class OrderItem
{
public int Id { get; private set; }
public int ProductId { get; private set; }
public int Quantity { get; private set; }
public decimal UnitPrice { get; private set; }
internal OrderItem(int productId, int quantity, decimal unitPrice)
{
ProductId = productId;
Quantity = quantity;
UnitPrice = unitPrice;
}
private OrderItem() { } // EF Core constructor
}
```
### EF Core Configuration for Aggregates
```csharp
public sealed class OrderConfiguration : IEntityTypeConfiguration<Order>
{
public void Configure(EntityTypeBuilder<Order> builder)
{
builder.HasKey(o => o.Id);
builder.Property(o => o.CustomerId).IsRequired().HasMaxLength(50);
builder.Property(o => o.Status).HasConversion<string>();
// Owned collection navigation -- cascade delete, no independent DbSet
builder.OwnsMany(o => o.Items, items =>
{
items.WithOwner().HasForeignKey("OrderId");
items.Property(i => i.ProductId).IsRequired();
});
// Alternatively, if OrderItem needs its own table with explicit FK:
// builder.HasMany(o => o.Items)
// .WithOne()
// .HasForeignKey("OrderId")
// .OnDelete(DeleteBehavior.Cascade);
//
// builder.Navigation(o => o.Items)
// .UsePropertyAccessMode(PropertyAccessMode.Field);
}
}
```
### Aggregate Design Rules
1. **Load the entire aggregate** -- do not load partial aggregates. Use `Include()` for the owned collections.
2. **Save through the aggregate root** -- call `SaveChangesAsync()` on the root, not on child entities independently.
3. **Reference other aggregates by ID** -- do not create navigation properties between aggregate roots. Use `CustomerId` (foreign key value), not `Customer` (navigation property).
4. **Keep aggregates small** -- large aggregates cause lock contention and slow loads. If a collection grows unbounded (e.g., audit logs), it does not belong in the aggregate.
5. **One aggregate per transaction** -- modifying multiple aggregates in a single transaction creates coupling. Use domain events or eventual consistency for cross-aggregate operations.
---
## Repository Policy
Whether to use the repository pattern or access `DbContext` directly is a team decision. Both approaches are valid in .NET.
### Option A: Direct DbContext Access
```csharp
public sealed class CreateOrderHandler(WriteDbContext db)
{
public async Task<int> HandleAsync(
CreateOrderCommand command,
CancellationToken ct)
{
var order = new Order(command.CustomerId);
foreach (var item in command.Items)
{
oRelated in General
modeling-omnistudio-epc-catalog
IncludedSalesforce Industries CME EPC product-modeling skill for Product2-based catalog creation. Use when creating EPC products, configuring product attributes, building offer bundles with Product Child Items, or reviewing EPC DataPack JSON metadata for product catalog changes. TRIGGER when: user creates or updates Product2 EPC records, AttributeAssignment payloads, AttributeMetadata/AttributeDefaultValues, Offer bundles, or ProductChildItem relationships. DO NOT TRIGGER when: designing OmniScripts/FlexCards/Integration Procedures (use building-omnistudio-omniscript, building-omnistudio-flexcard, or building-omnistudio-integration-procedure), implementing Apex business logic (use generating-apex), or troubleshooting deployment pipelines (use deploying-metadata).
relationship-science-coach
IncludedUse this skill for direct, practical adult relationship coaching: couples conflict, repair, trust, marriage, dating, flirting, attachment patterns, emotional connection, sex, desire differences, eroticism, kink negotiation, affection, love languages, breakups, and long-term passion. Draw on Gottman, EFT and Hold Me Tight, attachment science, modern sex research, Perel, Nagoski, Kerner, Schnarch, Love and Stosny, and flexible love-language tools. Be concrete and low-hedge. Redirect only for imminent danger, abuse, coercive control, minors, non-consent, self-harm, stalking, or medical/legal/psychiatric decisions.
building-sf-integrations
IncludedSalesforce integration architecture and runtime plumbing with 120-point scoring. Use this skill to set up Named Credentials, External Credentials, External Services, REST/SOAP callout patterns, Platform Events, and Change Data Capture. TRIGGER when: user sets up Named Credentials, External Services, REST/SOAP callouts, Platform Events, CDC, or touches .namedCredential-meta.xml files. DO NOT TRIGGER when: Connected App/OAuth config (use configuring-connected-apps), Apex-only logic (use generating-apex), or data import/export (use handling-sf-data).
venue-templates
IncludedAccess comprehensive LaTeX templates, formatting requirements, and submission guidelines for major scientific publication venues (Nature, Science, PLOS, IEEE, ACM), academic conferences (NeurIPS, ICML, CVPR, CHI), research posters, and grant proposals (NSF, NIH, DOE, DARPA). This skill should be used when preparing manuscripts for journal submission, conference papers, research posters, or grant proposals and need venue-specific formatting requirements and templates.
let-fate-decide
IncludedDraws the 12 Houses of the Zodiac Tarot spread to inject entropy into planning when prompts are vague, ambiguous, or casually delegated. Interprets the spread to guide next steps. Use when the user says 'let fate decide', 'YOLO', 'whatever', 'idk', or other nonchalant phrases, makes Yu-Gi-Oh references, or when you are about to arbitrarily pick between multiple reasonable approaches. Prefer over ask-questions-if-underspecified when the user's tone is casual or playful rather than precision-seeking.
net-ops
IncludedCross-platform network troubleshooting (Windows, macOS, Linux) via local or remote shell. Use for: DNS broken, can't resolve hostnames, nslookup/dig works but apps fail, NRPT, WFP, scutil, /etc/resolver, systemd-resolved, /etc/resolv.conf, NetworkManager, VPN DNS leak residue (ProtonVPN/Mullvad/WireGuard/AnyConnect), AV/firewall blocking DNS or DoH, Tailscale DNS interaction, intermittent connectivity, remote diagnostics over SSH.