Pandatech.SharedKernel
2.2.6
dotnet add package Pandatech.SharedKernel --version 2.2.6
NuGet\Install-Package Pandatech.SharedKernel -Version 2.2.6
<PackageReference Include="Pandatech.SharedKernel" Version="2.2.6" />
<PackageVersion Include="Pandatech.SharedKernel" Version="2.2.6" />
<PackageReference Include="Pandatech.SharedKernel" />
paket add Pandatech.SharedKernel --version 2.2.6
#r "nuget: Pandatech.SharedKernel, 2.2.6"
#:package Pandatech.SharedKernel@2.2.6
#addin nuget:?package=Pandatech.SharedKernel&version=2.2.6
#tool nuget:?package=Pandatech.SharedKernel&version=2.2.6
Pandatech.SharedKernel
Opinionated ASP.NET Core 10 infrastructure library for PandaTech projects. It consolidates logging, OpenAPI, validation, CORS, SignalR, telemetry, health checks, maintenance mode, and resilience into a single package so every service starts from the same baseline.
The package is publicly available but is designed for internal use. If you want to adopt it, fork the repository and customize it to your own conventions.
Requires .NET 10.0. Uses C# 14 extension members throughout and cannot be downgraded to earlier TFMs.
Table of Contents
- Installation
- Quick Start
- Assembly Registry
- OpenAPI
- Logging
- MediatR and FluentValidation
- CORS
- Resilience Pipelines
- Controllers
- SignalR
- OpenTelemetry
- Health Checks
- Maintenance Mode
- Utilities
Installation
dotnet add package Pandatech.SharedKernel
Quick Start
A complete Program.cs using every major feature:
var builder = WebApplication.CreateBuilder(args);
builder.LogStartAttempt();
AssemblyRegistry.Add(typeof(Program).Assembly);
builder
.ConfigureWithPandaVault()
.AddSerilog(LogBackend.ElasticSearch)
.AddResponseCrafter(NamingConvention.ToSnakeCase)
.AddOpenApi()
.AddMaintenanceMode()
.AddOpenTelemetry()
.AddMinimalApis(AssemblyRegistry.ToArray())
.AddControllers(AssemblyRegistry.ToArray())
.AddMediatrWithBehaviors(AssemblyRegistry.ToArray())
.AddResilienceDefaultPipeline()
.AddDistributedSignalR("localhost:6379", "app_name")
.AddCors()
.AddOutboundLoggingHandler()
.AddHealthChecks();
var app = builder.Build();
app
.UseRequestLogging()
.UseMaintenanceMode()
.UseResponseCrafter()
.UseCors()
.MapMinimalApis()
.MapHealthCheckEndpoints()
.MapPrometheusExporterEndpoints()
.EnsureHealthy()
.ClearAssemblyRegistry()
.UseOpenApi()
.MapControllers();
app.MapMaintenanceEndpoint();
app.LogStartSuccess();
app.Run();
Assembly Registry
AssemblyRegistry is a thread-safe static collection used to pass your project's assemblies from the builder phase to
the app phase without repeating typeof(Program).Assembly everywhere.
// Add once at startup
AssemblyRegistry.Add(typeof(Program).Assembly);
// Pass to any method that needs to scan for handlers, validators, or endpoints
builder.AddMediatrWithBehaviors(AssemblyRegistry.ToArray());
// Clear after app is built to free memory — the scanning is complete
app.ClearAssemblyRegistry();
OpenAPI
Wraps Microsoft.AspNetCore.OpenApi with SwaggerUI and Scalar, generating OpenAPI 3.1 specs. Supports multiple API
documents, custom security schemes, and enum string descriptions (including nullable enums).
Registration
builder.AddOpenApi();
var app = builder.Build();
app.UseOpenApi();
Custom schema transformers can be added via the options callback:
builder.AddOpenApi(options =>
{
options.AddSchemaTransformer<MyCustomTransformer>();
});
Configuration
{
"OpenApi": {
"DisabledEnvironments": [
"Production"
],
"SecuritySchemes": [
{
"HeaderName": "Authorization",
"Description": "Bearer access token."
},
{
"HeaderName": "Client-Type",
"Description": "Identifies the client type, e.g. '2'."
}
],
"Documents": [
{
"Title": "Admin Panel",
"Description": "Internal administrative endpoints.",
"GroupName": "admin-v1",
"Version": "v1",
"ForExternalUse": false
},
{
"Title": "Integration",
"Description": "Public integration endpoints.",
"GroupName": "integration-v1",
"Version": "v1",
"ForExternalUse": true
}
],
"Contact": {
"Name": "Pandatech",
"Url": "https://pandatech.it",
"Email": "info@pandatech.it"
}
}
}
UI URLs
| UI | URL | Notes |
|---|---|---|
| Swagger | /swagger |
All documents |
| Swagger | /swagger/integration-v1 |
External documents only (ForExternalUse) |
| Scalar | /scalar/admin-v1 |
One URL per document |
| Scalar | /scalar/integration-v1 |
One URL per document |
ForExternalUse: true creates a dedicated Swagger URL you can share with external partners while keeping internal
documents private. All documents still appear on the main /swagger page.
Logging
Wraps Serilog with structured output, request/response logging middleware, outbound HTTP logging, and automatic log cleanup.
Registration
// Synchronous sinks — safe for up to ~1000 req/s per pod
builder.AddSerilog(LogBackend.Loki);
// Asynchronous sinks — better throughput, small risk of losing logs on hard crash
builder.AddSerilog(
logBackend: LogBackend.ElasticSearch,
logAdditionalProperties: new Dictionary<string, string>
{
["ServiceName"] = "my-service"
},
daysToRetain: 14,
asyncSinks: true
);
Log Backends
| Value | Output format |
|---|---|
None |
Console only, no file output |
ElasticSearch |
ECS JSON to file (forward with Filebeat/Logstash) |
Loki |
Loki JSON to file (forward with Promtail) |
CompactJson |
Compact JSON to file |
Environment behavior
| Environment | Console | File |
|---|---|---|
| Local | Yes | No |
| Development / QA | Yes | Yes |
| Production | No | Yes |
Configuration
{
"Serilog": {
"MinimumLevel": {
"Default": "Information",
"Override": {
"Microsoft": "Information",
"System": "Information"
}
}
},
"RepositoryName": "my-service",
"ConnectionStrings": {
"PersistentStorage": "/persistence"
}
}
Log files are stored under {PersistentStorage}/{RepositoryName}/{env}/logs/. The LogCleanupHostedService runs
every 12 hours and deletes files older than daysToRetain.
Request logging middleware
app.UseRequestLogging(); // logs method, path, status code, elapsed ms, redacted headers/body
Paths under /swagger, /openapi, /above-board, and /favicon.ico are silently skipped. Bodies over 16 KB are
omitted.
Redaction is key-based: header names and JSON property names containing sensitive keywords (auth, token,
pass, secret, cookie, pan, cvv, ssn, tin, iban, etc.) are automatically replaced with [REDACTED].
Values are never inspected — only the property/header name determines whether redaction applies.
Outbound logging
Captures outbound HttpClient requests with the same redaction rules:
// Register the handler
builder.AddOutboundLoggingHandler();
// Attach to a specific HttpClient
builder.Services
.AddHttpClient("MyClient", c => c.BaseAddress = new Uri("https://example.com"))
.AddOutboundLoggingHandler();
Startup logging
builder.LogStartAttempt(); // prints banner to console at startup
app.LogStartSuccess(); // prints success banner with elapsed init time
MediatR and FluentValidation
Registers MediatR with a validation pipeline behavior that runs all FluentValidation validators before the handler.
Validation failures throw BadRequestException from Pandatech.ResponseCrafter.
Note: MediatR is pinned to version 12.5.0 — the last MIT-licensed release.
Registration
builder.AddMediatrWithBehaviors(AssemblyRegistry.ToArray());
CQRS interfaces
// Commands
public record CreateUserCommand(string Email) : ICommand<UserDto>;
public class CreateUserHandler : ICommandHandler<CreateUserCommand, UserDto> { ... }
// Queries
public record GetUserQuery(Guid Id) : IQuery<UserDto>;
public class GetUserHandler : IQueryHandler<GetUserQuery, UserDto> { ... }
FluentValidation extensions
String validators
RuleFor(x => x.Email).IsEmail();
RuleFor(x => x.Phone).IsPhoneNumber(); // Panda format: (374)91123456
RuleFor(x => x.Contact).IsEmailOrPhoneNumber();
RuleFor(x => x.Payload).IsValidJson();
RuleFor(x => x.Content).IsXssSanitized();
RuleFor(x => x.Card).IsCreditCardNumber();
Single file (IFormFile)
RuleFor(x => x.Avatar)
.HasMaxSizeMb(6)
.ExtensionIn(".jpg", ".jpeg", ".png");
File collection (IFormFileCollection)
RuleFor(x => x.Docs)
.MaxCount(10)
.EachHasMaxSizeMb(10)
.EachExtensionIn(CommonFileSets.Documents)
.TotalSizeMaxMb(50);
File presets
CommonFileSets.Images // .jpg .jpeg .png .webp .heic .heif .svg .avif
CommonFileSets.Documents // .pdf .txt .csv .json .xml .yaml .md .docx .xlsx .pptx ...
CommonFileSets.ImagesAndAnimations // Images + .gif
CommonFileSets.ImagesAndDocuments // Images + Documents
CommonFileSets.ImportFiles // .csv .xlsx
CORS
Development and non-production environments allow all origins. Production restricts to the configured list and
automatically adds both www and non-www variants.
Registration
builder.AddCors();
app.UseCors();
Production configuration
{
"Security": {
"AllowedCorsOrigins": "https://example.com,https://api.example.com"
}
}
The list accepts comma- or semicolon-separated URLs. Invalid entries are logged and filtered out.
Resilience Pipelines
Built on Polly via Microsoft.Extensions.Http.Resilience. Two pipeline variants share the same configuration constants
from a single source of truth:
- General pipeline — registered globally via
AddResilienceDefaultPipeline()on the builder, or used manually viaResiliencePipelineProvider<string> - HTTP pipeline — attached per-client via
AddResilienceDefaultPipeline()on anIHttpClientBuilder, with additionalRetry-Afterheader support for 429 responses
Options
1. Global — applies to all registered HttpClients:
builder.AddResilienceDefaultPipeline();
2. Per-client:
builder.Services.AddHttpClient("MyClient")
.AddResilienceDefaultPipeline();
3. Manual — for wrapping arbitrary async calls:
public class MyService(ResiliencePipelineProvider<string> provider)
{
public async Task CallAsync()
{
var pipeline = provider.GetDefaultPipeline();
var result = await pipeline.ExecuteAsync(() => _client.GetAsync("/endpoint"));
}
}
Default pipeline policies
| Policy | Configuration |
|---|---|
| Retry (429) | 5 retries, exponential backoff with jitter, respects Retry-After header |
| Retry (5xx/408) | 7 retries, exponential backoff from 800 ms with jitter |
| Circuit breaker | Opens at 50% failure rate over 30 s (min 200 requests), 45 s break duration |
| Timeout | 8 seconds per attempt |
The circuit breaker only trips on transient failures (HttpRequestException, TaskCanceledException) and non-success
HTTP status codes. Programming errors like ArgumentException will not open the circuit.
Controllers
For applications using classic MVC controllers alongside minimal APIs:
builder.AddControllers(AssemblyRegistry.ToArray());
app.MapControllers();
Controller and action names are automatically kebab-cased (UserProfile → user-profile).
SignalR
Local SignalR (single instance):
builder.AddSignalR();
Distributed SignalR backed by Redis (multi-instance):
builder.AddDistributedSignalR("localhost:6379", "app_name");
Both variants include:
SignalRLoggingHubFilter— logs hub method calls with redacted arguments and elapsed timeSignalRExceptionFilter— fromPandatech.ResponseCrafter, standardizes error responses- MessagePack protocol for compact binary serialization
OpenTelemetry
builder.AddOpenTelemetry();
app.MapPrometheusExporterEndpoints();
What is included
- ASP.NET Core metrics and traces
- HttpClient metrics and traces
- Entity Framework Core traces
- Runtime metrics
- Prometheus scraping endpoint at
/above-board/prometheus - Health metrics at
/above-board/prometheus/health
OTLP export
Set the following in your environment config or as an environment variable to enable OTLP export:
{
"OTEL_EXPORTER_OTLP_ENDPOINT": "http://localhost:4317"
}
Health Checks
builder.AddHealthChecks();
app.MapHealthCheckEndpoints(); // registers /above-board/ping and /above-board/health
app.EnsureHealthy(); // runs health checks at startup; throws if anything is unhealthy
EnsureHealthy skips MassTransit bus checks during startup (those take time to connect). The ping endpoint returns
"pong" as plain text. The health endpoint returns the full AspNetCore.HealthChecks.UI JSON format.
Additional health check registrations follow the standard builder.Services.AddHealthChecks().Add...() pattern — the
library does not wrap those.
Maintenance Mode
Three-mode global switch. Requires Pandatech.DistributedCache to synchronize state across instances.
| Mode | Effect |
|---|---|
Disabled |
Normal operation |
EnabledForClients |
All routes blocked except /api/admin/* and /hub/admin/* |
EnabledForAll |
All routes blocked except /above-board/* and OPTIONS preflight |
Registration
builder.AddMaintenanceMode();
app.UseMaintenanceMode(); // place after UseRequestLogging and before UseResponseCrafter
All calls are idempotent and safe to call multiple times. Registration state is tracked via DI, so
multiple WebApplicationFactory test hosts in the same process work correctly.
Controlling maintenance mode
Map the built-in endpoint and protect it with your own authorization:
app.MapMaintenanceEndpoint();
// PUT /above-board/maintenance body: { "mode": 1 }
Or protect with a shared secret query parameter (useful before auth is in place):
app.MapMaintenanceEndpoint(querySecret: "my-secret");
// PUT /above-board/maintenance?secret=my-secret
Programmatic control from application code:
public class AdminService(MaintenanceState state)
{
public Task EnableMaintenanceAsync(CancellationToken ct)
=> state.SetModeAsync(MaintenanceMode.EnabledForClients, ct);
}
Utilities
ValidationHelper
Static regex-based validators with a 50 ms timeout per expression.
ValidationHelper.IsEmail("user@example.com");
ValidationHelper.IsUri("https://example.com", allowNonSecure: false);
ValidationHelper.IsGuid("12345678-1234-1234-1234-123456789012");
ValidationHelper.IsPandaFormattedPhoneNumber("(374)91123456");
ValidationHelper.IsArmeniaSocialSecurityNumber("1234567890");
ValidationHelper.IsArmeniaIdCard("123456789");
ValidationHelper.IsArmeniaPassportNumber("AB1234567");
ValidationHelper.IsArmeniaTaxCode("12345678");
ValidationHelper.IsArmeniaStateRegistryNumber("123.456.78901");
ValidationHelper.IsIPv4("192.168.1.1");
ValidationHelper.IsIPv6("2001:db8::1");
ValidationHelper.IsIpAddress("192.168.1.1");
ValidationHelper.IsJson("{\"key\":\"value\"}");
ValidationHelper.IsCreditCardNumber("4111111111111111");
ValidationHelper.IsUsSocialSecurityNumber("123-45-6789");
ValidationHelper.IsUsername("user123");
LanguageIsoCodeHelper
LanguageIsoCodeHelper.IsValidLanguageCode("hy-AM"); // true
LanguageIsoCodeHelper.GetName("hy-AM"); // "Armenian (Armenia)"
LanguageIsoCodeHelper.GetCode("Armenian (Armenia)"); // "hy-AM"
Covers 170+ language-region combinations. The lookup table is initialized once at startup.
PhoneUtil
Normalizes Armenian phone numbers to +374XXXXXXXX format from a variety of input formats:
PhoneUtil.TryFormatArmenianMsisdn("(374)91123456", out var formatted); // "+37491123456"
PhoneUtil.TryFormatArmenianMsisdn("+374 91 12 34 56", out var formatted); // "+37491123456"
PhoneUtil.TryFormatArmenianMsisdn("091123456", out var formatted); // "+37491123456"
Returns false and the original input if the number cannot be parsed as an Armenian MSISDN.
UrlBuilder
var url = UrlBuilder.Create("https://api.example.com/users")
.AddParameter("page", "1")
.AddParameter("size", "20")
.Build();
// https://api.example.com/users?page=1&size=20
TimeZone extensions
// Set once at startup from appsettings DefaultTimeZone
builder.MapDefaultTimeZone();
// Convert any DateTime to the configured zone
var local = someUtcDateTime.ToDefaultTimeZone();
IHostEnvironment extensions
env.IsLocal();
env.IsQa();
env.IsLocalOrDevelopment();
env.IsLocalOrDevelopmentOrQa();
env.GetShortEnvironmentName(); // "local" | "dev" | "qa" | "staging" | ""
HttpContext extensions
// Mark a response as private (adds X-Private-Endpoint: 1 header)
context.MarkAsPrivateEndpoint();
Collection extensions
// IEnumerable / IQueryable
var filtered = items.WhereIf(condition, x => x.IsActive);
// In operator
if (status.In(Status.Active, Status.Pending)) { ... }
Dictionary extensions (zero-allocation via CollectionsMarshal)
dict.GetOrAdd(key, defaultValue);
dict.TryUpdate(key, newValue);
JsonConverters
| Converter | Behavior |
|---|---|
EnumConverterFactory |
Accepts enum by name or integer; serializes as name string |
CustomDateOnlyConverter |
Parses and writes DateOnly in dd-MM-yyyy format |
Register via JsonSerializerOptions.Converters or your ResponseCrafter setup.
MethodTimingStatistics
Development-only benchmarking helper. Not for production use (marked with #warning).
var ts = Stopwatch.GetTimestamp();
DoWork();
MethodTimingStatistics.RecordExecution("DoWork", ts);
MethodTimingStatistics.LogAll(logger);
PandaVault
builder.ConfigureWithPandaVault();
Loads secrets from PandaVault on all non-Local environments. On Local, the call is a no-op so local appsettings.json
is used unchanged.
Related Packages
| Package | Purpose |
|---|---|
Pandatech.ResponseCrafter |
Consistent API error responses |
Pandatech.DistributedCache |
Redis-backed hybrid cache (required for maintenance mode) |
Pandatech.Crypto |
Cryptographic utilities |
Pandatech.FluentMinimalApiMapper |
Minimal API endpoint mapping |
License
MIT
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- AspNetCore.HealthChecks.Prometheus.Metrics (>= 9.0.0)
- AspNetCore.HealthChecks.UI.Client (>= 9.0.0)
- Elastic.CommonSchema.Serilog (>= 9.0.0)
- FluentDateTime (>= 3.0.0)
- FluentValidation.DependencyInjectionExtensions (>= 12.1.1)
- HtmlSanitizer (>= 9.0.892)
- MediatR (= 12.5.0)
- Microsoft.AspNetCore.OpenApi (>= 10.0.3)
- Microsoft.AspNetCore.SignalR.Client (>= 10.0.3)
- Microsoft.AspNetCore.SignalR.Protocols.MessagePack (>= 10.0.3)
- Microsoft.AspNetCore.SignalR.StackExchangeRedis (>= 10.0.3)
- Microsoft.Extensions.Http.Resilience (>= 10.4.0)
- OpenTelemetry.Exporter.OpenTelemetryProtocol (>= 1.15.1)
- OpenTelemetry.Exporter.Prometheus.AspNetCore (>= 1.15.0-beta.1)
- OpenTelemetry.Extensions.Hosting (>= 1.15.1)
- OpenTelemetry.Instrumentation.AspNetCore (>= 1.15.1)
- OpenTelemetry.Instrumentation.EntityFrameworkCore (>= 1.15.0-beta.1)
- OpenTelemetry.Instrumentation.Http (>= 1.15.0)
- OpenTelemetry.Instrumentation.Runtime (>= 1.15.0)
- Pandatech.CommissionCalculator (>= 6.0.0)
- Pandatech.Crypto (>= 8.0.0)
- Pandatech.DistributedCache (>= 6.0.0)
- PandaTech.FileExporter (>= 7.0.0)
- PandaTech.FluentImporter (>= 5.0.0)
- Pandatech.FluentMinimalApiMapper (>= 4.0.0)
- Pandatech.PandaVaultClient (>= 6.0.0)
- Pandatech.ResponseCrafter (>= 7.0.0)
- Scalar.AspNetCore (>= 2.13.18)
- Serilog.AspNetCore (>= 10.0.0)
- Serilog.Sinks.Async (>= 2.1.0)
- Serilog.Sinks.Grafana.Loki (>= 8.3.2)
- Swashbuckle.AspNetCore.SwaggerUI (>= 10.1.7)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 2.2.6 | 42 | 4/2/2026 |
| 2.2.5 | 49 | 3/30/2026 |
| 2.2.4 | 59 | 3/10/2026 |
| 2.2.3 | 49 | 3/9/2026 |
| 2.2.2 | 49 | 3/5/2026 |
| 2.2.1 | 46 | 3/4/2026 |
| 2.2.0 | 74 | 2/28/2026 |
| 2.1.4 | 65 | 2/26/2026 |
| 2.1.3 | 72 | 2/10/2026 |
| 2.1.2 | 68 | 1/30/2026 |
| 2.1.1 | 67 | 1/27/2026 |
| 2.1.0 | 63 | 1/27/2026 |
| 2.0.0 | 70 | 12/29/2025 |
| 1.9.0 | 435 | 12/9/2025 |
| 1.8.10 | 158 | 12/5/2025 |
| 1.8.9 | 184 | 12/4/2025 |
| 1.8.8 | 168 | 12/4/2025 |
| 1.8.7 | 653 | 12/1/2025 |
| 1.8.6 | 197 | 10/30/2025 |
| 1.8.5 | 179 | 10/30/2025 |
Upgrade to OpenAPI 3.1, fix nullable enum schema descriptions, scope circuit breaker to transient exceptions only, consolidate resilience config into single source of truth, switch to DI-based idempotency guards, key-based-only log redaction