agentic-team-templates 0.13.1 → 0.14.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.
- package/README.md +6 -1
- package/package.json +1 -1
- package/src/index.js +22 -2
- package/src/index.test.js +5 -0
- package/templates/cpp-expert/.cursorrules/concurrency.md +211 -0
- package/templates/cpp-expert/.cursorrules/error-handling.md +170 -0
- package/templates/cpp-expert/.cursorrules/memory-and-ownership.md +220 -0
- package/templates/cpp-expert/.cursorrules/modern-cpp.md +211 -0
- package/templates/cpp-expert/.cursorrules/overview.md +87 -0
- package/templates/cpp-expert/.cursorrules/performance.md +223 -0
- package/templates/cpp-expert/.cursorrules/testing.md +230 -0
- package/templates/cpp-expert/.cursorrules/tooling.md +312 -0
- package/templates/cpp-expert/CLAUDE.md +242 -0
- package/templates/csharp-expert/.cursorrules/aspnet-core.md +311 -0
- package/templates/csharp-expert/.cursorrules/async-patterns.md +206 -0
- package/templates/csharp-expert/.cursorrules/dependency-injection.md +206 -0
- package/templates/csharp-expert/.cursorrules/error-handling.md +235 -0
- package/templates/csharp-expert/.cursorrules/language-features.md +204 -0
- package/templates/csharp-expert/.cursorrules/overview.md +92 -0
- package/templates/csharp-expert/.cursorrules/performance.md +251 -0
- package/templates/csharp-expert/.cursorrules/testing.md +282 -0
- package/templates/csharp-expert/.cursorrules/tooling.md +254 -0
- package/templates/csharp-expert/CLAUDE.md +360 -0
- package/templates/java-expert/.cursorrules/concurrency.md +209 -0
- package/templates/java-expert/.cursorrules/error-handling.md +205 -0
- package/templates/java-expert/.cursorrules/modern-java.md +216 -0
- package/templates/java-expert/.cursorrules/overview.md +81 -0
- package/templates/java-expert/.cursorrules/performance.md +239 -0
- package/templates/java-expert/.cursorrules/persistence.md +262 -0
- package/templates/java-expert/.cursorrules/spring-boot.md +262 -0
- package/templates/java-expert/.cursorrules/testing.md +272 -0
- package/templates/java-expert/.cursorrules/tooling.md +301 -0
- package/templates/java-expert/CLAUDE.md +325 -0
- package/templates/javascript-expert/.cursorrules/overview.md +5 -3
- package/templates/javascript-expert/.cursorrules/typescript-deep-dive.md +348 -0
- package/templates/javascript-expert/CLAUDE.md +34 -3
- package/templates/kotlin-expert/.cursorrules/coroutines.md +237 -0
- package/templates/kotlin-expert/.cursorrules/error-handling.md +149 -0
- package/templates/kotlin-expert/.cursorrules/frameworks.md +227 -0
- package/templates/kotlin-expert/.cursorrules/language-features.md +231 -0
- package/templates/kotlin-expert/.cursorrules/overview.md +77 -0
- package/templates/kotlin-expert/.cursorrules/performance.md +185 -0
- package/templates/kotlin-expert/.cursorrules/testing.md +213 -0
- package/templates/kotlin-expert/.cursorrules/tooling.md +258 -0
- package/templates/kotlin-expert/CLAUDE.md +276 -0
- package/templates/swift-expert/.cursorrules/concurrency.md +230 -0
- package/templates/swift-expert/.cursorrules/error-handling.md +213 -0
- package/templates/swift-expert/.cursorrules/language-features.md +246 -0
- package/templates/swift-expert/.cursorrules/overview.md +88 -0
- package/templates/swift-expert/.cursorrules/performance.md +260 -0
- package/templates/swift-expert/.cursorrules/swiftui.md +260 -0
- package/templates/swift-expert/.cursorrules/testing.md +286 -0
- package/templates/swift-expert/.cursorrules/tooling.md +285 -0
- package/templates/swift-expert/CLAUDE.md +275 -0
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
# ASP.NET Core Patterns
|
|
2
|
+
|
|
3
|
+
Modern ASP.NET Core web development. Minimal APIs, middleware, and production-grade service patterns.
|
|
4
|
+
|
|
5
|
+
## Minimal APIs
|
|
6
|
+
|
|
7
|
+
```csharp
|
|
8
|
+
var builder = WebApplication.CreateBuilder(args);
|
|
9
|
+
|
|
10
|
+
// Service registration
|
|
11
|
+
builder.Services.AddOrderServices();
|
|
12
|
+
builder.Services.AddOpenApi();
|
|
13
|
+
|
|
14
|
+
var app = builder.Build();
|
|
15
|
+
|
|
16
|
+
// Middleware pipeline — order matters
|
|
17
|
+
app.UseExceptionHandler();
|
|
18
|
+
app.UseStatusCodePages();
|
|
19
|
+
app.UseAuthentication();
|
|
20
|
+
app.UseAuthorization();
|
|
21
|
+
|
|
22
|
+
// Endpoint mapping
|
|
23
|
+
app.MapOrderEndpoints();
|
|
24
|
+
app.MapOpenApi();
|
|
25
|
+
|
|
26
|
+
app.Run();
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Endpoint Organization
|
|
30
|
+
|
|
31
|
+
```csharp
|
|
32
|
+
public static class OrderEndpoints
|
|
33
|
+
{
|
|
34
|
+
public static void MapOrderEndpoints(this IEndpointRouteBuilder app)
|
|
35
|
+
{
|
|
36
|
+
var group = app.MapGroup("/orders")
|
|
37
|
+
.WithTags("Orders")
|
|
38
|
+
.RequireAuthorization();
|
|
39
|
+
|
|
40
|
+
group.MapGet("/", GetAllOrders)
|
|
41
|
+
.WithName("GetOrders")
|
|
42
|
+
.Produces<IReadOnlyList<OrderResponse>>();
|
|
43
|
+
|
|
44
|
+
group.MapGet("/{id:int}", GetOrderById)
|
|
45
|
+
.WithName("GetOrder")
|
|
46
|
+
.Produces<OrderResponse>()
|
|
47
|
+
.ProducesProblem(StatusCodes.Status404NotFound);
|
|
48
|
+
|
|
49
|
+
group.MapPost("/", CreateOrder)
|
|
50
|
+
.WithName("CreateOrder")
|
|
51
|
+
.Produces<OrderResponse>(StatusCodes.Status201Created)
|
|
52
|
+
.ProducesValidationProblem();
|
|
53
|
+
|
|
54
|
+
group.MapDelete("/{id:int}", DeleteOrder)
|
|
55
|
+
.RequireAuthorization("AdminOnly");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
private static async Task<IResult> GetOrderById(
|
|
59
|
+
int id, IOrderService service, CancellationToken ct)
|
|
60
|
+
{
|
|
61
|
+
var order = await service.GetByIdAsync(id, ct);
|
|
62
|
+
return order is not null
|
|
63
|
+
? Results.Ok(order)
|
|
64
|
+
: Results.Problem(statusCode: 404, title: "Order not found");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private static async Task<IResult> CreateOrder(
|
|
68
|
+
CreateOrderRequest request,
|
|
69
|
+
IValidator<CreateOrderRequest> validator,
|
|
70
|
+
IOrderService service,
|
|
71
|
+
CancellationToken ct)
|
|
72
|
+
{
|
|
73
|
+
var validation = await validator.ValidateAsync(request, ct);
|
|
74
|
+
if (!validation.IsValid)
|
|
75
|
+
return Results.ValidationProblem(validation.ToDictionary());
|
|
76
|
+
|
|
77
|
+
var result = await service.CreateAsync(request, ct);
|
|
78
|
+
return result.Match(
|
|
79
|
+
order => Results.Created($"/orders/{order.Id}", order),
|
|
80
|
+
error => Results.Problem(error.Message, statusCode: 400));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Middleware
|
|
86
|
+
|
|
87
|
+
```csharp
|
|
88
|
+
// Request timing middleware
|
|
89
|
+
public class RequestTimingMiddleware(RequestDelegate next, ILogger<RequestTimingMiddleware> logger)
|
|
90
|
+
{
|
|
91
|
+
public async Task InvokeAsync(HttpContext context)
|
|
92
|
+
{
|
|
93
|
+
var stopwatch = Stopwatch.StartNew();
|
|
94
|
+
try
|
|
95
|
+
{
|
|
96
|
+
await next(context);
|
|
97
|
+
}
|
|
98
|
+
finally
|
|
99
|
+
{
|
|
100
|
+
stopwatch.Stop();
|
|
101
|
+
logger.LogInformation(
|
|
102
|
+
"HTTP {Method} {Path} responded {StatusCode} in {ElapsedMs}ms",
|
|
103
|
+
context.Request.Method,
|
|
104
|
+
context.Request.Path,
|
|
105
|
+
context.Response.StatusCode,
|
|
106
|
+
stopwatch.ElapsedMilliseconds);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Registration
|
|
112
|
+
app.UseMiddleware<RequestTimingMiddleware>();
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Configuration
|
|
116
|
+
|
|
117
|
+
```csharp
|
|
118
|
+
// Strongly typed with validation
|
|
119
|
+
public class DatabaseOptions
|
|
120
|
+
{
|
|
121
|
+
public const string SectionName = "Database";
|
|
122
|
+
|
|
123
|
+
[Required]
|
|
124
|
+
public required string ConnectionString { get; init; }
|
|
125
|
+
|
|
126
|
+
[Range(1, 100)]
|
|
127
|
+
public int MaxPoolSize { get; init; } = 20;
|
|
128
|
+
|
|
129
|
+
[Range(1, 300)]
|
|
130
|
+
public int CommandTimeoutSeconds { get; init; } = 30;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Registration with validation at startup
|
|
134
|
+
builder.Services
|
|
135
|
+
.AddOptions<DatabaseOptions>()
|
|
136
|
+
.BindConfiguration(DatabaseOptions.SectionName)
|
|
137
|
+
.ValidateDataAnnotations()
|
|
138
|
+
.ValidateOnStart();
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Health Checks
|
|
142
|
+
|
|
143
|
+
```csharp
|
|
144
|
+
builder.Services.AddHealthChecks()
|
|
145
|
+
.AddDbContextCheck<AppDbContext>("database")
|
|
146
|
+
.AddRedis(connectionString, "redis")
|
|
147
|
+
.AddCheck<ExternalApiHealthCheck>("external-api");
|
|
148
|
+
|
|
149
|
+
app.MapHealthChecks("/healthz", new HealthCheckOptions
|
|
150
|
+
{
|
|
151
|
+
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// Liveness vs readiness
|
|
155
|
+
app.MapHealthChecks("/healthz/live", new HealthCheckOptions
|
|
156
|
+
{
|
|
157
|
+
Predicate = _ => false // Just checks if the app is running
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
app.MapHealthChecks("/healthz/ready", new HealthCheckOptions
|
|
161
|
+
{
|
|
162
|
+
Predicate = check => check.Tags.Contains("ready")
|
|
163
|
+
});
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## Authentication & Authorization
|
|
167
|
+
|
|
168
|
+
```csharp
|
|
169
|
+
// JWT Bearer
|
|
170
|
+
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
|
171
|
+
.AddJwtBearer(options =>
|
|
172
|
+
{
|
|
173
|
+
options.Authority = builder.Configuration["Auth:Authority"];
|
|
174
|
+
options.Audience = builder.Configuration["Auth:Audience"];
|
|
175
|
+
options.TokenValidationParameters = new TokenValidationParameters
|
|
176
|
+
{
|
|
177
|
+
ValidateIssuer = true,
|
|
178
|
+
ValidateAudience = true,
|
|
179
|
+
ValidateLifetime = true,
|
|
180
|
+
ClockSkew = TimeSpan.FromSeconds(30)
|
|
181
|
+
};
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// Policy-based authorization
|
|
185
|
+
builder.Services.AddAuthorization(options =>
|
|
186
|
+
{
|
|
187
|
+
options.AddPolicy("AdminOnly", policy =>
|
|
188
|
+
policy.RequireClaim("role", "admin"));
|
|
189
|
+
|
|
190
|
+
options.AddPolicy("CanManageOrders", policy =>
|
|
191
|
+
policy.Requirements.Add(new OrderManagementRequirement()));
|
|
192
|
+
});
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## Output Caching (.NET 7+)
|
|
196
|
+
|
|
197
|
+
```csharp
|
|
198
|
+
builder.Services.AddOutputCache(options =>
|
|
199
|
+
{
|
|
200
|
+
options.AddBasePolicy(builder => builder.Expire(TimeSpan.FromMinutes(5)));
|
|
201
|
+
options.AddPolicy("Products", builder =>
|
|
202
|
+
builder.Tag("products").Expire(TimeSpan.FromMinutes(30)));
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
app.MapGet("/products", GetProducts)
|
|
206
|
+
.CacheOutput("Products");
|
|
207
|
+
|
|
208
|
+
// Invalidation
|
|
209
|
+
app.MapPost("/products", async (
|
|
210
|
+
CreateProductRequest request,
|
|
211
|
+
IOutputCacheStore store,
|
|
212
|
+
CancellationToken ct) =>
|
|
213
|
+
{
|
|
214
|
+
// ... create product ...
|
|
215
|
+
await store.EvictByTagAsync("products", ct);
|
|
216
|
+
});
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
## Rate Limiting
|
|
220
|
+
|
|
221
|
+
```csharp
|
|
222
|
+
builder.Services.AddRateLimiter(options =>
|
|
223
|
+
{
|
|
224
|
+
options.AddFixedWindowLimiter("api", config =>
|
|
225
|
+
{
|
|
226
|
+
config.PermitLimit = 100;
|
|
227
|
+
config.Window = TimeSpan.FromMinutes(1);
|
|
228
|
+
config.QueueLimit = 0;
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
options.AddTokenBucketLimiter("uploads", config =>
|
|
232
|
+
{
|
|
233
|
+
config.TokenLimit = 10;
|
|
234
|
+
config.ReplenishmentPeriod = TimeSpan.FromSeconds(10);
|
|
235
|
+
config.TokensPerPeriod = 2;
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
options.OnRejected = async (context, ct) =>
|
|
239
|
+
{
|
|
240
|
+
context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;
|
|
241
|
+
await context.HttpContext.Response.WriteAsJsonAsync(
|
|
242
|
+
new ProblemDetails { Title = "Too many requests" }, ct);
|
|
243
|
+
};
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
app.UseRateLimiter();
|
|
247
|
+
app.MapGet("/api/data", GetData).RequireRateLimiting("api");
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
## Background Services
|
|
251
|
+
|
|
252
|
+
```csharp
|
|
253
|
+
public class OrderProcessorService(
|
|
254
|
+
IServiceScopeFactory scopeFactory,
|
|
255
|
+
ILogger<OrderProcessorService> logger) : BackgroundService
|
|
256
|
+
{
|
|
257
|
+
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
|
258
|
+
{
|
|
259
|
+
logger.LogInformation("Order processor starting");
|
|
260
|
+
|
|
261
|
+
while (!stoppingToken.IsCancellationRequested)
|
|
262
|
+
{
|
|
263
|
+
try
|
|
264
|
+
{
|
|
265
|
+
using var scope = scopeFactory.CreateScope();
|
|
266
|
+
var processor = scope.ServiceProvider
|
|
267
|
+
.GetRequiredService<IOrderProcessor>();
|
|
268
|
+
|
|
269
|
+
await processor.ProcessPendingOrdersAsync(stoppingToken);
|
|
270
|
+
}
|
|
271
|
+
catch (Exception ex) when (ex is not OperationCanceledException)
|
|
272
|
+
{
|
|
273
|
+
logger.LogError(ex, "Error processing orders");
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
## Anti-Patterns
|
|
283
|
+
|
|
284
|
+
```csharp
|
|
285
|
+
// Never: business logic in endpoints
|
|
286
|
+
app.MapPost("/orders", async (CreateOrderRequest req, AppDbContext db) =>
|
|
287
|
+
{
|
|
288
|
+
// Validation, business rules, persistence all in one place
|
|
289
|
+
if (req.Items.Count == 0) return Results.BadRequest();
|
|
290
|
+
var order = new Order { /* ... */ };
|
|
291
|
+
db.Orders.Add(order);
|
|
292
|
+
await db.SaveChangesAsync();
|
|
293
|
+
return Results.Ok(order);
|
|
294
|
+
});
|
|
295
|
+
// Use services, keep endpoints thin
|
|
296
|
+
|
|
297
|
+
// Never: exposing entities directly
|
|
298
|
+
app.MapGet("/users/{id}", async (int id, AppDbContext db) =>
|
|
299
|
+
await db.Users.FindAsync(id)); // Exposes internal schema, password hashes, etc.
|
|
300
|
+
// Map to DTOs/response records
|
|
301
|
+
|
|
302
|
+
// Never: synchronous I/O in middleware
|
|
303
|
+
public void Configure(IApplicationBuilder app)
|
|
304
|
+
{
|
|
305
|
+
app.Use((context, next) =>
|
|
306
|
+
{
|
|
307
|
+
var data = File.ReadAllText("config.json"); // Blocks thread pool thread!
|
|
308
|
+
return next();
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
```
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# C# Async Patterns
|
|
2
|
+
|
|
3
|
+
async/await done right. Every pitfall known. Every pattern battle-tested.
|
|
4
|
+
|
|
5
|
+
## The Golden Rules
|
|
6
|
+
|
|
7
|
+
1. **Async all the way down.** Never mix sync and async.
|
|
8
|
+
2. **Never block on async.** No `.Result`, `.Wait()`, `.GetAwaiter().GetResult()` in application code.
|
|
9
|
+
3. **Always pass CancellationToken.** Every async method that does I/O should accept and honor one.
|
|
10
|
+
4. **ConfigureAwait(false) in library code.** Not needed in ASP.NET Core app code (no SynchronizationContext).
|
|
11
|
+
5. **Return Task, not void.** `async void` is only for event handlers.
|
|
12
|
+
|
|
13
|
+
## Proper Async Methods
|
|
14
|
+
|
|
15
|
+
```csharp
|
|
16
|
+
// Good: accepts CancellationToken, returns Task<T>
|
|
17
|
+
public async Task<User?> GetUserAsync(int id, CancellationToken cancellationToken = default)
|
|
18
|
+
{
|
|
19
|
+
return await _dbContext.Users
|
|
20
|
+
.AsNoTracking()
|
|
21
|
+
.FirstOrDefaultAsync(u => u.Id == id, cancellationToken);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Good: elide async/await when just forwarding
|
|
25
|
+
public Task<User?> GetUserAsync(int id, CancellationToken ct = default)
|
|
26
|
+
=> _dbContext.Users.AsNoTracking().FirstOrDefaultAsync(u => u.Id == id, ct);
|
|
27
|
+
// But: don't elide if there's a using/try-catch — the await ensures proper lifetime
|
|
28
|
+
|
|
29
|
+
// Bad: blocking on async
|
|
30
|
+
public User GetUser(int id)
|
|
31
|
+
{
|
|
32
|
+
return GetUserAsync(id).Result; // DEADLOCK RISK
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## CancellationToken
|
|
37
|
+
|
|
38
|
+
```csharp
|
|
39
|
+
// Thread it through every layer
|
|
40
|
+
public async Task<OrderResult> ProcessOrderAsync(
|
|
41
|
+
CreateOrderRequest request,
|
|
42
|
+
CancellationToken cancellationToken)
|
|
43
|
+
{
|
|
44
|
+
var user = await _userService.GetByIdAsync(request.UserId, cancellationToken);
|
|
45
|
+
var inventory = await _inventoryService.CheckAsync(request.Items, cancellationToken);
|
|
46
|
+
|
|
47
|
+
// Check cancellation before expensive operations
|
|
48
|
+
cancellationToken.ThrowIfCancellationRequested();
|
|
49
|
+
|
|
50
|
+
var order = Order.Create(user, inventory);
|
|
51
|
+
await _repository.SaveAsync(order, cancellationToken);
|
|
52
|
+
|
|
53
|
+
return new OrderResult(order.Id);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Link cancellation tokens
|
|
57
|
+
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(
|
|
58
|
+
cancellationToken,
|
|
59
|
+
_applicationLifetime.ApplicationStopping);
|
|
60
|
+
|
|
61
|
+
// Timeout with cancellation
|
|
62
|
+
using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
|
|
63
|
+
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(
|
|
64
|
+
cancellationToken, timeoutCts.Token);
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Concurrent Operations
|
|
68
|
+
|
|
69
|
+
```csharp
|
|
70
|
+
// Good: parallel independent operations with Task.WhenAll
|
|
71
|
+
public async Task<DashboardData> GetDashboardAsync(int userId, CancellationToken ct)
|
|
72
|
+
{
|
|
73
|
+
var userTask = _userService.GetByIdAsync(userId, ct);
|
|
74
|
+
var ordersTask = _orderService.GetRecentAsync(userId, ct);
|
|
75
|
+
var notificationsTask = _notificationService.GetUnreadAsync(userId, ct);
|
|
76
|
+
|
|
77
|
+
await Task.WhenAll(userTask, ordersTask, notificationsTask);
|
|
78
|
+
|
|
79
|
+
return new DashboardData(
|
|
80
|
+
User: await userTask,
|
|
81
|
+
Orders: await ordersTask,
|
|
82
|
+
Notifications: await notificationsTask);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Good: bounded concurrency with SemaphoreSlim
|
|
86
|
+
public async Task ProcessBatchAsync(
|
|
87
|
+
IReadOnlyList<Item> items, CancellationToken ct)
|
|
88
|
+
{
|
|
89
|
+
using var semaphore = new SemaphoreSlim(maxConcurrency: 10);
|
|
90
|
+
var tasks = items.Select(async item =>
|
|
91
|
+
{
|
|
92
|
+
await semaphore.WaitAsync(ct);
|
|
93
|
+
try
|
|
94
|
+
{
|
|
95
|
+
await ProcessItemAsync(item, ct);
|
|
96
|
+
}
|
|
97
|
+
finally
|
|
98
|
+
{
|
|
99
|
+
semaphore.Release();
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
await Task.WhenAll(tasks);
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Channels
|
|
108
|
+
|
|
109
|
+
```csharp
|
|
110
|
+
// Producer-consumer with bounded channels
|
|
111
|
+
public class EventProcessor
|
|
112
|
+
{
|
|
113
|
+
private readonly Channel<DomainEvent> _channel =
|
|
114
|
+
Channel.CreateBounded<DomainEvent>(new BoundedChannelOptions(1000)
|
|
115
|
+
{
|
|
116
|
+
FullMode = BoundedChannelFullMode.Wait,
|
|
117
|
+
SingleReader = true,
|
|
118
|
+
SingleWriter = false
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
public async ValueTask PublishAsync(DomainEvent @event, CancellationToken ct)
|
|
122
|
+
{
|
|
123
|
+
await _channel.Writer.WriteAsync(@event, ct);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
public async Task ProcessAsync(CancellationToken ct)
|
|
127
|
+
{
|
|
128
|
+
await foreach (var @event in _channel.Reader.ReadAllAsync(ct))
|
|
129
|
+
{
|
|
130
|
+
await HandleEventAsync(@event, ct);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## ValueTask
|
|
137
|
+
|
|
138
|
+
```csharp
|
|
139
|
+
// Use ValueTask when the result is often synchronous (cached, pooled)
|
|
140
|
+
public ValueTask<User?> GetCachedUserAsync(int id, CancellationToken ct)
|
|
141
|
+
{
|
|
142
|
+
if (_cache.TryGetValue(id, out var user))
|
|
143
|
+
return ValueTask.FromResult<User?>(user); // No allocation
|
|
144
|
+
|
|
145
|
+
return GetAndCacheUserAsync(id, ct); // Async path
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
private async ValueTask<User?> GetAndCacheUserAsync(int id, CancellationToken ct)
|
|
149
|
+
{
|
|
150
|
+
var user = await _repository.GetByIdAsync(id, ct);
|
|
151
|
+
if (user is not null) _cache.Set(id, user);
|
|
152
|
+
return user;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Rules for ValueTask:
|
|
156
|
+
// - Never await a ValueTask more than once
|
|
157
|
+
// - Never use .Result or .GetAwaiter().GetResult() on an incomplete ValueTask
|
|
158
|
+
// - Never use Task.WhenAll with ValueTask (convert with .AsTask() first)
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## IAsyncEnumerable
|
|
162
|
+
|
|
163
|
+
```csharp
|
|
164
|
+
// Streaming results without buffering
|
|
165
|
+
public async IAsyncEnumerable<LogEntry> StreamLogsAsync(
|
|
166
|
+
string filter,
|
|
167
|
+
[EnumeratorCancellation] CancellationToken ct = default)
|
|
168
|
+
{
|
|
169
|
+
await foreach (var line in _logSource.ReadLinesAsync(ct))
|
|
170
|
+
{
|
|
171
|
+
if (line.Contains(filter, StringComparison.OrdinalIgnoreCase))
|
|
172
|
+
{
|
|
173
|
+
yield return ParseLogEntry(line);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Consuming
|
|
179
|
+
await foreach (var entry in StreamLogsAsync("ERROR", ct))
|
|
180
|
+
{
|
|
181
|
+
await ProcessEntryAsync(entry, ct);
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## Anti-Patterns
|
|
186
|
+
|
|
187
|
+
```csharp
|
|
188
|
+
// Never: async void (unobservable exceptions)
|
|
189
|
+
async void OnButtonClick() { await DoWorkAsync(); }
|
|
190
|
+
// Use: async Task OnButtonClickAsync() { ... }
|
|
191
|
+
|
|
192
|
+
// Never: fire-and-forget without error handling
|
|
193
|
+
_ = DoWorkAsync(); // Exception silently swallowed
|
|
194
|
+
// Use: _ = Task.Run(async () => { try { ... } catch { _logger.LogError(...); } });
|
|
195
|
+
|
|
196
|
+
// Never: unnecessary async/await wrapper
|
|
197
|
+
async Task<int> GetValueAsync() { return await Task.FromResult(42); }
|
|
198
|
+
// Just: Task<int> GetValueAsync() => Task.FromResult(42);
|
|
199
|
+
|
|
200
|
+
// Never: Task.Run for I/O-bound work
|
|
201
|
+
await Task.Run(() => httpClient.GetAsync(url)); // Wastes a thread pool thread
|
|
202
|
+
// Just: await httpClient.GetAsync(url);
|
|
203
|
+
|
|
204
|
+
// Never: capturing loop variable in pre-C#5 style (modern C# handles this, but be aware)
|
|
205
|
+
// Never: using async lambdas with void-returning delegates (Action)
|
|
206
|
+
```
|