@ngxtm/devkit 3.11.1 → 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 CHANGED
@@ -1036,7 +1036,7 @@ Identifies high-quality leads for your product or service by analyzing your busi
1036
1036
  ### learn
1037
1037
  `skills/learn`
1038
1038
 
1039
- Interactive step-by-step learning mode. Teaches concepts from basics to advanced while solving real problems. Auto-detects language, verifies code at ...
1039
+ Interactive learning mode. Teaches by doing with verified code, adaptive difficulty, and Socratic questioning.
1040
1040
 
1041
1041
  ### legacy-modernizer
1042
1042
  `skills/legacy-modernizer`
@@ -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.11.1",
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-01-28T15:40:42.094Z",
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 step-by-step learning mode. Teaches concepts from basics to advanced while solving real problems. Auto-detects language, verifies code at each step, creates markdown tutorials. Triggers on"
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-code" } }
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
+ ```