.NET 10 / C# 14 Design Patterns & Style Guide
Provide high-performance, concise, and idiomatic C# 14 solutions. Target CLI-focused development (Zed/VS Code, dotnet CLI).
Core Principles
| Principle |
Approach |
| Conciseness |
Primary constructors, records, field keyword |
| Thread Safety |
Lazy<T>, ConcurrentDictionary, immutable types |
| No Bloat |
Avoid unnecessary abstract factories |
| Minimal APIs |
Prefer over traditional MVC controllers |
| Modular Monolith |
Prefer over complex microservices |
Output Format
- Context - Brief explanation of why the pattern fits
- Code - Modern C# 14 implementation
- Key Features - Highlight C# 14 features used
Pattern Quick Reference
Singleton (Thread-Safe)
public sealed class ConfigService(IOptions<AppSettings> options)
{
private static readonly Lazy<ConfigService> _instance = new(() =>
new ConfigService(/* resolve from DI */));
public static ConfigService Instance => _instance.Value;
public AppSettings Settings => options.Value;
}
Options Pattern (Preferred for Config)
// appsettings.json binding
public record DatabaseOptions
{
public const string Section = "Database";
public required string ConnectionString { get; init; }
public int MaxRetries { get; init; } = 3;
}
// Registration
builder.Services.Configure<DatabaseOptions>(
builder.Configuration.GetSection(DatabaseOptions.Section));
// Usage with primary constructor
public class UserRepository(IOptions<DatabaseOptions> options)
{
private readonly string _connectionString = options.Value.ConnectionString;
}
Factory Pattern (Modern)
public interface IPaymentProcessor { Task ProcessAsync(decimal amount); }
public class PaymentProcessorFactory(IServiceProvider sp)
{
public IPaymentProcessor Create(string type) => type switch
{
"stripe" => sp.GetRequiredService<StripeProcessor>(),
"paypal" => sp.GetRequiredService<PayPalProcessor>(),
_ => throw new ArgumentException($"Unknown processor: {type}")
};
}
Repository Pattern (Generic)
public interface IRepository<T> where T : class
{
ValueTask<T?> GetByIdAsync(int id, CancellationToken ct = default);
IAsyncEnumerable<T> GetAllAsync(CancellationToken ct = default);
Task AddAsync(T entity, CancellationToken ct = default);
Task UpdateAsync(T entity, CancellationToken ct = default);
Task DeleteAsync(int id, CancellationToken ct = default);
}
public class EfRepository<T>(AppDbContext db) : IRepository<T> where T : class
{
public ValueTask<T?> GetByIdAsync(int id, CancellationToken ct = default)
=> db.Set<T>().FindAsync([id], ct);
public IAsyncEnumerable<T> GetAllAsync(CancellationToken ct = default)
=> db.Set<T>().AsAsyncEnumerable();
public async Task AddAsync(T entity, CancellationToken ct = default)
{
db.Set<T>().Add(entity);
await db.SaveChangesAsync(ct);
}
public async Task UpdateAsync(T entity, CancellationToken ct = default)
{
db.Set<T>().Update(entity);
await db.SaveChangesAsync(ct);
}
public async Task DeleteAsync(int id, CancellationToken ct = default)
{
var entity = await GetByIdAsync(id, ct);
if (entity is not null)
{
db.Set<T>().Remove(entity);
await db.SaveChangesAsync(ct);
}
}
}
CQRS (Simplified)
// Commands
public record CreateOrderCommand(string CustomerId, List<OrderItem> Items);
public interface ICommandHandler<TCommand>
{
Task<Result> HandleAsync(TCommand command, CancellationToken ct = default);
}
// Queries
public record GetOrderQuery(int OrderId);
public interface IQueryHandler<TQuery, TResult>
{
Task<TResult> HandleAsync(TQuery query, CancellationToken ct = default);
}
// Minimal API integration
app.MapPost("/orders", async (
CreateOrderCommand cmd,
ICommandHandler<CreateOrderCommand> handler,
CancellationToken ct) =>
{
var result = await handler.HandleAsync(cmd, ct);
return result.IsSuccess ? Results.Created() : Results.BadRequest(result.Error);
});
Minimal API Patterns
var builder = WebApplication.CreateBuilder(args);
// Service registration
builder.Services.AddScoped<IUserService, UserService>();
builder.Services.Configure<DatabaseOptions>(
builder.Configuration.GetSection(DatabaseOptions.Section));
var app = builder.Build();
// Route groups
var api = app.MapGroup("/api/v1");
var users = api.MapGroup("/users").RequireAuthorization();
users.MapGet("/", async (IUserService svc, CancellationToken ct) =>
await svc.GetAllAsync(ct));
users.MapGet("/{id:int}", async (int id, IUserService svc, CancellationToken ct) =>
await svc.GetByIdAsync(id, ct) is { } user
? Results.Ok(user)
: Results.NotFound());
users.MapPost("/", async (CreateUserRequest req, IUserService svc, CancellationToken ct) =>
{
var user = await svc.CreateAsync(req, ct);
return Results.Created($"/api/v1/users/{user.Id}", user);
});
app.Run();
C# 14 Features to Use
| Feature |
Usage |
| Primary constructors |
DI injection: class Service(IRepo repo) |
field keyword |
Backing field access in properties |
| Records |
DTOs, Commands, Queries, Value Objects |
| Required members |
required string Name { get; init; } |
| Collection expressions |
[1, 2, 3] instead of new[] {1, 2, 3} |
| Pattern matching |
Switch expressions for complex conditionals |
Resources
- Design patterns details: See
references/patterns.md
- Minimal API patterns: See
references/minimal-api.md
1---2name: csharp-dotnet3description: Principal .NET 10 Architect providing high-performance, idiomatic C# 14 solutions. Use when user asks about: - .NET design patterns (Singleton, Factory, CQRS, Repository, Options Pattern) - C# coding style and best practices - Architecture decisions (Minimal APIs, Modular Monoliths) - Modern C# implementations with primary constructors, records, field keyword Triggers: ".NET", "C#", "design pattern", "architecture", "Minimal API", "primary constructor", "record type", "Options Pattern", "Singleton", "Factory", "CQRS", "Repository pattern", "dependency injection"4---5
6# .NET 10 / C# 14 Design Patterns & Style Guide
7
8Provide high-performance, concise, and idiomatic C# 14 solutions. Target CLI-focused development (Zed/VS Code, `dotnet` CLI).
9
10## Core Principles
11
12| Principle | Approach |
13|-----------|----------|
14| Conciseness | Primary constructors, records, `field` keyword |
15| Thread Safety | `Lazy<T>`, `ConcurrentDictionary`, immutable types |
16| No Bloat | Avoid unnecessary abstract factories |
17| Minimal APIs | Prefer over traditional MVC controllers |
18| Modular Monolith | Prefer over complex microservices |
19
20## Output Format
21
221. **Context** - Brief explanation of *why* the pattern fits
232. **Code** - Modern C# 14 implementation
243. **Key Features** - Highlight C# 14 features used
25
26## Pattern Quick Reference
27
28### Singleton (Thread-Safe)
29
30```csharp
31public sealed class ConfigService(IOptions<AppSettings> options)
32{
33 private static readonly Lazy<ConfigService> _instance = new(() =>
34 new ConfigService(/* resolve from DI */));
35
36 public static ConfigService Instance => _instance.Value;
37 public AppSettings Settings => options.Value;
38}
39```
40
41### Options Pattern (Preferred for Config)
42
43```csharp
44// appsettings.json binding
45public record DatabaseOptions
46{
47 public const string Section = "Database";
48 public required string ConnectionString { get; init; }
49 public int MaxRetries { get; init; } = 3;
50}
51
52// Registration
53builder.Services.Configure<DatabaseOptions>(
54 builder.Configuration.GetSection(DatabaseOptions.Section));
55
56// Usage with primary constructor
57public class UserRepository(IOptions<DatabaseOptions> options)
58{
59 private readonly string _connectionString = options.Value.ConnectionString;
60}
61```
62
63### Factory Pattern (Modern)
64
65```csharp
66public interface IPaymentProcessor { Task ProcessAsync(decimal amount); }
67
68public class PaymentProcessorFactory(IServiceProvider sp)
69{
70 public IPaymentProcessor Create(string type) => type switch
71 {
72 "stripe" => sp.GetRequiredService<StripeProcessor>(),
73 "paypal" => sp.GetRequiredService<PayPalProcessor>(),
74 _ => throw new ArgumentException($"Unknown processor: {type}")
75 };
76}
77```
78
79### Repository Pattern (Generic)
80
81```csharp
82public interface IRepository<T> where T : class
83{
84 ValueTask<T?> GetByIdAsync(int id, CancellationToken ct = default);
85 IAsyncEnumerable<T> GetAllAsync(CancellationToken ct = default);
86 Task AddAsync(T entity, CancellationToken ct = default);
87 Task UpdateAsync(T entity, CancellationToken ct = default);
88 Task DeleteAsync(int id, CancellationToken ct = default);
89}
90
91public class EfRepository<T>(AppDbContext db) : IRepository<T> where T : class
92{
93 public ValueTask<T?> GetByIdAsync(int id, CancellationToken ct = default)
94 => db.Set<T>().FindAsync([id], ct);
95
96 public IAsyncEnumerable<T> GetAllAsync(CancellationToken ct = default)
97 => db.Set<T>().AsAsyncEnumerable();
98
99 public async Task AddAsync(T entity, CancellationToken ct = default)
100 {
101 db.Set<T>().Add(entity);
102 await db.SaveChangesAsync(ct);
103 }
104
105 public async Task UpdateAsync(T entity, CancellationToken ct = default)
106 {
107 db.Set<T>().Update(entity);
108 await db.SaveChangesAsync(ct);
109 }
110
111 public async Task DeleteAsync(int id, CancellationToken ct = default)
112 {
113 var entity = await GetByIdAsync(id, ct);
114 if (entity is not null)
115 {
116 db.Set<T>().Remove(entity);
117 await db.SaveChangesAsync(ct);
118 }
119 }
120}
121```
122
123### CQRS (Simplified)
124
125```csharp
126// Commands
127public record CreateOrderCommand(string CustomerId, List<OrderItem> Items);
128public interface ICommandHandler<TCommand>
129{
130 Task<Result> HandleAsync(TCommand command, CancellationToken ct = default);
131}
132
133// Queries
134public record GetOrderQuery(int OrderId);
135public interface IQueryHandler<TQuery, TResult>
136{
137 Task<TResult> HandleAsync(TQuery query, CancellationToken ct = default);
138}
139
140// Minimal API integration
141app.MapPost("/orders", async (
142 CreateOrderCommand cmd,
143 ICommandHandler<CreateOrderCommand> handler,
144 CancellationToken ct) =>
145{
146 var result = await handler.HandleAsync(cmd, ct);
147 return result.IsSuccess ? Results.Created() : Results.BadRequest(result.Error);
148});
149```
150
151## Minimal API Patterns
152
153```csharp
154var builder = WebApplication.CreateBuilder(args);
155
156// Service registration
157builder.Services.AddScoped<IUserService, UserService>();
158builder.Services.Configure<DatabaseOptions>(
159 builder.Configuration.GetSection(DatabaseOptions.Section));
160
161var app = builder.Build();
162
163// Route groups
164var api = app.MapGroup("/api/v1");
165var users = api.MapGroup("/users").RequireAuthorization();
166
167users.MapGet("/", async (IUserService svc, CancellationToken ct) =>
168 await svc.GetAllAsync(ct));
169
170users.MapGet("/{id:int}", async (int id, IUserService svc, CancellationToken ct) =>
171 await svc.GetByIdAsync(id, ct) is { } user
172 ? Results.Ok(user)
173 : Results.NotFound());
174
175users.MapPost("/", async (CreateUserRequest req, IUserService svc, CancellationToken ct) =>
176{
177 var user = await svc.CreateAsync(req, ct);
178 return Results.Created($"/api/v1/users/{user.Id}", user);
179});
180
181app.Run();
182```
183
184## C# 14 Features to Use
185
186| Feature | Usage |
187|---------|-------|
188| Primary constructors | DI injection: `class Service(IRepo repo)` |
189| `field` keyword | Backing field access in properties |
190| Records | DTOs, Commands, Queries, Value Objects |
191| Required members | `required string Name { get; init; }` |
192| Collection expressions | `[1, 2, 3]` instead of `new[] {1, 2, 3}` |
193| Pattern matching | Switch expressions for complex conditionals |
194
195## Resources
196
197- **Design patterns details**: See `references/patterns.md`
198- **Minimal API patterns**: See `references/minimal-api.md`