ข้ามไปยังเนื้อหา

Configuration

เนื้อหานี้ยังไม่ได้แปลเป็นภาษาไทย แสดงเป็นภาษาอังกฤษแทน

Every CephalonEngine app reads configuration through the standard ASP.NET Core IConfiguration chain. The conventions documented here apply equally to:

  • appsettings.json / appsettings.<Environment>.json
  • Environment variables (using the __ separator)
  • Azure Key Vault, AWS Parameter Store, GCP Secret Manager
  • dotnet user-secrets (development)
  • Any custom IConfigurationProvider

The Engine:* namespace is the only key prefix CephalonEngine reads. Everything outside it is yours.

appsettings.json
{
"Engine": {
"Id": "acme-store", // required — unique identifier emitted in logs + manifest
"Deployment": {
"Id": "prod-eu-west", // optional — labels traces with deployment instance
"Topology": "SingleHost" // SingleHost | Microservice | MicroserviceSuite
},
"Composition": { "Model": "Modular" }, // Modular (only option currently)
"FeatureOrganization": "ModuleFirst", // ModuleFirst | VerticalSlice
"Transports": [ "RestApi" ], // RestApi | JsonRpc | Grpc | GraphQL | ServerSentEvents | WebSocket
"Data": {
"IdStrategy": "Sfid", // Sfid (default) | Guid | Long
"Provider": null, // null | "EntityFramework" | "Direct"
"ReadModel": { }, // optional — separate provider for read side
"WriteModel": { } // optional — separate provider for write side
},
"Identity": {
"Enabled": false, // capability gate
"Provider": null, // null | "Bearer" | "Cookie" | custom
"Authority": null, // for Bearer: IdP authority URL
"Audience": null // for Bearer: expected audience claim
},
"Tenancy": {
"Enabled": false, // capability gate
"Resolvers": [] // ["subdomain", "header", "claim", "path"]
},
"Audit": {
"Enabled": true, // default-on capability
"Provider": "EntityFramework" // null | "EntityFramework" | "InMemory"
},
"Messaging": {
"Enabled": false, // capability gate
"Provider": null, // null | "Wolverine"
"Wolverine": { /* provider-specific options */ }
},
"Observability": {
"Provider": "OpenTelemetry", // null | "OpenTelemetry" | "AzureMonitor" | "Serilog" | …
"Endpoint": "http://localhost:4317", // OTLP collector endpoint
"Headers": null, // OTLP headers (auth)
"Sampling": { "Ratio": 1.0 } // 0.0 - 1.0 trace sampling
}
},
"ConnectionStrings": {
"Default": "Host=…;Port=5432;Database=…;Username=…;Password=…"
},
"Logging": {
"LogLevel": { "Default": "Information", "Cephalon.Engine": "Information" }
}
}
KeyWhy requiredWhat happens if missing
Engine:IdEmits in every log entry, manifest, and OTLP resource attribute. Without it operators can’t correlate signals across replicas.Composition fails with EngineIdMissingException.

Everything else has a safe default — see each section below.

The standard ASP.NET Core override chain applies:

appsettings.json
↓ overridden by
appsettings.<ASPNETCORE_ENVIRONMENT>.json
↓ overridden by
User secrets (Development only)
↓ overridden by
Environment variables
↓ overridden by
Command-line args

Example pattern — connection strings:

appsettings.json
{
"ConnectionStrings": {
"Products": "" // placeholder; never commit a real connection string
}
}
appsettings.Development.json
{
"ConnectionStrings": {
"Products": "Host=localhost;Port=5432;Database=products_dev;Username=postgres;Password=postgres"
}
}
production environment variables
ASPNETCORE_ENVIRONMENT=Production
ConnectionStrings__Products=Host=prod-db;Port=5432;Database=products;Username=svc;Password=<from-vault>

ASP.NET Core’s __ separator converts the nested JSON shape into env-var names. Pattern: Engine__<Section>__<Key>.

Configuration keyEnvironment variable
Engine:IdEngine__Id
Engine:Deployment:TopologyEngine__Deployment__Topology
Engine:Data:ProviderEngine__Data__Provider
Engine:Tenancy:EnabledEngine__Tenancy__Enabled
Engine:Messaging:Wolverine:TransportEngine__Messaging__Wolverine__Transport
ConnectionStrings:DefaultConnectionStrings__Default
Logging:LogLevel:DefaultLogging__LogLevel__Default

Array values use indices:

Engine__Transports__0=RestApi
Engine__Transports__1=Grpc
Linux/macOS shells use the same syntax — just export instead of $env:. Docker Compose and Kubernetes ConfigMaps/Secrets work the same way. The __ separator is portable.

Connection strings, API keys, and IdP credentials should never be in appsettings.json. Use one of:

