@kodrunhq/opencode-autopilot 1.9.0 → 1.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,327 @@
1
+ ---
2
+ name: csharp-patterns
3
+ description: Idiomatic C# patterns including records, .NET DI, Entity Framework, and async best practices
4
+ stacks:
5
+ - csharp
6
+ requires:
7
+ - coding-standards
8
+ ---
9
+
10
+ # C# Patterns
11
+
12
+ Idiomatic C# patterns for modern (.NET 8+) projects. Covers language features, .NET dependency injection, Entity Framework conventions, async best practices, and common pitfalls. Apply these when writing, reviewing, or refactoring C# code.
13
+
14
+ ## 1. Modern C# Idioms
15
+
16
+ **DO:** Use modern C# features to write concise, null-safe, and expressive code.
17
+
18
+ - Use records for immutable data:
19
+ ```csharp
20
+ // Record: immutable, value equality, ToString auto-generated
21
+ public record UserDto(string Name, string Email, DateTimeOffset CreatedAt);
22
+
23
+ // With expression for non-destructive mutation
24
+ var updated = original with { Email = "new@example.com" };
25
+ ```
26
+ - Use pattern matching with switch expressions:
27
+ ```csharp
28
+ // DO: Switch expression with property patterns
29
+ string GetStatusMessage(Order order) => order switch
30
+ {
31
+ { Status: OrderStatus.Pending, Total: > 1000 } => "Large order pending approval",
32
+ { Status: OrderStatus.Pending } => "Awaiting processing",
33
+ { Status: OrderStatus.Shipped } => $"Shipped on {order.ShippedDate}",
34
+ { Status: OrderStatus.Delivered } => "Delivered",
35
+ _ => "Unknown status"
36
+ };
37
+ ```
38
+ - Enable nullable reference types and annotate nullability:
39
+ ```csharp
40
+ // In .csproj: <Nullable>enable</Nullable>
41
+
42
+ // Explicit nullability
43
+ public string GetDisplayName(User? user)
44
+ {
45
+ return user?.Name ?? "Anonymous";
46
+ }
47
+ ```
48
+ - Use `init`-only properties for immutable object initialization:
49
+ ```csharp
50
+ public class Config
51
+ {
52
+ public required string ConnectionString { get; init; }
53
+ public int MaxRetries { get; init; } = 3;
54
+ }
55
+
56
+ var config = new Config { ConnectionString = "Server=..." };
57
+ // config.ConnectionString = "other"; // Compile error
58
+ ```
59
+ - Use file-scoped namespaces to reduce nesting:
60
+ ```csharp
61
+ // DO: File-scoped namespace (one less indentation level)
62
+ namespace MyApp.Services;
63
+
64
+ public class OrderService { ... }
65
+ ```
66
+ - Use raw string literals for multi-line strings:
67
+ ```csharp
68
+ var query = """
69
+ SELECT u.Name, u.Email
70
+ FROM Users u
71
+ WHERE u.IsActive = 1
72
+ ORDER BY u.Name
73
+ """;
74
+ ```
75
+
76
+ **DON'T:**
77
+
78
+ - Ignore nullable warnings -- they prevent `NullReferenceException` at runtime
79
+ - Use `class` when `record` better represents the data (DTOs, value objects, events)
80
+ - Use verbose `if/else if` chains when switch expressions are clearer
81
+ - Use `string.Format()` when string interpolation (`$"Hello {name}"`) is available
82
+
83
+ ## 2. .NET Patterns
84
+
85
+ **DO:** Follow .NET conventions for dependency injection, configuration, and middleware.
86
+
87
+ - Register services with the built-in DI container:
88
+ ```csharp
89
+ // Program.cs
90
+ builder.Services.AddScoped<IOrderService, OrderService>();
91
+ builder.Services.AddSingleton<ICacheService, RedisCacheService>();
92
+ builder.Services.AddTransient<IEmailSender, SmtpEmailSender>();
93
+ ```
94
+ - Use `IOptions<T>` for strongly-typed configuration:
95
+ ```csharp
96
+ public record EmailOptions
97
+ {
98
+ public required string Host { get; init; }
99
+ public int Port { get; init; } = 587;
100
+ public bool UseTls { get; init; } = true;
101
+ }
102
+
103
+ // Registration
104
+ builder.Services.Configure<EmailOptions>(builder.Configuration.GetSection("Email"));
105
+
106
+ // Injection
107
+ public class EmailSender(IOptions<EmailOptions> options)
108
+ {
109
+ private readonly EmailOptions _options = options.Value;
110
+ }
111
+ ```
112
+ - Order middleware carefully -- order matters in the pipeline:
113
+ ```csharp
114
+ app.UseExceptionHandler("/error"); // First: catch everything
115
+ app.UseHttpsRedirection();
116
+ app.UseAuthentication(); // Before authorization
117
+ app.UseAuthorization(); // After authentication
118
+ app.UseRateLimiter();
119
+ app.MapControllers(); // Last: route to handlers
120
+ ```
121
+ - Use `IHostedService` for background work:
122
+ ```csharp
123
+ public class CleanupService : BackgroundService
124
+ {
125
+ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
126
+ {
127
+ while (!stoppingToken.IsCancellationRequested)
128
+ {
129
+ await CleanupExpiredSessions();
130
+ await Task.Delay(TimeSpan.FromHours(1), stoppingToken);
131
+ }
132
+ }
133
+ }
134
+ ```
135
+ - Use `ILogger<T>` for structured logging:
136
+ ```csharp
137
+ public class OrderService(ILogger<OrderService> logger)
138
+ {
139
+ public void PlaceOrder(Order order)
140
+ {
141
+ logger.LogInformation("Placing order {OrderId} for {CustomerId}",
142
+ order.Id, order.CustomerId);
143
+ }
144
+ }
145
+ ```
146
+
147
+ **DON'T:**
148
+
149
+ - Use `new` to create services inside other services -- always inject via constructor
150
+ - Register scoped services as singletons -- scoped dependencies in singletons cause captive dependency bugs
151
+ - Put business logic in middleware -- middleware is for cross-cutting concerns (auth, logging, CORS)
152
+ - Use `IServiceProvider` directly (service locator pattern) -- inject specific interfaces instead
153
+
154
+ ## 3. Entity Framework Conventions
155
+
156
+ **DO:** Use EF Core idiomatically with code-first migrations and proper lifetime management.
157
+
158
+ - Use code-first migrations for schema management:
159
+ ```bash
160
+ dotnet ef migrations add AddOrderTable
161
+ dotnet ef database update
162
+ ```
163
+ - Register `DbContext` as scoped (default and correct):
164
+ ```csharp
165
+ builder.Services.AddDbContext<AppDbContext>(options =>
166
+ options.UseNpgsql(connectionString));
167
+ ```
168
+ - Use navigation properties with explicit loading strategies:
169
+ ```csharp
170
+ // DO: Explicit includes for read paths
171
+ var orders = await context.Orders
172
+ .Include(o => o.Items)
173
+ .Include(o => o.Customer)
174
+ .Where(o => o.Status == OrderStatus.Pending)
175
+ .ToListAsync();
176
+ ```
177
+ - Use global query filters for soft delete:
178
+ ```csharp
179
+ // In DbContext.OnModelCreating
180
+ modelBuilder.Entity<Order>()
181
+ .HasQueryFilter(o => !o.IsDeleted);
182
+
183
+ // To include deleted: context.Orders.IgnoreQueryFilters()
184
+ ```
185
+ - Use `AsNoTracking()` for read-only queries (better performance):
186
+ ```csharp
187
+ var reports = await context.Orders
188
+ .AsNoTracking()
189
+ .Select(o => new OrderSummary(o.Id, o.Total))
190
+ .ToListAsync();
191
+ ```
192
+ - Use value converters for custom types:
193
+ ```csharp
194
+ modelBuilder.Entity<Order>()
195
+ .Property(o => o.Currency)
196
+ .HasConversion(
197
+ v => v.Code, // To database
198
+ v => Currency.FromCode(v)); // From database
199
+ ```
200
+ - Use owned entities for value objects:
201
+ ```csharp
202
+ modelBuilder.Entity<Order>().OwnsOne(o => o.ShippingAddress);
203
+ ```
204
+
205
+ **DON'T:**
206
+
207
+ - Return `IQueryable<T>` from repositories -- materialize queries before returning to prevent unintended SQL generation outside the repository
208
+ - Use `DbContext` as a singleton -- it is not thread-safe, always use scoped lifetime
209
+ - Skip migrations and modify the database manually -- migrations are the source of truth
210
+ - Load entire entity graphs when you only need a few fields -- use `Select` projections
211
+ - Forget to call `SaveChangesAsync()` -- EF tracks changes but does not persist until you call save
212
+
213
+ ## 4. Async Best Practices
214
+
215
+ **DO:** Use `async`/`await` consistently and propagate cancellation tokens.
216
+
217
+ - Go async all the way -- never block on async code:
218
+ ```csharp
219
+ // DO: Async all the way
220
+ public async Task<Order> GetOrderAsync(Guid id, CancellationToken ct)
221
+ {
222
+ var order = await _repository.FindByIdAsync(id, ct);
223
+ return order ?? throw new OrderNotFoundException(id);
224
+ }
225
+ ```
226
+ - Use `ConfigureAwait(false)` in library code:
227
+ ```csharp
228
+ // Library code: no need to capture synchronization context
229
+ public async Task<byte[]> DownloadAsync(string url, CancellationToken ct)
230
+ {
231
+ var response = await _client.GetAsync(url, ct).ConfigureAwait(false);
232
+ return await response.Content.ReadAsByteArrayAsync(ct).ConfigureAwait(false);
233
+ }
234
+ ```
235
+ - Propagate `CancellationToken` through all async methods:
236
+ ```csharp
237
+ // DO: Accept and pass CancellationToken
238
+ public async Task ProcessAsync(CancellationToken ct = default)
239
+ {
240
+ await StepOneAsync(ct);
241
+ await StepTwoAsync(ct);
242
+ }
243
+ ```
244
+ - Use `ValueTask` for hot paths that often complete synchronously:
245
+ ```csharp
246
+ public ValueTask<CacheEntry?> GetCachedAsync(string key)
247
+ {
248
+ if (_memoryCache.TryGetValue(key, out var entry))
249
+ return ValueTask.FromResult<CacheEntry?>(entry); // No allocation
250
+
251
+ return new ValueTask<CacheEntry?>(FetchFromRemoteCacheAsync(key));
252
+ }
253
+ ```
254
+ - Use `IAsyncEnumerable<T>` for streaming results:
255
+ ```csharp
256
+ public async IAsyncEnumerable<Order> StreamOrdersAsync(
257
+ [EnumeratorCancellation] CancellationToken ct = default)
258
+ {
259
+ await foreach (var order in context.Orders.AsAsyncEnumerable().WithCancellation(ct))
260
+ {
261
+ yield return order;
262
+ }
263
+ }
264
+ ```
265
+
266
+ **DON'T:**
267
+
268
+ - Use `.Result` or `.Wait()` on async methods -- this causes deadlocks in ASP.NET:
269
+ ```csharp
270
+ // DEADLOCK: synchronously blocking on async
271
+ var result = GetOrderAsync(id).Result; // NEVER do this
272
+ ```
273
+ - Use `async void` except for event handlers -- `async void` swallows exceptions
274
+ - Forget `CancellationToken` -- without it, cancelled HTTP requests keep executing server-side
275
+ - Use `Task.Run()` to wrap synchronous code and pretend it's async -- that just moves work to the thread pool without making it non-blocking
276
+
277
+ ## 5. Common Pitfalls
278
+
279
+ **Pitfall: Disposal Patterns**
280
+ Always dispose resources. Use `using` declarations for deterministic cleanup:
281
+ ```csharp
282
+ // DO: using declaration (disposes at end of scope)
283
+ await using var connection = new SqlConnection(connectionString);
284
+ await connection.OpenAsync(ct);
285
+
286
+ // For classes: implement IAsyncDisposable
287
+ public class ResourceManager : IAsyncDisposable
288
+ {
289
+ public async ValueTask DisposeAsync()
290
+ {
291
+ await ReleaseResourcesAsync();
292
+ GC.SuppressFinalize(this);
293
+ }
294
+ }
295
+ ```
296
+
297
+ **Pitfall: Captured Loop Variables in Closures**
298
+ In older C# (before foreach fix in C# 5), the loop variable was captured by reference. While fixed for `foreach`, be cautious with `for` loops:
299
+ ```csharp
300
+ // CAUTION with for loops
301
+ for (int i = 0; i < 10; i++)
302
+ {
303
+ var captured = i; // Capture a copy
304
+ tasks.Add(Task.Run(() => Process(captured)));
305
+ }
306
+ ```
307
+
308
+ **Pitfall: String Concatenation in Loops**
309
+ Strings are immutable in C#. Concatenation in loops creates N intermediate strings. Use `StringBuilder`:
310
+ ```csharp
311
+ // DO
312
+ var sb = new StringBuilder();
313
+ foreach (var line in lines)
314
+ sb.AppendLine(line);
315
+ return sb.ToString();
316
+
317
+ // DON'T
318
+ var result = "";
319
+ foreach (var line in lines)
320
+ result += line + "\n"; // N allocations
321
+ ```
322
+
323
+ **Pitfall: Deadlocks from Mixing Sync/Async**
324
+ Calling `.Result` or `.Wait()` on a `Task` in code that has a synchronization context (ASP.NET, WPF) causes deadlocks. The async continuation needs the context, but `.Result` is blocking it. Solution: go async all the way up.
325
+
326
+ **Pitfall: Service Locator Anti-Pattern**
327
+ Injecting `IServiceProvider` and resolving services manually defeats the purpose of DI. Dependencies become hidden and untestable. Inject specific interfaces instead.