@malamute/ai-rules 1.0.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +270 -121
- package/bin/cli.js +5 -2
- package/configs/_shared/.claude/rules/conventions/documentation.md +324 -0
- package/configs/_shared/.claude/rules/conventions/git.md +265 -0
- package/configs/_shared/.claude/rules/{performance.md → conventions/performance.md} +1 -1
- package/configs/_shared/.claude/rules/conventions/principles.md +334 -0
- package/configs/_shared/.claude/rules/devops/ci-cd.md +262 -0
- package/configs/_shared/.claude/rules/devops/docker.md +275 -0
- package/configs/_shared/.claude/rules/devops/nx.md +194 -0
- package/configs/_shared/.claude/rules/domain/backend/api-design.md +203 -0
- package/configs/_shared/.claude/rules/lang/csharp/async.md +220 -0
- package/configs/_shared/.claude/rules/lang/csharp/csharp.md +314 -0
- package/configs/_shared/.claude/rules/lang/csharp/linq.md +210 -0
- package/configs/_shared/.claude/rules/lang/python/async.md +337 -0
- package/configs/_shared/.claude/rules/lang/python/celery.md +476 -0
- package/configs/_shared/.claude/rules/lang/python/config.md +339 -0
- package/configs/{python/.claude/rules → _shared/.claude/rules/lang/python}/database/sqlalchemy.md +6 -1
- package/configs/_shared/.claude/rules/lang/python/deployment.md +523 -0
- package/configs/_shared/.claude/rules/lang/python/error-handling.md +330 -0
- package/configs/_shared/.claude/rules/lang/python/migrations.md +421 -0
- package/configs/_shared/.claude/rules/lang/python/python.md +172 -0
- package/configs/_shared/.claude/rules/lang/python/repository.md +383 -0
- package/configs/{python/.claude/rules → _shared/.claude/rules/lang/python}/testing.md +2 -69
- package/configs/_shared/.claude/rules/lang/typescript/async.md +447 -0
- package/configs/_shared/.claude/rules/lang/typescript/generics.md +356 -0
- package/configs/_shared/.claude/rules/lang/typescript/typescript.md +212 -0
- package/configs/_shared/.claude/rules/quality/error-handling.md +48 -0
- package/configs/_shared/.claude/rules/quality/logging.md +45 -0
- package/configs/_shared/.claude/rules/quality/observability.md +240 -0
- package/configs/_shared/.claude/rules/quality/testing-patterns.md +65 -0
- package/configs/_shared/.claude/rules/security/secrets-management.md +222 -0
- package/configs/_shared/.claude/skills/analysis/explore/SKILL.md +257 -0
- package/configs/_shared/.claude/skills/analysis/security-audit/SKILL.md +184 -0
- package/configs/_shared/.claude/skills/dev/api-endpoint/SKILL.md +126 -0
- package/configs/_shared/.claude/{commands/generate-tests.md → skills/dev/generate-tests/SKILL.md} +6 -0
- package/configs/_shared/.claude/{commands/fix-issue.md → skills/git/fix-issue/SKILL.md} +6 -0
- package/configs/_shared/.claude/{commands/review-pr.md → skills/git/review-pr/SKILL.md} +6 -0
- package/configs/_shared/.claude/skills/infra/deploy/SKILL.md +139 -0
- package/configs/_shared/.claude/skills/infra/docker/SKILL.md +95 -0
- package/configs/_shared/.claude/skills/infra/migration/SKILL.md +158 -0
- package/configs/_shared/.claude/skills/nx/nx-affected/SKILL.md +72 -0
- package/configs/_shared/.claude/skills/nx/nx-lib/SKILL.md +375 -0
- package/configs/_shared/CLAUDE.md +52 -149
- package/configs/angular/.claude/rules/{components.md → core/components.md} +69 -15
- package/configs/angular/.claude/rules/core/resource.md +285 -0
- package/configs/angular/.claude/rules/core/signals.md +323 -0
- package/configs/angular/.claude/rules/http.md +338 -0
- package/configs/angular/.claude/rules/routing.md +291 -0
- package/configs/angular/.claude/rules/ssr.md +312 -0
- package/configs/angular/.claude/rules/state/signal-store.md +408 -0
- package/configs/angular/.claude/rules/{state.md → state/state.md} +2 -2
- package/configs/angular/.claude/rules/testing.md +7 -7
- package/configs/angular/.claude/rules/ui/aria.md +422 -0
- package/configs/angular/.claude/rules/ui/forms.md +424 -0
- package/configs/angular/.claude/rules/ui/pipes-directives.md +335 -0
- package/configs/angular/.claude/settings.json +1 -0
- package/configs/angular/.claude/skills/ngrx-slice/SKILL.md +362 -0
- package/configs/angular/.claude/skills/signal-store/SKILL.md +445 -0
- package/configs/angular/CLAUDE.md +24 -216
- package/configs/dotnet/.claude/rules/background-services.md +552 -0
- package/configs/dotnet/.claude/rules/configuration.md +426 -0
- package/configs/dotnet/.claude/rules/ddd.md +447 -0
- package/configs/dotnet/.claude/rules/dependency-injection.md +343 -0
- package/configs/dotnet/.claude/rules/mediatr.md +320 -0
- package/configs/dotnet/.claude/rules/middleware.md +489 -0
- package/configs/dotnet/.claude/rules/result-pattern.md +363 -0
- package/configs/dotnet/.claude/rules/validation.md +388 -0
- package/configs/dotnet/.claude/settings.json +21 -3
- package/configs/dotnet/CLAUDE.md +53 -286
- package/configs/fastapi/.claude/rules/background-tasks.md +254 -0
- package/configs/fastapi/.claude/rules/dependencies.md +170 -0
- package/configs/{python → fastapi}/.claude/rules/fastapi.md +61 -1
- package/configs/fastapi/.claude/rules/lifespan.md +274 -0
- package/configs/fastapi/.claude/rules/middleware.md +229 -0
- package/configs/fastapi/.claude/rules/pydantic.md +433 -0
- package/configs/fastapi/.claude/rules/responses.md +251 -0
- package/configs/fastapi/.claude/rules/routers.md +202 -0
- package/configs/fastapi/.claude/rules/security.md +222 -0
- package/configs/fastapi/.claude/rules/testing.md +251 -0
- package/configs/fastapi/.claude/rules/websockets.md +298 -0
- package/configs/fastapi/.claude/settings.json +33 -0
- package/configs/fastapi/CLAUDE.md +144 -0
- package/configs/flask/.claude/rules/blueprints.md +208 -0
- package/configs/flask/.claude/rules/cli.md +285 -0
- package/configs/flask/.claude/rules/configuration.md +281 -0
- package/configs/flask/.claude/rules/context.md +238 -0
- package/configs/flask/.claude/rules/error-handlers.md +278 -0
- package/configs/flask/.claude/rules/extensions.md +278 -0
- package/configs/flask/.claude/rules/flask.md +171 -0
- package/configs/flask/.claude/rules/marshmallow.md +206 -0
- package/configs/flask/.claude/rules/security.md +267 -0
- package/configs/flask/.claude/rules/testing.md +284 -0
- package/configs/flask/.claude/settings.json +33 -0
- package/configs/flask/CLAUDE.md +166 -0
- package/configs/nestjs/.claude/rules/common-patterns.md +300 -0
- package/configs/nestjs/.claude/rules/filters.md +376 -0
- package/configs/nestjs/.claude/rules/interceptors.md +317 -0
- package/configs/nestjs/.claude/rules/middleware.md +321 -0
- package/configs/nestjs/.claude/rules/modules.md +26 -0
- package/configs/nestjs/.claude/rules/pipes.md +351 -0
- package/configs/nestjs/.claude/rules/websockets.md +451 -0
- package/configs/nestjs/.claude/settings.json +16 -2
- package/configs/nestjs/CLAUDE.md +57 -215
- package/configs/nextjs/.claude/rules/api-routes.md +358 -0
- package/configs/nextjs/.claude/rules/authentication.md +355 -0
- package/configs/nextjs/.claude/rules/components.md +52 -0
- package/configs/nextjs/.claude/rules/data-fetching.md +249 -0
- package/configs/nextjs/.claude/rules/database.md +400 -0
- package/configs/nextjs/.claude/rules/middleware.md +303 -0
- package/configs/nextjs/.claude/rules/routing.md +324 -0
- package/configs/nextjs/.claude/rules/seo.md +350 -0
- package/configs/nextjs/.claude/rules/server-actions.md +353 -0
- package/configs/nextjs/.claude/rules/state/zustand.md +6 -6
- package/configs/nextjs/.claude/settings.json +5 -0
- package/configs/nextjs/CLAUDE.md +69 -331
- package/package.json +23 -9
- package/src/cli.js +220 -0
- package/src/config.js +29 -0
- package/src/index.js +13 -0
- package/src/installer.js +361 -0
- package/src/merge.js +116 -0
- package/src/tech-config.json +29 -0
- package/src/utils.js +96 -0
- package/configs/python/.claude/rules/flask.md +0 -332
- package/configs/python/.claude/settings.json +0 -18
- package/configs/python/CLAUDE.md +0 -273
- package/src/install.js +0 -315
- /package/configs/_shared/.claude/rules/{accessibility.md → domain/frontend/accessibility.md} +0 -0
- /package/configs/_shared/.claude/rules/{security.md → security/security.md} +0 -0
- /package/configs/_shared/.claude/skills/{debug → dev/debug}/SKILL.md +0 -0
- /package/configs/_shared/.claude/skills/{learning → dev/learning}/SKILL.md +0 -0
- /package/configs/_shared/.claude/skills/{spec → dev/spec}/SKILL.md +0 -0
- /package/configs/_shared/.claude/skills/{review → git/review}/SKILL.md +0 -0
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "**/*Middleware.cs"
|
|
4
|
+
- "**/Middleware/**/*.cs"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# .NET Middleware
|
|
8
|
+
|
|
9
|
+
## Basic Middleware
|
|
10
|
+
|
|
11
|
+
```csharp
|
|
12
|
+
// Middleware/RequestLoggingMiddleware.cs
|
|
13
|
+
public class RequestLoggingMiddleware
|
|
14
|
+
{
|
|
15
|
+
private readonly RequestDelegate _next;
|
|
16
|
+
private readonly ILogger<RequestLoggingMiddleware> _logger;
|
|
17
|
+
|
|
18
|
+
public RequestLoggingMiddleware(RequestDelegate next, ILogger<RequestLoggingMiddleware> logger)
|
|
19
|
+
{
|
|
20
|
+
_next = next;
|
|
21
|
+
_logger = logger;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
public async Task InvokeAsync(HttpContext context)
|
|
25
|
+
{
|
|
26
|
+
var stopwatch = Stopwatch.StartNew();
|
|
27
|
+
|
|
28
|
+
try
|
|
29
|
+
{
|
|
30
|
+
await _next(context);
|
|
31
|
+
}
|
|
32
|
+
finally
|
|
33
|
+
{
|
|
34
|
+
stopwatch.Stop();
|
|
35
|
+
|
|
36
|
+
_logger.LogInformation(
|
|
37
|
+
"{Method} {Path} responded {StatusCode} in {ElapsedMs}ms",
|
|
38
|
+
context.Request.Method,
|
|
39
|
+
context.Request.Path,
|
|
40
|
+
context.Response.StatusCode,
|
|
41
|
+
stopwatch.ElapsedMilliseconds);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Extension method
|
|
47
|
+
public static class RequestLoggingMiddlewareExtensions
|
|
48
|
+
{
|
|
49
|
+
public static IApplicationBuilder UseRequestLogging(this IApplicationBuilder builder)
|
|
50
|
+
{
|
|
51
|
+
return builder.UseMiddleware<RequestLoggingMiddleware>();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Correlation ID Middleware
|
|
57
|
+
|
|
58
|
+
```csharp
|
|
59
|
+
// Middleware/CorrelationIdMiddleware.cs
|
|
60
|
+
public class CorrelationIdMiddleware
|
|
61
|
+
{
|
|
62
|
+
private const string CorrelationIdHeader = "X-Correlation-ID";
|
|
63
|
+
private readonly RequestDelegate _next;
|
|
64
|
+
|
|
65
|
+
public CorrelationIdMiddleware(RequestDelegate next)
|
|
66
|
+
{
|
|
67
|
+
_next = next;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
public async Task InvokeAsync(HttpContext context)
|
|
71
|
+
{
|
|
72
|
+
var correlationId = GetOrCreateCorrelationId(context);
|
|
73
|
+
|
|
74
|
+
// Add to response headers
|
|
75
|
+
context.Response.OnStarting(() =>
|
|
76
|
+
{
|
|
77
|
+
context.Response.Headers.TryAdd(CorrelationIdHeader, correlationId);
|
|
78
|
+
return Task.CompletedTask;
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Store in Items for access throughout request
|
|
82
|
+
context.Items["CorrelationId"] = correlationId;
|
|
83
|
+
|
|
84
|
+
// Add to logging scope
|
|
85
|
+
using (_logger.BeginScope(new Dictionary<string, object>
|
|
86
|
+
{
|
|
87
|
+
["CorrelationId"] = correlationId
|
|
88
|
+
}))
|
|
89
|
+
{
|
|
90
|
+
await _next(context);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private static string GetOrCreateCorrelationId(HttpContext context)
|
|
95
|
+
{
|
|
96
|
+
if (context.Request.Headers.TryGetValue(CorrelationIdHeader, out var existingId))
|
|
97
|
+
{
|
|
98
|
+
return existingId.ToString();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return Guid.NewGuid().ToString();
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Exception Handling Middleware
|
|
107
|
+
|
|
108
|
+
```csharp
|
|
109
|
+
// Middleware/ExceptionHandlingMiddleware.cs
|
|
110
|
+
public class ExceptionHandlingMiddleware
|
|
111
|
+
{
|
|
112
|
+
private readonly RequestDelegate _next;
|
|
113
|
+
private readonly ILogger<ExceptionHandlingMiddleware> _logger;
|
|
114
|
+
private readonly IHostEnvironment _env;
|
|
115
|
+
|
|
116
|
+
public ExceptionHandlingMiddleware(
|
|
117
|
+
RequestDelegate next,
|
|
118
|
+
ILogger<ExceptionHandlingMiddleware> logger,
|
|
119
|
+
IHostEnvironment env)
|
|
120
|
+
{
|
|
121
|
+
_next = next;
|
|
122
|
+
_logger = logger;
|
|
123
|
+
_env = env;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
public async Task InvokeAsync(HttpContext context)
|
|
127
|
+
{
|
|
128
|
+
try
|
|
129
|
+
{
|
|
130
|
+
await _next(context);
|
|
131
|
+
}
|
|
132
|
+
catch (Exception ex)
|
|
133
|
+
{
|
|
134
|
+
await HandleExceptionAsync(context, ex);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
private async Task HandleExceptionAsync(HttpContext context, Exception exception)
|
|
139
|
+
{
|
|
140
|
+
var (statusCode, title, detail) = exception switch
|
|
141
|
+
{
|
|
142
|
+
ValidationException ex => (
|
|
143
|
+
StatusCodes.Status400BadRequest,
|
|
144
|
+
"Validation Error",
|
|
145
|
+
string.Join("; ", ex.Errors.Select(e => e.ErrorMessage))),
|
|
146
|
+
|
|
147
|
+
NotFoundException ex => (
|
|
148
|
+
StatusCodes.Status404NotFound,
|
|
149
|
+
"Not Found",
|
|
150
|
+
ex.Message),
|
|
151
|
+
|
|
152
|
+
UnauthorizedAccessException => (
|
|
153
|
+
StatusCodes.Status401Unauthorized,
|
|
154
|
+
"Unauthorized",
|
|
155
|
+
"Authentication required"),
|
|
156
|
+
|
|
157
|
+
ForbiddenException => (
|
|
158
|
+
StatusCodes.Status403Forbidden,
|
|
159
|
+
"Forbidden",
|
|
160
|
+
"Insufficient permissions"),
|
|
161
|
+
|
|
162
|
+
ConflictException ex => (
|
|
163
|
+
StatusCodes.Status409Conflict,
|
|
164
|
+
"Conflict",
|
|
165
|
+
ex.Message),
|
|
166
|
+
|
|
167
|
+
_ => (
|
|
168
|
+
StatusCodes.Status500InternalServerError,
|
|
169
|
+
"Internal Server Error",
|
|
170
|
+
_env.IsDevelopment() ? exception.Message : "An error occurred")
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
_logger.LogError(exception, "Exception occurred: {Message}", exception.Message);
|
|
174
|
+
|
|
175
|
+
context.Response.StatusCode = statusCode;
|
|
176
|
+
context.Response.ContentType = "application/problem+json";
|
|
177
|
+
|
|
178
|
+
var problemDetails = new ProblemDetails
|
|
179
|
+
{
|
|
180
|
+
Status = statusCode,
|
|
181
|
+
Title = title,
|
|
182
|
+
Detail = detail,
|
|
183
|
+
Instance = context.Request.Path
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
if (_env.IsDevelopment())
|
|
187
|
+
{
|
|
188
|
+
problemDetails.Extensions["stackTrace"] = exception.StackTrace;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
await context.Response.WriteAsJsonAsync(problemDetails);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## Rate Limiting Middleware
|
|
197
|
+
|
|
198
|
+
```csharp
|
|
199
|
+
// Middleware/RateLimitingMiddleware.cs
|
|
200
|
+
public class RateLimitingMiddleware
|
|
201
|
+
{
|
|
202
|
+
private readonly RequestDelegate _next;
|
|
203
|
+
private readonly IDistributedCache _cache;
|
|
204
|
+
private readonly RateLimitOptions _options;
|
|
205
|
+
|
|
206
|
+
public RateLimitingMiddleware(
|
|
207
|
+
RequestDelegate next,
|
|
208
|
+
IDistributedCache cache,
|
|
209
|
+
IOptions<RateLimitOptions> options)
|
|
210
|
+
{
|
|
211
|
+
_next = next;
|
|
212
|
+
_cache = cache;
|
|
213
|
+
_options = options.Value;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
public async Task InvokeAsync(HttpContext context)
|
|
217
|
+
{
|
|
218
|
+
var clientId = GetClientIdentifier(context);
|
|
219
|
+
var key = $"rate-limit:{clientId}";
|
|
220
|
+
|
|
221
|
+
var currentCount = await GetRequestCountAsync(key);
|
|
222
|
+
|
|
223
|
+
if (currentCount >= _options.MaxRequests)
|
|
224
|
+
{
|
|
225
|
+
context.Response.StatusCode = StatusCodes.Status429TooManyRequests;
|
|
226
|
+
context.Response.Headers.RetryAfter = _options.WindowSeconds.ToString();
|
|
227
|
+
await context.Response.WriteAsJsonAsync(new
|
|
228
|
+
{
|
|
229
|
+
error = "Too many requests",
|
|
230
|
+
retryAfter = _options.WindowSeconds
|
|
231
|
+
});
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
await IncrementRequestCountAsync(key);
|
|
236
|
+
|
|
237
|
+
context.Response.Headers.Append("X-RateLimit-Limit", _options.MaxRequests.ToString());
|
|
238
|
+
context.Response.Headers.Append("X-RateLimit-Remaining", (options.MaxRequests - currentCount - 1).ToString());
|
|
239
|
+
|
|
240
|
+
await _next(context);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
private string GetClientIdentifier(HttpContext context)
|
|
244
|
+
{
|
|
245
|
+
// Prefer authenticated user ID, fallback to IP
|
|
246
|
+
return context.User.Identity?.IsAuthenticated == true
|
|
247
|
+
? context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? context.Connection.RemoteIpAddress?.ToString() ?? "anonymous"
|
|
248
|
+
: context.Connection.RemoteIpAddress?.ToString() ?? "anonymous";
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
private async Task<int> GetRequestCountAsync(string key)
|
|
252
|
+
{
|
|
253
|
+
var value = await _cache.GetStringAsync(key);
|
|
254
|
+
return int.TryParse(value, out var count) ? count : 0;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
private async Task IncrementRequestCountAsync(string key)
|
|
258
|
+
{
|
|
259
|
+
var count = await GetRequestCountAsync(key) + 1;
|
|
260
|
+
await _cache.SetStringAsync(
|
|
261
|
+
key,
|
|
262
|
+
count.ToString(),
|
|
263
|
+
new DistributedCacheEntryOptions
|
|
264
|
+
{
|
|
265
|
+
AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(_options.WindowSeconds)
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
public class RateLimitOptions
|
|
271
|
+
{
|
|
272
|
+
public int MaxRequests { get; set; } = 100;
|
|
273
|
+
public int WindowSeconds { get; set; } = 60;
|
|
274
|
+
}
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
## Security Headers Middleware
|
|
278
|
+
|
|
279
|
+
```csharp
|
|
280
|
+
// Middleware/SecurityHeadersMiddleware.cs
|
|
281
|
+
public class SecurityHeadersMiddleware
|
|
282
|
+
{
|
|
283
|
+
private readonly RequestDelegate _next;
|
|
284
|
+
|
|
285
|
+
public SecurityHeadersMiddleware(RequestDelegate next)
|
|
286
|
+
{
|
|
287
|
+
_next = next;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
public async Task InvokeAsync(HttpContext context)
|
|
291
|
+
{
|
|
292
|
+
// Security headers
|
|
293
|
+
context.Response.Headers.Append("X-Content-Type-Options", "nosniff");
|
|
294
|
+
context.Response.Headers.Append("X-Frame-Options", "DENY");
|
|
295
|
+
context.Response.Headers.Append("X-XSS-Protection", "1; mode=block");
|
|
296
|
+
context.Response.Headers.Append("Referrer-Policy", "strict-origin-when-cross-origin");
|
|
297
|
+
context.Response.Headers.Append(
|
|
298
|
+
"Content-Security-Policy",
|
|
299
|
+
"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline';");
|
|
300
|
+
context.Response.Headers.Append(
|
|
301
|
+
"Permissions-Policy",
|
|
302
|
+
"camera=(), microphone=(), geolocation=()");
|
|
303
|
+
|
|
304
|
+
// Remove server header
|
|
305
|
+
context.Response.Headers.Remove("Server");
|
|
306
|
+
context.Response.Headers.Remove("X-Powered-By");
|
|
307
|
+
|
|
308
|
+
await _next(context);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
## Request Timeout Middleware
|
|
314
|
+
|
|
315
|
+
```csharp
|
|
316
|
+
// Middleware/RequestTimeoutMiddleware.cs
|
|
317
|
+
public class RequestTimeoutMiddleware
|
|
318
|
+
{
|
|
319
|
+
private readonly RequestDelegate _next;
|
|
320
|
+
private readonly TimeSpan _timeout;
|
|
321
|
+
|
|
322
|
+
public RequestTimeoutMiddleware(RequestDelegate next, TimeSpan timeout)
|
|
323
|
+
{
|
|
324
|
+
_next = next;
|
|
325
|
+
_timeout = timeout;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
public async Task InvokeAsync(HttpContext context)
|
|
329
|
+
{
|
|
330
|
+
using var cts = CancellationTokenSource.CreateLinkedTokenSource(context.RequestAborted);
|
|
331
|
+
cts.CancelAfter(_timeout);
|
|
332
|
+
|
|
333
|
+
context.RequestAborted = cts.Token;
|
|
334
|
+
|
|
335
|
+
try
|
|
336
|
+
{
|
|
337
|
+
await _next(context);
|
|
338
|
+
}
|
|
339
|
+
catch (OperationCanceledException) when (cts.IsCancellationRequested)
|
|
340
|
+
{
|
|
341
|
+
context.Response.StatusCode = StatusCodes.Status408RequestTimeout;
|
|
342
|
+
await context.Response.WriteAsJsonAsync(new { error = "Request timeout" });
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
## Tenant Resolution Middleware
|
|
349
|
+
|
|
350
|
+
```csharp
|
|
351
|
+
// Middleware/TenantMiddleware.cs
|
|
352
|
+
public class TenantMiddleware
|
|
353
|
+
{
|
|
354
|
+
private readonly RequestDelegate _next;
|
|
355
|
+
|
|
356
|
+
public TenantMiddleware(RequestDelegate next)
|
|
357
|
+
{
|
|
358
|
+
_next = next;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
public async Task InvokeAsync(HttpContext context, ITenantResolver tenantResolver)
|
|
362
|
+
{
|
|
363
|
+
var tenantId = ResolveTenantId(context);
|
|
364
|
+
|
|
365
|
+
if (string.IsNullOrEmpty(tenantId))
|
|
366
|
+
{
|
|
367
|
+
context.Response.StatusCode = StatusCodes.Status400BadRequest;
|
|
368
|
+
await context.Response.WriteAsJsonAsync(new { error = "Tenant not specified" });
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
var tenant = await tenantResolver.ResolveAsync(tenantId);
|
|
373
|
+
|
|
374
|
+
if (tenant == null)
|
|
375
|
+
{
|
|
376
|
+
context.Response.StatusCode = StatusCodes.Status404NotFound;
|
|
377
|
+
await context.Response.WriteAsJsonAsync(new { error = "Tenant not found" });
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
context.Items["Tenant"] = tenant;
|
|
382
|
+
context.Items["TenantId"] = tenant.Id;
|
|
383
|
+
|
|
384
|
+
await _next(context);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
private static string? ResolveTenantId(HttpContext context)
|
|
388
|
+
{
|
|
389
|
+
// From header
|
|
390
|
+
if (context.Request.Headers.TryGetValue("X-Tenant-ID", out var headerValue))
|
|
391
|
+
{
|
|
392
|
+
return headerValue.ToString();
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// From subdomain
|
|
396
|
+
var host = context.Request.Host.Host;
|
|
397
|
+
var subdomain = host.Split('.').FirstOrDefault();
|
|
398
|
+
if (!string.IsNullOrEmpty(subdomain) && subdomain != "www")
|
|
399
|
+
{
|
|
400
|
+
return subdomain;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// From claim
|
|
404
|
+
return context.User.FindFirst("tenant_id")?.Value;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
## Middleware Registration Order
|
|
410
|
+
|
|
411
|
+
```csharp
|
|
412
|
+
// Program.cs
|
|
413
|
+
var app = builder.Build();
|
|
414
|
+
|
|
415
|
+
// 1. Exception handling (first to catch all)
|
|
416
|
+
app.UseMiddleware<ExceptionHandlingMiddleware>();
|
|
417
|
+
|
|
418
|
+
// 2. Security headers
|
|
419
|
+
app.UseMiddleware<SecurityHeadersMiddleware>();
|
|
420
|
+
|
|
421
|
+
// 3. Correlation ID (before logging)
|
|
422
|
+
app.UseMiddleware<CorrelationIdMiddleware>();
|
|
423
|
+
|
|
424
|
+
// 4. Request logging
|
|
425
|
+
app.UseMiddleware<RequestLoggingMiddleware>();
|
|
426
|
+
|
|
427
|
+
// 5. Rate limiting
|
|
428
|
+
app.UseMiddleware<RateLimitingMiddleware>();
|
|
429
|
+
|
|
430
|
+
// 6. Built-in middleware
|
|
431
|
+
app.UseHttpsRedirection();
|
|
432
|
+
app.UseAuthentication();
|
|
433
|
+
app.UseAuthorization();
|
|
434
|
+
|
|
435
|
+
// 7. Tenant resolution (after auth)
|
|
436
|
+
app.UseMiddleware<TenantMiddleware>();
|
|
437
|
+
|
|
438
|
+
// 8. Endpoints
|
|
439
|
+
app.MapControllers();
|
|
440
|
+
|
|
441
|
+
app.Run();
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
## Anti-patterns
|
|
445
|
+
|
|
446
|
+
```csharp
|
|
447
|
+
// BAD: Middleware with scoped dependencies in constructor
|
|
448
|
+
public class BadMiddleware
|
|
449
|
+
{
|
|
450
|
+
private readonly IDbContext _context; // Scoped service!
|
|
451
|
+
|
|
452
|
+
public BadMiddleware(RequestDelegate next, IDbContext context)
|
|
453
|
+
{
|
|
454
|
+
_context = context; // Will use same instance for all requests!
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// GOOD: Inject scoped services in InvokeAsync
|
|
459
|
+
public async Task InvokeAsync(HttpContext context, IDbContext dbContext)
|
|
460
|
+
{
|
|
461
|
+
// dbContext is resolved per request
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// BAD: Blocking in middleware
|
|
465
|
+
public async Task InvokeAsync(HttpContext context)
|
|
466
|
+
{
|
|
467
|
+
var result = _service.GetData().Result; // Deadlock risk!
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// GOOD: Use async/await
|
|
471
|
+
public async Task InvokeAsync(HttpContext context)
|
|
472
|
+
{
|
|
473
|
+
var result = await _service.GetDataAsync();
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// BAD: Modifying response after body written
|
|
477
|
+
public async Task InvokeAsync(HttpContext context)
|
|
478
|
+
{
|
|
479
|
+
await _next(context);
|
|
480
|
+
context.Response.Headers.Add("X-Custom", "value"); // May fail!
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// GOOD: Use OnStarting callback
|
|
484
|
+
context.Response.OnStarting(() =>
|
|
485
|
+
{
|
|
486
|
+
context.Response.Headers.Add("X-Custom", "value");
|
|
487
|
+
return Task.CompletedTask;
|
|
488
|
+
});
|
|
489
|
+
```
|