Terminal window
cd src/Acme.Store.Host
dotnet user-secrets init
dotnet user-secrets set "ConnectionStrings:Products" "Host=…"

Secrets are stored at %APPDATA%\Microsoft\UserSecrets\<id>\secrets.json (Windows) or ~/.microsoft/usersecrets/<id>/secrets.json (Linux/macOS). Not in your repo.

Program.cs
builder.Configuration.AddAzureKeyVault(
new Uri($"https://{builder.Configuration["KeyVault:Name"]}.vault.azure.net/"),
new DefaultAzureCredential());

Key Vault secrets named Engine--Data--Provider map to Engine:Data:Provider (Key Vault uses -- because : isn’t allowed in names).

Use the Amazon.Extensions.Configuration.SystemsManager package; parameter names map the same way (/Engine/Data/Provider).

Mount as env vars in the pod spec:

deployment.yaml
env:
- name: ConnectionStrings__Products
valueFrom:
secretKeyRef:
name: acme-store-secrets
key: products-connection

The sub-pages below document each Engine:* section in full. The most-used keys for adopters:

SectionPurposeRequired for
Engine:Id and deployment identity (covered inline above)Engine ID, deployment ID, topologyAlways
Engine:DataId strategy, provider selection, read/write splitAny data-using module
Engine:IdentityBearer/Cookie auth, JWT validation, claim mapping, scope enforcementIdentity-aware apps
Engine:TenancyResolvers (subdomain/header/claim/path), sharding, governanceMulti-tenant apps
Engine:Audit (coming soon)Audit storage and policyAudit-tracked apps
Engine:MessagingWolverine + transport (in-memory/RabbitMQ/Azure SB/SQS/Kafka), routing, DLQEvent-driven apps
Engine:Transports (covered inline above)Enabling transports per host (RestApi, Grpc, JsonRpc, …)All HTTP/gRPC apps
Engine:ObservabilityOTel provider, endpoint, sampling, logs/metrics/traces, 14 cloud adaptersAll production apps
Error handling (coming soon)ProblemDetails shape, exception mapping, validation contractAll apps

Pattern: feature-flagged capability via configuration

Section titled “Pattern: feature-flagged capability via configuration”

You want to roll out a new capability per environment without code changes.

appsettings.json (default off)
{ "Engine": { "Messaging": { "Enabled": false } } }
appsettings.Production.json (on in prod)
{ "Engine": { "Messaging": { "Enabled": true, "Provider": "Wolverine" } } }

In code, the module declares the capability conditionally:

public override ModuleDescriptor Describe() => new(
name: "Acme.Orders",
version: "1.0.0",
capabilities: messagingEnabled
? [Capability.Data, Capability.Eventing]
: [Capability.Data]);

Pattern: per-tenant data sharding via configuration

Section titled “Pattern: per-tenant data sharding via configuration”

Some tenants live on a dedicated database. Configure via a connection-string map:

appsettings.json
{
"Engine": {
"Tenancy": {
"Enabled": true,
"Resolvers": ["subdomain"],
"Sharding": {
"default": "ConnectionStrings:DefaultShard",
"acme": "ConnectionStrings:AcmeShard",
"globex": "ConnectionStrings:GlobexShard"
}
}
}
}

In the data module, resolve the connection based on the current tenant:

services.AddCephalonEntityFramework<AppDb>((sp, opts) =>
{
var tenant = sp.GetRequiredService<ITenantContext>();
var key = sp.GetRequiredService<IConfiguration>()
.GetSection($"Engine:Tenancy:Sharding:{tenant.Id}")
.Value ?? "ConnectionStrings:DefaultShard";
var conn = sp.GetRequiredService<IConfiguration>()[key];
opts.UseNpgsql(conn);
});

Pattern: configuration validation at startup

Section titled “Pattern: configuration validation at startup”

Use the standard IOptions<T> + OptionsValidator pattern:

public sealed class ProductsOptions
{
[Required]
public string ConnectionString { get; init; } = string.Empty;
[Range(1, 100)]
public int PageSize { get; init; } = 25;
}
services.AddOptions<ProductsOptions>()
.Bind(configuration.GetSection("Engine:Products"))
.ValidateDataAnnotations()
.ValidateOnStart(); // fail composition if invalid
services.Configure<ProductsOptions>(configuration.GetSection("Engine:Products"));
// Inject IOptionsMonitor<ProductsOptions> — re-fires when appsettings.json changes
Don’t hot-reload across capability boundaries. Turning Engine:Messaging:Enabled from falsetrue at runtime won’t register the Wolverine provider mid-process — composition is one-shot. Restart for capability changes; only “tunable” options should be hot-reloaded.

Configuration validation in cephalon doctor

Section titled “Configuration validation in cephalon doctor”

