Skip to content

Commit 1b99f07

Browse files
committed
Merge branch 'development'
2 parents d857140 + b43e4d6 commit 1b99f07

21 files changed

+510
-385
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -486,4 +486,5 @@ $RECYCLE.BIN/
486486
# Added by me
487487
logs
488488
LocalFileStorage
489-
*.db*
489+
*.db*
490+
**.claude

CLAUDE.md

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
# Pandatech.SharedKernel
2+
3+
Opinionated ASP.NET Core 10 infrastructure kernel. Not a general-purpose NuGet — meant to be forked and customized per project.
4+
5+
## Build & Test
6+
7+
```bash
8+
dotnet build src/SharedKernel/SharedKernel.csproj
9+
dotnet test test/SharedKernel.Tests/SharedKernel.Tests.csproj
10+
```
11+
12+
- .NET 10 SDK required (see `global.json` for exact version)
13+
- C# 14 features used: extension members, collection expressions, primary constructors
14+
- CI publishes to NuGet on push to `main` via GitHub Actions
15+
16+
## Project Structure
17+
18+
```
19+
src/SharedKernel/ # Library source
20+
Constants/ # EndpointConstants (/above-board base path)
21+
Extensions/ # DI registration & middleware extension methods
22+
Helpers/ # AssemblyRegistry, ValidationHelper, PhoneUtil, etc.
23+
JsonConverters/ # Enum-as-string, DateOnly format converters
24+
Logging/ # Serilog setup + HTTP request/response logging middleware
25+
Middleware/ # RequestLoggingMiddleware, OutboundLoggingHandler, RedactionHelper
26+
Maintenance/ # Distributed maintenance mode (HybridCache + poller)
27+
OpenApi/ # Multi-document Swagger/Scalar UI + config
28+
Resilience/ # Polly retry, circuit breaker, timeout pipelines
29+
ValidatorAndMediatR/ # CQRS interfaces (ICommand/IQuery) + FluentValidation pipeline
30+
Behaviors/ # MediatR validation pipeline behaviors
31+
Validators/ # String, file, XSS validators + file presets
32+
test/SharedKernel.Tests/ # xUnit tests (PhoneUtil, ValidationHelper, TimeZone)
33+
SharedKernel.Demo/ # Working reference implementation showing full setup
34+
```
35+
36+
## Key Conventions
37+
38+
### Startup Registration Order
39+
40+
Builder phase (`WebApplicationBuilder`):
41+
1. `LogStartAttempt()` + `AssemblyRegistry.Add()`
42+
2. `.ConfigureWithPandaVault()` (secrets, non-Local only)
43+
3. `.AddSerilog(LogBackend.*)` (logging)
44+
4. `.AddResponseCrafter()` (exception standardization)
45+
5. `.AddOpenApi()`, `.AddMaintenanceMode()`, `.AddOpenTelemetry()`
46+
6. `.AddMinimalApis()`, `.AddControllers()`, `.AddMediatrWithBehaviors()` (all take assemblies)
47+
7. `.AddResilienceDefaultPipeline()`, `.AddSignalR()` or `.AddDistributedSignalR()`
48+
8. `.AddCors()`, `.AddOutboundLoggingHandler()`, `.AddHealthChecks()`
49+
50+
App phase (`WebApplication`) — middleware order matters:
51+
1. `.UseRequestLogging()` (must be first to capture full request lifecycle)
52+
2. `.UseMaintenanceMode()` (before ResponseCrafter)
53+
3. `.UseResponseCrafter()`
54+
4. `.UseCors()`
55+
5. `.MapMinimalApis()`, `.MapHealthCheckEndpoints()`, `.MapPrometheusExporterEndpoints()`
56+
6. `.EnsureHealthy()` (blocks startup if unhealthy)
57+
7. `.ClearAssemblyRegistry()` (frees memory)
58+
8. `.UseOpenApi()`, `.MapControllers()`
59+
60+
### CQRS Pattern
61+
62+
Use `ICommand<T>` / `ICommand` for writes, `IQuery<T>` / `IQuery` for reads. Handlers: `ICommandHandler<,>`, `IQueryHandler<,>`. FluentValidation validators are auto-discovered and run as MediatR pipeline behaviors.
63+
64+
### Reserved Paths
65+
66+
All infrastructure endpoints live under `/above-board/`:
67+
- `GET /above-board/ping` — returns "pong"
68+
- `GET /above-board/health` — full health check JSON
69+
- `GET /above-board/prometheus` — metrics scrape
70+
- `PUT /above-board/maintenance` — set maintenance mode
71+
72+
These paths are excluded from request logging and pass through maintenance mode.
73+
74+
### Environment Conventions
75+
76+
- `IsLocal()` — developer machine, console-only logging, PandaVault skipped
77+
- `IsDevelopment()` — console + file logging
78+
- `IsQa()`, `IsStaging()` — file logging, PandaVault enabled
79+
- `IsProduction()` — file logging only, restricted CORS origins
80+
81+
### Configuration (appsettings.json)
82+
83+
Required keys:
84+
- `RepositoryName` — used for log file paths
85+
- `ConnectionStrings:PersistentStorage` — base path for log files
86+
- `Security:AllowedCorsOrigins` — comma/semicolon-separated origins (production only)
87+
- `DefaultTimeZone` — e.g. "Caucasus Standard Time"
88+
- `OpenApi` section — documents, security schemes, contact info
89+
90+
Optional:
91+
- `OTEL_EXPORTER_OTLP_ENDPOINT` env var — enables OTLP export
92+
93+
### Logging
94+
95+
- Request/response bodies captured up to 16KB, with automatic sensitive-key redaction
96+
- Redaction is key-based: headers and JSON property names containing `auth`, `token`, `pass`, `secret`, `cookie`, `pan`, `cvv`, `ssn`, etc. are redacted
97+
- Log backends: `ElasticSearch` (ECS format), `Loki` (Grafana JSON), `CompactJson`
98+
- Paths ignored: `/swagger`, `/openapi`, `/above-board/*`, `/favicon.ico`
99+
- OutboundLoggingHandler logs HttpClient calls with same redaction rules
100+
101+
### Resilience
102+
103+
Two pipeline variants sharing the same configuration constants:
104+
- **General** (`ResiliencePipelineProvider<string>`) — for non-HttpClient use
105+
- **HTTP** (`IHttpClientBuilder.AddResilienceDefaultPipeline()`) — respects Retry-After headers
106+
107+
Policies: 429 retry (5 attempts), 5xx/408 retry (7 attempts), circuit breaker (50% failure / 30s window / 200 min requests), 8s timeout per attempt.
108+
109+
### Maintenance Mode
110+
111+
Three modes: `Disabled`, `EnabledForClients` (blocks non-admin routes), `EnabledForAll` (blocks everything except `/above-board/*`). State synchronized across instances via HybridCache + background poller (7s interval). Admin routes are identified by `/api/admin` and `/hub/admin` path prefixes.
112+
113+
## Dependency Notes
114+
115+
- **MediatR** pinned to `[12.5.0]` — last free version (13+ is commercial)
116+
- **Pandatech.\*** packages are internal PandaTech libraries (ResponseCrafter, DistributedCache, Crypto, PandaVaultClient, etc.)
117+
- Analyzers (Pandatech.Analyzers, SonarAnalyzer) are build-only, not shipped to consumers
118+
119+
## Code Style
120+
121+
- 3-space indentation
122+
- File-scoped namespaces
123+
- C# 14 extension members for clean builder APIs
124+
- `partial class` + `[LoggerMessage]` for high-performance structured logging
125+
- `FrozenSet<T>` for static lookup data

