@ngxtm/devkit 3.11.0 → 3.12.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/SKILLS_INDEX.md +1 -1
- package/cli/init.js +1 -1
- package/cli/update.js +1 -1
- package/merged-commands/learn.md +84 -413
- package/merged-commands/skill/sync.md +108 -0
- package/package.json +3 -2
- package/rules-index.json +8 -1
- package/scripts/merge-commands.js +0 -1
- package/skills-index.json +1 -1
- package/templates/base/rules/auto-skill.md +3 -2
- package/templates/base/rules/command-routing.md +71 -0
- package/templates/dotnet/rules/dotnet/aspnet-core/SKILL.md +92 -0
- package/templates/dotnet/rules/dotnet/aspnet-core/references/REFERENCE.md +335 -0
- package/templates/dotnet/rules/dotnet/best-practices/SKILL.md +101 -0
- package/templates/dotnet/rules/dotnet/best-practices/references/REFERENCE.md +256 -0
- package/templates/dotnet/rules/dotnet/blazor/SKILL.md +146 -0
- package/templates/dotnet/rules/dotnet/blazor/references/REFERENCE.md +392 -0
- package/templates/dotnet/rules/dotnet/language/SKILL.md +82 -0
- package/templates/dotnet/rules/dotnet/language/references/REFERENCE.md +222 -0
- package/templates/dotnet/rules/dotnet/patterns.rule.md +388 -0
- package/templates/dotnet/rules/dotnet/razor-pages/SKILL.md +124 -0
- package/templates/dotnet/rules/dotnet/razor-pages/references/REFERENCE.md +321 -0
- package/templates/dotnet/rules/dotnet/security/SKILL.md +89 -0
- package/templates/dotnet/rules/dotnet/security/references/REFERENCE.md +295 -0
- package/templates/dotnet/rules/dotnet/tooling/SKILL.md +92 -0
- package/templates/dotnet/rules/dotnet/tooling/references/REFERENCE.md +300 -0
- package/merged-commands/scout/ext.md +0 -35
- package/merged-commands/scout.md +0 -28
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Sync new skills from upstream repos with AI evaluation
|
|
3
|
+
argument-hint: [options]
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Sync Skills from Upstream
|
|
7
|
+
|
|
8
|
+
> AI-assisted upstream skill sync: fetch, evaluate, select, build.
|
|
9
|
+
|
|
10
|
+
## Workflow
|
|
11
|
+
|
|
12
|
+
### Step 1: Pre-flight Check
|
|
13
|
+
|
|
14
|
+
Verify clean working directory:
|
|
15
|
+
```bash
|
|
16
|
+
git status --porcelain
|
|
17
|
+
```
|
|
18
|
+
If not clean → ask user to commit or stash first.
|
|
19
|
+
|
|
20
|
+
### Step 2: Fetch Upstream
|
|
21
|
+
|
|
22
|
+
Run the sync script to clone upstream repos and get a report:
|
|
23
|
+
```bash
|
|
24
|
+
npm run sync:upstream
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
This creates a sync branch and clones repos to temp directory.
|
|
28
|
+
|
|
29
|
+
### Step 3: Evaluate New Skills
|
|
30
|
+
|
|
31
|
+
After the script runs, it shows which skills are new. For each **new** skill:
|
|
32
|
+
|
|
33
|
+
1. Read its `SKILL.md` from the temp directory
|
|
34
|
+
2. Evaluate:
|
|
35
|
+
- **Useful**: Skill covers a distinct domain not already well-covered
|
|
36
|
+
- **Duplicate**: Similar skill already exists in the local collection
|
|
37
|
+
- **Irrelevant**: Too niche or low quality
|
|
38
|
+
|
|
39
|
+
3. Present summary to user:
|
|
40
|
+
```
|
|
41
|
+
Upstream sync found N new skills:
|
|
42
|
+
|
|
43
|
+
✅ Useful (X):
|
|
44
|
+
- skill-name: short reason
|
|
45
|
+
- skill-name: short reason
|
|
46
|
+
|
|
47
|
+
⚠️ Possibly duplicate (Y):
|
|
48
|
+
- skill-name: overlaps with existing-skill
|
|
49
|
+
|
|
50
|
+
❌ Skip (Z):
|
|
51
|
+
- skill-name: reason
|
|
52
|
+
|
|
53
|
+
Sync options:
|
|
54
|
+
1. Sync all useful (X skills)
|
|
55
|
+
2. Sync all useful + duplicates (X+Y skills)
|
|
56
|
+
3. Let me choose manually
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Step 4: Copy Selected Skills
|
|
60
|
+
|
|
61
|
+
Use `AskUserQuestion` to get user's choice. Then for selected skills:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
cp -r /tmp/devkit-sync/{source}/{skill-name} ./skills/{skill-name}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Step 5: Rebuild Indexes
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
npm run build
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
This regenerates all indexes (merged-commands, rules-index, skills-index, skills-compact).
|
|
74
|
+
|
|
75
|
+
### Step 6: Review & Commit
|
|
76
|
+
|
|
77
|
+
Show summary of changes:
|
|
78
|
+
```bash
|
|
79
|
+
git diff --stat
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Ask user if they want to commit:
|
|
83
|
+
```bash
|
|
84
|
+
git add skills/ merged-commands/ SKILLS_INDEX.md skills-index.json skills-compact.json rules-index.json
|
|
85
|
+
git commit -m "feat(skills): sync N new skills from upstream"
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Options
|
|
89
|
+
|
|
90
|
+
- `$ARGUMENTS` can specify:
|
|
91
|
+
- `--auto`: Skip evaluation, sync all new skills automatically
|
|
92
|
+
- `--dry-run`: Only show report, don't copy anything
|
|
93
|
+
- A specific upstream name to sync from (e.g., `antigravity` or `agent-assistant`)
|
|
94
|
+
|
|
95
|
+
## Evaluation Criteria
|
|
96
|
+
|
|
97
|
+
When evaluating skills, consider:
|
|
98
|
+
- Does it cover a technology the project uses or might use?
|
|
99
|
+
- Is there already a similar skill? (check by name and description)
|
|
100
|
+
- Is the SKILL.md well-structured with actionable content?
|
|
101
|
+
- Is it too generic (just says "be a senior X engineer")?
|
|
102
|
+
|
|
103
|
+
## Important
|
|
104
|
+
|
|
105
|
+
- **NEVER** auto-sync without user confirmation (unless `--auto` flag)
|
|
106
|
+
- **ALWAYS** run `npm run build` after copying skills
|
|
107
|
+
- Keep the sync branch for PR review if needed
|
|
108
|
+
- The temp directory is at the path shown by the sync script output
|
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ngxtm/devkit",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.12.0",
|
|
4
4
|
"description": "Per-project AI skills with smart tech detection - lightweight and context-optimized",
|
|
5
5
|
"main": "cli/index.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"devkit": "cli/index.js"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
|
-
"build": "npm run merge-commands && npm run organize-rules && npm run generate-index",
|
|
10
|
+
"build": "npm run merge-commands && npm run organize-rules && npm run generate-index && npm run build-compact-index",
|
|
11
|
+
"build-compact-index": "node scripts/build-compact-index.js",
|
|
11
12
|
"merge-commands": "node scripts/merge-commands.js",
|
|
12
13
|
"organize-rules": "node scripts/organize-rules.js",
|
|
13
14
|
"generate-index": "node scripts/generate-index.js",
|
package/rules-index.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": "1.0.0",
|
|
3
|
-
"generatedAt": "2026-
|
|
3
|
+
"generatedAt": "2026-02-18T04:28:45.416Z",
|
|
4
4
|
"templates": {
|
|
5
5
|
"dart": {
|
|
6
6
|
"path": "templates/dart/rules",
|
|
@@ -9,6 +9,13 @@
|
|
|
9
9
|
],
|
|
10
10
|
"sizeKB": 4
|
|
11
11
|
},
|
|
12
|
+
"dotnet": {
|
|
13
|
+
"path": "templates/dotnet/rules",
|
|
14
|
+
"rules": [
|
|
15
|
+
"dotnet"
|
|
16
|
+
],
|
|
17
|
+
"sizeKB": 92
|
|
18
|
+
},
|
|
12
19
|
"flutter": {
|
|
13
20
|
"path": "templates/flutter/rules",
|
|
14
21
|
"rules": [
|
package/skills-index.json
CHANGED
|
@@ -1977,7 +1977,7 @@
|
|
|
1977
1977
|
{
|
|
1978
1978
|
"name": "learn",
|
|
1979
1979
|
"path": "skills/learn",
|
|
1980
|
-
"description": "Interactive
|
|
1980
|
+
"description": "Interactive learning mode. Teaches by doing with verified code, adaptive difficulty, and Socratic questioning."
|
|
1981
1981
|
},
|
|
1982
1982
|
{
|
|
1983
1983
|
"name": "legacy-modernizer",
|
|
@@ -48,11 +48,12 @@ User Request → Extract keywords (react, auth, test, etc.)
|
|
|
48
48
|
### Step 1: Read Compact Index
|
|
49
49
|
```
|
|
50
50
|
Read: .claude/skills-compact.json
|
|
51
|
-
Format: { "_categories": {...}, "skills": { "skill-name": "category
|
|
51
|
+
Format: { "_categories": {...}, "skills": { "skill-name": { "c": "category", "d": "short description" } } }
|
|
52
|
+
Note: Some skills may be just a category string if no description available.
|
|
52
53
|
```
|
|
53
54
|
|
|
54
55
|
### Step 2: Match by Name/Keyword
|
|
55
|
-
Look for skills whose name contains user's keywords:
|
|
56
|
+
Look for skills whose name OR description contains user's keywords:
|
|
56
57
|
- User says "react" → find skills with "react" in name
|
|
57
58
|
- User says "authentication" → find "auth" skills
|
|
58
59
|
- User says "docker" → find "docker" skills
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Command Routing Guide
|
|
2
|
+
|
|
3
|
+
> Help Claude choose the right command and execution mode for each task.
|
|
4
|
+
|
|
5
|
+
## Decision Tree
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
Is this a learning/educational request?
|
|
9
|
+
→ /learn
|
|
10
|
+
|
|
11
|
+
Is this brainstorming/ideation only (no code)?
|
|
12
|
+
→ /brainstorm
|
|
13
|
+
|
|
14
|
+
Is this just planning (no implementation)?
|
|
15
|
+
→ /plan
|
|
16
|
+
|
|
17
|
+
Is this a bug fix?
|
|
18
|
+
→ /fix (simple) or /fix:hard (complex)
|
|
19
|
+
|
|
20
|
+
Is this a small, well-defined task?
|
|
21
|
+
→ /cook:fast
|
|
22
|
+
|
|
23
|
+
Is this a medium task with clear requirements?
|
|
24
|
+
→ /cook:auto (auto: plan → code → commit)
|
|
25
|
+
→ OR /plan → /code (manual control)
|
|
26
|
+
|
|
27
|
+
Is this a complex, full-lifecycle feature?
|
|
28
|
+
→ /cook:hard (includes brainstorm+research+plan+code+test+review)
|
|
29
|
+
→ OR /brainstorm → /plan → /code (manual, more control)
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Anti-Patterns
|
|
33
|
+
|
|
34
|
+
- **NEVER** chain `/brainstorm` → `/plan` before `/cook:hard` — it already includes both internally, causing duplicate work
|
|
35
|
+
- **NEVER** use `/cook:hard` for trivial fixes — overkill, wastes context
|
|
36
|
+
- **NEVER** use `/cook:fast` for complex features — skips research/design, leads to rework
|
|
37
|
+
|
|
38
|
+
## Command Reference
|
|
39
|
+
|
|
40
|
+
| Command | Complexity | Multi-Agent | Best For |
|
|
41
|
+
|---------|-----------|-------------|----------|
|
|
42
|
+
| `/cook:fast` | Low | Light (scouter, engineer) | Well-defined small tasks |
|
|
43
|
+
| `/cook` | Medium | Yes (full team) | Standard feature work |
|
|
44
|
+
| `/cook:auto` | Medium | Yes (plan → code → git) | Autonomous implementation |
|
|
45
|
+
| `/cook:hard` | High | Full (8 phases, all agents) | Complex features from scratch |
|
|
46
|
+
| `/brainstorm` | N/A | No | Ideation, design exploration |
|
|
47
|
+
| `/plan` | N/A | Optional (researcher) | Creating implementation plans |
|
|
48
|
+
| `/code` | Medium | Yes (tester, reviewer) | Executing an existing plan |
|
|
49
|
+
| `/learn` | N/A | No | Interactive tutorials |
|
|
50
|
+
| `/fix` | Low | No | Quick bug fixes |
|
|
51
|
+
| `/fix:hard` | Medium | Yes (debugger, tester) | Complex debugging |
|
|
52
|
+
|
|
53
|
+
## Multi-Agent Phases in /cook:hard
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
Phase 1: brainstormer → Requirements discovery
|
|
57
|
+
Phase 2: researcher → Best practices research
|
|
58
|
+
Phase 3: scouter → Codebase analysis
|
|
59
|
+
Phase 4: designer → UI/UX design (if needed)
|
|
60
|
+
Phase 5: planner → Implementation plan
|
|
61
|
+
Phase 6: tech-lead → Routes to specialist engineers
|
|
62
|
+
Phase 7: tester → Testing
|
|
63
|
+
Phase 8: reviewer → Code review
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## When to Suggest Skills
|
|
67
|
+
|
|
68
|
+
After choosing the right command, check if a domain-specific skill would help:
|
|
69
|
+
- User says "react" → suggest `/react-expert` skill alongside chosen command
|
|
70
|
+
- User says "auth" → suggest `/auth-implementation-patterns` skill
|
|
71
|
+
- See `auto-skill.md` for full detection flow
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ASP.NET Core Web API
|
|
3
|
+
description: Modern ASP.NET Core patterns for building RESTful APIs.
|
|
4
|
+
metadata:
|
|
5
|
+
labels: [aspnet, webapi, rest, minimal-api]
|
|
6
|
+
triggers:
|
|
7
|
+
files: ['**/*.cs', 'Program.cs']
|
|
8
|
+
keywords: [WebApplication, MapGet, MapPost, Controller, ApiController]
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# ASP.NET Core Web API
|
|
12
|
+
|
|
13
|
+
## **Priority: P1 (OPERATIONAL)**
|
|
14
|
+
|
|
15
|
+
Modern ASP.NET Core patterns for building RESTful APIs.
|
|
16
|
+
|
|
17
|
+
## Implementation Guidelines
|
|
18
|
+
|
|
19
|
+
- **Minimal APIs**: Use for simple endpoints. `app.MapGet()`, route groups, endpoint filters.
|
|
20
|
+
- **Controllers**: Use `[ApiController]` for automatic model validation and binding.
|
|
21
|
+
- **Middleware**: Order matters. Custom middleware with `app.Use()`.
|
|
22
|
+
- **Filters**: Action filters for cross-cutting concerns. Exception filters for error handling.
|
|
23
|
+
- **Validation**: FluentValidation or DataAnnotations. Return `ProblemDetails` on errors.
|
|
24
|
+
- **Versioning**: URL-based (`/api/v1/`) or header-based versioning.
|
|
25
|
+
- **OpenAPI**: Always configure Swagger for API documentation.
|
|
26
|
+
- **Response Types**: Use `TypedResults` for compile-time safety.
|
|
27
|
+
|
|
28
|
+
## Anti-Patterns
|
|
29
|
+
|
|
30
|
+
- **No `HttpClient` without `IHttpClientFactory`**: Socket exhaustion risk.
|
|
31
|
+
- **No blocking async**: Never `.Result` or `.Wait()` in controllers.
|
|
32
|
+
- **No business logic in controllers**: Controllers should only orchestrate.
|
|
33
|
+
- **No returning `Task` without `async`**: Use `async` keyword or return directly.
|
|
34
|
+
|
|
35
|
+
## Code
|
|
36
|
+
|
|
37
|
+
```csharp
|
|
38
|
+
// Minimal API with route groups
|
|
39
|
+
var app = WebApplication.CreateBuilder(args).Build();
|
|
40
|
+
|
|
41
|
+
var users = app.MapGroup("/api/users")
|
|
42
|
+
.WithTags("Users")
|
|
43
|
+
.RequireAuthorization();
|
|
44
|
+
|
|
45
|
+
users.MapGet("/", async (IUserService service) =>
|
|
46
|
+
TypedResults.Ok(await service.GetAllAsync()));
|
|
47
|
+
|
|
48
|
+
users.MapGet("/{id:int}", async (int id, IUserService service) =>
|
|
49
|
+
await service.GetByIdAsync(id) is { } user
|
|
50
|
+
? TypedResults.Ok(user)
|
|
51
|
+
: TypedResults.NotFound());
|
|
52
|
+
|
|
53
|
+
users.MapPost("/", async (CreateUserDto dto, IUserService service) =>
|
|
54
|
+
{
|
|
55
|
+
var user = await service.CreateAsync(dto);
|
|
56
|
+
return TypedResults.Created($"/api/users/{user.Id}", user);
|
|
57
|
+
}).AddEndpointFilter<ValidationFilter<CreateUserDto>>();
|
|
58
|
+
|
|
59
|
+
// Controller with proper patterns
|
|
60
|
+
[ApiController]
|
|
61
|
+
[Route("api/[controller]")]
|
|
62
|
+
[Produces("application/json")]
|
|
63
|
+
public class OrdersController(IOrderService orderService) : ControllerBase
|
|
64
|
+
{
|
|
65
|
+
[HttpGet("{id:int}")]
|
|
66
|
+
[ProducesResponseType<OrderDto>(StatusCodes.Status200OK)]
|
|
67
|
+
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
|
68
|
+
public async Task<IActionResult> GetOrder(int id, CancellationToken ct)
|
|
69
|
+
{
|
|
70
|
+
var order = await orderService.GetByIdAsync(id, ct);
|
|
71
|
+
return order is null ? NotFound() : Ok(order);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
[HttpPost]
|
|
75
|
+
[ProducesResponseType<OrderDto>(StatusCodes.Status201Created)]
|
|
76
|
+
[ProducesResponseType<ValidationProblemDetails>(StatusCodes.Status400BadRequest)]
|
|
77
|
+
public async Task<IActionResult> CreateOrder(CreateOrderDto dto, CancellationToken ct)
|
|
78
|
+
{
|
|
79
|
+
var order = await orderService.CreateAsync(dto, ct);
|
|
80
|
+
return CreatedAtAction(nameof(GetOrder), new { id = order.Id }, order);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Reference & Examples
|
|
86
|
+
|
|
87
|
+
For middleware, exception handling, and HttpClientFactory:
|
|
88
|
+
See [references/REFERENCE.md](references/REFERENCE.md).
|
|
89
|
+
|
|
90
|
+
## Related Topics
|
|
91
|
+
|
|
92
|
+
security | razor-pages | blazor
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
# ASP.NET Core Web API Reference
|
|
2
|
+
|
|
3
|
+
Middleware, exception handling, and advanced patterns.
|
|
4
|
+
|
|
5
|
+
## References
|
|
6
|
+
|
|
7
|
+
- [**Middleware**](middleware.md) - Custom middleware patterns.
|
|
8
|
+
- [**Exception Handling**](exception-handling.md) - Global error handling.
|
|
9
|
+
- [**HttpClientFactory**](httpclient-factory.md) - Typed HTTP clients.
|
|
10
|
+
|
|
11
|
+
## Global Exception Handling Middleware
|
|
12
|
+
|
|
13
|
+
```csharp
|
|
14
|
+
public class ExceptionMiddleware(
|
|
15
|
+
RequestDelegate next,
|
|
16
|
+
ILogger<ExceptionMiddleware> logger,
|
|
17
|
+
IHostEnvironment env)
|
|
18
|
+
{
|
|
19
|
+
public async Task InvokeAsync(HttpContext context)
|
|
20
|
+
{
|
|
21
|
+
try
|
|
22
|
+
{
|
|
23
|
+
await next(context);
|
|
24
|
+
}
|
|
25
|
+
catch (Exception ex)
|
|
26
|
+
{
|
|
27
|
+
logger.LogError(ex, "Unhandled exception: {Message}", ex.Message);
|
|
28
|
+
await HandleExceptionAsync(context, ex);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
private async Task HandleExceptionAsync(HttpContext context, Exception exception)
|
|
33
|
+
{
|
|
34
|
+
context.Response.ContentType = "application/problem+json";
|
|
35
|
+
|
|
36
|
+
var (statusCode, title) = exception switch
|
|
37
|
+
{
|
|
38
|
+
NotFoundException => (StatusCodes.Status404NotFound, "Not Found"),
|
|
39
|
+
ValidationException => (StatusCodes.Status400BadRequest, "Validation Error"),
|
|
40
|
+
UnauthorizedAccessException => (StatusCodes.Status401Unauthorized, "Unauthorized"),
|
|
41
|
+
ForbiddenException => (StatusCodes.Status403Forbidden, "Forbidden"),
|
|
42
|
+
_ => (StatusCodes.Status500InternalServerError, "Internal Server Error")
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
context.Response.StatusCode = statusCode;
|
|
46
|
+
|
|
47
|
+
var problem = new ProblemDetails
|
|
48
|
+
{
|
|
49
|
+
Status = statusCode,
|
|
50
|
+
Title = title,
|
|
51
|
+
Detail = env.IsDevelopment() ? exception.Message : null,
|
|
52
|
+
Instance = context.Request.Path
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
if (exception is ValidationException validationEx)
|
|
56
|
+
{
|
|
57
|
+
problem.Extensions["errors"] = validationEx.Errors;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
await context.Response.WriteAsJsonAsync(problem);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Registration
|
|
65
|
+
app.UseMiddleware<ExceptionMiddleware>();
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Minimal API Endpoint Filters
|
|
69
|
+
|
|
70
|
+
```csharp
|
|
71
|
+
// Validation filter
|
|
72
|
+
public class ValidationFilter<T> : IEndpointFilter where T : class
|
|
73
|
+
{
|
|
74
|
+
public async ValueTask<object?> InvokeAsync(
|
|
75
|
+
EndpointFilterInvocationContext context,
|
|
76
|
+
EndpointFilterDelegate next)
|
|
77
|
+
{
|
|
78
|
+
var model = context.Arguments.OfType<T>().FirstOrDefault();
|
|
79
|
+
|
|
80
|
+
if (model is null)
|
|
81
|
+
return TypedResults.BadRequest("Request body is required");
|
|
82
|
+
|
|
83
|
+
var validator = context.HttpContext.RequestServices.GetService<IValidator<T>>();
|
|
84
|
+
|
|
85
|
+
if (validator is not null)
|
|
86
|
+
{
|
|
87
|
+
var result = await validator.ValidateAsync(model);
|
|
88
|
+
if (!result.IsValid)
|
|
89
|
+
{
|
|
90
|
+
return TypedResults.ValidationProblem(
|
|
91
|
+
result.ToDictionary());
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return await next(context);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Logging filter
|
|
100
|
+
public class LoggingFilter(ILogger<LoggingFilter> logger) : IEndpointFilter
|
|
101
|
+
{
|
|
102
|
+
public async ValueTask<object?> InvokeAsync(
|
|
103
|
+
EndpointFilterInvocationContext context,
|
|
104
|
+
EndpointFilterDelegate next)
|
|
105
|
+
{
|
|
106
|
+
var path = context.HttpContext.Request.Path;
|
|
107
|
+
logger.LogInformation("Request started: {Path}", path);
|
|
108
|
+
|
|
109
|
+
var sw = Stopwatch.StartNew();
|
|
110
|
+
var result = await next(context);
|
|
111
|
+
sw.Stop();
|
|
112
|
+
|
|
113
|
+
logger.LogInformation("Request completed: {Path} in {ElapsedMs}ms",
|
|
114
|
+
path, sw.ElapsedMilliseconds);
|
|
115
|
+
|
|
116
|
+
return result;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Usage
|
|
121
|
+
app.MapPost("/api/users", handler)
|
|
122
|
+
.AddEndpointFilter<ValidationFilter<CreateUserDto>>()
|
|
123
|
+
.AddEndpointFilter<LoggingFilter>();
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## IHttpClientFactory Typed Clients
|
|
127
|
+
|
|
128
|
+
```csharp
|
|
129
|
+
// Typed client
|
|
130
|
+
public class PaymentClient(HttpClient httpClient)
|
|
131
|
+
{
|
|
132
|
+
public async Task<PaymentResult?> ProcessPaymentAsync(PaymentRequest request)
|
|
133
|
+
{
|
|
134
|
+
var response = await httpClient.PostAsJsonAsync("/payments", request);
|
|
135
|
+
response.EnsureSuccessStatusCode();
|
|
136
|
+
return await response.Content.ReadFromJsonAsync<PaymentResult>();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
public async Task<PaymentStatus?> GetStatusAsync(string paymentId)
|
|
140
|
+
{
|
|
141
|
+
return await httpClient.GetFromJsonAsync<PaymentStatus>($"/payments/{paymentId}");
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Registration with resilience
|
|
146
|
+
builder.Services.AddHttpClient<PaymentClient>(client =>
|
|
147
|
+
{
|
|
148
|
+
client.BaseAddress = new Uri(builder.Configuration["Payment:BaseUrl"]!);
|
|
149
|
+
client.DefaultRequestHeaders.Add("X-Api-Key",
|
|
150
|
+
builder.Configuration["Payment:ApiKey"]);
|
|
151
|
+
client.Timeout = TimeSpan.FromSeconds(30);
|
|
152
|
+
})
|
|
153
|
+
.AddStandardResilienceHandler(); // Polly retry, circuit breaker
|
|
154
|
+
|
|
155
|
+
// Manual resilience configuration
|
|
156
|
+
builder.Services.AddHttpClient<PaymentClient>()
|
|
157
|
+
.AddResilienceHandler("payment-pipeline", builder =>
|
|
158
|
+
{
|
|
159
|
+
builder
|
|
160
|
+
.AddRetry(new HttpRetryStrategyOptions
|
|
161
|
+
{
|
|
162
|
+
MaxRetryAttempts = 3,
|
|
163
|
+
Delay = TimeSpan.FromSeconds(1),
|
|
164
|
+
BackoffType = DelayBackoffType.Exponential
|
|
165
|
+
})
|
|
166
|
+
.AddCircuitBreaker(new HttpCircuitBreakerStrategyOptions
|
|
167
|
+
{
|
|
168
|
+
SamplingDuration = TimeSpan.FromSeconds(30),
|
|
169
|
+
FailureRatio = 0.5,
|
|
170
|
+
MinimumThroughput = 10,
|
|
171
|
+
BreakDuration = TimeSpan.FromSeconds(30)
|
|
172
|
+
})
|
|
173
|
+
.AddTimeout(TimeSpan.FromSeconds(10));
|
|
174
|
+
});
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Background Services
|
|
178
|
+
|
|
179
|
+
```csharp
|
|
180
|
+
public class OrderProcessingService(
|
|
181
|
+
IServiceScopeFactory scopeFactory,
|
|
182
|
+
ILogger<OrderProcessingService> logger) : BackgroundService
|
|
183
|
+
{
|
|
184
|
+
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
|
185
|
+
{
|
|
186
|
+
logger.LogInformation("Order processing service started");
|
|
187
|
+
|
|
188
|
+
while (!stoppingToken.IsCancellationRequested)
|
|
189
|
+
{
|
|
190
|
+
try
|
|
191
|
+
{
|
|
192
|
+
await ProcessPendingOrdersAsync(stoppingToken);
|
|
193
|
+
}
|
|
194
|
+
catch (Exception ex)
|
|
195
|
+
{
|
|
196
|
+
logger.LogError(ex, "Error processing orders");
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private async Task ProcessPendingOrdersAsync(CancellationToken ct)
|
|
204
|
+
{
|
|
205
|
+
using var scope = scopeFactory.CreateScope();
|
|
206
|
+
var orderService = scope.ServiceProvider.GetRequiredService<IOrderService>();
|
|
207
|
+
|
|
208
|
+
var pendingOrders = await orderService.GetPendingOrdersAsync(ct);
|
|
209
|
+
|
|
210
|
+
foreach (var order in pendingOrders)
|
|
211
|
+
{
|
|
212
|
+
await orderService.ProcessAsync(order.Id, ct);
|
|
213
|
+
logger.LogInformation("Processed order {OrderId}", order.Id);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Registration
|
|
219
|
+
builder.Services.AddHostedService<OrderProcessingService>();
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## Health Checks
|
|
223
|
+
|
|
224
|
+
```csharp
|
|
225
|
+
// Registration
|
|
226
|
+
builder.Services.AddHealthChecks()
|
|
227
|
+
.AddDbContextCheck<AppDbContext>("database")
|
|
228
|
+
.AddRedis(builder.Configuration.GetConnectionString("Redis")!, "redis")
|
|
229
|
+
.AddUrlGroup(new Uri("https://api.external.com/health"), "external-api")
|
|
230
|
+
.AddCheck<CustomHealthCheck>("custom");
|
|
231
|
+
|
|
232
|
+
// Custom health check
|
|
233
|
+
public class CustomHealthCheck(IOrderService orderService) : IHealthCheck
|
|
234
|
+
{
|
|
235
|
+
public async Task<HealthCheckResult> CheckHealthAsync(
|
|
236
|
+
HealthCheckContext context,
|
|
237
|
+
CancellationToken ct = default)
|
|
238
|
+
{
|
|
239
|
+
try
|
|
240
|
+
{
|
|
241
|
+
var count = await orderService.GetPendingCountAsync(ct);
|
|
242
|
+
|
|
243
|
+
if (count > 1000)
|
|
244
|
+
{
|
|
245
|
+
return HealthCheckResult.Degraded(
|
|
246
|
+
$"High pending orders: {count}");
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return HealthCheckResult.Healthy();
|
|
250
|
+
}
|
|
251
|
+
catch (Exception ex)
|
|
252
|
+
{
|
|
253
|
+
return HealthCheckResult.Unhealthy("Order service unavailable", ex);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Endpoints
|
|
259
|
+
app.MapHealthChecks("/health", new HealthCheckOptions
|
|
260
|
+
{
|
|
261
|
+
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
app.MapHealthChecks("/health/ready", new HealthCheckOptions
|
|
265
|
+
{
|
|
266
|
+
Predicate = check => check.Tags.Contains("ready")
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
app.MapHealthChecks("/health/live", new HealthCheckOptions
|
|
270
|
+
{
|
|
271
|
+
Predicate = _ => false // Just checks if app is running
|
|
272
|
+
});
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
## Model Binding and Validation
|
|
276
|
+
|
|
277
|
+
```csharp
|
|
278
|
+
// FluentValidation
|
|
279
|
+
public class CreateOrderDtoValidator : AbstractValidator<CreateOrderDto>
|
|
280
|
+
{
|
|
281
|
+
public CreateOrderDtoValidator()
|
|
282
|
+
{
|
|
283
|
+
RuleFor(x => x.CustomerId)
|
|
284
|
+
.NotEmpty()
|
|
285
|
+
.WithMessage("Customer ID is required");
|
|
286
|
+
|
|
287
|
+
RuleFor(x => x.Items)
|
|
288
|
+
.NotEmpty()
|
|
289
|
+
.WithMessage("Order must have at least one item");
|
|
290
|
+
|
|
291
|
+
RuleForEach(x => x.Items).ChildRules(item =>
|
|
292
|
+
{
|
|
293
|
+
item.RuleFor(x => x.ProductId).NotEmpty();
|
|
294
|
+
item.RuleFor(x => x.Quantity).GreaterThan(0);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
RuleFor(x => x.ShippingAddress)
|
|
298
|
+
.NotNull()
|
|
299
|
+
.SetValidator(new AddressValidator());
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Registration
|
|
304
|
+
builder.Services.AddValidatorsFromAssemblyContaining<CreateOrderDtoValidator>();
|
|
305
|
+
|
|
306
|
+
// Pipeline behavior for MediatR
|
|
307
|
+
public class ValidationBehavior<TRequest, TResponse>(
|
|
308
|
+
IEnumerable<IValidator<TRequest>> validators)
|
|
309
|
+
: IPipelineBehavior<TRequest, TResponse>
|
|
310
|
+
where TRequest : notnull
|
|
311
|
+
{
|
|
312
|
+
public async Task<TResponse> Handle(
|
|
313
|
+
TRequest request,
|
|
314
|
+
RequestHandlerDelegate<TResponse> next,
|
|
315
|
+
CancellationToken ct)
|
|
316
|
+
{
|
|
317
|
+
if (!validators.Any())
|
|
318
|
+
return await next();
|
|
319
|
+
|
|
320
|
+
var context = new ValidationContext<TRequest>(request);
|
|
321
|
+
var results = await Task.WhenAll(
|
|
322
|
+
validators.Select(v => v.ValidateAsync(context, ct)));
|
|
323
|
+
|
|
324
|
+
var failures = results
|
|
325
|
+
.SelectMany(r => r.Errors)
|
|
326
|
+
.Where(f => f != null)
|
|
327
|
+
.ToList();
|
|
328
|
+
|
|
329
|
+
if (failures.Count != 0)
|
|
330
|
+
throw new ValidationException(failures);
|
|
331
|
+
|
|
332
|
+
return await next();
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
```
|