---
name: dotnet-backend-patterns
description: "C#/.NET backend patterns for building APIs, MCP servers, and enterprise applications. Covers async/await, dependency injection, Entity Framework Core, Dapper, configuration, caching, and testing with xUnit. Use when developing .NET backends, reviewing C# code, or designing API architectures."
enabled: false
source: github:JuanJoseGonGi/skills
imported-from: github:JuanJoseGonGi/skills
---

# .NET Backend Development Patterns

Production-grade C#/.NET patterns for APIs, MCP servers, and enterprise backends (2024/2025).

## Project Structure (Clean Architecture)

```
src/
├── Domain/           # Entities, interfaces, value objects (no dependencies)
├── Application/      # Services, DTOs, validators, use cases
├── Infrastructure/   # EF Core, Dapper, Redis, HTTP clients, DI registration
└── Api/              # Controllers/MinimalAPI, middleware, filters, Program.cs
```

## Dependency Injection

| Lifetime | When to use | Example |
|----------|------------|---------|
| **Scoped** | Per-request state, DB contexts, services | `AddScoped<IOrderService, OrderService>()` |
| **Singleton** | Stateless/thread-safe shared instances | `AddSingleton<ICacheService, RedisCacheService>()` |
| **Transient** | Lightweight stateless, validators | `AddTransient<IValidator<T>, Validator<T>>()` |

Key patterns:
- **Factory pattern**: `AddScoped<IService>(sp => condition ? sp.Get<A>() : sp.Get<B>())`
- **Keyed services** (.NET 8+): `AddKeyedScoped<IPayment, Stripe>("stripe")` → inject with `[FromKeyedServices("stripe")]`
- **Options pattern**: `Configure<T>(configuration.GetSection("Name"))`

→ Full examples: `references/dependency-injection-patterns.md`

## Async/Await Rules

```csharp
// ✅ Async all the way down — always pass CancellationToken
public async Task<Product> GetProductAsync(string id, CancellationToken ct = default)
    => await _repository.GetByIdAsync(id, ct);

// ✅ Parallel independent work
var stockTask = _stockService.GetAsync(id, ct);
var priceTask = _priceService.GetAsync(id, ct);
await Task.WhenAll(stockTask, priceTask);

// ✅ ValueTask for hot paths with caching
public ValueTask<Product?> GetCachedAsync(string id)
{
    if (_cache.TryGetValue(id, out Product? p)) return ValueTask.FromResult(p);
    return new ValueTask<Product?>(GetFromDatabaseAsync(id));
}

// ✅ ConfigureAwait(false) in library code
var result = await _http.GetAsync(url, ct).ConfigureAwait(false);
```

**Hard rules:**
- Never `.Result` or `.GetAwaiter().GetResult()` — deadlock risk
- Never `async void` except event handlers — exceptions are lost
- Never `await Task.Run(async () => await X())` — wastes a thread
- Always propagate `CancellationToken`

## Configuration (IOptions)

| Interface | Lifetime | Reloads on change? | Use when |
|-----------|----------|-------------------|----------|
| `IOptions<T>` | Singleton | No | Static config |
| `IOptionsSnapshot<T>` | Scoped | Yes, per request | Request-scoped dynamic config |
| `IOptionsMonitor<T>` | Singleton | Yes, via callback | Long-lived services needing updates |

Pattern: typed class with `const string SectionName`, default values, registered via `Configure<T>(section)`.

→ Full examples: `references/configuration-patterns.md`

## Result Pattern

Return `Result<T>` instead of throwing for expected failures (validation, business rules):

```csharp
public static Result<T> Success(T value) => ...;
public static Result<T> Failure(string error, string? code = null) => ...;
```

Service returns `Result<Order>` with error codes like `"VALIDATION_ERROR"`, `"INSUFFICIENT_STOCK"`.
Endpoint maps: success → `Results.Created(...)`, failure → `Results.BadRequest(...)`.

→ Full implementation: `references/result-pattern.md`

## Data Access

### EF Core — for rich domain models, change tracking, migrations

```csharp
// Entity configuration
public class ProductConfiguration : IEntityTypeConfiguration<Product>
{
    public void Configure(EntityTypeBuilder<Product> builder)
    {
        builder.ToTable("Products");
        builder.HasKey(p => p.Id);
        builder.Property(p => p.Name).HasMaxLength(200).IsRequired();
        builder.Property(p => p.Price).HasPrecision(18, 2);
        builder.HasIndex(p => p.Sku).IsUnique();
        builder.HasIndex(p => new { p.CategoryId, p.Name });
    }
}

// Repository pattern
public async Task<IReadOnlyList<Product>> SearchAsync(
    ProductSearchCriteria criteria, CancellationToken ct = default)
{
    var query = _context.Products.AsNoTracking();

    if (!string.IsNullOrWhiteSpace(criteria.SearchTerm))
        query = query.Where(p => EF.Functions.Like(p.Name, $"%{criteria.SearchTerm}%"));

    if (criteria.CategoryId.HasValue)
        query = query.Where(p => p.CategoryId == criteria.CategoryId);

    return await query
        .OrderBy(p => p.Name)
        .Skip((criteria.Page - 1) * criteria.PageSize)
        .Take(criteria.PageSize)
        .ToListAsync(ct);
}
```

