@pratikpsl/agent-skills 0.2.0 → 0.4.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/LICENSE +201 -201
- package/package.json +2 -2
- package/src/templates/dotnet/AgentSkills/README.md +40 -40
- package/src/templates/dotnet/AgentSkills/agents/README.md +11 -11
- package/src/templates/dotnet/AgentSkills/agents/architect.agent.md +59 -59
- package/src/templates/dotnet/AgentSkills/agents/developer.agent.md +88 -88
- package/src/templates/dotnet/AgentSkills/entry-points/AGENTS.md +34 -0
- package/src/templates/dotnet/AgentSkills/entry-points/CLAUDE.md +34 -0
- package/src/templates/dotnet/AgentSkills/entry-points/copilot-instructions.md +34 -0
- package/src/templates/dotnet/AgentSkills/entry-points/cursor-instructions.md +34 -0
- package/src/templates/dotnet/AgentSkills/memory/index.md +22 -22
- package/src/templates/dotnet/AgentSkills/memory/lessons/api.md +3 -3
- package/src/templates/dotnet/AgentSkills/memory/lessons/auth.md +3 -3
- package/src/templates/dotnet/AgentSkills/memory/lessons/csharp.md +3 -3
- package/src/templates/dotnet/AgentSkills/memory/lessons/db.md +3 -3
- package/src/templates/dotnet/AgentSkills/memory/lessons/design.md +3 -3
- package/src/templates/dotnet/AgentSkills/memory/lessons/infra.md +3 -2
- package/src/templates/dotnet/AgentSkills/memory/lessons/mcp.md +3 -3
- package/src/templates/dotnet/AgentSkills/memory/lessons/testing.md +3 -3
- package/src/templates/dotnet/AgentSkills/memory/schema.md +26 -26
- package/src/templates/dotnet/AgentSkills/skills/INDEX.md +8 -8
- package/src/templates/dotnet/AgentSkills/skills/code-standards/SKILL.md +122 -122
- package/src/templates/dotnet/AgentSkills/skills/core/SKILL.md +23 -23
- package/src/templates/dotnet/AgentSkills/skills/csharp-xunit/SKILL.md +67 -67
- package/src/templates/dotnet/AgentSkills/skills/design/SKILL.md +108 -108
- package/src/templates/dotnet/AgentSkills/skills/dotnet-api/SKILL.md +303 -303
- package/src/templates/dotnet/AgentSkills/skills/dotnet-best-practices/SKILL.md +170 -170
- package/src/templates/dotnet/AgentSkills/skills/harness/SKILL.md +62 -62
- package/src/templates/dotnet/AgentSkills/skills/mcp_dotnet/SKILL.md +54 -54
- package/src/templates/dotnet/manifest.json +1 -1
|
@@ -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
|