Skip to content
bitzorcas
EN

Reference

Web / API pipeline building block

ASP.NET Core Minimal API pipeline — Mediator integration, Mapster object mapping, System.Text.Json source-generated serialization, Scalar OpenAPI, ProblemDetails error handling, and rate limiting.

Last updated

The Web building block configures the ASP.NET Core API pipeline for BitzOrcas. It lives in the API Host’s composition root (BitzOrcas.Api.Composition) and follows a 4-stage registration pattern.

4-stage DI registration

// Stage 1: Core runtime (no DB dependency)
builder.Services.AddBitzOrcasCoreRuntime(configuration);
// Stage 2: Persistence adapters (requires ConnectionStrings + RabbitMq)
builder.Services.AddBitzOrcasPersistenceAdapters(configuration);
// Stage 3: Authentication (JWT / HMAC / API Key)
builder.Services.AddBitzOrcasAuthentication(configuration, environment);
// Stage 4: API pipeline (Mediator + JSON + OpenAPI + RateLimiter)
builder.Services.AddBitzOrcasApiPipeline(configuration);

Middleware pipeline order

ExceptionHandler → CorrelationId → Authentication → DelegationToken
→ TenantResolution → RequestAudit → Authorization → RateLimiter → Endpoints
MiddlewarePurpose
ExceptionHandlerGlobal RFC 9457 ProblemDetails handler (outermost)
CorrelationIdMiddlewareGenerates/propagates X-Correlation-Id header
AuthenticationASP.NET Core built-in (JWT / HMAC / API Key)
DelegationTokenMiddlewareValidates delegation (impersonation) tokens
TenantResolutionMiddlewareResolves ITenantContext from request
RequestAuditMiddlewareEnqueues audit entries for background processing
AuthorizationASP.NET Core policy-based authorization
RateLimiterTenant + caller-partitioned rate limiting

Mediator integration

BitzOrcas uses Mediator with source-generation — no reflection at runtime:

services.AddMediator(options =>
{
options.Assemblies = [typeof(PingQuery).Assembly];
options.ServiceLifetime = ServiceLifetime.Scoped;
options.PipelineBehaviors = [
typeof(LoggingPipelineBehavior<,>),
typeof(AuthorizationPipelineBehavior<,>),
typeof(ValidationPipelineBehavior<,>),
typeof(IdempotencyPipelineBehavior<,>),
typeof(TransactionPipelineBehavior<,>),
typeof(ActivityAuditPipelineBehavior<,>),
];
});

Mapster object mapping

Object-to-object mapping via Mapster (reflection-based; planned migration to Mapperly for AOT compatibility per ADR 0006):

// Scan all TypeAdapter configurations from Application assembly
TypeAdapterConfig.GlobalSettings.Scan(typeof(MappingConfig).Assembly);

System.Text.Json (AOT-safe)

All API serialization uses source-generated JSON contexts — compatible with Native AOT trimming:

services.ConfigureHttpJsonOptions(options =>
{
options.SerializerOptions.TypeInfoResolverChain.Insert(0,
ApiJsonSerializerContext.Default);
});

OpenAPI + Scalar UI

BitzOrcas uses the built-in .NET OpenAPI support with Scalar as the API documentation UI:

services.AddOpenApi(); // .NET built-in
// Scalar UI served at /scalar

Rate limiting

Three partitioned rate-limiting policies with per-tenant + per-caller keys:

PolicyAlgorithmUse case
userPolicyToken BucketGeneral REST API endpoints
sensitivePolicySliding WindowLogin, OTP, password reset
fixedPolicyFixed WindowInternal/low-frequency services

Partition key format: {tenant_id}:{user_id|client_id} — prevents application callers (without user_id) from sharing the same anonymous bucket.

ProblemDetails error handling

All exceptions are converted to RFC 9457 ProblemDetails by GlobalExceptionHandler:

{
"type": "https://docs.bitzsoft.com/problems/not-found",
"title": "Not Found",
"status": 404,
"detail": "Note with ID 'abc' not found.",
"instance": "/api/notes/abc",
"traceId": "00-abc123-...",
"correlationId": "corr-456",
"requestId": "req-789",
"errorCode": "Note.NotFound"
}

Result<T> to HTTP translation

ResultExtensions provides extension methods for converting Result<T> to HTTP responses in Minimal API endpoints:

app.MapPost("/api/notes", async (
CreateNoteCommand cmd, IMediator mediator) =>
await mediator.Send(cmd) switch
{
{ IsSuccess: true } result => Results.Created(
$"/api/notes/{result.Value.Id}", result.Value),
var result => result.ToProblemDetails()
});