@malamute/ai-rules 1.0.0 → 1.3.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 +272 -121
- package/bin/cli.js +5 -2
- package/configs/_shared/CLAUDE.md +52 -149
- package/configs/_shared/rules/conventions/documentation.md +324 -0
- package/configs/_shared/rules/conventions/git.md +265 -0
- package/configs/_shared/rules/conventions/npm.md +80 -0
- package/configs/_shared/{.claude/rules → rules/conventions}/performance.md +1 -1
- package/configs/_shared/rules/conventions/principles.md +334 -0
- package/configs/_shared/rules/devops/ci-cd.md +262 -0
- package/configs/_shared/rules/devops/docker.md +275 -0
- package/configs/_shared/rules/devops/nx.md +194 -0
- package/configs/_shared/rules/domain/backend/api-design.md +203 -0
- package/configs/_shared/rules/lang/csharp/async.md +220 -0
- package/configs/_shared/rules/lang/csharp/csharp.md +314 -0
- package/configs/_shared/rules/lang/csharp/linq.md +210 -0
- package/configs/_shared/rules/lang/python/async.md +337 -0
- package/configs/_shared/rules/lang/python/celery.md +476 -0
- package/configs/_shared/rules/lang/python/config.md +339 -0
- package/configs/{python/.claude/rules → _shared/rules/lang/python}/database/sqlalchemy.md +6 -1
- package/configs/_shared/rules/lang/python/deployment.md +523 -0
- package/configs/_shared/rules/lang/python/error-handling.md +330 -0
- package/configs/_shared/rules/lang/python/migrations.md +421 -0
- package/configs/_shared/rules/lang/python/python.md +172 -0
- package/configs/_shared/rules/lang/python/repository.md +383 -0
- package/configs/{python/.claude/rules → _shared/rules/lang/python}/testing.md +2 -69
- package/configs/_shared/rules/lang/typescript/async.md +447 -0
- package/configs/_shared/rules/lang/typescript/generics.md +356 -0
- package/configs/_shared/rules/lang/typescript/typescript.md +212 -0
- package/configs/_shared/rules/quality/error-handling.md +48 -0
- package/configs/_shared/rules/quality/logging.md +45 -0
- package/configs/_shared/rules/quality/observability.md +240 -0
- package/configs/_shared/rules/quality/testing-patterns.md +65 -0
- package/configs/_shared/rules/security/secrets-management.md +222 -0
- package/configs/_shared/skills/analysis/explore/SKILL.md +257 -0
- package/configs/_shared/skills/analysis/security-audit/SKILL.md +184 -0
- package/configs/_shared/skills/dev/api-endpoint/SKILL.md +126 -0
- package/configs/_shared/{.claude/commands/generate-tests.md → skills/dev/generate-tests/SKILL.md} +6 -0
- package/configs/_shared/{.claude/commands/fix-issue.md → skills/git/fix-issue/SKILL.md} +6 -0
- package/configs/_shared/{.claude/commands/review-pr.md → skills/git/review-pr/SKILL.md} +6 -0
- package/configs/_shared/skills/infra/deploy/SKILL.md +139 -0
- package/configs/_shared/skills/infra/docker/SKILL.md +95 -0
- package/configs/_shared/skills/infra/migration/SKILL.md +158 -0
- package/configs/_shared/skills/nx/nx-affected/SKILL.md +72 -0
- package/configs/_shared/skills/nx/nx-lib/SKILL.md +375 -0
- package/configs/angular/CLAUDE.md +24 -216
- package/configs/angular/{.claude/rules → rules/core}/components.md +69 -15
- package/configs/angular/rules/core/resource.md +285 -0
- package/configs/angular/rules/core/signals.md +323 -0
- package/configs/angular/rules/http.md +338 -0
- package/configs/angular/rules/routing.md +291 -0
- package/configs/angular/rules/ssr.md +312 -0
- package/configs/angular/rules/state/signal-store.md +408 -0
- package/configs/angular/{.claude/rules → rules/state}/state.md +2 -2
- package/configs/angular/{.claude/rules → rules}/testing.md +7 -7
- package/configs/angular/rules/ui/aria.md +422 -0
- package/configs/angular/rules/ui/forms.md +424 -0
- package/configs/angular/rules/ui/pipes-directives.md +335 -0
- package/configs/angular/{.claude/settings.json → settings.json} +3 -0
- package/configs/dotnet/CLAUDE.md +53 -286
- package/configs/dotnet/rules/background-services.md +552 -0
- package/configs/dotnet/rules/configuration.md +426 -0
- package/configs/dotnet/rules/ddd.md +447 -0
- package/configs/dotnet/rules/dependency-injection.md +343 -0
- package/configs/dotnet/rules/mediatr.md +320 -0
- package/configs/dotnet/rules/middleware.md +489 -0
- package/configs/dotnet/rules/result-pattern.md +363 -0
- package/configs/dotnet/rules/validation.md +388 -0
- package/configs/dotnet/settings.json +29 -0
- package/configs/fastapi/CLAUDE.md +144 -0
- package/configs/fastapi/rules/background-tasks.md +254 -0
- package/configs/fastapi/rules/dependencies.md +170 -0
- package/configs/{python/.claude → fastapi}/rules/fastapi.md +61 -1
- package/configs/fastapi/rules/lifespan.md +274 -0
- package/configs/fastapi/rules/middleware.md +229 -0
- package/configs/fastapi/rules/pydantic.md +433 -0
- package/configs/fastapi/rules/responses.md +251 -0
- package/configs/fastapi/rules/routers.md +202 -0
- package/configs/fastapi/rules/security.md +222 -0
- package/configs/fastapi/rules/testing.md +251 -0
- package/configs/fastapi/rules/websockets.md +298 -0
- package/configs/fastapi/settings.json +35 -0
- package/configs/flask/CLAUDE.md +166 -0
- package/configs/flask/rules/blueprints.md +208 -0
- package/configs/flask/rules/cli.md +285 -0
- package/configs/flask/rules/configuration.md +281 -0
- package/configs/flask/rules/context.md +238 -0
- package/configs/flask/rules/error-handlers.md +278 -0
- package/configs/flask/rules/extensions.md +278 -0
- package/configs/flask/rules/flask.md +171 -0
- package/configs/flask/rules/marshmallow.md +206 -0
- package/configs/flask/rules/security.md +267 -0
- package/configs/flask/rules/testing.md +284 -0
- package/configs/flask/settings.json +35 -0
- package/configs/nestjs/CLAUDE.md +57 -215
- package/configs/nestjs/rules/common-patterns.md +300 -0
- package/configs/nestjs/rules/filters.md +376 -0
- package/configs/nestjs/rules/interceptors.md +317 -0
- package/configs/nestjs/rules/middleware.md +321 -0
- package/configs/nestjs/{.claude/rules → rules}/modules.md +26 -0
- package/configs/nestjs/rules/pipes.md +351 -0
- package/configs/nestjs/rules/websockets.md +451 -0
- package/configs/nestjs/settings.json +31 -0
- package/configs/nextjs/CLAUDE.md +69 -331
- package/configs/nextjs/rules/api-routes.md +358 -0
- package/configs/nextjs/rules/authentication.md +355 -0
- package/configs/nextjs/{.claude/rules → rules}/components.md +52 -0
- package/configs/nextjs/rules/data-fetching.md +249 -0
- package/configs/nextjs/rules/database.md +400 -0
- package/configs/nextjs/rules/middleware.md +303 -0
- package/configs/nextjs/rules/routing.md +324 -0
- package/configs/nextjs/rules/seo.md +350 -0
- package/configs/nextjs/rules/server-actions.md +353 -0
- package/configs/nextjs/{.claude/rules → rules}/state/zustand.md +6 -6
- package/configs/nextjs/{.claude/settings.json → settings.json} +7 -0
- package/package.json +24 -9
- package/src/cli.js +218 -0
- package/src/config.js +63 -0
- package/src/index.js +4 -0
- package/src/installer.js +414 -0
- package/src/merge.js +109 -0
- package/src/tech-config.json +45 -0
- package/src/utils.js +88 -0
- package/configs/dotnet/.claude/settings.json +0 -9
- package/configs/nestjs/.claude/settings.json +0 -15
- package/configs/python/.claude/rules/flask.md +0 -332
- package/configs/python/.claude/settings.json +0 -18
- package/configs/python/CLAUDE.md +0 -273
- package/src/install.js +0 -315
- /package/configs/_shared/{.claude/rules → rules/domain/frontend}/accessibility.md +0 -0
- /package/configs/_shared/{.claude/rules → rules/security}/security.md +0 -0
- /package/configs/_shared/{.claude/skills → skills/dev}/debug/SKILL.md +0 -0
- /package/configs/_shared/{.claude/skills → skills/dev}/learning/SKILL.md +0 -0
- /package/configs/_shared/{.claude/skills → skills/dev}/spec/SKILL.md +0 -0
- /package/configs/_shared/{.claude/skills → skills/git}/review/SKILL.md +0 -0
- /package/configs/dotnet/{.claude/rules → rules}/api.md +0 -0
- /package/configs/dotnet/{.claude/rules → rules}/architecture.md +0 -0
- /package/configs/dotnet/{.claude/rules → rules}/database/efcore.md +0 -0
- /package/configs/dotnet/{.claude/rules → rules}/testing.md +0 -0
- /package/configs/nestjs/{.claude/rules → rules}/auth.md +0 -0
- /package/configs/nestjs/{.claude/rules → rules}/database/prisma.md +0 -0
- /package/configs/nestjs/{.claude/rules → rules}/database/typeorm.md +0 -0
- /package/configs/nestjs/{.claude/rules → rules}/testing.md +0 -0
- /package/configs/nestjs/{.claude/rules → rules}/validation.md +0 -0
- /package/configs/nextjs/{.claude/rules → rules}/state/redux-toolkit.md +0 -0
- /package/configs/nextjs/{.claude/rules → rules}/testing.md +0 -0
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "**/controllers/**"
|
|
4
|
+
- "**/routes/**"
|
|
5
|
+
- "**/routers/**"
|
|
6
|
+
- "**/endpoints/**"
|
|
7
|
+
- "**/*.controller.ts"
|
|
8
|
+
- "**/*_router.py"
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# API Design Principles
|
|
12
|
+
|
|
13
|
+
## REST Conventions
|
|
14
|
+
|
|
15
|
+
### HTTP Methods
|
|
16
|
+
|
|
17
|
+
| Method | Usage | Idempotent | Response |
|
|
18
|
+
|--------|-------|------------|----------|
|
|
19
|
+
| `GET` | Read resource(s) | Yes | 200 + data |
|
|
20
|
+
| `POST` | Create resource | No | 201 + created resource |
|
|
21
|
+
| `PUT` | Full update | Yes | 200 + updated resource |
|
|
22
|
+
| `PATCH` | Partial update | Yes | 200 + updated resource |
|
|
23
|
+
| `DELETE` | Remove resource | Yes | 204 No Content |
|
|
24
|
+
|
|
25
|
+
### URL Structure
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
GET /api/v1/users # List users
|
|
29
|
+
GET /api/v1/users/:id # Get single user
|
|
30
|
+
POST /api/v1/users # Create user
|
|
31
|
+
PUT /api/v1/users/:id # Replace user
|
|
32
|
+
PATCH /api/v1/users/:id # Update user fields
|
|
33
|
+
DELETE /api/v1/users/:id # Delete user
|
|
34
|
+
|
|
35
|
+
# Nested resources (max 2 levels)
|
|
36
|
+
GET /api/v1/users/:id/orders
|
|
37
|
+
POST /api/v1/users/:id/orders
|
|
38
|
+
|
|
39
|
+
# Actions (when CRUD doesn't fit)
|
|
40
|
+
POST /api/v1/users/:id/activate
|
|
41
|
+
POST /api/v1/orders/:id/cancel
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Naming Rules
|
|
45
|
+
|
|
46
|
+
- Use **plural nouns**: `/users`, `/orders`, `/products`
|
|
47
|
+
- Use **kebab-case**: `/user-profiles`, `/order-items`
|
|
48
|
+
- Avoid verbs in URLs (use HTTP methods instead)
|
|
49
|
+
- Use query params for filtering: `/users?status=active&role=admin`
|
|
50
|
+
|
|
51
|
+
## Response Format
|
|
52
|
+
|
|
53
|
+
### Success Response
|
|
54
|
+
|
|
55
|
+
```json
|
|
56
|
+
{
|
|
57
|
+
"data": { ... },
|
|
58
|
+
"meta": {
|
|
59
|
+
"timestamp": "2024-01-15T10:30:00Z",
|
|
60
|
+
"requestId": "abc-123"
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### List Response with Pagination
|
|
66
|
+
|
|
67
|
+
```json
|
|
68
|
+
{
|
|
69
|
+
"data": [ ... ],
|
|
70
|
+
"meta": {
|
|
71
|
+
"total": 100,
|
|
72
|
+
"page": 1,
|
|
73
|
+
"pageSize": 20,
|
|
74
|
+
"totalPages": 5,
|
|
75
|
+
"hasNext": true,
|
|
76
|
+
"hasPrevious": false
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Error Response (RFC 7807 Problem Details)
|
|
82
|
+
|
|
83
|
+
```json
|
|
84
|
+
{
|
|
85
|
+
"type": "https://api.example.com/errors/validation",
|
|
86
|
+
"title": "Validation Error",
|
|
87
|
+
"status": 400,
|
|
88
|
+
"detail": "One or more fields failed validation",
|
|
89
|
+
"instance": "/api/v1/users",
|
|
90
|
+
"errors": [
|
|
91
|
+
{ "field": "email", "message": "Invalid email format" },
|
|
92
|
+
{ "field": "age", "message": "Must be at least 18" }
|
|
93
|
+
],
|
|
94
|
+
"traceId": "abc-123"
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Status Codes
|
|
99
|
+
|
|
100
|
+
| Code | When to Use |
|
|
101
|
+
|------|-------------|
|
|
102
|
+
| `200` | Successful GET, PUT, PATCH |
|
|
103
|
+
| `201` | Successful POST (resource created) |
|
|
104
|
+
| `204` | Successful DELETE (no content) |
|
|
105
|
+
| `400` | Bad request (validation error) |
|
|
106
|
+
| `401` | Unauthorized (not authenticated) |
|
|
107
|
+
| `403` | Forbidden (authenticated but not allowed) |
|
|
108
|
+
| `404` | Resource not found |
|
|
109
|
+
| `409` | Conflict (duplicate resource) |
|
|
110
|
+
| `422` | Unprocessable entity (business rule violation) |
|
|
111
|
+
| `429` | Too many requests (rate limited) |
|
|
112
|
+
| `500` | Internal server error |
|
|
113
|
+
|
|
114
|
+
## Pagination
|
|
115
|
+
|
|
116
|
+
### Offset-based (simple, for small datasets)
|
|
117
|
+
|
|
118
|
+
```
|
|
119
|
+
GET /api/v1/users?page=2&pageSize=20
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Cursor-based (performant, for large datasets)
|
|
123
|
+
|
|
124
|
+
```
|
|
125
|
+
GET /api/v1/users?cursor=eyJpZCI6MTAwfQ&limit=20
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Response includes next cursor:
|
|
129
|
+
```json
|
|
130
|
+
{
|
|
131
|
+
"data": [...],
|
|
132
|
+
"meta": {
|
|
133
|
+
"nextCursor": "eyJpZCI6MTIwfQ",
|
|
134
|
+
"hasMore": true
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Filtering & Sorting
|
|
140
|
+
|
|
141
|
+
```
|
|
142
|
+
# Filtering
|
|
143
|
+
GET /api/v1/users?status=active&role=admin
|
|
144
|
+
GET /api/v1/orders?createdAt[gte]=2024-01-01&createdAt[lte]=2024-12-31
|
|
145
|
+
|
|
146
|
+
# Sorting
|
|
147
|
+
GET /api/v1/users?sort=createdAt:desc
|
|
148
|
+
GET /api/v1/users?sort=lastName:asc,firstName:asc
|
|
149
|
+
|
|
150
|
+
# Field selection (sparse fieldsets)
|
|
151
|
+
GET /api/v1/users?fields=id,name,email
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Versioning
|
|
155
|
+
|
|
156
|
+
### URL Path (recommended)
|
|
157
|
+
|
|
158
|
+
```
|
|
159
|
+
/api/v1/users
|
|
160
|
+
/api/v2/users
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Header-based (alternative)
|
|
164
|
+
|
|
165
|
+
```
|
|
166
|
+
Accept: application/vnd.api+json; version=1
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## Rate Limiting
|
|
170
|
+
|
|
171
|
+
Include headers in response:
|
|
172
|
+
|
|
173
|
+
```
|
|
174
|
+
X-RateLimit-Limit: 100
|
|
175
|
+
X-RateLimit-Remaining: 95
|
|
176
|
+
X-RateLimit-Reset: 1640000000
|
|
177
|
+
Retry-After: 60 (when 429)
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## HATEOAS (optional, for discoverability)
|
|
181
|
+
|
|
182
|
+
```json
|
|
183
|
+
{
|
|
184
|
+
"data": {
|
|
185
|
+
"id": "123",
|
|
186
|
+
"name": "John"
|
|
187
|
+
},
|
|
188
|
+
"links": {
|
|
189
|
+
"self": "/api/v1/users/123",
|
|
190
|
+
"orders": "/api/v1/users/123/orders",
|
|
191
|
+
"profile": "/api/v1/users/123/profile"
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## Anti-patterns
|
|
197
|
+
|
|
198
|
+
- Verbs in URLs: `/api/getUsers` → `/api/users`
|
|
199
|
+
- Singular nouns: `/api/user/123` → `/api/users/123`
|
|
200
|
+
- Deeply nested: `/api/users/1/orders/2/items/3` → `/api/order-items/3`
|
|
201
|
+
- Inconsistent casing: `/api/userProfiles` → `/api/user-profiles`
|
|
202
|
+
- Returning 200 for errors
|
|
203
|
+
- Exposing internal IDs or sensitive data
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "**/*.cs"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# C# Async Patterns
|
|
7
|
+
|
|
8
|
+
## Task vs ValueTask
|
|
9
|
+
|
|
10
|
+
```csharp
|
|
11
|
+
// Use Task for most async operations
|
|
12
|
+
public async Task<User> GetUserAsync(int id)
|
|
13
|
+
|
|
14
|
+
// Use ValueTask when:
|
|
15
|
+
// 1. Method often completes synchronously
|
|
16
|
+
// 2. Hot path with many allocations
|
|
17
|
+
public async ValueTask<User?> GetCachedUserAsync(int id)
|
|
18
|
+
{
|
|
19
|
+
if (_cache.TryGetValue(id, out var user))
|
|
20
|
+
return user; // Synchronous path - no allocation
|
|
21
|
+
|
|
22
|
+
return await _repository.GetByIdAsync(id);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// BAD - ValueTask misuse (awaiting multiple times)
|
|
26
|
+
var task = GetValueTaskAsync();
|
|
27
|
+
await task;
|
|
28
|
+
await task; // Undefined behavior!
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Parallel Execution
|
|
32
|
+
|
|
33
|
+
```csharp
|
|
34
|
+
// GOOD - parallel independent operations
|
|
35
|
+
var userTask = _userService.GetUserAsync(userId);
|
|
36
|
+
var ordersTask = _orderService.GetOrdersAsync(userId);
|
|
37
|
+
var prefsTask = _prefService.GetPreferencesAsync(userId);
|
|
38
|
+
|
|
39
|
+
await Task.WhenAll(userTask, ordersTask, prefsTask);
|
|
40
|
+
|
|
41
|
+
var user = await userTask;
|
|
42
|
+
var orders = await ordersTask;
|
|
43
|
+
var prefs = await prefsTask;
|
|
44
|
+
|
|
45
|
+
// BAD - sequential when parallel is possible
|
|
46
|
+
var user = await _userService.GetUserAsync(userId);
|
|
47
|
+
var orders = await _orderService.GetOrdersAsync(userId);
|
|
48
|
+
var prefs = await _prefService.GetPreferencesAsync(userId);
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Task.WhenAll Error Handling
|
|
52
|
+
|
|
53
|
+
```csharp
|
|
54
|
+
// Handle all exceptions from parallel tasks
|
|
55
|
+
try
|
|
56
|
+
{
|
|
57
|
+
await Task.WhenAll(task1, task2, task3);
|
|
58
|
+
}
|
|
59
|
+
catch (Exception)
|
|
60
|
+
{
|
|
61
|
+
// Only first exception is thrown
|
|
62
|
+
// Check individual tasks for all errors
|
|
63
|
+
var exceptions = new[] { task1, task2, task3 }
|
|
64
|
+
.Where(t => t.IsFaulted)
|
|
65
|
+
.SelectMany(t => t.Exception!.InnerExceptions);
|
|
66
|
+
|
|
67
|
+
foreach (var ex in exceptions)
|
|
68
|
+
{
|
|
69
|
+
_logger.LogError(ex, "Task failed");
|
|
70
|
+
}
|
|
71
|
+
throw;
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Cancellation
|
|
76
|
+
|
|
77
|
+
```csharp
|
|
78
|
+
// GOOD - check cancellation in loops
|
|
79
|
+
public async Task ProcessBatchAsync(
|
|
80
|
+
IEnumerable<Item> items,
|
|
81
|
+
CancellationToken cancellationToken)
|
|
82
|
+
{
|
|
83
|
+
foreach (var item in items)
|
|
84
|
+
{
|
|
85
|
+
cancellationToken.ThrowIfCancellationRequested();
|
|
86
|
+
await ProcessItemAsync(item, cancellationToken);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// GOOD - cancellation with timeout
|
|
91
|
+
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
|
|
92
|
+
try
|
|
93
|
+
{
|
|
94
|
+
await LongOperationAsync(cts.Token);
|
|
95
|
+
}
|
|
96
|
+
catch (OperationCanceledException)
|
|
97
|
+
{
|
|
98
|
+
_logger.LogWarning("Operation timed out");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Link multiple cancellation tokens
|
|
102
|
+
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(
|
|
103
|
+
requestToken,
|
|
104
|
+
applicationStoppingToken);
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Async Streams
|
|
108
|
+
|
|
109
|
+
```csharp
|
|
110
|
+
// GOOD - IAsyncEnumerable for streaming data
|
|
111
|
+
public async IAsyncEnumerable<User> GetUsersStreamAsync(
|
|
112
|
+
[EnumeratorCancellation] CancellationToken cancellationToken = default)
|
|
113
|
+
{
|
|
114
|
+
await foreach (var user in _context.Users.AsAsyncEnumerable()
|
|
115
|
+
.WithCancellation(cancellationToken))
|
|
116
|
+
{
|
|
117
|
+
yield return user;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Consuming async stream
|
|
122
|
+
await foreach (var user in GetUsersStreamAsync(cancellationToken))
|
|
123
|
+
{
|
|
124
|
+
await ProcessUserAsync(user);
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Avoid Async Pitfalls
|
|
129
|
+
|
|
130
|
+
```csharp
|
|
131
|
+
// BAD - async void (except event handlers)
|
|
132
|
+
public async void ProcessAsync() // Exceptions lost, can't await
|
|
133
|
+
{
|
|
134
|
+
await Task.Delay(1000);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// GOOD - return Task
|
|
138
|
+
public async Task ProcessAsync()
|
|
139
|
+
{
|
|
140
|
+
await Task.Delay(1000);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// BAD - blocking on async (.Result, .Wait())
|
|
144
|
+
public void Process()
|
|
145
|
+
{
|
|
146
|
+
var result = GetDataAsync().Result; // Deadlock risk!
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// BAD - unnecessary async/await
|
|
150
|
+
public async Task<int> GetCountAsync()
|
|
151
|
+
{
|
|
152
|
+
return await _repository.CountAsync(); // Just return the task
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// GOOD - return task directly when no additional await needed
|
|
156
|
+
public Task<int> GetCountAsync()
|
|
157
|
+
{
|
|
158
|
+
return _repository.CountAsync();
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## Thread Safety
|
|
163
|
+
|
|
164
|
+
```csharp
|
|
165
|
+
// GOOD - use concurrent collections
|
|
166
|
+
private readonly ConcurrentDictionary<int, User> _cache = new();
|
|
167
|
+
|
|
168
|
+
// GOOD - SemaphoreSlim for async locking
|
|
169
|
+
private readonly SemaphoreSlim _semaphore = new(1, 1);
|
|
170
|
+
|
|
171
|
+
public async Task<User> GetOrCreateUserAsync(int id)
|
|
172
|
+
{
|
|
173
|
+
await _semaphore.WaitAsync();
|
|
174
|
+
try
|
|
175
|
+
{
|
|
176
|
+
if (!_cache.TryGetValue(id, out var user))
|
|
177
|
+
{
|
|
178
|
+
user = await _repository.GetByIdAsync(id);
|
|
179
|
+
_cache[id] = user;
|
|
180
|
+
}
|
|
181
|
+
return user;
|
|
182
|
+
}
|
|
183
|
+
finally
|
|
184
|
+
{
|
|
185
|
+
_semaphore.Release();
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// BAD - lock with async (will not compile correctly)
|
|
190
|
+
lock (_syncObject)
|
|
191
|
+
{
|
|
192
|
+
await SomeAsyncOperation(); // Can't await inside lock
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## Channel for Producer/Consumer
|
|
197
|
+
|
|
198
|
+
```csharp
|
|
199
|
+
public class BackgroundProcessor
|
|
200
|
+
{
|
|
201
|
+
private readonly Channel<WorkItem> _channel =
|
|
202
|
+
Channel.CreateBounded<WorkItem>(new BoundedChannelOptions(100)
|
|
203
|
+
{
|
|
204
|
+
FullMode = BoundedChannelFullMode.Wait
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
public async ValueTask QueueWorkAsync(WorkItem item)
|
|
208
|
+
{
|
|
209
|
+
await _channel.Writer.WriteAsync(item);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
public async Task ProcessAsync(CancellationToken stoppingToken)
|
|
213
|
+
{
|
|
214
|
+
await foreach (var item in _channel.Reader.ReadAllAsync(stoppingToken))
|
|
215
|
+
{
|
|
216
|
+
await ProcessItemAsync(item);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
```
|