README.md

Lines changed: 37 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -72,13 +72,15 @@ app
7272
.UseResponseCrafter()
7373
.UseCors()
7474
.MapMinimalApis()
75-
.EnsureHealthy()
7675
.MapHealthCheckEndpoints()
7776
.MapPrometheusExporterEndpoints()
77+
.EnsureHealthy()
7878
.ClearAssemblyRegistry()
7979
.UseOpenApi()
8080
.MapControllers();
8181

82+
app.MapMaintenanceEndpoint();
83+
8284
app.LogStartSuccess();
8385
app.Run();
8486
```
@@ -87,8 +89,8 @@ app.Run();
8789

8890
## Assembly Registry
8991

90-
`AssemblyRegistry` is a thread-safe static list used to pass your project's assemblies from the builder phase to the
91-
app phase without repeating `typeof(Program).Assembly` everywhere.
92+
`AssemblyRegistry` is a thread-safe static collection used to pass your project's assemblies from the builder phase to
93+
the app phase without repeating `typeof(Program).Assembly` everywhere.
9294

9395
```csharp
9496
// Add once at startup
@@ -105,8 +107,8 @@ app.ClearAssemblyRegistry();
105107

106108
## OpenAPI
107109

108-
Wraps `Microsoft.AspNetCore.OpenApi` with SwaggerUI and Scalar, supporting multiple API documents, custom security
109-
schemes, and enum string descriptions.
110+
Wraps `Microsoft.AspNetCore.OpenApi` with SwaggerUI and Scalar, generating **OpenAPI 3.1** specs. Supports multiple API
111+
documents, custom security schemes, and enum string descriptions (including nullable enums).
110112

