@pratikpsl/agent-skills 0.1.0 → 0.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.
Files changed (26) hide show
  1. package/LICENSE +201 -201
  2. package/package.json +2 -2
  3. package/src/templates/dotnet/AgentSkills/README.md +40 -40
  4. package/src/templates/dotnet/AgentSkills/agents/README.md +11 -11
  5. package/src/templates/dotnet/AgentSkills/agents/architect.agent.md +59 -59
  6. package/src/templates/dotnet/AgentSkills/agents/developer.agent.md +88 -88
  7. package/src/templates/dotnet/AgentSkills/memory/index.md +21 -21
  8. package/src/templates/dotnet/AgentSkills/memory/lessons/api.md +3 -3
  9. package/src/templates/dotnet/AgentSkills/memory/lessons/auth.md +3 -3
  10. package/src/templates/dotnet/AgentSkills/memory/lessons/csharp.md +3 -3
  11. package/src/templates/dotnet/AgentSkills/memory/lessons/db.md +3 -3
  12. package/src/templates/dotnet/AgentSkills/memory/lessons/design.md +3 -3
  13. package/src/templates/dotnet/AgentSkills/memory/lessons/infra.md +2 -2
  14. package/src/templates/dotnet/AgentSkills/memory/lessons/mcp.md +3 -3
  15. package/src/templates/dotnet/AgentSkills/memory/lessons/testing.md +3 -3
  16. package/src/templates/dotnet/AgentSkills/memory/schema.md +26 -26
  17. package/src/templates/dotnet/AgentSkills/skills/INDEX.md +8 -8
  18. package/src/templates/dotnet/AgentSkills/skills/code-standards/SKILL.md +122 -122
  19. package/src/templates/dotnet/AgentSkills/skills/core/SKILL.md +23 -23
  20. package/src/templates/dotnet/AgentSkills/skills/csharp-xunit/SKILL.md +67 -67
  21. package/src/templates/dotnet/AgentSkills/skills/design/SKILL.md +108 -108
  22. package/src/templates/dotnet/AgentSkills/skills/dotnet-api/SKILL.md +303 -303
  23. package/src/templates/dotnet/AgentSkills/skills/dotnet-best-practices/SKILL.md +170 -170
  24. package/src/templates/dotnet/AgentSkills/skills/harness/SKILL.md +62 -62
  25. package/src/templates/dotnet/AgentSkills/skills/mcp_dotnet/SKILL.md +54 -54
  26. package/src/templates/dotnet/manifest.json +2 -2