EF Core essentials:
- `AsNoTracking()` for all read-only queries
- `ApplyConfigurationsFromAssembly()` for auto-discovering `IEntityTypeConfiguration`
- `HasQueryFilter()` for soft deletes
- `AsSplitQuery()` to avoid cartesian explosion on multiple Includes
- `ExecuteUpdateAsync`/`ExecuteDeleteAsync` for bulk operations (.NET 7+)
- `AddDbContextPool<T>()` to reduce allocation overhead
- Compiled queries for hot paths: `EF.CompileAsyncQuery(...)`

→ Full guide: `references/ef-core-best-practices.md`

### Dapper — for performance-critical reads, complex SQL

```csharp
// Always use CommandDefinition for cancellation support
var product = await connection.QueryFirstOrDefaultAsync<Product>(
    new CommandDefinition(
        "SELECT Id, Name, Price FROM Products WHERE Id = @Id",
        new { Id = id },
        cancellationToken: ct));

// Multi-mapping for joins
await connection.QueryAsync<Order, OrderItem, Product, Order>(
    sql, (order, item, product) => { /* map */ },
    splitOn: "Id,Id");

// Multiple result sets
using var multi = await connection.QueryMultipleAsync(sql, parameters);
var count = await multi.ReadSingleAsync<int>();
var items = (await multi.ReadAsync<Product>()).ToList();
```

Dapper essentials:
- Let Dapper manage connection open/close — don't call `OpenAsync()` manually
- Use `DynamicParameters` for dynamic query building
- Use `QueryUnbufferedAsync` for large result sets (streams as `IAsyncEnumerable`)
- Register `SqlMapper.TypeHandler<T>` for custom type mapping (e.g., JSON columns)
- Always parameterize — never string-interpolate user input into SQL

→ Full guide: `references/dapper-patterns.md`

### When to use which?

| Scenario | Choose |
|----------|--------|
| Rich domain, relationships, migrations | EF Core |
| Read-heavy hot paths, complex SQL | Dapper |
| CTEs, window functions, legacy schemas | Dapper |
| Change tracking, LINQ composition | EF Core |
| Both in same project | ✅ Fine — share the connection string |

## Caching

**Multi-level strategy:** L1 (IMemoryCache, ~1min) → L2 (IDistributedCache/Redis, ~15min) → L3 (Database)

Pattern:
1. Check L1 → return on hit
2. Check L2 → populate L1, return on hit
3. Fetch from DB → populate L1 + L2

Invalidation: remove from both caches on writes.

**Stale-while-revalidate:** Return stale data immediately, refresh in background `Task.Run`.

→ Full implementation: `references/caching-patterns.md`

## Testing

### Unit tests (xUnit + Moq)

- Constructor sets up mocks with sensible defaults (e.g., validation passes)
- `[Fact]` for single cases, `[Theory] + [InlineData]` for parameterized
- Arrange/Act/Assert pattern
- `Mock.Verify()` to assert interactions (e.g., repository never called on validation failure)

### Integration tests (WebApplicationFactory)

- `IClassFixture<WebApplicationFactory<Program>>`
- Replace DB with `UseInMemoryDatabase`, Redis with `AddDistributedMemoryCache()`
- Seed data via scoped `DbContext`, test via `HttpClient`

→ Full examples: `references/testing-patterns.md`

## Best Practices

### DO

1. Async/await all the way — propagate `CancellationToken`
2. Constructor injection for dependencies
3. `IOptions<T>` for typed configuration
4. `Result<T>` over exceptions for business logic flow
5. `AsNoTracking()` for read-only EF Core queries
6. Dapper for read-heavy performance-critical paths
7. Multi-level caching with proper invalidation
8. Record types for DTOs and immutable data
9. Unit tests for logic, integration tests for APIs
10. `IHttpClientFactory` — never `new HttpClient()`

### DON'T

1. Block on async (`.Result`, `.Wait()`)
2. `async void` (except event handlers)
3. Catch generic `Exception` without re-throw/log
4. Hardcode configuration values
5. Expose EF entities in API responses — use DTOs
6. Skip `AsNoTracking()` on reads
7. Ignore `CancellationToken` parameters
8. Mix sync and async code

## Common Pitfalls

| Pitfall | Fix |
|---------|-----|
| N+1 queries | `.Include()` or explicit joins |
| Memory leaks | Dispose `IDisposable`, use `using` |
| Deadlocks | Don't mix sync/async; `ConfigureAwait(false)` in libs |
| Over-fetching | `.Select()` projections, fetch only needed columns |
| Missing indexes | Check query plans, index common filter columns |
| Cache stampede | Distributed locks for cache population |
| Timeout issues | Configure timeouts on `HttpClient` and DB commands |

## References

- `references/dependency-injection-patterns.md` — Full DI registration examples
- `references/configuration-patterns.md` — IOptions pattern with full code
- `references/result-pattern.md` — Result<T> implementation and endpoint mapping
- `references/caching-patterns.md` — Multi-level cache and stale-while-revalidate
- `references/testing-patterns.md` — xUnit/Moq unit tests and WebApplicationFactory integration tests
- `references/ef-core-best-practices.md` — EF Core optimization guide
- `references/dapper-patterns.md` — Advanced Dapper usage patterns