111113
### Registration
112114

@@ -208,7 +210,7 @@ builder.AddSerilog(
208210
### Log Backends
209211

210212
| Value | Output format |
211-
|-----------------|---------------------------------------------------|
213+
|-----------------|----------------------------------------------------|
212214
| `None` | Console only, no file output |
213215
| `ElasticSearch` | ECS JSON to file (forward with Filebeat/Logstash) |
214216
| `Loki` | Loki JSON to file (forward with Promtail) |
@@ -251,10 +253,13 @@ every 12 hours and deletes files older than `daysToRetain`.
251253
app.UseRequestLogging(); // logs method, path, status code, elapsed ms, redacted headers/body
252254
```
253255

254-
Paths under `/swagger`, `/openapi`, `/above-board`, and `/favicon.ico` are silently skipped. Sensitive header names
255-
(auth, token, cookie, pan, cvv, etc.) and matching JSON properties are redacted automatically. Bodies over 16 KB are
256+
Paths under `/swagger`, `/openapi`, `/above-board`, and `/favicon.ico` are silently skipped. Bodies over 16 KB are
256257
omitted.
257258

259+
**Redaction is key-based:** header names and JSON property names containing sensitive keywords (`auth`, `token`,
260+
`pass`, `secret`, `cookie`, `pan`, `cvv`, `ssn`, `tin`, `iban`, etc.) are automatically replaced with `[REDACTED]`.
261+
Values are never inspected — only the property/header name determines whether redaction applies.
262+
258263
### Outbound logging
259264

260265
Captures outbound `HttpClient` requests with the same redaction rules:
@@ -283,6 +288,8 @@ app.LogStartSuccess(); // prints success banner with elapsed init time
283288
Registers MediatR with a validation pipeline behavior that runs all FluentValidation validators before the handler.
284289
Validation failures throw `BadRequestException` from `Pandatech.ResponseCrafter`.
285290

291+
> **Note:** MediatR is pinned to version 12.5.0 — the last MIT-licensed release.
292+
286293
### Registration
287294

288295
```csharp
@@ -311,6 +318,7 @@ RuleFor(x => x.Phone).IsPhoneNumber(); // Panda format: (374)91123456
311318
RuleFor(x => x.Contact).IsEmailOrPhoneNumber();
312319
RuleFor(x => x.Payload).IsValidJson();
313320
RuleFor(x => x.Content).IsXssSanitized();
321+
RuleFor(x => x.Card).IsCreditCardNumber();
314322
```
315323

316324
**Single file (`IFormFile`)**
@@ -371,8 +379,11 @@ The list accepts comma- or semicolon-separated URLs. Invalid entries are logged
371379

372380
## Resilience Pipelines
373381

374-
Built on Polly via `Microsoft.Extensions.Http.Resilience`. Provides retry, circuit breaker, and timeout policies for
375-
`HttpClient` calls.
382+
Built on Polly via `Microsoft.Extensions.Http.Resilience`. Two pipeline variants share the same configuration constants
383+
from a single source of truth:
384+
385+
- **General pipeline** — registered globally via `AddResilienceDefaultPipeline()` on the builder, or used manually via `ResiliencePipelineProvider<string>`
386+
- **HTTP pipeline** — attached per-client via `AddResilienceDefaultPipeline()` on an `IHttpClientBuilder`, with additional `Retry-After` header support for 429 responses
376387

377388
### Options
378389

@@ -404,12 +415,15 @@ public class MyService(ResiliencePipelineProvider<string> provider)
404415

405416
### Default pipeline policies
406417

407-
| Policy | Configuration |
408-
|-----------------|--------------------------------------------------------|
409-
| Retry (429) | 5 retries, exponential backoff, respects `Retry-After` |
410-
| Retry (5xx/408) | 7 retries, exponential backoff from 800ms |
411-
| Circuit breaker | Opens at 50% failure rate over 30 s, min 200 requests |
412-
| Timeout | 8 seconds per attempt |
418+
| Policy | Configuration |
419+
|-----------------|-----------------------------------------------------------------------------|
420+
| Retry (429) | 5 retries, exponential backoff with jitter, respects `Retry-After` header |
421+
| Retry (5xx/408) | 7 retries, exponential backoff from 800 ms with jitter |
422+
| Circuit breaker | Opens at 50% failure rate over 30 s (min 200 requests), 45 s break duration |
423+
| Timeout | 8 seconds per attempt |
424+
425+
The circuit breaker only trips on transient failures (`HttpRequestException`, `TaskCanceledException`) and non-success
426+
HTTP status codes. Programming errors like `ArgumentException` will not open the circuit.
413427

414428
---
415429

@@ -481,8 +495,8 @@ Set the following in your environment config or as an environment variable to en
481495
```csharp
482496
builder.AddHealthChecks();
483497

484-
app.EnsureHealthy(); // runs health checks at startup; throws if anything is unhealthy
485498
app.MapHealthCheckEndpoints(); // registers /above-board/ping and /above-board/health
499+
app.EnsureHealthy(); // runs health checks at startup; throws if anything is unhealthy
486500
```
487501

488502
`EnsureHealthy` skips MassTransit bus checks during startup (those take time to connect). The ping endpoint returns
@@ -507,9 +521,12 @@ Three-mode global switch. Requires `Pandatech.DistributedCache` to synchronize s
507521

508522
```csharp
509523
builder.AddMaintenanceMode();
510-
app.UseMaintenanceMode(); // place before UseResponseCrafter and UseCors
524+
app.UseMaintenanceMode(); // place after UseRequestLogging and before UseResponseCrafter
511525
```
512526

527+
All calls are idempotent and safe to call multiple times. Registration state is tracked via DI, so
528+
multiple `WebApplicationFactory` test hosts in the same process work correctly.
529+
513530
### Controlling maintenance mode
514531

515532
Map the built-in endpoint and protect it with your own authorization:
@@ -542,7 +559,7 @@ public class AdminService(MaintenanceState state)
542559

543560
### ValidationHelper
544561

545-
Static regex-based validators with a 50ms timeout per expression.
562+
Static regex-based validators with a 50 ms timeout per expression.
546563

547564
```csharp
548565
ValidationHelper.IsEmail("user@example.com");
@@ -685,4 +702,4 @@ is used unchanged.
685702

686703
## License
687704

688-
MIT
705+
MIT

SharedKernel.Demo/LoggingTestEndpoints.cs

Lines changed: 0 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -175,37 +175,6 @@ public void AddRoutes(IEndpointRouteBuilder app)
175175
})
176176
.WithSummary("Outbound FormUrlEncodedContent");
177177