@@ -1,303 +1,303 @@
1
- ---
2
- name: dotnet-api
3
- description: >
4
- Load when creating, structuring, or reviewing .NET 10 Web API projects with Swagger/OpenAPI, CORS, compression, auth, versioning, and error handling.
5
- ---
6
-
7
- # SKILL: .NET 10 RESTful API — Best Practices
8
-
9
- ## 1. URL & RESOURCE NAMING
10
-
11
- - **Nouns only**, plural: `/api/v1/articles`, `/api/v1/users`
12
- - HTTP verb = action; path = resource
13
- - Nest max 2 levels: `GET /articles/123/comments`
14
- - Beyond 2 levels → return URI in JSON body: `"author": "/users/3"`
15
-
16
- | ❌ Bad | ✅ Good |
17
- |---|---|
18
- | `GET /getArticles` | `GET /articles` |
19
- | `POST /createUser` | `POST /users` |
20
-
21
- ---
22
-
23
- ## 2. HTTP METHODS
24
-
25
- | Method | Use | Idempotent |
26
- |---|---|---|
27
- | GET | Retrieve | ✅ |
28
- | POST | Create | ❌ |
29
- | PUT | Full replace | ✅ |
30
- | PATCH | Partial update | ✅ |
31
- | DELETE | Remove | ✅ |
32
-
33
- ---
34
-
35
- ## 3. RESPONSE FORMAT & HEADERS
36
-
37
- - Always return `application/json`
38
- - Use `camelCase` for JSON keys (default in .NET)
39
- - No outer wrappers — return resource directly
40
-
41
- ```csharp
42
- builder.Services.AddControllers()
43
- .AddJsonOptions(o => o.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase);
44
- ```
45
-
46
- ---
47
-
48
- ## 4. HTTP STATUS CODES
49
-
50
- | Code | When |
51
- |---|---|
52
- | 200 | Success (GET, PUT, PATCH) |
53
- | 201 | Created (POST) — include `Location` header |
54
- | 204 | No content (DELETE) |
55
- | 400 | Bad request / validation failed |
56
- | 401 | Not authenticated |
57
- | 403 | Authenticated but forbidden |
58
- | 404 | Resource not found |
59
- | 500 | Unhandled server error |
60
-
61
- ---
62
-
63
- ## 5. GLOBAL ERROR HANDLING
64
-
65
- Use `IExceptionHandler` (dotnet 8+) — no stack traces in production.
66
-
67
- ```csharp
68
- // GlobalExceptionHandler.cs
69
- public class GlobalExceptionHandler : IExceptionHandler
70
- {
71
- public async ValueTask<bool> TryHandleAsync(HttpContext ctx, Exception ex, CancellationToken ct)
72
- {
73
- ctx.Response.StatusCode = StatusCodes.Status500InternalServerError;
74
- await ctx.Response.WriteAsJsonAsync(new { error = "An unexpected error occurred." }, ct);
75
- return true;
76
- }
77
- }
78
-
79
- // Program.cs
80
- builder.Services.AddExceptionHandler<GlobalExceptionHandler>();
81
- builder.Services.AddProblemDetails();
82
- app.UseExceptionHandler();
83
- ```
84
-
85
- Structured error shape — never raw exceptions:
86
- ```json
87
- { "status": 400, "error": "Validation failed", "code": "INVALID_EMAIL" }
88
- ```
89
-
90
- ---
91
-
92
- ## 6. FILTERING, SORTING & PAGINATION
93
-
94
- Query params only — never in the body for GET.
95
-
96
- ```
97
- GET /products?category=electronics&sort=-price&page=2&limit=20
98
- ```
99
-
100
- ```csharp
101
- [HttpGet]
102
- public IActionResult GetProducts([FromQuery] string? category, [FromQuery] string? sort,
103
- [FromQuery] int page = 1, [FromQuery] int limit = 20) { ... }
104
- ```
105
-
106
- - `sort=price` → ascending, `sort=-price` → descending
107
- - Always return pagination metadata: `{ total, page, limit, data: [] }`
108
-
109
- ---
110
-
111
- ## 7. VERSIONING
112
-
113
- ```csharp
114
- // Program.cs
115
- builder.Services.AddApiVersioning(o => {
116
- o.DefaultApiVersion = new ApiVersion(1);
117
- o.AssumeDefaultVersionWhenUnspecified = true;
118
- o.ReportApiVersions = true;
119
- }).AddApiExplorer(o => {
120
- o.GroupNameFormat = "'v'V";
121
- o.SubstituteApiVersionInUrl = true;
122
- });
123
- ```
124
-
125
- ```csharp
126
- [ApiVersion(1)]
127
- [Route("api/v{v:apiVersion}/articles")]
128
- public class ArticlesController : ControllerBase { }
129
- ```
130
-
131
- Package: `Asp.Versioning.Mvc` + `Asp.Versioning.Mvc.ApiExplorer`
132
-
133
- ---
134
-
135
- ## 8. SWAGGER / OPENAPI
136
-
137
- Add to `.csproj`:
138
- ```xml
139
- <GenerateDocumentationFile>true</GenerateDocumentationFile>
140
- ```
141
-
142
- Setup: `AddEndpointsApiExplorer()` + `AddSwaggerGen()` with:
143
- - `SwaggerDoc` per version
144
- - `AddSecurityDefinition("Bearer", ...)` + matching `AddSecurityRequirement`
145
- - `IncludeXmlComments(...)` for XML doc file
146
-
147
- Controller annotation:
148
- ```csharp
149
- /// <summary>Get all articles</summary>
150
- /// <response code="200">Returns list of articles</response>
151
- [HttpGet]
152
- [ProducesResponseType(typeof(List<ArticleDto>), 200)]
153
- [ProducesResponseType(404)]
154
- public IActionResult GetAll([FromQuery] int page = 1) { ... }
155
- ```
156
-
157
- Expose only in Development:
158
- ```csharp
159
- if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); }
160
- ```
161
-
162
- ---
163
-
164
- ## 9. RESPONSE COMPRESSION
165
-
166
- ```csharp
167
- builder.Services.AddResponseCompression(o => {
168
- o.EnableForHttps = true;
169
- o.Providers.Add<BrotliCompressionProvider>();
170
- o.Providers.Add<GzipCompressionProvider>();
171
- });
172
- builder.Services.Configure<BrotliCompressionProviderOptions>(o => o.Level = CompressionLevel.Fastest);
173
-
174
- app.UseResponseCompression(); // must be first in pipeline
175
- ```
176
-
177
- Package: `Microsoft.AspNetCore.ResponseCompression` (built-in)
178
-
179
- ---
180
-
181
- ## 10. CORS (No Wildcard in Production)
182
-
183
- ```csharp
184
- builder.Services.AddCors(o => o.AddPolicy("ApiPolicy", p => p
185
- .WithOrigins("https://myfrontend.com") // ❌ never .AllowAnyOrigin() in prod
186
- .WithMethods("GET", "POST", "PUT", "DELETE", "PATCH")
187
- .WithHeaders("Authorization", "Content-Type")
188
- .AllowCredentials()));
189
-
190
- app.UseCors("ApiPolicy"); // before UseAuthentication
191
- ```
192
-
193
- Dev-only: use a separate named policy with `AllowAnyOrigin()`.
194
-
195
- ---
196
-
197
- ## 11. SECURITY
198
-
199
- ```csharp
200
- builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
201
- .AddJwtBearer(o => {
202
- o.TokenValidationParameters = new TokenValidationParameters {
203
- ValidateIssuer = true, ValidIssuer = config["Jwt:Issuer"],
204
- ValidateAudience = true, ValidAudience = config["Jwt:Audience"],
205
- ValidateLifetime = true,
206
- IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(config["Jwt:Key"]!))
207
- };
208
- });
209
- ```
210
-
211
- - `app.UseHttpsRedirection()` — mandatory
212
- - Never expose DB schema in endpoint design
213
- - Use `[Authorize]` / `[AllowAnonymous]` explicitly on controllers
214
-
215
- ---
216
-
217
- ## 12. CACHING
218
-
219
- Options in order of complexity:
220
-
221
- | Strategy | Use when |
222
- |---|---|
223
- | `[OutputCache(Duration = 60)]` | Single-server, simple GET caching |
224
- | `IDistributedCache` + Redis | Multi-instance / shared cache |
225
- | `Cache-Control` header | Client/CDN-side caching |
226
-
227
- ```csharp
228
- // Distributed Redis
229
- builder.Services.AddStackExchangeRedisCache(o => o.Configuration = "localhost:6379");
230
-
231
- // Client hint
232
- Response.Headers.CacheControl = "public, max-age=60";
233
- ```
234
-
235
- Register `AddOutputCache()` + `app.UseOutputCache()` for attribute-based caching.
236
-
237
- ---
238
-
239
- ## 13. RATE LIMITING (dotnet 7+ built-in)
240
-
241
- ```csharp
242
- builder.Services.AddRateLimiter(o => o.AddFixedWindowLimiter("fixed", opt => {
243
- opt.PermitLimit = 100;
244
- opt.Window = TimeSpan.FromMinutes(1);
245
- opt.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
246
- opt.QueueLimit = 5;
247
- }));
248
-
249
- app.UseRateLimiter();
250
-
251
- [HttpGet, EnableRateLimiting("fixed")]
252
- public IActionResult Get() { ... }
253
- ```
254
-
255
- ---
256
-
257
- ## 14. INPUT VALIDATION
258
-
259
- ```csharp
260
- builder.Services.AddControllers()
261
- .ConfigureApiBehaviorOptions(o => o.InvalidModelStateResponseFactory = ctx =>
262
- new BadRequestObjectResult(new {
263
- status = 400,
264
- errors = ctx.ModelState.Values
265
- .SelectMany(v => v.Errors)
266
- .Select(e => e.ErrorMessage)
267
- }));
268
- ```
269
-
270
- Use `FluentValidation` for complex rules; Data Annotations for simple constraints.
271
-
272
- ---
273
-
274
- ## 15. PIPELINE ORDER (Critical)
275
-
276
- ```csharp
277
- app.UseResponseCompression();
278
- app.UseHttpsRedirection();
279
- app.UseCors("ApiPolicy");
280
- app.UseRateLimiter();
281
- app.UseAuthentication();
282
- app.UseAuthorization();
283
- app.UseOutputCache();
284
- app.UseExceptionHandler();
285
- app.MapControllers();
286
- ```
287
-
288
- ---
289
-
290
- ## QUICK CHECKLIST
291
-
292
- - [ ] Routes: plural nouns, no verbs, max 2 nesting levels
293
- - [ ] `201 Created` returns `Location` header; `204` for DELETE
294
- - [ ] `IExceptionHandler` registered — structured JSON errors, no stack traces
295
- - [ ] Collections: filtering + sorting + pagination with metadata envelope
296
- - [ ] Versioning: `Asp.Versioning.Mvc`, `ReportApiVersions = true`
297
- - [ ] Swagger: XML docs + `ProducesResponseType` on every action + JWT security scheme
298
- - [ ] Compression: Brotli + Gzip, `EnableForHttps = true`, first in pipeline
299
- - [ ] CORS: explicit `WithOrigins` — `AllowAnyOrigin` dev policy only
300
- - [ ] JWT: all 3 validate flags on (`Issuer`, `Audience`, `Lifetime`)
301
- - [ ] Rate limiting: `AddFixedWindowLimiter` + `EnableRateLimiting` per controller/action
302
- - [ ] `InvalidModelStateResponseFactory` returns structured 400 with field errors
303
- - [ ] DTOs only in responses — no raw EF entities exposed
1
+ ---
2
+ name: dotnet-api
3
+ description: >
4
+ Load when creating, structuring, or reviewing .NET 10 Web API projects with Swagger/OpenAPI, CORS, compression, auth, versioning, and error handling.
5
+ ---
6
+
7
+ # SKILL: .NET 10 RESTful API — Best Practices
8
+
9
+ ## 1. URL & RESOURCE NAMING
10
+
11
+ - **Nouns only**, plural: `/api/v1/articles`, `/api/v1/users`
12
+ - HTTP verb = action; path = resource
13
+ - Nest max 2 levels: `GET /articles/123/comments`
14
+ - Beyond 2 levels → return URI in JSON body: `"author": "/users/3"`
15
+
16
+ | ❌ Bad | ✅ Good |
17
+ |---|---|
18
+ | `GET /getArticles` | `GET /articles` |
19
+ | `POST /createUser` | `POST /users` |
20
+
21
+ ---
22
+
23
+ ## 2. HTTP METHODS
24
+
25
+ | Method | Use | Idempotent |
26
+ |---|---|---|
27
+ | GET | Retrieve | ✅ |
28
+ | POST | Create | ❌ |
29
+ | PUT | Full replace | ✅ |
30
+ | PATCH | Partial update | ✅ |
31
+ | DELETE | Remove | ✅ |
32
+
33
+ ---
34
+
35
+ ## 3. RESPONSE FORMAT & HEADERS
36
+
37
+ - Always return `application/json`
38
+ - Use `camelCase` for JSON keys (default in .NET)
39
+ - No outer wrappers — return resource directly
40
+
41
+ ```csharp
42
+ builder.Services.AddControllers()
43
+ .AddJsonOptions(o => o.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase);
44
+ ```
45
+
46
+ ---
47
+
48
+ ## 4. HTTP STATUS CODES
49
+
50
+ | Code | When |
51
+ |---|---|
52
+ | 200 | Success (GET, PUT, PATCH) |
53
+ | 201 | Created (POST) — include `Location` header |
54
+ | 204 | No content (DELETE) |
55
+ | 400 | Bad request / validation failed |
56
+ | 401 | Not authenticated |
57
+ | 403 | Authenticated but forbidden |
58
+ | 404 | Resource not found |
59
+ | 500 | Unhandled server error |
60
+
61
+ ---
62
+
63
+ ## 5. GLOBAL ERROR HANDLING
64
+
65
+ Use `IExceptionHandler` (dotnet 8+) — no stack traces in production.
66
+
67
+ ```csharp
68
+ // GlobalExceptionHandler.cs
69
+ public class GlobalExceptionHandler : IExceptionHandler
70
+ {
71
+ public async ValueTask<bool> TryHandleAsync(HttpContext ctx, Exception ex, CancellationToken ct)
72
+ {
73
+ ctx.Response.StatusCode = StatusCodes.Status500InternalServerError;
74
+ await ctx.Response.WriteAsJsonAsync(new { error = "An unexpected error occurred." }, ct);
75
+ return true;
76
+ }
77
+ }
78
+
79
+ // Program.cs
80
+ builder.Services.AddExceptionHandler<GlobalExceptionHandler>();
81
+ builder.Services.AddProblemDetails();
82
+ app.UseExceptionHandler();
83
+ ```
84
+
85
+ Structured error shape — never raw exceptions:
86
+ ```json
87
+ { "status": 400, "error": "Validation failed", "code": "INVALID_EMAIL" }
88
+ ```
89
+
90
+ ---
91
+
92
+ ## 6. FILTERING, SORTING & PAGINATION
93
+
94
+ Query params only — never in the body for GET.
95
+
96
+ ```
97
+ GET /products?category=electronics&sort=-price&page=2&limit=20
98
+ ```
99
+
100
+ ```csharp
101
+ [HttpGet]
102
+ public IActionResult GetProducts([FromQuery] string? category, [FromQuery] string? sort,
103
+ [FromQuery] int page = 1, [FromQuery] int limit = 20) { ... }
104
+ ```
105
+
106
+ - `sort=price` → ascending, `sort=-price` → descending
107
+ - Always return pagination metadata: `{ total, page, limit, data: [] }`
108
+
109
+ ---
110
+
111
+ ## 7. VERSIONING
112
+
113
+ ```csharp
114
+ // Program.cs
115
+ builder.Services.AddApiVersioning(o => {
116
+ o.DefaultApiVersion = new ApiVersion(1);
117
+ o.AssumeDefaultVersionWhenUnspecified = true;
118
+ o.ReportApiVersions = true;
119
+ }).AddApiExplorer(o => {
120
+ o.GroupNameFormat = "'v'V";
121
+ o.SubstituteApiVersionInUrl = true;
122
+ });
123
+ ```
124
+
125
+ ```csharp
126
+ [ApiVersion(1)]
127
+ [Route("api/v{v:apiVersion}/articles")]
128
+ public class ArticlesController : ControllerBase { }
129
+ ```
130
+
131
+ Package: `Asp.Versioning.Mvc` + `Asp.Versioning.Mvc.ApiExplorer`
132
+
133
+ ---
134
+
135
+ ## 8. SWAGGER / OPENAPI
136
+
137
+ Add to `.csproj`:
138
+ ```xml
139
+ <GenerateDocumentationFile>true</GenerateDocumentationFile>
140
+ ```
141
+
142
+ Setup: `AddEndpointsApiExplorer()` + `AddSwaggerGen()` with:
143
+ - `SwaggerDoc` per version
144
+ - `AddSecurityDefinition("Bearer", ...)` + matching `AddSecurityRequirement`
145
+ - `IncludeXmlComments(...)` for XML doc file
146
+
147
+ Controller annotation:
148
+ ```csharp
149
+ /// <summary>Get all articles</summary>
150
+ /// <response code="200">Returns list of articles</response>
151
+ [HttpGet]
152
+ [ProducesResponseType(typeof(List<ArticleDto>), 200)]
153
+ [ProducesResponseType(404)]
154
+ public IActionResult GetAll([FromQuery] int page = 1) { ... }
155
+ ```
156
+
157
+ Expose only in Development:
158
+ ```csharp
159
+ if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); }
160
+ ```
161
+
162
+ ---
163
+
164
+ ## 9. RESPONSE COMPRESSION
165
+
166
+ ```csharp
167
+ builder.Services.AddResponseCompression(o => {
168
+ o.EnableForHttps = true;
169
+ o.Providers.Add<BrotliCompressionProvider>();
170
+ o.Providers.Add<GzipCompressionProvider>();
171
+ });
172
+ builder.Services.Configure<BrotliCompressionProviderOptions>(o => o.Level = CompressionLevel.Fastest);
173
+
174
+ app.UseResponseCompression(); // must be first in pipeline
175
+ ```
176
+
177
+ Package: `Microsoft.AspNetCore.ResponseCompression` (built-in)
178
+
179
+ ---
180
+
181
+ ## 10. CORS (No Wildcard in Production)
182
+
183
+ ```csharp
184
+ builder.Services.AddCors(o => o.AddPolicy("ApiPolicy", p => p
185
+ .WithOrigins("https://myfrontend.com") // ❌ never .AllowAnyOrigin() in prod
186
+ .WithMethods("GET", "POST", "PUT", "DELETE", "PATCH")
187
+ .WithHeaders("Authorization", "Content-Type")
188
+ .AllowCredentials()));
189
+
190
+ app.UseCors("ApiPolicy"); // before UseAuthentication
191
+ ```
192
+
193
+ Dev-only: use a separate named policy with `AllowAnyOrigin()`.
194
+
195
+ ---
196
+
197
+ ## 11. SECURITY
198
+
199
+ ```csharp
200
+ builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
201
+ .AddJwtBearer(o => {
202
+ o.TokenValidationParameters = new TokenValidationParameters {
203
+ ValidateIssuer = true, ValidIssuer = config["Jwt:Issuer"],
204
+ ValidateAudience = true, ValidAudience = config["Jwt:Audience"],
205
+ ValidateLifetime = true,
206
+ IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(config["Jwt:Key"]!))
207
+ };
208
+ });
209
+ ```
210
+
211
+ - `app.UseHttpsRedirection()` — mandatory
212
+ - Never expose DB schema in endpoint design
213
+ - Use `[Authorize]` / `[AllowAnonymous]` explicitly on controllers
214
+
215
+ ---
216
+
217
+ ## 12. CACHING
218
+
219
+ Options in order of complexity:
220
+
221
+ | Strategy | Use when |
222
+ |---|---|
223
+ | `[OutputCache(Duration = 60)]` | Single-server, simple GET caching |
224
+ | `IDistributedCache` + Redis | Multi-instance / shared cache |
225
+ | `Cache-Control` header | Client/CDN-side caching |
226
+
227
+ ```csharp
228
+ // Distributed Redis
229
+ builder.Services.AddStackExchangeRedisCache(o => o.Configuration = "localhost:6379");
230
+
231
+ // Client hint
232
+ Response.Headers.CacheControl = "public, max-age=60";
233
+ ```
234
+
235
+ Register `AddOutputCache()` + `app.UseOutputCache()` for attribute-based caching.
236
+
237
+ ---
238
+
239
+ ## 13. RATE LIMITING (dotnet 7+ built-in)
240
+
241
+ ```csharp
242
+ builder.Services.AddRateLimiter(o => o.AddFixedWindowLimiter("fixed", opt => {
243
+ opt.PermitLimit = 100;
244
+ opt.Window = TimeSpan.FromMinutes(1);
245
+ opt.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
246
+ opt.QueueLimit = 5;
247
+ }));
248
+
249
+ app.UseRateLimiter();
250
+
251
+ [HttpGet, EnableRateLimiting("fixed")]
252
+ public IActionResult Get() { ... }
253
+ ```
254
+
255
+ ---
256
+
257
+ ## 14. INPUT VALIDATION
258
+
259
+ ```csharp
260
+ builder.Services.AddControllers()
261
+ .ConfigureApiBehaviorOptions(o => o.InvalidModelStateResponseFactory = ctx =>
262
+ new BadRequestObjectResult(new {
263
+ status = 400,
264
+ errors = ctx.ModelState.Values
265
+ .SelectMany(v => v.Errors)
266
+ .Select(e => e.ErrorMessage)
267
+ }));
268
+ ```
269
+
270
+ Use `FluentValidation` for complex rules; Data Annotations for simple constraints.
271
+
272
+ ---
273
+
274
+ ## 15. PIPELINE ORDER (Critical)
275
+
276
+ ```csharp
277
+ app.UseResponseCompression();
278
+ app.UseHttpsRedirection();
279
+ app.UseCors("ApiPolicy");
280
+ app.UseRateLimiter();
281
+ app.UseAuthentication();
282
+ app.UseAuthorization();
283
+ app.UseOutputCache();
284
+ app.UseExceptionHandler();
285
+ app.MapControllers();
286
+ ```
287
+
288
+ ---
289
+
290
+ ## QUICK CHECKLIST
291
+
292
+ - [ ] Routes: plural nouns, no verbs, max 2 nesting levels
293
+ - [ ] `201 Created` returns `Location` header; `204` for DELETE
294
+ - [ ] `IExceptionHandler` registered — structured JSON errors, no stack traces
295
+ - [ ] Collections: filtering + sorting + pagination with metadata envelope
296
+ - [ ] Versioning: `Asp.Versioning.Mvc`, `ReportApiVersions = true`
297
+ - [ ] Swagger: XML docs + `ProducesResponseType` on every action + JWT security scheme
298
+ - [ ] Compression: Brotli + Gzip, `EnableForHttps = true`, first in pipeline
299
+ - [ ] CORS: explicit `WithOrigins` — `AllowAnyOrigin` dev policy only
300
+ - [ ] JWT: all 3 validate flags on (`Issuer`, `Audience`, `Lifetime`)
301
+ - [ ] Rate limiting: `AddFixedWindowLimiter` + `EnableRateLimiting` per controller/action
302
+ - [ ] `InvalidModelStateResponseFactory` returns structured 400 with field errors
303
+ - [ ] DTOs only in responses — no raw EF entities exposed