cephalon doctor --config ./appsettings.json (planned for the 0.2.0-preview release) will pre-validate the Engine:* schema before you run the host. Today, the same validation runs during composition — composition fails fast with a clear error.

  • Connection strings live under ConnectionStrings:*, not Engine:Data:ConnectionString. This follows the ASP.NET Core convention; many libraries (EF Core, dependency-health probes) look at ConnectionStrings:* automatically.
  • The Engine:* schema is versioned with the engine. Adding new options is additive within a preview minor; renames or removals require a major bump. See Migration → Breaking changes.
  • Configuration is one-shot for capabilities. Flipping Enabled: true → false for Identity, Tenancy, Messaging, etc. requires a host restart (the capability registry is built during composition).
  • Per-module configuration uses Engine:<ModuleName>:* by convention so the runtime can introspect per-module settings. Modules that read from arbitrary IConfiguration paths work but won’t show up in module-introspection tooling.

Practical guidance for keeping Engine:* configuration sane across environments.

  • Use appsettings.<Env>.json for environment-specific defaults, not absolute values. Don’t repeat what’s already in appsettings.json.
  • Use environment variables for secrets + ops toggles only. Code-level defaults stay in JSON so a developer can read them.
  • Never commit appsettings.Production.json with real values. It exists to document the shape and provide placeholders; production fills in via env vars / Key Vault.
  • Treat Engine:* as read-only after composition. Anything tunable at runtime should live under its own Engine:<Feature>:* subsection so it’s clear what’s safe to change.
  • Validate at startup, not on first request. IOptions<T> with .ValidateOnStart() makes config errors crash the host visibly, instead of throwing 500s once traffic arrives.
  • Use IOptionsMonitor<T> only for genuinely tunable values (timeouts, page sizes, feature flags). Boot-time options should be IOptions<T> so changes only take effect on restart — clearer mental model.
  • Never echo a connection string in logs, even at Debug level. Use [Sensitive] attributes if you bind config to POCOs you ever serialise.
  • Treat env vars as plaintext. They live in /proc/*/environ and process listings. Use Key Vault / Parameter Store for the actual secret; env vars carry the reference, not the value.
  • Rotate connection strings as a deploy event, not a config-reload event. Hot-reloading a connection string mid-request can corrupt connection pools.
ThingConventionExample
Connection-string keyConnectionStrings:<Module> (PascalCase)ConnectionStrings:Products
Engine ID<product>-<role> (kebab, no env)acme-store
Deployment ID<env>-<region> (kebab)prod-eu-west-1
Custom config sectionEngine:<ModuleName>:*Engine:Products:CatalogTtl
OTLP endpointfull URL incl. schemehttps://otlp.example/v1/traces
  • Env var case matters on Linux. Engine__Id is not the same as engine__id. Match the JSON path exactly.
  • __ separator only, not : in env vars. : isn’t a legal env-var character on many shells.
  • Arrays via env vars need indices. Engine__Transports__0=RestApi then Engine__Transports__1=Grpc — not Engine__Transports=RestApi,Grpc.
  • Empty string ≠ unset. Engine__Identity__Authority= (empty) is treated as “value is empty string”, not “missing”. Configure validators accordingly.
appsettings.json ← shape + safe defaults (committed)
appsettings.Development.json ← dev-loop convenience (committed)
appsettings.<Env>.json ← env-specific config (committed, no secrets)
secrets via env vars / Key Vault ← per-deploy
  • /engine/snapshot returns the resolved configuration as the engine sees it. Useful for “is my env var being read?”.
  • dotnet user-secrets list in dev shows what your local user-secrets actually contain.
  • In Azure App Service / Container Apps, the portal shows env vars as Application Settings. Match them against /engine/snapshot to verify wiring.
Don’tDo
Commit appsettings.Production.json with real connection stringsUse env vars or Key Vault references; appsettings.Production.json has placeholders
Read IConfiguration directly inside handlersBind to a strongly-typed POCO and inject IOptions<T>
Have Engine:* keys outside the documented schemaUse a custom namespace (Acme:* or Engine:<ModuleName>:*); engine-blessed keys stay typed
Toggle capabilities at runtime via hot-reloadRestart the host — capability registration is one-shot
Treat appsettings.json as the source of truth in CI testsPass overrides via env vars in CI so tests reflect production wiring
  • Engine:Data — every Id, Provider, Outbox, Inbox, Migrations, and Sfid option, with 4 end-to-end scenarios.
  • Engine:Identity — Bearer + Cookie schemes, JWT validation, claim mapping, scope policy.
  • Engine:Tenancy — resolver pipeline, sharding strategy, per-tenant data isolation.
  • Engine:Messaging — Wolverine transports, routing, retries, dead-letter, scheduled delivery.
  • Engine:Observability — OTel collector targets, sampling, logs/metrics/traces, 14 cloud-vendor presets.