@malamute/ai-rules 1.0.0 → 1.2.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 +270 -121
- package/bin/cli.js +5 -2
- package/configs/_shared/.claude/rules/conventions/documentation.md +324 -0
- package/configs/_shared/.claude/rules/conventions/git.md +265 -0
- package/configs/_shared/.claude/rules/{performance.md → conventions/performance.md} +1 -1
- package/configs/_shared/.claude/rules/conventions/principles.md +334 -0
- package/configs/_shared/.claude/rules/devops/ci-cd.md +262 -0
- package/configs/_shared/.claude/rules/devops/docker.md +275 -0
- package/configs/_shared/.claude/rules/devops/nx.md +194 -0
- package/configs/_shared/.claude/rules/domain/backend/api-design.md +203 -0
- package/configs/_shared/.claude/rules/lang/csharp/async.md +220 -0
- package/configs/_shared/.claude/rules/lang/csharp/csharp.md +314 -0
- package/configs/_shared/.claude/rules/lang/csharp/linq.md +210 -0
- package/configs/_shared/.claude/rules/lang/python/async.md +337 -0
- package/configs/_shared/.claude/rules/lang/python/celery.md +476 -0
- package/configs/_shared/.claude/rules/lang/python/config.md +339 -0
- package/configs/{python/.claude/rules → _shared/.claude/rules/lang/python}/database/sqlalchemy.md +6 -1
- package/configs/_shared/.claude/rules/lang/python/deployment.md +523 -0
- package/configs/_shared/.claude/rules/lang/python/error-handling.md +330 -0
- package/configs/_shared/.claude/rules/lang/python/migrations.md +421 -0
- package/configs/_shared/.claude/rules/lang/python/python.md +172 -0
- package/configs/_shared/.claude/rules/lang/python/repository.md +383 -0
- package/configs/{python/.claude/rules → _shared/.claude/rules/lang/python}/testing.md +2 -69
- package/configs/_shared/.claude/rules/lang/typescript/async.md +447 -0
- package/configs/_shared/.claude/rules/lang/typescript/generics.md +356 -0
- package/configs/_shared/.claude/rules/lang/typescript/typescript.md +212 -0
- package/configs/_shared/.claude/rules/quality/error-handling.md +48 -0
- package/configs/_shared/.claude/rules/quality/logging.md +45 -0
- package/configs/_shared/.claude/rules/quality/observability.md +240 -0
- package/configs/_shared/.claude/rules/quality/testing-patterns.md +65 -0
- package/configs/_shared/.claude/rules/security/secrets-management.md +222 -0
- package/configs/_shared/.claude/skills/analysis/explore/SKILL.md +257 -0
- package/configs/_shared/.claude/skills/analysis/security-audit/SKILL.md +184 -0
- package/configs/_shared/.claude/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/.claude/skills/infra/deploy/SKILL.md +139 -0
- package/configs/_shared/.claude/skills/infra/docker/SKILL.md +95 -0
- package/configs/_shared/.claude/skills/infra/migration/SKILL.md +158 -0
- package/configs/_shared/.claude/skills/nx/nx-affected/SKILL.md +72 -0
- package/configs/_shared/.claude/skills/nx/nx-lib/SKILL.md +375 -0
- package/configs/_shared/CLAUDE.md +52 -149
- package/configs/angular/.claude/rules/{components.md → core/components.md} +69 -15
- package/configs/angular/.claude/rules/core/resource.md +285 -0
- package/configs/angular/.claude/rules/core/signals.md +323 -0
- package/configs/angular/.claude/rules/http.md +338 -0
- package/configs/angular/.claude/rules/routing.md +291 -0
- package/configs/angular/.claude/rules/ssr.md +312 -0
- package/configs/angular/.claude/rules/state/signal-store.md +408 -0
- package/configs/angular/.claude/rules/{state.md → state/state.md} +2 -2
- package/configs/angular/.claude/rules/testing.md +7 -7
- package/configs/angular/.claude/rules/ui/aria.md +422 -0
- package/configs/angular/.claude/rules/ui/forms.md +424 -0
- package/configs/angular/.claude/rules/ui/pipes-directives.md +335 -0
- package/configs/angular/.claude/settings.json +1 -0
- package/configs/angular/.claude/skills/ngrx-slice/SKILL.md +362 -0
- package/configs/angular/.claude/skills/signal-store/SKILL.md +445 -0
- package/configs/angular/CLAUDE.md +24 -216
- package/configs/dotnet/.claude/rules/background-services.md +552 -0
- package/configs/dotnet/.claude/rules/configuration.md +426 -0
- package/configs/dotnet/.claude/rules/ddd.md +447 -0
- package/configs/dotnet/.claude/rules/dependency-injection.md +343 -0
- package/configs/dotnet/.claude/rules/mediatr.md +320 -0
- package/configs/dotnet/.claude/rules/middleware.md +489 -0
- package/configs/dotnet/.claude/rules/result-pattern.md +363 -0
- package/configs/dotnet/.claude/rules/validation.md +388 -0
- package/configs/dotnet/.claude/settings.json +21 -3
- package/configs/dotnet/CLAUDE.md +53 -286
- package/configs/fastapi/.claude/rules/background-tasks.md +254 -0
- package/configs/fastapi/.claude/rules/dependencies.md +170 -0
- package/configs/{python → fastapi}/.claude/rules/fastapi.md +61 -1
- package/configs/fastapi/.claude/rules/lifespan.md +274 -0
- package/configs/fastapi/.claude/rules/middleware.md +229 -0
- package/configs/fastapi/.claude/rules/pydantic.md +433 -0
- package/configs/fastapi/.claude/rules/responses.md +251 -0
- package/configs/fastapi/.claude/rules/routers.md +202 -0
- package/configs/fastapi/.claude/rules/security.md +222 -0
- package/configs/fastapi/.claude/rules/testing.md +251 -0
- package/configs/fastapi/.claude/rules/websockets.md +298 -0
- package/configs/fastapi/.claude/settings.json +33 -0
- package/configs/fastapi/CLAUDE.md +144 -0
- package/configs/flask/.claude/rules/blueprints.md +208 -0
- package/configs/flask/.claude/rules/cli.md +285 -0
- package/configs/flask/.claude/rules/configuration.md +281 -0
- package/configs/flask/.claude/rules/context.md +238 -0
- package/configs/flask/.claude/rules/error-handlers.md +278 -0
- package/configs/flask/.claude/rules/extensions.md +278 -0
- package/configs/flask/.claude/rules/flask.md +171 -0
- package/configs/flask/.claude/rules/marshmallow.md +206 -0
- package/configs/flask/.claude/rules/security.md +267 -0
- package/configs/flask/.claude/rules/testing.md +284 -0
- package/configs/flask/.claude/settings.json +33 -0
- package/configs/flask/CLAUDE.md +166 -0
- package/configs/nestjs/.claude/rules/common-patterns.md +300 -0
- package/configs/nestjs/.claude/rules/filters.md +376 -0
- package/configs/nestjs/.claude/rules/interceptors.md +317 -0
- package/configs/nestjs/.claude/rules/middleware.md +321 -0
- package/configs/nestjs/.claude/rules/modules.md +26 -0
- package/configs/nestjs/.claude/rules/pipes.md +351 -0
- package/configs/nestjs/.claude/rules/websockets.md +451 -0
- package/configs/nestjs/.claude/settings.json +16 -2
- package/configs/nestjs/CLAUDE.md +57 -215
- package/configs/nextjs/.claude/rules/api-routes.md +358 -0
- package/configs/nextjs/.claude/rules/authentication.md +355 -0
- package/configs/nextjs/.claude/rules/components.md +52 -0
- package/configs/nextjs/.claude/rules/data-fetching.md +249 -0
- package/configs/nextjs/.claude/rules/database.md +400 -0
- package/configs/nextjs/.claude/rules/middleware.md +303 -0
- package/configs/nextjs/.claude/rules/routing.md +324 -0
- package/configs/nextjs/.claude/rules/seo.md +350 -0
- package/configs/nextjs/.claude/rules/server-actions.md +353 -0
- package/configs/nextjs/.claude/rules/state/zustand.md +6 -6
- package/configs/nextjs/.claude/settings.json +5 -0
- package/configs/nextjs/CLAUDE.md +69 -331
- package/package.json +23 -9
- package/src/cli.js +220 -0
- package/src/config.js +29 -0
- package/src/index.js +13 -0
- package/src/installer.js +361 -0
- package/src/merge.js +116 -0
- package/src/tech-config.json +29 -0
- package/src/utils.js +96 -0
- 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/{accessibility.md → domain/frontend/accessibility.md} +0 -0
- /package/configs/_shared/.claude/rules/{security.md → security/security.md} +0 -0
- /package/configs/_shared/.claude/skills/{debug → dev/debug}/SKILL.md +0 -0
- /package/configs/_shared/.claude/skills/{learning → dev/learning}/SKILL.md +0 -0
- /package/configs/_shared/.claude/skills/{spec → dev/spec}/SKILL.md +0 -0
- /package/configs/_shared/.claude/skills/{review → git/review}/SKILL.md +0 -0
package/configs/dotnet/CLAUDE.md
CHANGED
|
@@ -4,316 +4,83 @@
|
|
|
4
4
|
|
|
5
5
|
## Stack
|
|
6
6
|
|
|
7
|
-
- .NET 8
|
|
7
|
+
- .NET 9 / .NET 8 (LTS)
|
|
8
8
|
- ASP.NET Core Web API
|
|
9
9
|
- Entity Framework Core
|
|
10
|
-
- C#
|
|
11
|
-
- xUnit
|
|
10
|
+
- C# 13+
|
|
11
|
+
- xUnit + NSubstitute + FluentAssertions
|
|
12
12
|
|
|
13
|
-
## Architecture
|
|
14
|
-
|
|
15
|
-
### Clean Architecture (Recommended)
|
|
13
|
+
## Architecture - Clean Architecture
|
|
16
14
|
|
|
17
15
|
```
|
|
18
16
|
src/
|
|
19
|
-
├── Domain/
|
|
20
|
-
│
|
|
21
|
-
|
|
22
|
-
│
|
|
23
|
-
|
|
24
|
-
│
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
│ └── Interfaces/
|
|
28
|
-
│ └── IUserRepository.cs
|
|
29
|
-
│
|
|
30
|
-
├── Application/ # Use cases, CQRS, DTOs
|
|
31
|
-
│ ├── Common/
|
|
32
|
-
│ │ ├── Behaviors/
|
|
33
|
-
│ │ │ ├── ValidationBehavior.cs
|
|
34
|
-
│ │ │ └── LoggingBehavior.cs
|
|
35
|
-
│ │ ├── Interfaces/
|
|
36
|
-
│ │ │ └── IApplicationDbContext.cs
|
|
37
|
-
│ │ └── Mappings/
|
|
38
|
-
│ │ └── MappingProfile.cs
|
|
39
|
-
│ ├── Users/
|
|
40
|
-
│ │ ├── Commands/
|
|
41
|
-
│ │ │ ├── CreateUser/
|
|
42
|
-
│ │ │ │ ├── CreateUserCommand.cs
|
|
43
|
-
│ │ │ │ ├── CreateUserCommandHandler.cs
|
|
44
|
-
│ │ │ │ └── CreateUserCommandValidator.cs
|
|
45
|
-
│ │ │ └── UpdateUser/
|
|
46
|
-
│ │ └── Queries/
|
|
47
|
-
│ │ ├── GetUser/
|
|
48
|
-
│ │ │ ├── GetUserQuery.cs
|
|
49
|
-
│ │ │ ├── GetUserQueryHandler.cs
|
|
50
|
-
│ │ │ └── UserDto.cs
|
|
51
|
-
│ │ └── GetUsers/
|
|
52
|
-
│ └── DependencyInjection.cs
|
|
53
|
-
│
|
|
54
|
-
├── Infrastructure/ # External concerns
|
|
55
|
-
│ ├── Data/
|
|
56
|
-
│ │ ├── ApplicationDbContext.cs
|
|
57
|
-
│ │ ├── Configurations/
|
|
58
|
-
│ │ │ └── UserConfiguration.cs
|
|
59
|
-
│ │ └── Migrations/
|
|
60
|
-
│ ├── Repositories/
|
|
61
|
-
│ │ └── UserRepository.cs
|
|
62
|
-
│ ├── Services/
|
|
63
|
-
│ │ └── DateTimeService.cs
|
|
64
|
-
│ └── DependencyInjection.cs
|
|
65
|
-
│
|
|
66
|
-
└── WebApi/ # Presentation layer
|
|
67
|
-
├── Controllers/
|
|
68
|
-
│ └── UsersController.cs
|
|
69
|
-
├── Filters/
|
|
70
|
-
│ └── ApiExceptionFilterAttribute.cs
|
|
71
|
-
├── Middleware/
|
|
72
|
-
│ └── ExceptionHandlingMiddleware.cs
|
|
73
|
-
├── Program.cs
|
|
74
|
-
└── appsettings.json
|
|
17
|
+
├── Domain/ # Entities, ValueObjects, Interfaces
|
|
18
|
+
│ # ZERO external dependencies
|
|
19
|
+
├── Application/ # Commands, Queries, DTOs, Validators
|
|
20
|
+
│ # MediatR, FluentValidation
|
|
21
|
+
├── Infrastructure/ # DbContext, Repositories, External services
|
|
22
|
+
│ # EF Core, external APIs
|
|
23
|
+
└── WebApi/ # Controllers/Endpoints, Middleware
|
|
24
|
+
# Presentation layer
|
|
75
25
|
```
|
|
76
26
|
|
|
77
|
-
|
|
27
|
+
**Dependencies**:
|
|
28
|
+
- `WebApi → Application → Domain`
|
|
29
|
+
- `Infrastructure → Application → Domain`
|
|
78
30
|
|
|
79
|
-
|
|
80
|
-
WebApi → Application → Domain
|
|
81
|
-
WebApi → Infrastructure → Application → Domain
|
|
82
|
-
```
|
|
31
|
+
## Core Principles
|
|
83
32
|
|
|
84
|
-
|
|
33
|
+
### CQRS with MediatR
|
|
85
34
|
|
|
86
|
-
|
|
35
|
+
- **Commands** (write): Modify state, return minimal data (ID or void)
|
|
36
|
+
- **Queries** (read): Return data, never modify state
|
|
37
|
+
- Commands and queries implement `IRequest<T>`
|
|
87
38
|
|
|
88
|
-
###
|
|
39
|
+
### C# 12 Style
|
|
89
40
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
| Properties | PascalCase | `FirstName` |
|
|
96
|
-
| Private fields | _camelCase | `_userRepository` |
|
|
97
|
-
| Parameters | camelCase | `userId` |
|
|
98
|
-
| Constants | PascalCase | `MaxRetryCount` |
|
|
99
|
-
| Async methods | Suffix Async | `GetUserAsync` |
|
|
41
|
+
- File-scoped namespaces
|
|
42
|
+
- Primary constructors for DI
|
|
43
|
+
- Records for DTOs
|
|
44
|
+
- Nullable reference types enabled
|
|
45
|
+
- Required members with `required` keyword
|
|
100
46
|
|
|
101
|
-
###
|
|
47
|
+
### Naming
|
|
102
48
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
49
|
+
| Element | Convention |
|
|
50
|
+
|---------|------------|
|
|
51
|
+
| Classes/Methods | PascalCase |
|
|
52
|
+
| Interfaces | IPascalCase |
|
|
53
|
+
| Private fields | _camelCase |
|
|
54
|
+
| Async methods | Suffix `Async` |
|
|
55
|
+
| Constants | PascalCase |
|
|
106
56
|
|
|
107
|
-
|
|
57
|
+
### API Patterns
|
|
108
58
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
```
|
|
59
|
+
- **Minimal APIs**: Preferred for new projects (93% less memory in .NET 9)
|
|
60
|
+
- **Controllers**: Only for complex model binding scenarios
|
|
61
|
+
- Use `ISender` from MediatR for dispatching commands/queries
|
|
62
|
+
- Return `TypedResults.Ok()`, `TypedResults.NotFound()`, etc.
|
|
63
|
+
- Built-in OpenAPI: `AddOpenApi()` + `MapOpenApi()`
|
|
115
64
|
|
|
116
|
-
###
|
|
65
|
+
### Validation
|
|
117
66
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
{
|
|
122
|
-
public async Task<User?> GetByIdAsync(Guid id)
|
|
123
|
-
{
|
|
124
|
-
logger.LogInformation("Getting user {UserId}", id);
|
|
125
|
-
return await userRepository.GetByIdAsync(id);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// Use traditional constructors when you need field assignment or validation
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
### Records for DTOs
|
|
133
|
-
|
|
134
|
-
```csharp
|
|
135
|
-
// Immutable DTOs
|
|
136
|
-
public record UserDto(Guid Id, string Email, string Name);
|
|
137
|
-
|
|
138
|
-
public record CreateUserRequest(string Email, string Password, string Name);
|
|
139
|
-
|
|
140
|
-
// With validation attributes
|
|
141
|
-
public record CreateUserCommand(
|
|
142
|
-
[Required][EmailAddress] string Email,
|
|
143
|
-
[Required][MinLength(8)] string Password,
|
|
144
|
-
[Required] string Name
|
|
145
|
-
) : IRequest<Guid>;
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
### Nullable Reference Types
|
|
149
|
-
|
|
150
|
-
```csharp
|
|
151
|
-
// Enable in .csproj
|
|
152
|
-
<Nullable>enable</Nullable>
|
|
153
|
-
|
|
154
|
-
// Be explicit about nullability
|
|
155
|
-
public async Task<User?> GetByIdAsync(Guid id); // Can return null
|
|
156
|
-
public async Task<User> GetByIdOrThrowAsync(Guid id); // Never null
|
|
157
|
-
```
|
|
67
|
+
- FluentValidation for complex rules
|
|
68
|
+
- Data annotations for simple DTOs
|
|
69
|
+
- Validation in MediatR pipeline behavior
|
|
158
70
|
|
|
159
71
|
## Commands
|
|
160
72
|
|
|
161
73
|
```bash
|
|
162
|
-
#
|
|
163
|
-
dotnet
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
dotnet build
|
|
167
|
-
dotnet publish -c Release
|
|
168
|
-
|
|
169
|
-
# Tests
|
|
170
|
-
dotnet test
|
|
171
|
-
dotnet test --filter "Category=Unit"
|
|
172
|
-
dotnet test --collect:"XPlat Code Coverage"
|
|
173
|
-
|
|
174
|
-
# EF Core migrations
|
|
175
|
-
dotnet ef migrations add InitialCreate -p src/Infrastructure -s src/WebApi
|
|
74
|
+
dotnet run --project src/WebApi # Dev
|
|
75
|
+
dotnet build # Build
|
|
76
|
+
dotnet test # Test
|
|
77
|
+
dotnet ef migrations add Name -p src/Infrastructure -s src/WebApi
|
|
176
78
|
dotnet ef database update -p src/Infrastructure -s src/WebApi
|
|
177
|
-
|
|
178
|
-
# Format
|
|
179
|
-
dotnet format
|
|
180
79
|
```
|
|
181
80
|
|
|
182
|
-
##
|
|
183
|
-
|
|
184
|
-
### Minimal API Endpoints
|
|
185
|
-
|
|
186
|
-
```csharp
|
|
187
|
-
// Program.cs or endpoint extension
|
|
188
|
-
app.MapGet("/users/{id:guid}", async (Guid id, ISender sender) =>
|
|
189
|
-
{
|
|
190
|
-
var user = await sender.Send(new GetUserQuery(id));
|
|
191
|
-
return user is not null ? Results.Ok(user) : Results.NotFound();
|
|
192
|
-
})
|
|
193
|
-
.WithName("GetUser")
|
|
194
|
-
.WithOpenApi()
|
|
195
|
-
.RequireAuthorization();
|
|
196
|
-
|
|
197
|
-
app.MapPost("/users", async (CreateUserCommand command, ISender sender) =>
|
|
198
|
-
{
|
|
199
|
-
var id = await sender.Send(command);
|
|
200
|
-
return Results.CreatedAtRoute("GetUser", new { id }, id);
|
|
201
|
-
})
|
|
202
|
-
.WithName("CreateUser")
|
|
203
|
-
.WithOpenApi();
|
|
204
|
-
```
|
|
205
|
-
|
|
206
|
-
### Controller-Based API
|
|
207
|
-
|
|
208
|
-
```csharp
|
|
209
|
-
[ApiController]
|
|
210
|
-
[Route("api/[controller]")]
|
|
211
|
-
public class UsersController(ISender sender) : ControllerBase
|
|
212
|
-
{
|
|
213
|
-
[HttpGet("{id:guid}")]
|
|
214
|
-
[ProducesResponseType<UserDto>(StatusCodes.Status200OK)]
|
|
215
|
-
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
|
216
|
-
public async Task<IActionResult> Get(Guid id)
|
|
217
|
-
{
|
|
218
|
-
var user = await sender.Send(new GetUserQuery(id));
|
|
219
|
-
return user is not null ? Ok(user) : NotFound();
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
[HttpPost]
|
|
223
|
-
[ProducesResponseType<Guid>(StatusCodes.Status201Created)]
|
|
224
|
-
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
|
225
|
-
public async Task<IActionResult> Create(CreateUserCommand command)
|
|
226
|
-
{
|
|
227
|
-
var id = await sender.Send(command);
|
|
228
|
-
return CreatedAtAction(nameof(Get), new { id }, id);
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
```
|
|
232
|
-
|
|
233
|
-
### Global Exception Handling
|
|
234
|
-
|
|
235
|
-
```csharp
|
|
236
|
-
public class ExceptionHandlingMiddleware(RequestDelegate next, ILogger<ExceptionHandlingMiddleware> logger)
|
|
237
|
-
{
|
|
238
|
-
public async Task InvokeAsync(HttpContext context)
|
|
239
|
-
{
|
|
240
|
-
try
|
|
241
|
-
{
|
|
242
|
-
await next(context);
|
|
243
|
-
}
|
|
244
|
-
catch (ValidationException ex)
|
|
245
|
-
{
|
|
246
|
-
context.Response.StatusCode = StatusCodes.Status400BadRequest;
|
|
247
|
-
await context.Response.WriteAsJsonAsync(new ProblemDetails
|
|
248
|
-
{
|
|
249
|
-
Status = 400,
|
|
250
|
-
Title = "Validation Error",
|
|
251
|
-
Detail = string.Join(", ", ex.Errors.Select(e => e.ErrorMessage))
|
|
252
|
-
});
|
|
253
|
-
}
|
|
254
|
-
catch (NotFoundException ex)
|
|
255
|
-
{
|
|
256
|
-
context.Response.StatusCode = StatusCodes.Status404NotFound;
|
|
257
|
-
await context.Response.WriteAsJsonAsync(new ProblemDetails
|
|
258
|
-
{
|
|
259
|
-
Status = 404,
|
|
260
|
-
Title = "Not Found",
|
|
261
|
-
Detail = ex.Message
|
|
262
|
-
});
|
|
263
|
-
}
|
|
264
|
-
catch (Exception ex)
|
|
265
|
-
{
|
|
266
|
-
logger.LogError(ex, "Unhandled exception");
|
|
267
|
-
context.Response.StatusCode = StatusCodes.Status500InternalServerError;
|
|
268
|
-
await context.Response.WriteAsJsonAsync(new ProblemDetails
|
|
269
|
-
{
|
|
270
|
-
Status = 500,
|
|
271
|
-
Title = "Server Error"
|
|
272
|
-
});
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
```
|
|
277
|
-
|
|
278
|
-
### Dependency Injection Setup
|
|
279
|
-
|
|
280
|
-
```csharp
|
|
281
|
-
// Application/DependencyInjection.cs
|
|
282
|
-
public static class DependencyInjection
|
|
283
|
-
{
|
|
284
|
-
public static IServiceCollection AddApplication(this IServiceCollection services)
|
|
285
|
-
{
|
|
286
|
-
services.AddMediatR(cfg => {
|
|
287
|
-
cfg.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly());
|
|
288
|
-
cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));
|
|
289
|
-
cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>));
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly());
|
|
293
|
-
services.AddAutoMapper(Assembly.GetExecutingAssembly());
|
|
294
|
-
|
|
295
|
-
return services;
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
// Infrastructure/DependencyInjection.cs
|
|
300
|
-
public static class DependencyInjection
|
|
301
|
-
{
|
|
302
|
-
public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration configuration)
|
|
303
|
-
{
|
|
304
|
-
services.AddDbContext<ApplicationDbContext>(options =>
|
|
305
|
-
options.UseSqlServer(configuration.GetConnectionString("DefaultConnection")));
|
|
306
|
-
|
|
307
|
-
services.AddScoped<IApplicationDbContext>(sp =>
|
|
308
|
-
sp.GetRequiredService<ApplicationDbContext>());
|
|
309
|
-
|
|
310
|
-
services.AddScoped<IUserRepository, UserRepository>();
|
|
311
|
-
|
|
312
|
-
return services;
|
|
313
|
-
}
|
|
314
|
-
}
|
|
81
|
+
## Code Style
|
|
315
82
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
83
|
+
- `readonly` for immutable fields
|
|
84
|
+
- Expression-bodied members when simple
|
|
85
|
+
- Pattern matching over type checks
|
|
86
|
+
- `async`/`await` all the way down
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "**/*.py"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# FastAPI Background Tasks
|
|
7
|
+
|
|
8
|
+
## Simple Background Tasks
|
|
9
|
+
|
|
10
|
+
```python
|
|
11
|
+
from fastapi import BackgroundTasks
|
|
12
|
+
|
|
13
|
+
def write_log(message: str):
|
|
14
|
+
with open("log.txt", "a") as f:
|
|
15
|
+
f.write(f"{datetime.now()}: {message}\n")
|
|
16
|
+
|
|
17
|
+
@router.post("/items")
|
|
18
|
+
async def create_item(
|
|
19
|
+
item: ItemCreate,
|
|
20
|
+
background_tasks: BackgroundTasks,
|
|
21
|
+
) -> ItemResponse:
|
|
22
|
+
# Process item
|
|
23
|
+
created_item = await item_service.create(item)
|
|
24
|
+
|
|
25
|
+
# Queue background task
|
|
26
|
+
background_tasks.add_task(write_log, f"Item created: {created_item.id}")
|
|
27
|
+
|
|
28
|
+
# Return immediately
|
|
29
|
+
return created_item
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Async Background Tasks
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
async def send_notification(user_id: int, message: str):
|
|
36
|
+
async with httpx.AsyncClient() as client:
|
|
37
|
+
await client.post(
|
|
38
|
+
"https://notification-service/send",
|
|
39
|
+
json={"user_id": user_id, "message": message},
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
@router.post("/orders")
|
|
43
|
+
async def create_order(
|
|
44
|
+
order: OrderCreate,
|
|
45
|
+
background_tasks: BackgroundTasks,
|
|
46
|
+
db: DbSession,
|
|
47
|
+
) -> OrderResponse:
|
|
48
|
+
created_order = await order_service.create(db, order)
|
|
49
|
+
|
|
50
|
+
# Async task
|
|
51
|
+
background_tasks.add_task(
|
|
52
|
+
send_notification,
|
|
53
|
+
order.user_id,
|
|
54
|
+
f"Order {created_order.id} confirmed",
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
return created_order
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Multiple Background Tasks
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
@router.post("/signup")
|
|
64
|
+
async def signup(
|
|
65
|
+
user: UserCreate,
|
|
66
|
+
background_tasks: BackgroundTasks,
|
|
67
|
+
) -> UserResponse:
|
|
68
|
+
created_user = await user_service.create(user)
|
|
69
|
+
|
|
70
|
+
# Queue multiple tasks
|
|
71
|
+
background_tasks.add_task(send_welcome_email, created_user.email)
|
|
72
|
+
background_tasks.add_task(notify_admin, created_user.id)
|
|
73
|
+
background_tasks.add_task(update_analytics, "new_signup")
|
|
74
|
+
|
|
75
|
+
return created_user
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Background Tasks in Dependencies
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
async def log_request(
|
|
82
|
+
request: Request,
|
|
83
|
+
background_tasks: BackgroundTasks,
|
|
84
|
+
):
|
|
85
|
+
background_tasks.add_task(
|
|
86
|
+
log_to_database,
|
|
87
|
+
path=request.url.path,
|
|
88
|
+
method=request.method,
|
|
89
|
+
timestamp=datetime.now(),
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
@router.get("/data", dependencies=[Depends(log_request)])
|
|
93
|
+
async def get_data() -> dict:
|
|
94
|
+
return {"data": "value"}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Long-Running Tasks with Celery
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
# tasks.py
|
|
101
|
+
from celery import Celery
|
|
102
|
+
|
|
103
|
+
celery_app = Celery(
|
|
104
|
+
"tasks",
|
|
105
|
+
broker="redis://localhost:6379/0",
|
|
106
|
+
backend="redis://localhost:6379/0",
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
@celery_app.task
|
|
110
|
+
def process_large_file(file_path: str) -> dict:
|
|
111
|
+
# Long-running process
|
|
112
|
+
result = heavy_computation(file_path)
|
|
113
|
+
return {"status": "completed", "result": result}
|
|
114
|
+
|
|
115
|
+
# router.py
|
|
116
|
+
from tasks import process_large_file
|
|
117
|
+
|
|
118
|
+
@router.post("/process")
|
|
119
|
+
async def process_file(file: UploadFile) -> dict:
|
|
120
|
+
# Save file
|
|
121
|
+
file_path = f"/tmp/{file.filename}"
|
|
122
|
+
async with aiofiles.open(file_path, "wb") as f:
|
|
123
|
+
await f.write(await file.read())
|
|
124
|
+
|
|
125
|
+
# Queue Celery task
|
|
126
|
+
task = process_large_file.delay(file_path)
|
|
127
|
+
|
|
128
|
+
return {"task_id": task.id, "status": "processing"}
|
|
129
|
+
|
|
130
|
+
@router.get("/process/{task_id}")
|
|
131
|
+
async def get_task_status(task_id: str) -> dict:
|
|
132
|
+
task = process_large_file.AsyncResult(task_id)
|
|
133
|
+
|
|
134
|
+
if task.ready():
|
|
135
|
+
return {"status": "completed", "result": task.result}
|
|
136
|
+
|
|
137
|
+
return {"status": task.status}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## ARQ (Async Redis Queue)
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
# tasks.py
|
|
144
|
+
from arq import create_pool
|
|
145
|
+
from arq.connections import RedisSettings
|
|
146
|
+
|
|
147
|
+
async def send_email_task(ctx, email: str, subject: str, body: str):
|
|
148
|
+
await email_service.send(email, subject, body)
|
|
149
|
+
|
|
150
|
+
class WorkerSettings:
|
|
151
|
+
functions = [send_email_task]
|
|
152
|
+
redis_settings = RedisSettings()
|
|
153
|
+
|
|
154
|
+
# router.py
|
|
155
|
+
from arq import create_pool
|
|
156
|
+
|
|
157
|
+
@router.post("/send-email")
|
|
158
|
+
async def send_email(email: EmailSchema) -> dict:
|
|
159
|
+
redis = await create_pool(RedisSettings())
|
|
160
|
+
|
|
161
|
+
job = await redis.enqueue_job(
|
|
162
|
+
"send_email_task",
|
|
163
|
+
email.to,
|
|
164
|
+
email.subject,
|
|
165
|
+
email.body,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
return {"job_id": job.job_id, "status": "queued"}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Periodic Tasks
|
|
172
|
+
|
|
173
|
+
```python
|
|
174
|
+
# Using APScheduler
|
|
175
|
+
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
|
176
|
+
|
|
177
|
+
scheduler = AsyncIOScheduler()
|
|
178
|
+
|
|
179
|
+
async def cleanup_expired_sessions():
|
|
180
|
+
async with async_session() as db:
|
|
181
|
+
await db.execute(
|
|
182
|
+
delete(Session).where(Session.expires_at < datetime.utcnow())
|
|
183
|
+
)
|
|
184
|
+
await db.commit()
|
|
185
|
+
|
|
186
|
+
@asynccontextmanager
|
|
187
|
+
async def lifespan(app: FastAPI):
|
|
188
|
+
# Start scheduler
|
|
189
|
+
scheduler.add_job(
|
|
190
|
+
cleanup_expired_sessions,
|
|
191
|
+
"interval",
|
|
192
|
+
hours=1,
|
|
193
|
+
)
|
|
194
|
+
scheduler.start()
|
|
195
|
+
yield
|
|
196
|
+
# Shutdown
|
|
197
|
+
scheduler.shutdown()
|
|
198
|
+
|
|
199
|
+
app = FastAPI(lifespan=lifespan)
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## Task Status Tracking
|
|
203
|
+
|
|
204
|
+
```python
|
|
205
|
+
from enum import Enum
|
|
206
|
+
import uuid
|
|
207
|
+
|
|
208
|
+
class TaskStatus(str, Enum):
|
|
209
|
+
PENDING = "pending"
|
|
210
|
+
RUNNING = "running"
|
|
211
|
+
COMPLETED = "completed"
|
|
212
|
+
FAILED = "failed"
|
|
213
|
+
|
|
214
|
+
# In-memory store (use Redis in production)
|
|
215
|
+
tasks_store: dict[str, dict] = {}
|
|
216
|
+
|
|
217
|
+
async def process_with_tracking(task_id: str, data: dict):
|
|
218
|
+
tasks_store[task_id]["status"] = TaskStatus.RUNNING
|
|
219
|
+
|
|
220
|
+
try:
|
|
221
|
+
result = await heavy_process(data)
|
|
222
|
+
tasks_store[task_id].update({
|
|
223
|
+
"status": TaskStatus.COMPLETED,
|
|
224
|
+
"result": result,
|
|
225
|
+
})
|
|
226
|
+
except Exception as e:
|
|
227
|
+
tasks_store[task_id].update({
|
|
228
|
+
"status": TaskStatus.FAILED,
|
|
229
|
+
"error": str(e),
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
@router.post("/process")
|
|
233
|
+
async def start_processing(
|
|
234
|
+
data: ProcessData,
|
|
235
|
+
background_tasks: BackgroundTasks,
|
|
236
|
+
) -> dict:
|
|
237
|
+
task_id = str(uuid.uuid4())
|
|
238
|
+
|
|
239
|
+
tasks_store[task_id] = {
|
|
240
|
+
"status": TaskStatus.PENDING,
|
|
241
|
+
"created_at": datetime.utcnow(),
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
background_tasks.add_task(process_with_tracking, task_id, data.dict())
|
|
245
|
+
|
|
246
|
+
return {"task_id": task_id}
|
|
247
|
+
|
|
248
|
+
@router.get("/process/{task_id}")
|
|
249
|
+
async def get_status(task_id: str) -> dict:
|
|
250
|
+
if task_id not in tasks_store:
|
|
251
|
+
raise HTTPException(404, "Task not found")
|
|
252
|
+
|
|
253
|
+
return tasks_store[task_id]
|
|
254
|
+
```
|