178-
grp.MapGet("/outbound/form-as-string", async (IHttpClientFactory factory) =>
179-
{
180-
var client = factory.CreateClient("RandomApiClient");
181-
182-
// Use FormUrlEncodedContent instead of StringContent for proper form encoding
183-
var formContent = new FormUrlEncodedContent(new Dictionary<string, string>
184-
{
185-
["grant_type"] = "password",
186-
["username"] = "testuser",
187-
["password"] = "secret123",
188-
["scope"] = "openid"
189-
});
190-
191-
var response = await client.PostAsync("tests/form-urlencoded", formContent);
192-
193-
// Handle non-success responses gracefully
194-
if (!response.IsSuccessStatusCode)
195-
{
196-
var errorBody = await response.Content.ReadAsStringAsync();
197-
return Results.Json(new {
198-
success = false,
199-
statusCode = (int)response.StatusCode,
200-
error = errorBody
201-
});
202-
}
203-
204-
return Results.Ok(await response.Content.ReadFromJsonAsync<FormUrlDto>());
205-
})
206-
.WithSummary("Outbound form-urlencoded - now using FormUrlEncodedContent for proper binding");
207-
208-
209178
grp.MapGet("/outbound/multipart", async (IHttpClientFactory factory) =>
210179
{
211180
var client = factory.CreateClient("RandomApiClient");

0 commit comments

Comments
 (0)