Configuration
Cratis Arc can be configured both through appsettings.json and programmatically to customize its behavior. The main configuration is handled through the ArcOptions class.
This page covers the ASP.NET Core host specifically. Code blocks are configuration/service fragments for an existing Cratis.Arc web project; import Cratis.Arc, System.Text.Json, and Microsoft.Extensions.Options as used. Types such as MyCustomIdentityDetailsProvider and MyCustomConverter are application-owned examples, not built-in Arc types. For the lightweight Arc.Core host and its listen URL configuration, see Arc.Core Getting Started.
Default Configuration Section
Section titled “Default Configuration Section”By default, Arc looks for configuration under the Cratis:Arc section in your appsettings.json file. The same keys can be supplied through environment variables — .NET maps the __ separator onto nested keys, so Cratis:Arc:GeneratedApis:RoutePrefix becomes Cratis__Arc__GeneratedApis__RoutePrefix.
Configuration Options
Section titled “Configuration Options”Configuration example
Section titled “Configuration example”Here’s an example covering the most common options, bound from appsettings.json under Cratis:Arc. The complete options tree also documents ExposeExceptionDetails, Tenancy.BaseDomain, and host-only settings.
{ "Cratis": { "Arc": { "CorrelationId": { "HttpHeader": "X-Correlation-ID" }, "Tenancy": { "ResolverType": "Header", "HttpHeader": "x-cratis-tenant-id" }, "GeneratedApis": { "RoutePrefix": "api", "SegmentsToSkipForRoute": 0, "IncludeCommandNameInRoute": true, "IncludeQueryNameInRoute": true }, "Query": { "KeepAliveInterval": "00:00:30" } } }}Configuration Properties
Section titled “Configuration Properties”CorrelationId
Section titled “CorrelationId”Controls how correlation IDs are handled in HTTP requests.
- HttpHeader (string, default:
"X-Correlation-ID"): The HTTP header name to use for correlation ID tracking.
Tenancy
Section titled “Tenancy”Controls how the active tenant is resolved on each request.
- ResolverType (
TenantResolverType, default:Header): How to resolve the tenant —Header,Query,Claim,Subdomain,Development, orFixed. - HttpHeader (string, default:
"x-cratis-tenant-id"): The HTTP header used whenResolverTypeisHeader, and the fallback header when it isSubdomain. - QueryParameter (string, default:
"tenantId"): The query-string parameter used whenResolverTypeisQuery. - ClaimType (string, default:
"tenant_id"): The claim used whenResolverTypeisClaim. - FixedTenantId (string, default:
"development"): The tenant every request resolves to whenResolverTypeisFixedorDevelopment. PreferFixedfor a single-tenant production deployment —Developmentresolves identically but is named for an environment it does not check. - DevelopmentTenantId (string, default:
"development"): The same value under its original name — reading or writing either key sets both. Supply only one; if both are present the binder’s property order decides.
GeneratedApis
Section titled “GeneratedApis”Controls how automatically generated API endpoints are configured for commands and queries.
- RoutePrefix (string, default:
"api"): The base route prefix for all generated API endpoints. - SegmentsToSkipForRoute (int, default:
0): Number of namespace segments to skip when constructing routes from type namespaces. - IncludeCommandNameInRoute (bool, default:
true): Whether to include the command type name as the last segment of the route for command endpoints. - IncludeQueryNameInRoute (bool, default:
true): Whether to include the query name as the last segment of the route for query endpoints. - EnableQueryHttpMethod (bool, default:
true): Whether generated queries also accept HTTPQUERYwith arguments in a JSON body. GET remains available. See HTTP QUERY.
Controls observable (real-time) queries.
- KeepAliveInterval (
TimeSpan, default:00:00:30): How often a keep-alive frame is sent on an open observable-query connection.
IdentityDetailsProvider
Section titled “IdentityDetailsProvider”- IdentityDetailsProvider (Type, default:
null): Specifies a custom identity details provider type. If not specified, the system will use type discovery to find one automatically.
Setup Methods
Section titled “Setup Methods”The ASP.NET Core host bootstraps with WebApplication.CreateBuilder, registers Arc with AddCratisArc, and activates it with UseCratisArc.
Using Configuration File
Section titled “Using Configuration File”The most common approach is to use the configuration file with the default section:
var builder = WebApplication.CreateBuilder(args);
// Bind the default configuration section (Cratis:Arc)builder.AddCratisArc();
var app = builder.Build();app.UseCratisArc();app.Run();Using Custom Configuration Section
Section titled “Using Custom Configuration Section”You can specify a custom configuration section path:
var builder = WebApplication.CreateBuilder(args);
// Bind a custom configuration sectionbuilder.AddCratisArc(configSectionPath: "MyApp:CratisConfig");
var app = builder.Build();app.UseCratisArc();app.Run();Programmatic Configuration
Section titled “Programmatic Configuration”You can configure the options through code with the configureOptions callback:
var builder = WebApplication.CreateBuilder(args);
builder.AddCratisArc(options =>{ // Configure correlation ID options.CorrelationId.HttpHeader = "X-My-Correlation-ID";
// Configure tenancy options.Tenancy.HttpHeader = "X-Custom-Tenant";
// Configure generated APIs options.GeneratedApis.RoutePrefix = "myapi"; options.GeneratedApis.SegmentsToSkipForRoute = 2; options.GeneratedApis.IncludeCommandNameInRoute = false; options.GeneratedApis.IncludeQueryNameInRoute = false;
// Set a custom identity details provider options.IdentityDetailsProvider = typeof(MyCustomIdentityDetailsProvider);});
var app = builder.Build();app.UseCratisArc();app.Run();Hybrid Configuration
Section titled “Hybrid Configuration”AddCratisArc binds appsettings.json first and then applies the configureOptions callback, so the callback acts as a programmatic override on top of file-based settings:
var builder = WebApplication.CreateBuilder(args);
// Values come from Cratis:Arc; the callback overrides specific settingsbuilder.AddCratisArc(options =>{ options.GeneratedApis.RoutePrefix = "v1/api";});
var app = builder.Build();app.UseCratisArc();app.Run();Turning controllers off
Section titled “Turning controllers off”AddCratisArc registers ASP.NET Core MVC and discovers the controllers in your project assemblies. An application that only uses model-bound commands and queries doesn’t need MVC. Turn it off with WithoutControllers on the Arc builder:
var builder = WebApplication.CreateBuilder(args);
builder.AddCratisArc(configureBuilder: arc => arc.WithoutControllers());
var app = builder.Build();app.UseCratisArc();app.Run();Controllers are on unless you call WithoutControllers or turn them off with the CratisArcControllersSupport property (see below). With controllers off:
- Model-bound commands and queries, observable queries included, work as before.
UseCratisArcmaps their endpoints without MVC. - Controller-based commands and queries aren’t available, because no controllers are discovered.
- MVC-only features are unavailable. Calling
MapControllersfails, because MVC isn’t registered, and[FromRequest]model binding,[AspNetResult]and MVC model validation are unavailable. Arc still validates model-bound commands and queries. - OpenAPI documents describe the model-bound endpoints only.
- The application registers the ASP.NET Core services it used to get from MVC. Add
builder.Services.AddCors()beforeUseCors, andbuilder.Services.AddEndpointsApiExplorer()when you use Swashbuckle. Arc registers the ASP.NET Core authentication and authorization services, soUseAuthentication,UseAuthorizationand policies work without callingAddAuthorization. Authentication schemes still come from your ownAddAuthentication(...), and protected introspection needs a default scheme, as it does with controllers on.
WithoutControllers works with AddCratis too, through configureArcBuilder. The IHostBuilder overload of AddCratisArc doesn’t support configureBuilder (passing one throws), so there it’s only the MSBuild property below that turns controllers off.
Removing MVC from a trimmed or NativeAOT publish
Section titled “Removing MVC from a trimmed or NativeAOT publish”WithoutControllers is a runtime choice. AddCratisArc still references the code that registers MVC, so a trimmed or NativeAOT publish keeps it, along with the trim warnings MVC raises. To let the trimmer remove it, turn controllers off in the application’s project file instead:
<PropertyGroup> <CratisArcControllersSupport>false</CratisArcControllersSupport></PropertyGroup>The Cratis.Arc package turns the property into the Cratis.Arc.Controllers.IsSupported runtime switch in the application’s runtimeconfig.json. With it set to false:
- Every
AddCratisArcoverload, theIHostBuilderone included, registers no MVC and discovers no controllers, exactly asWithoutControllersdoes, and everything listed above applies. You don’t need to callWithoutControllersas well. - A trimmed or NativeAOT publish removes the code that registers MVC, so MVC’s trim warnings and most MVC assemblies drop out of the output. Arc still references a few MVC types elsewhere, so
Microsoft.AspNetCore.Mvc.CoreandMicrosoft.AspNetCore.Mvc.Abstractionsstay. This works fornet8.0,net9.0andnet10.0. - The rest of Arc isn’t trim or NativeAOT compatible yet, so a trimmed publish still reports trim warnings from Arc itself.
The switch is written into the runtimeconfig.json of the project that sets the property, so it applies to whichever process runs. A test project that hosts the application, for example through WebApplicationFactory<Program>, runs with its own runtimeconfig.json and keeps MVC unless it sets the property too. Set the property in a Directory.Build.props shared by the application and its tests, so tests run the way the published application does.
Leave the property unset, or set it to true, to keep controllers on. The property only has an effect through the Cratis.Arc package; a project that references Arc’s source through a ProjectReference has to import build/Cratis.Arc.targets itself.
Configuring MVC when controllers are on
Section titled “Configuring MVC when controllers are on”With controllers on, AddCratisArc registers MVC after the configureBuilder callback returns. Configure MVC after AddCratisArc returns, not inside configureBuilder:
builder.AddCratisArc();builder.Services.Configure<MvcOptions>(options => { /* ... */ });builder.Services.PostConfigure<Microsoft.AspNetCore.Mvc.JsonOptions>(options => { /* ... */ });MVC settings made inside the callback run before Arc’s own setup and before MVC’s defaults exist. Arc’s JSON configuration then overrides yours, Arc’s model binder goes ahead of one you insert, and removing a default formatter finds nothing to remove.
Hosting under a path prefix
Section titled “Hosting under a path prefix”For an app reached at /workbench, configure ASP.NET Core’s UsePathBase before UseRouting and UseCratisArc. It removes the prefix from the request path before endpoint matching; leave Arc’s generated API route prefix (normally /api) unchanged.
var builder = WebApplication.CreateBuilder(args);builder.AddCratisArc();
var app = builder.Build();app.UsePathBase("/workbench");app.UseRouting();app.UseCratisArc();app.Run();Set <Arc apiBasePath="/workbench"> in the frontend and set the application router’s basename to /workbench separately. Neither setting changes the server’s routes; see React provider configuration. If a reverse proxy already removes the prefix, the server sees unprefixed paths, so configure the frontend and ingress accordingly.
Environment-Specific Configuration
Section titled “Environment-Specific Configuration”You can use different configurations for different environments using the standard ASP.NET Core configuration pattern:
appsettings.json (base configuration):
{ "Cratis": { "Arc": { "GeneratedApis": { "RoutePrefix": "api" } } }}appsettings.Development.json (development overrides):
{ "Cratis": { "Arc": { "CorrelationId": { "HttpHeader": "X-Dev-Correlation-ID" } } }}appsettings.Production.json (production overrides):
{ "Cratis": { "Arc": { "GeneratedApis": { "RoutePrefix": "v1" } } }}Route Generation Examples
Section titled “Route Generation Examples”The GeneratedApis configuration affects how routes are generated for your commands and queries. Here are some examples:
Given a command class MyApp.Sales.Commands.CreateOrderCommand:
Default Configuration
Section titled “Default Configuration”{ "GeneratedApis": { "RoutePrefix": "api", "SegmentsToSkipForRoute": 0, "IncludeCommandNameInRoute": true, "IncludeQueryNameInRoute": true }}Generated route: /api/my-app/sales/commands/create-order-command
Skip Namespace Segments
Section titled “Skip Namespace Segments”{ "GeneratedApis": { "RoutePrefix": "api", "SegmentsToSkipForRoute": 2, "IncludeCommandNameInRoute": true, "IncludeQueryNameInRoute": true }}Generated route: /api/commands/create-order-command (skips MyApp and Sales).
Exclude Type Names
Section titled “Exclude Type Names”{ "GeneratedApis": { "RoutePrefix": "api", "SegmentsToSkipForRoute": 2, "IncludeCommandNameInRoute": false, "IncludeQueryNameInRoute": false }}Generated route: /api/commands for this single command. A single query with location MyApp.Sales.Queries would use /api/queries with these options.
For example, with the configuration above:
- If you have only
MyApp.Sales.Commands.CreateOrder, the route will be/api/commands - If you have both
MyApp.Sales.Commands.CreateOrderandMyApp.Sales.Commands.UpdateOrder, the routes will be/api/commands/create-orderand/api/commands/update-orderrespectively (type names added automatically to avoid conflict)
JSON Serialization
Section titled “JSON Serialization”Arc-generated endpoints use ArcOptions.JsonSerializerOptions. Arc’s MVC post-configuration copies only the property naming policy and appends Arc converters; it does not copy the entire options object. Settings such as DefaultIgnoreCondition, NumberHandling, and other MVC serializer options require separate MVC configuration if controller output must match. Plain ASP.NET minimal API endpoints receive only Arc’s concept converters by default (single concepts, concept collections and dictionaries keyed by concepts). Concepts in request and response bodies use their underlying primitive JSON values; other types retain ASP.NET’s JSON defaults. Manual serialization must explicitly use the Arc options.
Default Configuration
Section titled “Default Configuration”Arc’s own JsonSerializerOptions has the following defaults (not all are copied to MVC):
- Property Naming: Camel case with acronym-friendly handling (e.g.,
XMLParserbecomesxmlParser) - Null Handling: Null values are ignored when writing JSON
- Enums: Serialized as integers (not strings)
- Concepts: Full support for Cratis Concepts (strongly-typed primitives)
- Date/Time: Support for
DateOnlyandTimeOnlytypes - Types: Support for
System.TypeandSystem.Uriserialization - Derived Types: Polymorphic serialization support when derived types are discovered
Accessing JsonSerializerOptions
Section titled “Accessing JsonSerializerOptions”The configured JsonSerializerOptions is available through dependency injection:
using System.Text.Json;
public class MessageSerializer(JsonSerializerOptions jsonOptions){ public string Serialize(string message) => JsonSerializer.Serialize(new { Message = message }, jsonOptions);}Or through ArcOptions:
public class MyService{ public MyService(IOptions<ArcOptions> arcOptions) { var jsonOptions = arcOptions.Value.JsonSerializerOptions; }}Customizing JSON Serialization
Section titled “Customizing JSON Serialization”You can add custom converters or modify the configuration through the options pattern:
builder.AddCratisArc(options =>{ // Add a custom converter options.JsonSerializerOptions.Converters.Add(new MyCustomConverter());});For controllers, configure null omission, number handling, and other non-copied settings separately through MVC’s AddJsonOptions. Arc’s post-configuration still sets the naming policy, removes the first JsonStringEnumConverter if present, and appends Arc converters. ConfigureHttpJsonOptions configures ASP.NET minimal API serialization; it does not replace Arc’s serializer for generated endpoints. For minimal APIs, ASP.NET Core’s camel-case naming remains unchanged, including when you explicitly choose JsonNamingPolicy.CamelCase. Configure ConfigureHttpJsonOptions to choose another naming policy, opt out of primitive concept serialization by registering an application converter for that concept type (for example, a JsonConverter<AccountId> that writes and reads { "value": "..." }), or set null omission, number handling, and other serializer settings. Application converters keep their order and take precedence over Arc’s appended concept converters. Converter order matters: the first matching converter wins. Enum, date/time, URI, type, geospatial and derived-type converters are not added to plain minimal APIs; configure those explicitly if needed. Test the actual request/response contract; see OpenAPI enum schemas.
Adding Source-Generated Metadata
Section titled “Adding Source-Generated Metadata”Arc resolves the metadata for its own wire types - CommandResult, QueryResult, change sets, observable query messages and the identity responses - from a source-generated JsonSerializerContext it ships. Everything else, such as your read models, command responses and query arguments, resolves through reflection. In a trimmed or NativeAOT application reflection-based serialization is disabled, so Arc needs metadata for those types from you.
Declare a JsonSerializerContext for your types and add it with AddJsonTypeInfoResolver:
using System.Text.Json.Serialization;using Cratis.Arc.Commands;
[JsonSourceGenerationOptions(GenerationMode = JsonSourceGenerationMode.Metadata)][JsonSerializable(typeof(Order))][JsonSerializable(typeof(IEnumerable<Order>))][JsonSerializable(typeof(PlaceOrder))][JsonSerializable(typeof(CommandResult<OrderId>))]public partial class AppJsonSerializerContext : JsonSerializerContext;builder.AddCratisArc(options => options.AddJsonTypeInfoResolver(AppJsonSerializerContext.Default));Order and PlaceOrder stand in for your own read model and command, and OrderId for the response PlaceOrder returns. Arc’s JsonSerializerOptions look up a type’s metadata in this order:
- The resolvers you add with
AddJsonTypeInfoResolver. Your contracts win, including any you customize for Arc’s own types. They are consulted first whether you add them before or after you wrap, replace or clearTypeInfoResolver; added while Arc’s resolver is still in the chain, they are consulted in the order you add them. - Resolvers you append to
JsonSerializerOptions.TypeInfoResolverChainwithAdd, in the order you append them. - Arc’s source-generated metadata for its own wire types.
- The reflection-based resolver, only when reflection-based serialization is enabled (
JsonSerializer.IsReflectionEnabledByDefault). This is the resolverSystem.Text.Jsonfalls back to for options without one, so an application that adds nothing serializes exactly as before.
The serializer options still decide naming, null handling, number handling and converters, so the JSON is the same whichever resolver supplies the metadata. Use JsonSourceGenerationMode.Metadata, as Arc does. Arc’s options carry custom converters, so System.Text.Json cannot use a generated serialization fast path with them; generating one only adds code.
Keep these in mind:
- Register
CommandResult<TResponse>for every response type your commands return, such asCommandResult<OrderId>above. Arc writes a command’s response wrapped inCommandResult<TResponse>, whereTResponseis the response’s runtime type - a generic type its own metadata cannot cover, so registeringTResponsealone is not enough. Without it, a command that returns a response fails with aNotSupportedExceptionwhen reflection is disabled. - Register the runtime types of query data, not only the declared ones.
QueryResult.Dataand change-set items are written by their runtime type, so if a query returns aList<Order>, registerList<Order>. - Add resolvers while configuring Arc.
JsonSerializerOptionsbecome read-only once they have been used. - Arc keeps a single resolver of its own in
TypeInfoResolverChain, after the resolvers added withAddJsonTypeInfoResolverand before anything you append. That resolver consults the resolvers appended after it first, so a resolver you append - for instance aDefaultJsonTypeInfoResolverwithModifiersthat rename or ignore members - still decides the contract of every type it knows, Arc’s own included, as it did when it was the only resolver in the chain. Types it does not know fall through to Arc’s metadata and, when enabled, reflection. The same holds for a copy of Arc’s options,new JsonSerializerOptions(options): a resolver appended to the copy wins for the copy and leaves Arc’s options alone. - Assigning
JsonSerializerOptions.TypeInfoResolverreplaces the whole chain, Arc’s resolver included. The resolver you assign then serializes every type on its own, so with reflection disabled it has to cover Arc’s wire types too. PreferAddJsonTypeInfoResolver. - To add modifiers to every contract, wrap the resolver the options carry:
options.TypeInfoResolver = options.TypeInfoResolver!.WithAddedModifier(...). Arc’s resolver stays inside the wrapper, so the modifiers apply to what it resolves too. On .NET 8 and .NET 9, wrapTypeInfoResolverbefore appending anything toTypeInfoResolverChain, or build the combination explicitly withJsonTypeInfoResolver.Combine(...): wrapping after appending crashes with a stack overflow on those versions. That isSystem.Text.Jsonbehavior, not Arc’s. - Arc’s options always carry a resolver.
TypeInfoResolver ??= x, orif (TypeInfoResolver is null) { ... }, therefore no longer assigns anything - useAddJsonTypeInfoResolver,TypeInfoResolverChain.Addor an explicit assignment instead. AndTypeInfoResolver?.WithAddedModifier(...)now takes effect. - Assigning
options.TypeInfoResolver = JsonTypeInfoResolver.Combine(options.TypeInfoResolver, other)flattens into Arc’s resolver followed byother, sootherwins for the types it knows, like any appended resolver. Only appending a combination that contains Arc’s resolver,options.TypeInfoResolverChain.Add(JsonTypeInfoResolver.Combine(options.TypeInfoResolver, other)), resolves in the order you compose it: Arc’s resolver, and with it Arc’s metadata and reflection, beforeother. AddJsonTypeInfoResolverresolvers are consulted first in all these cases: after you wrapTypeInfoResolverthey go ahead of the wrapper, and after you set it tonullArc’s resolver is composed anew behind them, so Arc’s metadata and the reflection fallback are kept. Once you have wrapped the resolver, a resolver you add later is consulted before the ones you added earlier.- The resolvers apply to Arc’s own endpoints and serialization. MVC controllers and plain minimal APIs keep their own serializer options.
When you create options with ConfigureArcDefaults() outside ArcOptions, a resolver the options already carry stays first, followed by Arc’s resolver: new JsonSerializerOptions { TypeInfoResolver = AppJsonSerializerContext.Default }.ConfigureArcDefaults().
Best Practices
Section titled “Best Practices”-
Use Configuration Files: For most scenarios, use
appsettings.jsonconfiguration as it allows easy environment-specific overrides without code changes. -
Environment-Specific Settings: Use
appsettings.{Environment}.jsonfiles for environment-specific configurations. -
Programmatic Configuration: Use programmatic configuration when you need to:
- Set configuration based on runtime conditions
- Use custom identity providers
- Override specific settings that can’t be easily expressed in JSON
-
Route Planning: Consider your API route structure carefully when configuring
GeneratedApisoptions, especially in public-facing APIs where route stability is important. -
Header Standardization: Use standard HTTP header names for correlation IDs and tenant IDs that align with your organization’s conventions and any API gateways or load balancers in use.
-
JSON Consistency: Use the injected
JsonSerializerOptionsfor manual serialization that must match generated Arc endpoints. Configure and test MVC’s remaining options separately when controller output must match too.