Instruction file imported from Avanade/CoreEx (
.github/instructions/coreex-host-setup.instructions.md). Copyright stays with the author.
Host Setup Conventions (Program.cs)
The host is a composition root only — no business logic. There are three host types in a CoreEx solution depending on the capabilities required. Each follows the same opening skeleton, then diverges based on its responsibilities.
Split vs. consolidate: Api/Relay/Subscribe as separate processes is a workload-isolation convention, not a technical requirement — see Hosts Layer Guide § Choosing a Host Topology before assuming a small/low-traffic solution needs all three.
Related skill: to scaffold a solution or an additional host (Api / Subscribe / Relay), invoke the
coreex-scaffoldskill. This file holds the invariants that must hold on any edit to a hostProgram.cs; the skill drives the step-by-step creation procedure. (The per-host "Scaffolding an … host" blocks below stay here — they carry the Feature Configuration recover/record guardrails that must be honoured whenever a host is added.)
Further Reading: Hosts Layer Guide · Layer Dependencies · Pattern Catalog (docs-sync cache; after
/coreex-docs-sync). Source: samples/docs/hosts-layer.md · samples/docs/layers.md · samples/docs/patterns.md.
Scaffolding an API host
An API host is not part of the base coreex solution — it is added on demand when the user asks to expose functionality over HTTP (e.g. "create a CRUD API for Employee", or "create the Employee service and an API"). Creating it is an explicit-ask action — confirm before scaffolding (per the always-on "Do Not Create Projects" rule); never auto-create it to satisfy a feature request.
Agent instruction: When an API is requested and no Api host exists:
- Detect the host: look for
**/*.Api/*.Api.csproj. The file system is authoritative for project existence (unlike database state) — no further checking is needed.- If present, skip to authoring controllers (see coreex-api-controllers).
- If absent, confirm creation with the user — default the name to
{Solution}.Apiand the physical location tosrc/(the template default). Do not create it without confirmation.- Recover the original selections from the solution-root
AGENTS.md"Feature Configuration" (cross-checkdbex.yaml). These default thecoreex-apitemplate — which takesdata-provider,refdata-enabled,outbox-enabled(a subset of the solution options). Re-state the resolved values for confirmation rather than re-prompting; ifAGENTS.mdanddbex.yamldisagree, stop and flag rather than guessing.- Scaffold with the recovered values, naming consistently with the solution so the derived
domain-name/solution-nametokens align with the existing projects:Run it from the solution root — the directory that already containsdotnet new coreex-api -n {Solution}.Api --data-provider <X> --refdata-enabled <bool> --outbox-enabled <bool>src/andtests/. The template is rooted atsrc/...andtests/...(and usespreferNameDirectory: false, so it does not create a name-based subfolder); it merges itssrc//tests/into the existing ones. Running it from insidesrc/produces nestedsrc/src/...paths. If that happens, delete the misplaced output and re-run from the solution root — do not hand-move the generated files to "fix" the layout. Summarise the output on success; relay it verbatim on failure.- Author the controller(s) for the requested entity/operations per coreex-api-controllers, exposing only the confirmed operations.
- Verify by building the project directly — not the solution. The host is not yet in the
.slnx(that is step 9), so a solution-wide build would silently skip it and give false confidence. Build and test the new project(s) by path — this compiles them and their referenced projects regardless of solution membership:Fix any errors here, in-session, before handing off — compilation does not require the project to be in the solution.dotnet build <path/to/Api>.csproj dotnet test <path/to/Test.Api>.csproj- Update the recording: amend the solution-root
AGENTS.md— add the Api host to the Project Structure block and note it under hosts. (The feature selections themselves do not change.)- Add the new project(s) to the solution — final step, in-session. Once the changes are verified (step 7), wire the projects into the
.slnxfrom the solution root, batched by target folder, using the actual generated paths:Always viadotnet sln <Solution>.slnx add <path/to/Api>.csproj --solution-folder hosts dotnet sln <Solution>.slnx add <path/to/Test.Api>.csproj --solution-folder testsdotnet sln add— never hand-edit the.slnxXML (manual edits are error-prone and have wiped solution files). This is the last action, after the code is verified andAGENTS.mdis updated, so it cannot interrupt pending work. A finaldotnet build <Solution>.slnxconfirms the wiring. Exception — Visual Studio with the solution open: writing the.slnxtriggers an IDE reload that can interrupt and discard pending changes. Only in that case, defer these commands to an end-of-task Manual steps list for the user to run instead. The default environment (Claude Code / Copilot / CLI) runs them in-session.
The scaffolded Program.cs wiring is described below; it is generated complete and ready-to-compile via the option-driven #if blocks — there is no post-CodeGen uncomment / "phase 2" step.
Reference-data wiring is automatic — nothing to uncomment. The host wires reference data and dynamic services without referencing any CodeGen-generated type, so it compiles at scaffold time and is correct once CodeGen runs:
// refdata-enabled hosts only (template #if): builder.Services.AddReferenceDataOrchestrator(); // non-generic — resolves the IReferenceDataProvider from DI at runtime // all hosts — scans the assemblies (via the stable AssemblyMarker types) for [ScopedService] types, registering them all (incl. CodeGen-generated): builder.Services.AddDynamicServicesUsing(typeof({Solution}.Application.AssemblyMarker).Assembly, typeof({Solution}.Infrastructure.AssemblyMarker).Assembly);The generated
ReferenceDataServiceis[ScopedService]-decorated, so the assembly scan picks it up automatically and the orchestrator binds to it viaIReferenceDataProvider— no manual step. Before CodeGen there are simply no ref-data entities to serve (requests return empty), which is correct. Do not addAddReferenceDataOrchestrator<ReferenceDataService>()orAddDynamicServicesUsing<…generated types…>()— that reintroduces the compile-time dependency the marker approach removes.
Scaffolding a Subscribe host
A Subscribe host is not part of the base coreex solution — it is added on demand when the user explicitly asks to "add/create the subscribe host", "consume events", or similar. Creating it is an explicit-ask action — confirm before scaffolding (per the always-on "Do Not Create Projects" rule); never auto-create it.
Like the Relay host, the Subscribe host is fully template-generated: the coreex-subscribe template emits a complete, compilable Program.cs and test project with no Phase-2 / uncomment step. Unlike the Relay, the Subscribe host does have follow-on authoring work — subscribers are added to it over time as new event types are consumed.
Critical naming rule — always pass
-nwith the full host-suffixed name: Thecoreex-subscribetemplate'ssourceNameisapp-name.Subscribe— the.Subscribesuffix is part of the template source token, not appended automatically. The-nvalue replaces that entire token, so you must include the suffix:dotnet new coreex-subscribe -n {Solution}.Subscribe ... ✓ correct dotnet new coreex-subscribe -n {Solution} ... ✗ wrong — project loses the .Subscribe suffix and derived solution-name / domain-name tokens resolve incorrectly, breaking cross-project referencesOmitting
-nentirely is equally wrong —dotnet newfalls back to the directory name (typically the solution root, e.g.Foo.Bar), which has the same effect as passing the bare solution name.
Agent instruction: When the user asks to add/create the Subscribe host:
- Detect it: look for
**/*.Subscribe/*.Subscribe.csproj. The file system is authoritative (unlike database state) — no further checking is needed.- If present, skip to authoring subscribers (see coreex-event-subscribers).
- If absent, confirm creation with the user — default the name to
{Solution}.Subscribeand the physical location tosrc/(the template default). Do not create it without confirmation.- Recover the original selections from the solution-root
AGENTS.md"Feature Configuration" (cross-checkdbex.yaml). Thecoreex-subscribetemplate takesdata-provider,messaging-provider, andrefdata-enabled— default all three from the recordedcoreexselections. Re-state the resolved values for confirmation rather than re-prompting; ifAGENTS.mdanddbex.yamldisagree, stop and flag rather than guessing.- Scaffold with the recovered values, naming consistently with the solution so the derived
domain-name/solution-nametokens align with the existing projects:Run it from the solution root — the directory that already containsdotnet new coreex-subscribe -n {Solution}.Subscribe --data-provider <X> --messaging-provider <Y> --refdata-enabled <bool>src/andtests/. The template is rooted atsrc/.../tests/...(and usespreferNameDirectory: false, so it merges into the existing folders). Running it from insidesrc/produces nestedsrc/src/...paths; if that happens, delete the misplaced output and re-run from the solution root — do not hand-move the files. Summarise the output on success; relay it verbatim on failure.- Author the subscriber(s) for any event types the user has asked to consume, per coreex-event-subscribers.
- Verify by building the project directly — not the solution. The host is not yet in the
.slnx(it is added in step 9), so a solution-wide build would silently skip it:Fix any errors here, in-session, before handing off — compilation does not require the project to be in the solution.dotnet build <path/to/Subscribe>.csproj dotnet test <path/to/Test.Subscribe>.csproj- Update the recording: amend the solution-root
AGENTS.md— add the Subscribe host to the Project Structure block and note it under hosts. (The feature selections themselves do not change.)- Add the new project(s) to the solution — final step, in-session. Once the changes are verified (step 7), wire the projects into the
.slnxfrom the solution root:Always viadotnet sln <Solution>.slnx add <path/to/Subscribe>.csproj --solution-folder hosts dotnet sln <Solution>.slnx add <path/to/Test.Subscribe>.csproj --solution-folder testsdotnet sln add— never hand-edit the.slnxXML. A finaldotnet build <Solution>.slnxconfirms the wiring. Exception — Visual Studio with the solution open: defer these to a Manual steps list (the IDE-reload caveat from the Api host workflow applies identically).
The scaffolded Subscribe Program.cs wiring is described in Subscribe Host below; the template emits it complete via the option-driven #if blocks — there is no post-CodeGen uncomment / "phase 2" step.
Scaffolding an Outbox Relay host
An Outbox Relay host is not part of the base coreex solution — it is added on demand when the user explicitly asks to "add/create the outbox relay" (or similar). Creating it is an explicit-ask action — confirm before scaffolding (per the always-on "Do Not Create Projects" rule); never auto-create it.
Unlike the Api host, the Relay is a one-off, fully template-generated host: it has no controllers, no reference-data wiring, no application services, and no Phase-2 / uncomment step. Once scaffolded there is nothing further to author — do not add any logic, registration, or test beyond what the coreex-relay template emits.
Agent instruction: When the user asks to add/create the Outbox Relay host:
- Detect it: look for
**/*.Relay/*.Relay.csproj. The file system is authoritative (unlike database state) — no further checking is needed.- If present, STOP — it already exists. The Relay is a single per-solution one-off; immediately report it as pre-existing and do nothing else — do not re-scaffold, modify, or "augment" it. (Contrast the Api host, where an existing host means "go author controllers"; the Relay has no follow-on work.)
- If absent, confirm creation with the user — default the name to
{Solution}.Relayand the physical location tosrc/(the template default). Do not create it without confirmation.- Recover the original selections from the solution-root
AGENTS.md"Feature Configuration" (cross-checkdbex.yaml). Thecoreex-relaytemplate takesdata-providerandmessaging-provider— default both from the recordedcoreexselections. Re-state the resolved values for confirmation rather than re-prompting; ifAGENTS.mdanddbex.yamldisagree, stop and flag rather than guessing.- Scaffold with the recovered values, naming consistently with the solution so the derived
domain-name/solution-nametokens align with the existing projects:Run it from the solution root — the directory that already containsdotnet new coreex-relay -n {Solution}.Relay --data-provider <X> --messaging-provider <Y>src/andtests/. The template is rooted atsrc/.../tests/...(and usespreferNameDirectory: false, so it merges into the existing folders). Running it from insidesrc/produces nestedsrc/src/...paths; if that happens, delete the misplaced output and re-run from the solution root — do not hand-move the files. Summarise the output on success; relay it verbatim on failure.- Verify by building the project directly — not the solution. The host is not yet in the
.slnx(it is added in step 8), so a solution-wide build would silently skip it:Fix any errors here, in-session. There is no further wiring — the template output is complete as-is.dotnet build <path/to/Relay>.csproj dotnet test <path/to/Test.Relay>.csproj- Update the recording: amend the solution-root
AGENTS.md— add the Relay host to the Project Structure block and note it under hosts. (The feature selections themselves do not change.)- Add the new project(s) to the solution — final step, in-session (exactly as for the Api host), batched by target folder, using the actual generated paths:
Always viadotnet sln <Solution>.slnx add <path/to/Relay>.csproj --solution-folder hosts dotnet sln <Solution>.slnx add <path/to/Test.Relay>.csproj --solution-folder testsdotnet sln add— never hand-edit the.slnxXML. A finaldotnet build <Solution>.slnxconfirms the wiring. Exception — Visual Studio with the solution open: defer these to a Manual steps list (the IDE-reload caveat from the Api host workflow applies identically).
The scaffolded Relay Program.cs wiring is described in Outbox Relay Host below; the template emits it complete via the option-driven #if blocks (data-provider + messaging-provider), so a correctly-defaulted scaffold compiles and runs without any manual fix-up.
Key Registrations by Host Type
API Host
| Package | Key registrations |
|---|---|
CoreEx.AspNetCore |
AddMvcWebApi(), AddHttpWebApi(), AddExecutionContext(), UseCoreExExceptionHandler(), UseExecutionContext(), UseIdempotencyKey(), MapHealthChecks() |
CoreEx.AspNetCore.NSwag |
AddOpenApiDocument(), AddCoreExConfiguration(), UseOpenApi(), UseSwaggerUi() |
CoreEx.Caching.FusionCache |
AddFusionCache(), AddFusionHybridCache(), AddDefaultCacheKeyProvider(), AddHybridCacheIdempotencyProvider() |
CoreEx.Database.SqlServer |
AddSqlServerDatabase(), AddSqlServerUnitOfWork(), AddSqlServerOutboxPublisher(), AddSqlServerClient("SqlServer") |
CoreEx.Database.Postgres |
AddPostgresDatabase(), AddPostgresUnitOfWork(), AddPostgresOutboxPublisher(), AddAzureNpgsqlDataSource("Postgres") |
CoreEx.EntityFrameworkCore |
AddDbContext<T>(), AddEfDb<T>() |
CoreEx.Events |
AddEventFormatter() |
CoreEx.RefData |
AddReferenceDataOrchestrator<T>() |
CoreEx.Data.GraphQL |
AddCoreExGraphQLLite((o, sp) => o.AddQuery(...).AddGet(...)), MapCoreExGraphQLLite("/query") — additive, opt-in GraphQL-lite bridge over existing QueryArgsConfig-driven QueryAsync/GetAsync methods; add only when explicitly asked |
Aspire.StackExchange.Redis.DistributedCaching |
AddRedisDistributedCache("redis") |
FusionCache.Backplane.StackExchangeRedis |
RedisBackplane, RedisBackplaneOptions |
OpenTelemetry.* |
WithCoreExTelemetry(), WithCoreExSqlServerTelemetry() / WithCoreExPostgresTelemetry(), UseOtlpExporter() |
Subscribe Host
| Package | Key registrations |
|---|---|
CoreEx.AspNetCore |
AddMvcWebApi(), AddHttpWebApi(), AddExecutionContext(), AddHostedServiceManager(), UseCoreExExceptionHandler(), UseExecutionContext(), MapHealthChecks(), MapHostedServices() |
CoreEx.Caching.FusionCache |
AddFusionCache(), AddFusionHybridCache(), AddDefaultCacheKeyProvider(), AddHybridCacheIdempotencyProvider() |
CoreEx.Events |
AddEventFormatter(), AddSubscribedManager() |
CoreEx.Database.SqlServer |
AddSqlServerDatabase(), AddSqlServerUnitOfWork(), AddSqlServerOutboxPublisher(), AddSqlServerClient("SqlServer") |
CoreEx.Database.Postgres |
AddPostgresDatabase(), AddPostgresUnitOfWork(), AddPostgresOutboxPublisher(), AddAzureNpgsqlDataSource("Postgres") |
CoreEx.EntityFrameworkCore |
AddDbContext<T>(), AddEfDb<T>() |
CoreEx.RefData |
AddReferenceDataOrchestrator<T>() |
CoreEx.Azure.Messaging.ServiceBus |
AddAzureServiceBusClient("ServiceBus"), AddAzureServiceBusPublisher(..., addAsDefaultIEventPublisher: false), AzureServiceBusReceiving(), WithCoreExServiceBusTelemetry() |
Aspire.StackExchange.Redis.DistributedCaching |
AddRedisDistributedCache("redis") |
FusionCache.Backplane.StackExchangeRedis |
RedisBackplane, RedisBackplaneOptions |
OpenTelemetry.* |
WithCoreExTelemetry(), WithCoreExServiceBusTelemetry(), WithCoreExSqlServerTelemetry() / WithCoreExPostgresTelemetry(), UseOtlpExporter() |
Outbox Relay Host
| Package | Key registrations |
|---|---|
CoreEx.AspNetCore |
AddMvcWebApi(), AddHttpWebApi(), AddExecutionContext(), AddHostedServiceManager(), UseCoreExExceptionHandler(), UseExecutionContext(), MapHealthChecks(), MapHostedServices() |
CoreEx.Database.SqlServer |
AddSqlServerDatabase(), AddSqlServerUnitOfWork(), AddSqlServerOutboxRelay(), AddSqlServerOutboxRelayHostedService() |
CoreEx.Database.Postgres |
AddPostgresDatabase(), AddPostgresUnitOfWork(), AddPostgresOutboxRelay(), AddPostgresOutboxRelayHostedService() |
CoreEx.Azure.Messaging.ServiceBus |
AddAzureServiceBusClient("ServiceBus"), AddAzureServiceBusPublisher(...), ServiceBusSessionStrategy |
OpenTelemetry.* |
WithCoreExTelemetry(), WithCoreExSqlServerTelemetry() / WithCoreExPostgresTelemetry(), WithCoreExServiceBusTelemetry(), UseOtlpExporter() |
API Host
The API host is the primary HTTP composition root. It exposes controllers, OpenAPI docs, reference-data endpoints, and idempotency support.
var builder = WebApplication.CreateBuilder(args);
builder.AddHostSettings();
builder.Services
.AddPrecisionTimeProvider()
.AddExecutionContext()
.AddReferenceDataOrchestrator() // non-generic — binds the IReferenceDataProvider (CodeGen-generated ReferenceDataService) from DI at runtime
.AddMvcWebApi()
.AddHttpWebApi();
builder.Services.AddDynamicServicesUsing(typeof(MyApp.Application.AssemblyMarker).Assembly, typeof(MyApp.Infrastructure.AssemblyMarker).Assembly);
// L1/L2 caching with FusionCache + Redis backplane.
builder.Services.AddMemoryCache();
builder.AddRedisDistributedCache("redis");
builder.Services.AddFusionCache()
.WithRegisteredMemoryCache()
.WithRegisteredDistributedCache()
.WithBackplane(sp => new RedisBackplane(new RedisBackplaneOptions { Configuration = ... }))
.WithSystemTextJsonSerializer(JsonDefaults.SerializerOptions);
builder.Services
.AddFusionHybridCache()
.AddDefaultCacheKeyProvider()
.AddHybridCacheIdempotencyProvider();
// Database, EF, outbox publisher.
// SQL Server variant:
builder.AddSqlServerClient("SqlServer");
builder.Services
.AddSqlServerDatabase()
.AddSqlServerUnitOfWork()
.AddEventFormatter()
.AddSqlServerOutboxPublisher()
.AddDbContext<MyDbContext>()
.AddEfDb<MyEfDb>();
// PostgreSQL variant (use instead of SQL Server):
// builder.AddAzureNpgsqlDataSource("Postgres");
// builder.Services
// .AddPostgresDatabase()
// .AddPostgresUnitOfWork()
// .AddEventFormatter()
// .AddPostgresOutboxPublisher()
// .AddDbContext<MyDbContext>()
// .AddEfDb<MyEfDb>();
builder.Services.PostConfigureAllHealthChecks();
builder.Services.AddControllers();
builder.Services.AddOpenApiDocument(s => { s.Title = builder.Environment.ApplicationName; s.AddCoreExConfiguration(); });
builder.WithCoreExTelemetry().WithCoreExSqlServerTelemetry().UseOtlpExporter();
var app = builder.Build();
app.UseCoreExExceptionHandler();
app.UseHttpsRedirection();
// app.UseAuthentication(); // TODO: register an authentication scheme (builder.Services.AddAuthentication(...)) then uncomment.
app.UseAuthorization();
app.UseExecutionContext();
app.UseIdempotencyKey(); // After UseExecutionContext.
app.MapControllers();
app.UseOpenApi();
app.UseSwaggerUi();
app.MapHealthChecks(new HealthCheckOptions { AreDetailedEndpointsEnabled = true } /*, detailedGroupConfigure: g => g.RequireAuthorization() */);
app.Run();
Key points:
AddDynamicServicesUsing(...)registers per assembly, not per service. It scans each supplied assembly for all[ScopedService]-decorated types and registers them. The template anchors each assembly on its neutralAssemblyMarkertype (typeof(MyApp.Application.AssemblyMarker).Assembly,…Infrastructure…) — these are the only markers the clean scaffold ships, and they exist solely for this anchoring. Use the neutral marker, not a domain type liketypeof(ReferenceDataService).Assembly: the marker reads as "scan this whole assembly," never misleads a reader into thinking the scan is scoped to one type, always exists (so there is no compile-time dependency on CodeGen output and no bootstrap/uncomment step), and survives renames. Adding a new entity does not change this line — the new service/repository is picked up automatically by the existing assembly scan. Add another assembly only when you introduce a new project containing[ScopedService]types (e.g. the Subscribe host passestypeof(Program).Assemblyfor its subscribers). Do not add per-namespace placeholder/marker types to satisfyglobal usings — those follow the code (seecoreex-conventions.instructions.md).- Prefer the non-generic
AddReferenceDataOrchestrator()(bindsIReferenceDataProviderfrom DI at runtime) overAddReferenceDataOrchestrator<ReferenceDataService>()— the non-generic form needs no generated type, so it compiles from scaffold time. It also auto-registersReferenceDataQuery.Default, enablingReferenceDataOrchestrator.QueryAsync<TRef>(QueryArgs?, PagingArgs?, CancellationToken)on all ref-data endpoints out of the box. AddReferenceDataOrchestrator()andAddDynamicServicesUsing(...)are shared with Subscribe hosts — both API and Subscribe hosts are full application-layer consumers.- FusionCache (L1/L2) and
AddHybridCacheIdempotencyProvider()are shared with Subscribe hosts — both need caching for reference data and idempotency for safe duplicate handling. AddEventFormatter()is required wherever events are published or parsed.AddSqlServerOutboxPublisher()/AddPostgresOutboxPublisher()take no generic type parameter.UseIdempotencyKey()must come afterUseExecutionContext().- If the domain also publishes directly to Service Bus (e.g. for cross-domain adapters), add
AddAzureServiceBusPublisher(..., addAsDefaultIEventPublisher: false)so the outbox publisher remains the defaultIEventPublisher. MapHealthChecks()'s basiclive/startup/readyendpoints are intentionally left anonymous — they're conventionally probed by container orchestrators without credentials. Its detailed endpoints (/health/*/detailed) are different: they emit the fullHealthReport, which can include component names and exception details, soHealthCheckOptions.AreDetailedEndpointsEnableddefaults tofalse(secure by default) and must be explicitly opted into, as shown above. Once opted in, they should also be secured — passdetailedGroupConfigure: g => g.RequireAuthorization(), commented out by default (as shown above) since it 500s until an authentication scheme and authorization services are registered;MapHealthCheckslogs aWarningat startup if detailed endpoints are enabled without it. Uncomment alongsideUseAuthentication()once a scheme is configured.CoreEx.Data.GraphQLis additive, not part of the base scaffold — add it only when explicitly asked for a GraphQL query surface; use thecoreex-graphqlskill to wire it (.github/skills/coreex-graphql/SKILL.md) rather than improvising. Register roots withbuilder.Services.AddCoreExGraphQLLite((o, sp) => o.AddQuery<ProductLite>("products", ProductQueryArgsConfig.Default, async (qa, pa, ct) => await CoreEx.ExecutionContext.GetRequiredService<IProductReadService>().QueryAsync(qa, pa, ct).ConfigureAwait(false)).AddGet<Product>("product", (args, ct) => CoreEx.ExecutionContext.GetRequiredService<IProductReadService>().GetAsync(args.GetIdentifier<string>(), ct)))—GetIdentifier<TId>validates presence and type of the named argument (default"id") — it casts toTId, it does not convert — and throws anArgumentException, mapped by the engine toARGUMENT_ERROR, if it's missing/empty/wrong-typed — thenapp.MapCoreExGraphQLLite("/query");afterapp.MapControllers();. Resolve scoped dependencies (repositories, application services) per-invocation viaCoreEx.ExecutionContext.GetRequiredService<T>()— never capture an instance from the rootIServiceProviderat registration time, sinceIGraphQLEngineis a singleton. It bridges GraphQLwhere/orderBy1:1 onto the entity's existingQueryArgsConfig— no new resolver/filter logic.MapCoreExGraphQLLiteexecutes throughWebApi.PostAsync<GraphQLLiteResponse>(...), so standard exception-handling middleware applies as a safety net; add.WithCoreExGraphQLTelemetry()alongside the host's other OpenTelemetry tracing extensions to traceExecuteAsynccalls. To expose all reference data types as GraphQL roots in one call, useo.AddReferenceDataQueries(sp, ReferenceDataQueryArgsConfig.Default)— this bulk-registers every type known to theReferenceDataOrchestrator, keyed asref_<name>where<name>is the type'sAlternateNamesentry where registered, otherwise its ownType.Name; passexcludeTypesto opt specific types out. SeeCoreEx.Data.GraphQLAGENTS.md.
Subscribe Host
The Subscribe host receives broker messages and delegates to Application-layer services. Subscribers are full application-layer consumers — they invoke application services that may validate, persist data, and publish outbound events. Therefore, Subscribe hosts include reference data, caching, database, and idempotency support.
var builder = WebApplication.CreateBuilder(args);
builder.AddHostSettings();
builder.Services
.AddPrecisionTimeProvider()
.AddExecutionContext()
.AddReferenceDataOrchestrator() // non-generic — binds the IReferenceDataProvider from DI at runtime
.AddMvcWebApi()
.AddHttpWebApi()
.AddHostedServiceManager();
builder.Services.AddDynamicServicesUsing(typeof(Program).Assembly, typeof(MyApp.Application.AssemblyMarker).Assembly, typeof(MyApp.Infrastructure.AssemblyMarker).Assembly); // this host (subscribers) + Application + Infrastructure
// L1/L2 caching with FusionCache + Redis backplane.
builder.Services.AddMemoryCache();
builder.AddRedisDistributedCache("redis");
builder.Services.AddFusionCache()
.WithRegisteredMemoryCache()
.WithRegisteredDistributedCache()
.WithBackplane(sp => new RedisBackplane(new RedisBackplaneOptions { Configuration = ... }))
.WithSystemTextJsonSerializer(JsonDefaults.SerializerOptions);
builder.Services
.AddFusionHybridCache()
.AddDefaultCacheKeyProvider()
.AddHybridCacheIdempotencyProvider();
// Domain database + outbox publisher.
// SQL Server variant:
builder.AddSqlServerClient("SqlServer");
builder.Services
.AddSqlServerDatabase()
.AddSqlServerUnitOfWork()
.AddSqlServerOutboxPublisher()
.AddDbContext<MyDbContext>()
.AddEfDb<MyEfDb>();
// PostgreSQL variant (use instead of SQL Server):
// builder.AddAzureNpgsqlDataSource("Postgres");
// builder.Services
// .AddPostgresDatabase()
// .AddPostgresUnitOfWork()
// .AddPostgresOutboxPublisher()
// .AddDbContext<MyDbContext>()
// .AddEfDb<MyEfDb>();
// Service Bus: keep outbox publisher as the default IEventPublisher.
builder.AddAzureServiceBusClient("ServiceBus");
builder.Services.AddAzureServiceBusPublisher((_, c) =>
{
c.SessionIdStrategy = ServiceBusSessionStrategy.UsePartitionKeyConvertedToAnId;
}, addAsDefaultIEventPublisher: false);
// Subscriber wiring.
builder.Services
.AddEventFormatter()
.AddSubscribedManager((_, c) => c.AddSubscribersUsing<MySubscriber>());
builder.Services.AzureServiceBusReceiving()
.WithSessionReceiver(_ =>
{
var o = ServiceBusSessionReceiverOptions.CreateForTopicSubscription();
o.SessionProcessorOptions.MaxConcurrentSessions = 4;
return o;
})
.WithSubscribedSubscriber()
.WithHostedService()
.Build();
builder.Services.PostConfigureAllHealthChecks();
builder.Services.AddControllers();
builder.Services.AddOpenApiDocument(s => { s.Title = builder.Environment.ApplicationName; s.AddCoreExConfiguration(); });
builder.WithCoreExTelemetry()
.WithCoreExServiceBusTelemetry()
.WithCoreExSqlServerTelemetry() // or .WithCoreExPostgresTelemetry() for PostgreSQL
.UseOtlpExporter();
var app = builder.Build();
app.UseCoreExExceptionHandler();
app.UseHttpsRedirection();
// app.UseAuthentication(); // TODO: register an authentication scheme (builder.Services.AddAuthentication(...)) then uncomment.
app.UseAuthorization();
app.UseExecutionContext();
app.MapControllers();
app.UseOpenApi();
app.UseSwaggerUi();
app.MapHealthChecks(new HealthCheckOptions { AreDetailedEndpointsEnabled = true } /*, detailedGroupConfigure: g => g.RequireAuthorization() */);
app.MapHostedServices(/* groupConfigure: g => g.RequireAuthorization() */); // Exposes pause/resume management endpoints — must follow MapHealthChecks.
app.Run();
Key points:
- Subscribe hosts do include
AddReferenceDataOrchestrator()andAddDynamicServicesUsing(...)— subscribers call application services that need reference data for validation and business logic. The Subscribe host passes three assemblies toAddDynamicServicesUsing: its own (typeof(Program).Assembly, for the subscriber types) plus the Application and InfrastructureAssemblyMarkerassemblies. - Subscribe hosts do include FusionCache (L1/L2) and
AddHybridCacheIdempotencyProvider()— caching is required for reference data; idempotency is required to safely handle duplicate message delivery. - Subscribe hosts do include database/EF Core and outbox publisher — subscribers persist domain data and publish outbound events.
AddHostedServiceManager()must be registered beforeAzureServiceBusReceiving().AddSubscribersUsing<T>()scans the assembly ofTand auto-registers all[Subscribe]-decorated classes — no manual registration per subscriber.AddAzureServiceBusPublisher(..., addAsDefaultIEventPublisher: false)keeps the outbox publisher as the defaultIEventPublisherfor transactional writes.MapHostedServices()must come afterMapHealthChecks().MapHealthChecks()'s detailed endpoints andMapHostedServices()'s pause/resume endpoints are both intended to be secured viaRequireAuthorization()(both admin/diagnostic surfaces), but the calls above are commented out by default — they 500 until an authentication scheme is registered. Uncomment alongsideUseAuthentication(). The basiclive/startup/readyhealth checks stay anonymous for orchestrator probes.HealthCheckOptions.AreDetailedEndpointsEnableddefaults tofalse(secure by default), so it must be explicitly opted into as shown above;MapHealthCheckslogs aWarningat startup if left enabled withoutdetailedGroupConfigure. See the equivalent API host bullet for the reasoning.
Outbox Relay Host
The Outbox Relay host is minimal: it polls the outbox table and forwards committed events to Azure Service Bus. It has no application logic — no controllers, no OpenAPI, no FusionCache, no reference data, no EF Core DbContext. It only needs database connectivity to read the outbox table and Service Bus connectivity to publish.
builder.Services
.AddPrecisionTimeProvider()
.AddExecutionContext()
.AddMvcWebApi()
.AddHttpWebApi()
.AddHostedServiceManager();
// SQL Server example; use Postgres equivalents for PostgreSQL domains.
builder.AddSqlServerClient("SqlServer");
builder.Services
.AddSqlServerDatabase()
.AddSqlServerUnitOfWork()
.AddSqlServerOutboxRelay(); // No configuration lambda required.
builder.AddSqlServerOutboxRelayHostedService();
// PostgreSQL variant:
// builder.AddAzureNpgsqlDataSource("Postgres");
// builder.Services
// .AddPostgresDatabase()
// .AddPostgresUnitOfWork()
// .AddPostgresOutboxRelay();
// builder.AddPostgresOutboxRelayHostedService();
// Service Bus publisher — this IS the default IEventPublisher for the relay.
builder.AddAzureServiceBusClient("ServiceBus");
builder.Services.AddAzureServiceBusPublisher((_, c) =>
{
c.SessionIdStrategy = ServiceBusSessionStrategy.UsePartitionKeyConvertedToAnId;
});
builder.Services.PostConfigureAllHealthChecks();
builder.WithCoreExTelemetry().WithCoreExSqlServerTelemetry().WithCoreExServiceBusTelemetry().UseOtlpExporter();
var app = builder.Build();
app.UseCoreExExceptionHandler();
app.UseHttpsRedirection();
// app.UseAuthentication(); // TODO: register an authentication scheme (builder.Services.AddAuthentication(...)) then uncomment.
// app.UseAuthorization(); // TODO: register authorization services (builder.Services.AddAuthorization(...)) then uncomment.
app.UseExecutionContext();
app.MapHealthChecks(new HealthCheckOptions { AreDetailedEndpointsEnabled = true } /*, detailedGroupConfigure: g => g.RequireAuthorization() */);
app.MapHostedServices(/* groupConfigure: g => g.RequireAuthorization() */);
app.Run();
Key points:
- The Relay host has no application-layer dependencies — no
AddReferenceDataOrchestrator, noAddDynamicServicesUsing, no FusionCache, no EF Core DbContext, no domain services. AddSqlServerOutboxRelay()/AddPostgresOutboxRelay()take no configuration lambda.AddSqlServerOutboxRelayHostedService()/AddPostgresOutboxRelayHostedService()register the background relay pump — call these onbuilder, notbuilder.Services.UseAuthentication()/UseAuthorization()are present but commented out — the Relay host (unlike API/Subscribe) has noAddControllers()or other MVC registration, so callingUseAuthorization()without first registeringbuilder.Services.AddAuthorization()throws at startup (confirmed:AddControllers()in the API/Subscribe hosts registers authorization services transitively, which is why they can call it directly). Uncomment both together, once a scheme and policy are registered, so thedetailedGroupConfigure/groupConfigurecalls below can also be uncommented and take effect.MapHealthChecks()'s basiclive/startup/readyendpoints are intentionally left anonymous for container-orchestrator probes; its detailed endpoints emit the fullHealthReport(component names, exception details), soHealthCheckOptions.AreDetailedEndpointsEnableddefaults tofalse(secure by default) and is explicitly opted into above, and should be secured viadetailedGroupConfigure: g => g.RequireAuthorization().MapHostedServices()'s pause/resume admin endpoints should be secured the same way viagroupConfigure(its remarks call this out as "highly recommended"). BothdetailedGroupConfigureandgroupConfigureare commented out by default here — same asUseAuthentication()/UseAuthorization()above — since they'd otherwise 500 every request until an authentication scheme is actually registered; uncomment alongside those.- No
AddControllers(), noAddOpenApiDocument(), noUseOpenApi(), noUseSwaggerUi(), noUseIdempotencyKey().
