BitzOrcas.Modern’s composition root is src/Hosts/BitzOrcas.Api/Program.cs. It’s deliberately explicit — every registration call is visible, no auto-discovery magic, no convention-based scanning that hides wiring failures. Four registration stages, each with a clear responsibility:
var builder = WebApplication.CreateBuilder(args);
// 1. Cross-host defaults (OTel + Health Checks)builder.AddServiceDefaults();
// 2. Core runtime (context, clock, audit, tenancy, placeholder adapters)builder.Services.AddBitzOrcasCoreRuntime();
// 3. Persistence adapters (SqlSugar/EfCore — conditional on config)builder.Services.AddBitzOrcasPersistenceAdapters();
// 4. Authentication (JWT/HMAC/ApiKey)builder.Services.AddBitzOrcasAuthentication();
// 5. API pipeline (Mediator + Mapster + JSON + OpenAPI + RateLimiter)builder.Services.AddBitzOrcasApiPipeline();
var app = builder.Build();app.MapBitzOrcasEndpoints();await app.RunAsync();The four registration stages
1. AddServiceDefaults() — cross-host defaults
Registered in BitzOrcas.ServiceDefaults. This is the only place OTel is registered — individual hosts must not call AddOpenTelemetry() independently. Registers:
- Traces: ASP.NET Core + HttpClient + custom BitzOrcas ActivitySources
- Metrics: ASP.NET Core + HttpClient + Runtime
- Exporter: OTLP (gRPC) to
OTEL_EXPORTER_OTLP_ENDPOINT(defaultlocalhost:4317) - Resource attributes:
service.name,service.version,deployment.environment - Health Checks:
selfcheck withLivenessTag
2. AddBitzOrcasCoreRuntime() — cross-cutting context
Registers the foundational services that every host needs:
ISystemClock/FixedClock— deterministic timeICurrentUser/AnonymousCurrentUser— unified caller model (Api uses JWT-populated, JobHost uses anonymous)ITenantContext/ITenantResolver— 8-level tenant resolution chainIUnitOfWork/NullUnitOfWork— placeholder, overridden by persistence adapters- 7 audit sinks (all
Null*AuditSinkdefaults) — overridden by production audit IFileStorage/LocalFileStorage— local filesystem defaultINotificationPublisher/NullNotificationPublisher— placeholderIAppCache/FusionCacheAppCache— cachingICorrelationContext— correlation ID propagation- Mapster global configuration scanning
3. AddBitzOrcasPersistenceAdapters() — ORM adapter override
Conditional registration: only activates when both ConnectionStrings:Default AND RabbitMq:Host are configured. When active, it overrides the Null/InMemory defaults with production adapters:
| Service | Default (CoreRuntime) | Production Override |
|---|---|---|
IUnitOfWork | NullUnitOfWork | SqlSugarUnitOfWork or CapSqlSugarUnitOfWork |
IAuditLogStore | UnavailableAuditLogStore (fail-loud) | SqlSugarAuditLogStore |
IFileAssetRepository | InMemoryFileAssetRepository | SqlSugarFileAssetRepository |
ITicketRepository | InMemoryTicketRepository | SqlSugarTicketRepository |
ICatalogRepository | InMemoryCatalogRepository | SqlSugarCatalogRepository |
| … (all platform ports) | InMemory* / Null* | SqlSugar* |
4. AddBitzOrcasAuthentication() — multi-scheme auth
Three mutually exclusive authentication schemes:
| Scheme | Handler | Purpose |
|---|---|---|
| JWT Bearer | Microsoft.AspNetCore.Authentication.JwtBearer | User authentication |
| HMAC | HmacAuthenticationHandler | External application auth (nonce anti-replay) |
| ApiKey | ApiKeyAuthenticationHandler | API key auth (SHA-256 hashed) |
5. AddBitzOrcasApiPipeline() — Mediator + API surface
- Mediator 3 source-generated dispatch
- Mapster object mapping
- JSON serialization options
- OpenAPI + Scalar UI
- ProblemDetails (RFC 9457)
- Rate limiter (3 policies: TokenBucket, SlidingWindow, FixedWindow)
The middleware pipeline
Explicitly ordered in Program.cs, never extracted into a helper:
UseExceptionHandler (ProblemDetails) → CorrelationIdMiddleware → UseAuthentication → DelegationTokenMiddleware → TenantResolutionMiddleware → RequestAuditMiddleware → UseAuthorization → UseRateLimiter → EndpointsOrdering rules:
- CorrelationId before everything. Every log line, trace span, and audit row carries the correlation ID.
- Authentication before tenant resolution. Tenant resolution needs claims from authenticated users.
- Tenant resolution before request audit. The audit envelope needs the tenant context.
- Rate limiter after authorization. Unauthenticated traffic shouldn’t consume rate limit tokens.
Schema initialization modes
The API supports CLI-driven schema initialization — triggered before the web pipeline starts:
| Flag | Behavior |
|---|---|
--init-schema | Create tables (CodeFirst), create audit sharded tables, seed 14 platform system tables |
--seed-demo | Same as --init-schema |
--seed-only | Seed data only (no schema creation) |
--no-seed | Schema creation only (no seed data) |
When any init flag is present, the host performs schema init and exits without starting the web pipeline. This is how the first-run experience works.
The two host profiles
| Host | Purpose | Auth | Tenancy | Trim |
|---|---|---|---|---|
| BitzOrcas.Api | HTTP API | JWT + HMAC + ApiKey | 8-level chain | Yes (SqlSugar path) |
| BitzOrcas.JobHost | Background jobs | None | None (AnonymousCurrentUser) | No (Quartz) |
The JobHost uses a minimal Core Runtime — clock, anonymous user, audit, storage. No authentication, no tenant resolution, no rate limiting. ICurrentUser resolves to AnonymousCurrentUser with CallerType.System.
Related
- Modular monolith — the module governance system
- Vertical Slice — how features are structured
- ORM Adapter Pattern — visual adapter switching diagram
- Mediator Pipeline — visual pipeline flow