Skip to content

Customize a rendered application

A rendered Cratis application owns its Program.cs, project file, appsettings.json, and frontend shell. Every application render plans those files again, so an edit to them is lost or conflicts on the next render. Put your own code in a Customizations/ folder at the application root instead. The generated files already look for it.

FileWhat it is forPicked up when
Customizations/Program.csService registrations, options, and extra endpoints or middlewareThe C# project compiles
Customizations/*.csYour adapters and other typesThe C# project compiles
Customizations/Dependencies.propsProject references, package references, and MSBuild propertiesMSBuild evaluates the project
Customizations/styles.cssFont and --cratis-* token overrides for the frontendVite builds or serves the frontend

Every file is optional. Without them, the application builds and runs as rendered. The top-level Customizations/ directory is reserved: a model that would generate files there is rejected with STAGE-CRATIS-005, including case variants.

  • Render the application with an application-scope plan from CratisRendering. The hooks are part of the application scaffold; module, feature, and slice plans do not include Program.cs or the project file.
  • Check that the rendered Program.cs calls ConfigureServices(builder); and ConfigureApplication(app);, and that .frontend/main.tsx discovers Customizations/styles.css with import.meta.glob and awaits its loader before rendering React. An application rendered before these hooks existed has neither; render it again first.

The rendered Program.cs provides the hooks:

Program.cs (managed)
using Cratis.Arc.MongoDB;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHealthChecks();
builder.AddCratis(
configureArcBuilder: arc => arc.WithMongoDB(),
configureChronicleBuilder: chronicle => chronicle.WithCamelCaseNamingPolicy());
ConfigureServices(builder);
var app = builder.Build();
app.UseDefaultFiles();
app.UseStaticFiles();
app.UseCratis();
app.MapHealthChecks("/healthz");
app.MapFallbackToFile("/index.html");
ConfigureApplication(app);
await app.RunAsync();
// Optional partial methods disappear when Customizations/Program.cs supplies no implementation.
public partial class Program
{
static partial void ConfigureServices(WebApplicationBuilder builder);
static partial void ConfigureApplication(WebApplication app);
}

ConfigureServices runs after AddCratis and before Build. ConfigureApplication runs after UseCratis and the generated endpoints, and before RunAsync. When you do not implement a hook, the compiler removes the call.

The generated /healthz endpoint runs all health checks. With Chronicle 19.59.6 or later, it reports healthy only after the client is connected and Chronicle has successfully registered the application’s artifacts, including its read models. Wait for a healthy response before querying the generated application. Earlier Chronicle versions can report healthy before registration completes.

The repository also contains a catalog adapter and stylesheet in Samples/Customization/Customizations/, ready to copy into a rendered application.

This example adds a typed HttpClient for an external exchange-rate service, binds its base address from configuration, and exposes one read endpoint. It uses only the ASP.NET Core shared framework, so it needs no extra package.

Create the options type:

Customizations/ExchangeRatesOptions.cs
namespace Customizations;
public class ExchangeRatesOptions
{
public const string Section = "ExchangeRates";
public Uri? BaseAddress { get; set; }
}

Create the adapter. A missing rate returns null; any other failed response throws, so an outage does not look like an empty result:

Customizations/ExchangeRatesClient.cs
using System.Net;
namespace Customizations;
public record ExchangeRate(string Currency, decimal Rate);
public class ExchangeRatesClient(HttpClient client)
{
public async Task<ExchangeRate?> GetRate(string currency, CancellationToken cancellationToken)
{
using var response = await client.GetAsync($"rates/{Uri.EscapeDataString(currency)}", cancellationToken);
if (response.StatusCode == HttpStatusCode.NotFound)
{
return null;
}
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<ExchangeRate>(cancellationToken);
}
}

Implement the hooks:

Customizations/Program.cs
using Customizations;
using Microsoft.Extensions.Options;
public partial class Program
{
static partial void ConfigureServices(WebApplicationBuilder builder)
{
builder.Services
.AddOptions<ExchangeRatesOptions>()
.Bind(builder.Configuration.GetSection(ExchangeRatesOptions.Section))
.Validate(options => options.BaseAddress is not null, "ExchangeRates:BaseAddress is required.")
.ValidateOnStart();
builder.Services.AddHttpClient<ExchangeRatesClient>((services, client) =>
client.BaseAddress = services.GetRequiredService<IOptions<ExchangeRatesOptions>>().Value.BaseAddress);
}
static partial void ConfigureApplication(WebApplication app)
{
app.MapGet(
"/api/customizations/exchange-rates/{currency}",
async (string currency, ExchangeRatesClient rates, CancellationToken cancellationToken) =>
await rates.GetRate(currency, cancellationToken) is { } rate ? Results.Ok(rate) : Results.NotFound());
}
}

Keep Customizations/Program.cs in the global namespace, with no namespace declaration, and declare the class as public partial class Program or partial class Program. The method signatures must match the managed declarations exactly. A namespace or a changed signature fails the build with CS0759.

The endpoint sits under /api because the generated Vite development server proxies /api to the backend. Pick a path that cannot collide with a route Arc generates from your model.

Supply configuration without editing appsettings.json

Section titled “Supply configuration without editing appsettings.json”

appsettings.json is managed. Supply adapter settings through any other ASP.NET Core configuration source, such as environment variables:

Terminal window
export ExchangeRates__BaseAddress=https://rates.example.com/v1/
dotnet run

End the base address with / so the relative rates/... request keeps the /v1/ segment. With the validation above, the host refuses to start when the setting is missing instead of failing on the first request. Keep secrets in a secret store or environment variables, not in files under Customizations/.

The rendered project file imports Customizations/Dependencies.props when it exists. Use it to move adapters into their own project or to add a package the shared framework does not provide:

Customizations/Dependencies.props
<Project>
<ItemGroup>
<ProjectReference Include="$(MSBuildThisFileDirectory)../../Adapters/Adapters.csproj" />
</ItemGroup>
</Project>

Replace the path with your own project. $(MSBuildThisFileDirectory) anchors the path to the Customizations/ folder, so this example points at an Adapters folder next to the rendered application.

The rendered application disables central package management, so a PackageReference here needs an explicit Version. The import is the last element of the project body: a property you set in this file replaces the generated value, so set only what you mean to change.

The managed .frontend/index.css imports @cratis/components/tokens, then @cratis/components/styles, then sets the default font. It does not import @cratis/components/theme. .frontend/main.tsx then loads Customizations/styles.css through an awaited lazy import when the file exists. React does not render until that import completes, so your declarations load after the defaults.

Override the root font and the --cratis-* tokens your product needs:

Customizations/styles.css
:root {
font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
font-size: 16px;
--cratis-primary-color: #0f766e;
--cratis-primary-color-text: #ffffff;
--cratis-action-background: #0f766e;
--cratis-action-background-hover: #115e59;
--cratis-action-background-active: #134e4a;
--cratis-action-text: #ffffff;
--cratis-focus-ring: 0 0 0 3px #0f766e;
--cratis-border-radius: 4px;
}

To map a larger design system, follow Build a product theme and check token names in the Cratis token reference. The pinned Components version defines the token names; a name it does not use has no effect.

The file is plain CSS processed by Vite. It can @import other CSS files beside it with a relative path. It cannot add npm dependencies: package.json is managed, so a design-system package that is not already installed is not available. After you create or delete Customizations/styles.css, restart npm run dev so Vite re-evaluates the import.

A Stage plan never contains a path under Customizations/. Stage also does not read your files: it cannot report a broken customization, and it produces the same managed bytes whether or not the folder exists. A mistake shows up when you build or run the application.

CratisRendering does not write files. Whether Customizations/ survives a render depends on the caller that publishes the plan, such as the Cratis CLI or Studio. A publisher that writes only the planned paths leaves your files alone. A publisher that deletes files it did not plan, or that replaces the whole output folder, removes them. Keep Customizations/ under version control and check your publisher’s behavior before rendering over an application that has one.

  • Hooks do not bypass model admission. Event-triggered Automation slices render their reactions and own artifacts; captures, non-event triggers, opaque reaction bodies and Translate slices still fail STAGE-ESM-024. Registering a service does not bind it to a modeled file realization or make an unsupported construct renderable. See Event reaction rendering for the supported shape.
  • No authorization is applied to your code. Endpoints you map are anonymous unless you secure them with ASP.NET Core authorization yourself. Modeled authorization covers only the artifacts Stage renders.
  • Middleware order is fixed. ConfigureApplication runs last, so middleware you add there runs after the generated middleware. You cannot place middleware before static files or Cratis through this hook.
  • Registrations can replace Cratis services. ConfigureServices runs after AddCratis, and the last registration of a service wins. Register your own abstractions; do not re-register Cratis types unless you intend to replace them.
  • The frontend shell and screens stay managed. Customizations/styles.css changes styling only. Screens come from scene.json and the generated bindings.