# ADR: Program.cs, explained for a new developer

Status: accepted, 2026-09-02, shipped as 1.0.0.24; rewritten on 2026-09-28 for 1.0.3.39, when the file
became a composition root and its parts moved to files of their own (ADR: The composition root, split by
job). Written at Steve's request for a developer new to ASP.NET Core, or new to this codebase, who opens
the host file and wants to know what each part does and why it is the way it is.

## In plain words

Program.cs is the file that starts the API. It reads like a table of contents, one line per part of the app, and this page walks each line and the file it leads to, with the code shown live from the running build.

What that is worth: a developer new to ASP.NET Core or to this code finds their way in one read, and the organization's newest hire can find where a change belongs without asking.

## Context

Program.cs is the file that starts the API. It is now a table of contents: the builder, one line per
concern that gets registered, the host built and started, the middleware, one line per feature that maps
routes, and `app.Run()`. Each line leads to the file that shows how. This record walks those lines in
order, and the samples are those files, read from this build.

## The walk

### The files are found by walking up

The dataset, the README, the docs and the photo manifest are located once, at startup, in
`Composition/HostPaths.cs`. The API does not assume a fixed folder depth, because it is started from three
places: `dotnet run` from the project folder, the test host from `bin/Debug/...`, and the published image
from `/app`. `FindUpward` walks from the content root toward the disk root until it finds the named file,
and throws a clear `FileNotFoundException` if it never does. `HostPaths` is registered, so an endpoint that
serves a file asks for it as a parameter.

```csharp
// Started from three different folders (dotnet run, the test host's bin
// directory, /app in the image), so nothing may assume a fixed depth. Walking
// up until the file appears works from all three, and the throw names what
// was missing instead of failing later as a null.
private static string FindUpward(string startDirectory, string relativePath)
{
    for (var dir = new DirectoryInfo(startDirectory); dir is not null; dir = dir.Parent)
    {
        string candidate = Path.Combine(dir.FullName, relativePath);
        if (File.Exists(candidate))
        {
            return candidate;
        }
    }
    throw new FileNotFoundException($"Could not locate {relativePath} in or above {startDirectory}");
}
```
*Live from [`api/TheYard.Api/Composition/HostPaths.cs`](https://github.com/SteveStout/TheYard/blob/1d6b50b/api/TheYard.Api/Composition/HostPaths.cs#L49-L64), region `find-upward`, read from this build at 1d6b50b.*

### The table of contents

What the file is today:

```
54 total
  7 comment
  6 blank
  41 code, across 0 endpoints
```

No route is mapped in Program.cs itself. Until 1.0.3.39 it was one file of 2,667 lines holding 48
endpoints; the record that replaced that decision says what changed and holds the new shape with a test.

```csharp
var builder = WebApplication.CreateBuilder(args);
var host = new YardComposition
{
    Paths = HostPaths.Find(builder.Environment.ContentRootPath),
    Build = new BuildInfo(
        Environment.GetEnvironmentVariable("APP_VERSION") ?? "dev",
        Environment.GetEnvironmentVariable("APP_COMMIT") ?? "local"),
};
builder.Services.AddSingleton(host.Paths);
builder.Services.AddSingleton(host.Build);

await builder.AddTheYardStoresAsync(host);    // Composition/StoreRegistration.cs
builder.AddTheYardObservability(host);        // Composition/ObservabilityRegistration.cs
builder.AddTheYardSessions(host);             // Composition/AuthRegistration.cs
builder.AddTheYardActivity(host);             // Composition/ActivityRegistration.cs
builder.AddTheYardEmail();                    // Composition/EmailRegistration.cs
builder.AddTheYardAccounts(host);             // Composition/AuthRegistration.cs
builder.AddTheYardApi(host);                  // Composition/ApiRegistration.cs
builder.AddTheYardTelemetry();                // Composition/TelemetryRegistration.cs
builder.AddTheYardAdmin(host);                // Composition/AdminRegistration.cs
builder.AddTheYardCosts();                    // Composition/CostRegistration.cs

var app = builder.Build();
await app.StartTheYardAsync(host);            // Composition/Startup.cs
app.UseTheYardRequestPipeline(host);          // Composition/RequestPipeline.cs
```
*Live from [`api/TheYard.Api/Program.cs`](https://github.com/SteveStout/TheYard/blob/1d6b50b/api/TheYard.Api/Program.cs#L8-L32), region `composition`, read from this build at 1d6b50b.*

### No class, no Main

The file uses top-level statements: C# lets the entry file read like a script, and the compiler writes
the `Program` class and `Main` around it. The one visible trace is the last line,
`public sealed partial class Program;`, which exists so the integration tests can name the class when they
boot the host in memory with `WebApplicationFactory<Program>`.

```csharp
/// <summary>
/// The app's entry point, exposed so the integration tests can boot the real host.
/// </summary>
public sealed partial class Program;
```
*Live from [`api/TheYard.Api/Program.cs`](https://github.com/SteveStout/TheYard/blob/1d6b50b/api/TheYard.Api/Program.cs#L50-L53), region `records-and-test-hook`, read from this build at 1d6b50b.*

### What the steps hand one another

Registration is a sequence, and a later step needs what an earlier one made: the accounts need the
signing key, the kept log needs the stores, the page sweep needs the build's version. `YardComposition`
carries those between the steps, filled once at startup and never read by a request except through what
was registered. It is the one class in the composition with state, which is why it lives beside the other
state types at the project's root and not in `Composition/`, where every class is static.

### The stores come first

`builder.AddTheYardStoresAsync` (Composition/StoreRegistration.cs) is the only step that waits: it brings
both stores up before anything is registered, because the answer decides what gets registered. The
relational store is SQL Server when the deploy gave one and SQLite otherwise; the document store is there
when an account is configured; each stands behind the same ports, on the JSON files until it comes up, and
a store that refused at startup is asked again every thirty seconds for an hour (the second chance).

```csharp
// The relational store, on SQL Server or SQLite. A factory rather than a
// scoped context: the two sources and the bid store are singletons that each
// want a context for one operation, and there is no request scope at startup
// when the catalogue is read. The interceptor puts every statement on the Admin
// tab; it is attached here so YardConnection stays a description of where the
// database is.
// How long the first of the five asks waits after a refusal (two seconds unless the
// configuration shortens it, which only a test that proves the fallback does).
var firstWait = StorePrepare.FirstWaitFrom(builder.Configuration);
var sqlStartup = new StartupTimings();
var sqlState = await sqlStartup.Time("prepare", () => StorePrepare.WithTriesAsync(() => YardDatabase.PrepareAsync(yard, seedVehicles, seedPhotos), firstWait));
// The backend stands on the files until its store is attached: the same two
// ports answered out of the JSON, the null bid store, no accounts, with the
// synthetic scale-up still decorating the vehicle source (ADR: The relational
// store). Attached at once below when the store came up, or by the second
// chance when it comes up later (ADR: The relational store, the addendum on
// the second chance).
var sqlBackend = new Backend
{
    Key = "sql",
    Name = yard.Describe(),
    Database = sqlState,
    Startup = sqlStartup,
    Inventory = new InventoryService(new SyntheticVehicleSource(seedVehicles, targetCount), seedPhotos),
    Bids = new BidService(NullBidStore.Instance),
    Market = new MarketService(builder.Configuration.GetValue("Market:GraceSeconds", MarketService.DefaultGraceSeconds)),
    Probe = () => Task.FromResult(false),
    UserStore = _ => null,
};
// Everything that stands on the relational store, built from a state that
// says it came up. The loggers are attached the moment the host exists (the
// factory is read at call time, so an attach after startup gets them too).
StoreAttachment SqlAttachment(DatabaseState state)
{
    var sqlOptions = new DbContextOptionsBuilder<YardDbContext>();
    yard.Configure(sqlOptions);
    sqlOptions.AddInterceptors(new SqlLogInterceptor(sqlLog, currentRequest));
    var contexts = new ContextFactory(sqlOptions.Options);
    // The same database without the interceptor, for the activity counters:
    // a batch every five seconds would fill the SQL log with the feature that
    // reads the SQL log, the observer effect the request ring designed out
    // (ADR: Site activity, and the line an address does not cross).
    var quietOptions = new DbContextOptionsBuilder<YardDbContext>();
    yard.Configure(quietOptions);
    var quietContexts = new ContextFactory(quietOptions.Options);
    if (host.HostLoggers is not null)
    {
        contexts.Attach(host.HostLoggers);
    }
    return new StoreAttachment(
        state,
        contexts,
        quietContexts,
        new InventoryService(new SyntheticVehicleSource(new EfVehicleSource(contexts), targetCount), new EfPhotoManifestSource(contexts)),
        new BidService(new EfBidStore(contexts)),
        new EfActivityStore(quietContexts),
        () => YardDatabase.HasSeedAsync(contexts),
        // Identity's own store over the accounts tables, given a context from the
        // request's scope. The type is the one AddEntityFrameworkStores would have
        // registered for a user type with no roles; naming it here is what lets
        // the other backend register a different one under the same interface.
        services => new Microsoft.AspNetCore.Identity.EntityFrameworkCore.UserOnlyStore<YardUser, YardDbContext, string>(
            services.GetRequiredService<YardDbContext>(),
            services.GetService<IdentityErrorDescriber>()));
}
if (sqlState.Ready)
{
    sqlBackend.Attach(SqlAttachment(sqlState));
}
backendList.Add(sqlBackend);
```
*Live from [`api/TheYard.Api/Composition/StoreRegistration.cs`](https://github.com/SteveStout/TheYard/blob/1d6b50b/api/TheYard.Api/Composition/StoreRegistration.cs#L95-L164), region `sql-backend`, read from this build at 1d6b50b.*

Nearly every registration in the steps is `AddSingleton`, one instance for the life of the process,
because the dataset is loaded once and shared by every request and each store's standing bids live in
memory beside it. `InventoryService` holds the expanded dataset in a `Lazy`, so a `Scoped` registration
would expand 100,000 records per request. The two exceptions are decided per request and can only be:
`CurrentBackend`, which store this request is on, and Identity's user store over it.

### The rest of the registrations

Each is one file and one method, in the order the host needs them:

- `AddTheYardObservability` (Composition/ObservabilityRegistration.cs): the rings behind the Admin tab,
  the kept log, and the request hook that files every request once.
- `AddTheYardSessions` and `AddTheYardAccounts` (Composition/AuthRegistration.cs): the signing key and the
  session issuer first, because the activity feature keys visitor tokens with the same key; then Identity
  and the JWT read back from its cookie.
- `AddTheYardActivity` (Composition/ActivityRegistration.cs) and `AddTheYardEmail`
  (Composition/EmailRegistration.cs): the collectors that write off the request path, and the one email.
- `AddTheYardApi` (Composition/ApiRegistration.cs): snake_case bodies, one ProblemDetails for every
  failure, one log line per request, and the OpenAPI document.
- `AddTheYardTelemetry` and `AddTheYardAdmin`: Application Insights where it is configured, and what the
  Admin tab reads that is not a ring.

### Started before it serves

`app.StartTheYardAsync` (Composition/Startup.cs) runs after `Build()` and before anything is served: the
loggers attached, a store that did not come up said out loud, and the default store's catalogue and bids
loaded and timed. If the dataset is missing or malformed the process exits with the real error, the
container's health check fails, and the deploy's verify step stops the roll, which is a better place to
learn it than a 500 on the first visitor.

### Middleware order, the part that bites

`app.UseTheYardRequestPipeline` (Composition/RequestPipeline.cs) holds every piece of middleware in one
method, in the order it runs, with the reason beside each. Timing is outermost, so a failed request is
recorded with the status that was actually sent; then the problem shape, the request log, the user, the
session's renewal, the store's warmth, the error record, the cache rules, and the files last. Endpoints
run at the end of the pipeline whichever line maps them, so the order that matters is the order in this
one file.

```csharp
// Timing, and it has to be the outermost thing here.
//
// Outermost, above UseExceptionHandler, because unwinding runs inner to outer.
// Placed below the handler, a request that threw would reach this finally
// before the handler had written anything, so every failed request would be
// recorded as a 200 with the handler's time left out. Here it records the
// status the caller actually received and the whole time they waited.
//
// Above the handler it sees the status that was actually sent. Above
// UseAuthentication too, so a request rejected with 401 is counted rather than
// short-circuited before it ever reaches the ring.
app.Use(async (context, next) =>
{
    long start = Stopwatch.GetTimestamp();
    try
    {
        await next();
    }
    finally
    {
        host.RecordRequest(context, Stopwatch.GetElapsedTime(start));
    }
});
```
*Live from [`api/TheYard.Api/Composition/RequestPipeline.cs`](https://github.com/SteveStout/TheYard/blob/1d6b50b/api/TheYard.Api/Composition/RequestPipeline.cs#L25-L47), region `request-timing`, read from this build at 1d6b50b.*

```csharp
// What: every 500 and every exception goes into the error record the Admin tab
// reads. How: it records and rethrows rather than handling, so the exception
// handler above still owns the response. Why here: directly inside the
// exception handler, so it sees a failure in anything below it, signing in and
// renewing a session included. It sat below both until the pipeline lane, and
// an exception thrown while reading a token never reached the record.
app.Use(async (context, next) =>
{
    try
    {
        await next();
        if (context.Response.StatusCode >= 500)
        {
            errorLog.Record(context.Request.Path, context.Response.StatusCode, "server error response");
        }
    }
    catch (Exception ex)
    {
        // A caller who hung up is not a server error: the exception handler
        // answers it 499 and logs it at Information, so it is left out of
        // this record too, and the Admin tab's error count stays green.
        if (!ProblemHandler.CallerLeft(context, ex))
        {
            // The type and the frames, never the message. This record is
            // served at /api/errors with no sign-in, and an exception message
            // is where a framework writes a filesystem path, a connection
            // detail, or the value that broke a constraint; the exception
            // handler keeps it out of a response for the same reason. The
            // frames are source locations this repository already publishes.
            // The message is not lost: it goes to the console and to
            // Application Insights as a structured exception, behind a sign-in
            // (ADR: Error handling, the addendum on frames).
            errorLog.Record(context.Request.Path, 500, ex.GetType().Name, StackFrames.Of(ex));
        }
        throw;
    }
});
```
*Live from [`api/TheYard.Api/Composition/RequestPipeline.cs`](https://github.com/SteveStout/TheYard/blob/1d6b50b/api/TheYard.Api/Composition/RequestPipeline.cs#L90-L126), region `error-log`, read from this build at 1d6b50b.*

### Endpoints: binding, then delegation

Each feature is a static class under `Endpoints/` with one `Map...Endpoints` method and private, named
handlers: `VehicleEndpoints`, `BidEndpoints`, `DocumentationEndpoints`, `HealthEndpoints`, `ErrorEndpoints`,
`AdminEndpoints`, `AccountEndpoints`, and `ReferenceEndpoints` for the OpenAPI document and its page. A
handler binds the request, calls the service that owns the rule, and shapes the reply; the rules live in
Domain and Application. A handler's return type is a `Results<...>` union or a typed result, which is what
the OpenAPI document reads, so naming the handlers changed nothing a client sees.

```csharp
// All filters, sorting, and paging are optional GET parameters, applied
// server-side. The default page is the top 100 by auction time (live and
// ending soonest first). Responses are an envelope: { total, vehicles },
// with each vehicle carrying the server-derived auction facts.
// e.g. /api/vehicles?make=Ford&status=live&sort=price-asc&limit=100&offset=100
private static Results<Ok<VehiclePage>, ProblemHttpResult> List(
    CurrentBackend current,
    [AsParameters] VehicleQueryParams query)
{
    if (!query.TryBuildFilter(out var filter, out var clock, out var sort, out var error))
    {
        // One failure shape for the whole API: the message a person can act on goes in
        // `detail`, never in a key only this endpoint uses.
        return TypedResults.Problem(detail: error, statusCode: 400, title: "The query could not be read");
    }
    // The auction hands back each vehicle at its standing price with its sold flag, so a
    // listing never composes the bids, the room or the sale itself.
    var result = current.Auction.Search(filter, clock, sort, query.EffectiveLimit, query.EffectiveOffset);
    return TypedResults.Ok(new VehiclePage(result.Total, [.. result.Vehicles.Select(v => VehicleWire.ToWire(v, clock))]));
}
```
*Live from [`api/TheYard.Api/Endpoints/VehicleEndpoints.cs`](https://github.com/SteveStout/TheYard/blob/1d6b50b/api/TheYard.Api/Endpoints/VehicleEndpoints.cs#L43-L62), region `inventory-endpoint`, read from this build at 1d6b50b.*

The two bid endpoints share one method, `HandleBid`, which answers three questions in order: is this
session on this store, does the vehicle exist, does the domain accept the action, on the server's clock.

```csharp
// One handler behind both bid endpoints, answering three questions in order: is this session
// on this store, does the vehicle exist, does the domain accept the action on the server's
// clock. The status codes are the contract the browser relies on: 401, 404, then 400.
private static async Task<Results<Ok<BidResult>, ProblemHttpResult>> HandleBid(
    CurrentBackend current, HttpContext http, string id, Func<Auction, StandingVehicle, AuctionClock, Task<BidOutcome>> action)
{
    string userId = http.UserId();
    // #region session-per-store
    // A session bids where its account is. The header can put one request on the other
    // store, and a bid there would be a row under an id that store has no account for. The
    // token names the store that opened it, so this check costs no lookup.
    if (!http.SessionIsOn(current.Backend))
    {
        return TypedResults.Problem(
            detail: $"This session was opened on another store. Sign in on {current.Backend.Name} to bid here.",
            statusCode: 401, title: "The bid was rejected");
    }
    // #endregion session-per-store
    var auction = current.Auction;
    var clock = Clocks.Now();
    if (auction.Find(id) is not { } vehicle)
    {
        return TypedResults.Problem(detail: "No vehicle has that id.", statusCode: 404, title: "No such vehicle");
    }
    var outcome = await action(auction, vehicle, clock);
    if (outcome.Kind == BidOutcomeKind.Rejected)
    {
        return TypedResults.Problem(detail: outcome.Reason, statusCode: 400, title: "The bid was rejected");
    }
    return TypedResults.Ok(
        new BidResult(
            outcome.Kind.ToString().ToLowerInvariant(),
            outcome.Amount,
            // The room's answer rides back with the bid, so the badge is right the moment the
            // response lands. TryGetValue rather than the indexer, because a reset from the
            // same account in a second tab can land between the bid and this read.
            auction.BidsOf(userId).TryGetValue(id, out var view) ? view : null,
            // Read again after the bid, so the price and the sold flag are the ones it left.
            VehicleWire.ToWire(auction.Find(id) ?? vehicle, clock)));
}
```
*Live from [`api/TheYard.Api/Endpoints/BidEndpoints.cs`](https://github.com/SteveStout/TheYard/blob/1d6b50b/api/TheYard.Api/Endpoints/BidEndpoints.cs#L135-L174), region `bid-handling`, read from this build at 1d6b50b.*

### Liveness, readiness, and the Admin tab

`/healthz` answers "ok" if the process is up; the container's HEALTHCHECK asks it. `/readyz` runs the
checks that gate traffic and answers 503 until the files the app needs are in place; the deploy's verify
step asks it. `/api/health` returns every check with its timing for the Admin tab.

```csharp
// Three endpoints, three audiences. /healthz is the container's HEALTHCHECK
// and answers only "the process is up". /readyz is the deploy's Verify step
// and answers 503 until the files are in place. /api/health is the Admin tab
// and carries the timings. A process can be alive and not yet ready, and the
// orchestrator treats those differently (ADR-010).
app.MapGet("/healthz", Liveness)
    .WithName("Liveness")
    .WithTags("Health")
    .WithSummary("Is the process up")
    .WithDescription("The container's own health check. Answers ok the moment the process is listening and says nothing else.")
    .Produces<string>(StatusCodes.Status200OK, "text/plain")
    // Answered by routing itself, so a poll never reads the session cookie
    // (RequestPipeline.cs, the routing region).
    .ShortCircuit();

// Only the checks that gate it, and only those get run: the database probe is
// two SQL statements whose answer readiness discards, and this endpoint is
// polled by the orchestrator and by every deploy.
app.MapGet("/readyz", Readiness)
    .WithName("Readiness")
    .WithTags("Health")
    .WithSummary("Should traffic be sent here")
    .WithDescription("The deploy's verify step. Only the checks that gate readiness run, and the database is not one "
        + "of them: a container whose store is gone still serves the catalogue from files.")
    .Produces<string>(StatusCodes.Status200OK, "text/plain")
    .ProducesProblem(StatusCodes.Status503ServiceUnavailable)
    .ShortCircuit();

app.MapGet("/api/health", Health)
    .WithName("GetHealth")
    .WithTags("Health")
    .WithSummary("Every check, timed, and the build that answered")
    .WithDescription("The Admin tab's health card. One check per store, named by the store, so a container running "
        + "both says which one is unavailable.");
```
*Live from [`api/TheYard.Api/Endpoints/HealthEndpoints.cs`](https://github.com/SteveStout/TheYard/blob/1d6b50b/api/TheYard.Api/Endpoints/HealthEndpoints.cs#L25-L58), region `probes`, read from this build at 1d6b50b.*

### The single-page app, last

`app.MapTheYardSpa` (Composition/SpaRegistration.cs) answers any address that is not an API route or a
real file with index.html, so a missing bundle file is a 404 and never a page dressed as a script.

```csharp
// The SPA fallback serves index.html for app routes only; an address that
// looks like a file (a hashed bundle name that no longer exists, say) is a
// 404, never a page dressed as a script.
app.MapFallbackToFile("{*path:nonfile}", "index.html");
```
*Live from [`api/TheYard.Api/Composition/SpaRegistration.cs`](https://github.com/SteveStout/TheYard/blob/1d6b50b/api/TheYard.Api/Composition/SpaRegistration.cs#L12-L15), region `spa-fallback`, read from this build at 1d6b50b.*

## What to change when

- **A new endpoint:** a `Map...` line and a private handler in the feature's file under `Endpoints/`, or a
  new static class there with its own `Map...Endpoints` line in Program.cs; binding and delegation only,
  and a test in `api/TheYard.Tests` that boots the host and calls it.
- **A new service:** one line in the registration step it belongs to, `AddSingleton` unless it holds
  per-request state.
- **A new piece of middleware:** in `RequestPipeline.cs`, at the place its order demands, with the reason beside it.
- **A new document:** one line in `DocumentationCatalog.cs` and one in `src/library/records.ts` or `pages.ts`; a
  test fails if the two disagree.

## Addendum, 2026-09-08: two stores in one file

The walk above still reads top to bottom, and two of its stops changed shape
when the document store moved in beside the relational one (ADR: One
container, both stores).

The persistence region no longer chooses a store. It reads both settings, the
SQL connection string and the Cosmos DB endpoint, and creates the two rings
and the request describer early, because the interceptor that feeds the SQL
ring is attached to a context factory built by hand before the container
exists. The migrate-and-seed region then builds one `Backend` per store: the
relational one always, the document one when there is an endpoint, each
brought up and timed on its own, each holding its own `InventoryService`,
`BidService` and `MarketService`, and each saying how to build Identity's
store over its accounts. The three services left the container entirely; an
endpoint takes `CurrentBackend`, which is scoped to the request and resolved
once from the request's header, cookie or the container's default.

```csharp
// The relational store, on SQL Server or SQLite. A factory rather than a
// scoped context: the two sources and the bid store are singletons that each
// want a context for one operation, and there is no request scope at startup
// when the catalogue is read. The interceptor puts every statement on the Admin
// tab; it is attached here so YardConnection stays a description of where the
// database is.
// How long the first of the five asks waits after a refusal (two seconds unless the
// configuration shortens it, which only a test that proves the fallback does).
var firstWait = StorePrepare.FirstWaitFrom(builder.Configuration);
var sqlStartup = new StartupTimings();
var sqlState = await sqlStartup.Time("prepare", () => StorePrepare.WithTriesAsync(() => YardDatabase.PrepareAsync(yard, seedVehicles, seedPhotos), firstWait));
// The backend stands on the files until its store is attached: the same two
// ports answered out of the JSON, the null bid store, no accounts, with the
// synthetic scale-up still decorating the vehicle source (ADR: The relational
// store). Attached at once below when the store came up, or by the second
// chance when it comes up later (ADR: The relational store, the addendum on
// the second chance).
var sqlBackend = new Backend
{
    Key = "sql",
    Name = yard.Describe(),
    Database = sqlState,
    Startup = sqlStartup,
    Inventory = new InventoryService(new SyntheticVehicleSource(seedVehicles, targetCount), seedPhotos),
    Bids = new BidService(NullBidStore.Instance),
    Market = new MarketService(builder.Configuration.GetValue("Market:GraceSeconds", MarketService.DefaultGraceSeconds)),
    Probe = () => Task.FromResult(false),
    UserStore = _ => null,
};
// Everything that stands on the relational store, built from a state that
// says it came up. The loggers are attached the moment the host exists (the
// factory is read at call time, so an attach after startup gets them too).
StoreAttachment SqlAttachment(DatabaseState state)
{
    var sqlOptions = new DbContextOptionsBuilder<YardDbContext>();
    yard.Configure(sqlOptions);
    sqlOptions.AddInterceptors(new SqlLogInterceptor(sqlLog, currentRequest));
    var contexts = new ContextFactory(sqlOptions.Options);
    // The same database without the interceptor, for the activity counters:
    // a batch every five seconds would fill the SQL log with the feature that
    // reads the SQL log, the observer effect the request ring designed out
    // (ADR: Site activity, and the line an address does not cross).
    var quietOptions = new DbContextOptionsBuilder<YardDbContext>();
    yard.Configure(quietOptions);
    var quietContexts = new ContextFactory(quietOptions.Options);
    if (host.HostLoggers is not null)
    {
        contexts.Attach(host.HostLoggers);
    }
    return new StoreAttachment(
        state,
        contexts,
        quietContexts,
        new InventoryService(new SyntheticVehicleSource(new EfVehicleSource(contexts), targetCount), new EfPhotoManifestSource(contexts)),
        new BidService(new EfBidStore(contexts)),
        new EfActivityStore(quietContexts),
        () => YardDatabase.HasSeedAsync(contexts),
        // Identity's own store over the accounts tables, given a context from the
        // request's scope. The type is the one AddEntityFrameworkStores would have
        // registered for a user type with no roles; naming it here is what lets
        // the other backend register a different one under the same interface.
        services => new Microsoft.AspNetCore.Identity.EntityFrameworkCore.UserOnlyStore<YardUser, YardDbContext, string>(
            services.GetRequiredService<YardDbContext>(),
            services.GetService<IdentityErrorDescriber>()));
}
if (sqlState.Ready)
{
    sqlBackend.Attach(SqlAttachment(sqlState));
}
backendList.Add(sqlBackend);
```
*Live from [`api/TheYard.Api/Composition/StoreRegistration.cs`](https://github.com/SteveStout/TheYard/blob/1d6b50b/api/TheYard.Api/Composition/StoreRegistration.cs#L95-L164), region `sql-backend`, read from this build at 1d6b50b.*

```csharp
// The document store, when there is an account to talk to. Same question, same
// answer shape, other store: are the containers there with the keys this code
// was written for, and is the seed in them. A refusal falls through to the
// file-backed catalogue exactly as a missing schema does on SQL Server.
CosmosStore? cosmos = null;
StoreSecondChance.Plan? cosmosSecondChance = null;
if (cosmosConfigured)
{
    cosmos = CosmosStore.Connect(
        configuredCosmos!,
        builder.Configuration["Cosmos:Database"] ?? "theyard",
        builder.Configuration["Cosmos:ContainerPrefix"] ?? "",
        // "managed-identity" on the deployed container; anything else, which
        // is what a developer's machine and the test runner have, means the
        // signed-in Azure CLI session.
        builder.Configuration["Cosmos:Credential"] ?? "azure-cli",
        YardComposition.AzureClientId(builder.Configuration),
        storeLog);
    // Filed under the request that caused it from the first operation on: the
    // describer exists before the store does now, so no operation is
    // attributed to nobody (ADR: What the store is actually doing).
    cosmos.CurrentRequest = currentRequest;
    var cosmosStartup = new StartupTimings();
    var cosmosState = await cosmosStartup.Time("prepare", () => StorePrepare.WithTriesAsync(() => cosmos.PrepareAsync(seedVehicles, seedPhotos), firstWait));
    var store = cosmos;
    // The same shape as the relational backend: the files until the store is
    // attached, at once when it came up, by the second chance when it did not
    // (ADR: A second store on Cosmos DB, and what it costs).
    var cosmosBackend = new Backend
    {
        Key = "cosmos",
        Name = "Azure Cosmos DB",
        Database = cosmosState,
        Startup = cosmosStartup,
        Cosmos = cosmos,
        Inventory = new InventoryService(new SyntheticVehicleSource(seedVehicles, targetCount), seedPhotos),
        Bids = new BidService(NullBidStore.Instance),
        Market = new MarketService(builder.Configuration.GetValue("Market:GraceSeconds", MarketService.DefaultGraceSeconds)),
        Probe = () => Task.FromResult(false),
        UserStore = _ => null,
    };
    StoreAttachment CosmosAttachment(DatabaseState state) => new(
        state,
        null,
        null,
        // The same three ports, answered out of the document store.
        new InventoryService(new SyntheticVehicleSource(new CosmosVehicleSource(store), targetCount), new CosmosPhotoManifestSource(store)),
        new BidService(new CosmosBidStore(store)),
        new CosmosActivityStore(store),
        store.ProbeAsync,
        // One document per account and one per address, and none of Identity's
        // seven tables (ADR: Accounts on a document store).
        _ => new CosmosUserStore(store));
    if (cosmosState.Ready)
    {
        cosmosBackend.Attach(CosmosAttachment(cosmosState));
    }
    backendList.Add(cosmosBackend);
    cosmosSecondChance = new StoreSecondChance.Plan(
        cosmosBackend,
        () => store.PrepareAsync(seedVehicles, seedPhotos),
        async state =>
        {
            var attachment = CosmosAttachment(state);
            await attachment.Inventory.WarmAsync();
            await attachment.Bids.LoadAsync();
            cosmosBackend.Attach(attachment);
        });
}
```
*Live from [`api/TheYard.Api/Composition/StoreRegistration.cs`](https://github.com/SteveStout/TheYard/blob/1d6b50b/api/TheYard.Api/Composition/StoreRegistration.cs#L168-L236), region `cosmos-backend`, read from this build at 1d6b50b.*

Identity is registered once and unconditionally; its store is the one thing
about it chosen per request. The health check runs one database probe per
store. The metrics endpoint answers the store the request is on at the top
level, as it always did, and lists every store below it. And two endpoints
were added for the toggle at the top of the page.

```csharp
// The Store bar at the top of the page (ADR: One container, both stores, and
// its addendum on the toggle moving to the sites). What stores this container
// runs, which one is this site's default, which one this request is on, and
// the other site's address. There is no switch endpoint: the bar's other
// segment is a link to the other site, so the address bar changes and each
// site stays one store's site. A stale yard-store cookie is expired here, on
// the first page load that carries it.
app.MapGet("/api/stores", Stores)
    .WithName("GetStores")
    .WithTags("Stores")
    .WithSummary("Which stores this container runs, and which one this request is on")
    .WithDescription("A request names a store with the X-Yard-Store header (sql or cosmos) or gets the site's default. "
        + "The other site, if there is one, is the same code with the other default.");
```
*Live from [`api/TheYard.Api/Endpoints/AdminEndpoints.cs`](https://github.com/SteveStout/TheYard/blob/1d6b50b/api/TheYard.Api/Endpoints/AdminEndpoints.cs#L94-L106), region `stores-endpoints`, read from this build at 1d6b50b.*

The numbers in the section above are the file's numbers on the day this
addendum was written; the test that holds them to the file does not care
which day that was.

## Addendum, 2026-09-09: one endpoint for the bar, not two

The toggle moved to the sites the next morning (ADR: One container, both
stores, the addendum of that name), and the second of the two endpoints
above went with it. `CurrentBackend` is resolved from the request's header
or the container's default now, not from a cookie; `POST /api/stores/select`
is gone, and `GET /api/stores` expires the cookie the old toggle set when a
request still carries one. The live block above shows what is left.

## Addendum, 2026-09-15: the wire shape is a record now

The paragraph on `wireFormat` above says `VehicleWire.ToWire` serialises each
vehicle to a JSON node and appends the auction facts, which is why the host's
options could not reach it. That stopped being how it works when the API
started describing itself (ADR: The API describes itself): `ToWire` builds a
`VehicleView`, a record with the dataset's twenty-nine fields and the five
derived ones, and the public endpoints answer through `TypedResults.Ok`, which
serialises with the host's own options, the same snake_case policy that
`ConfigureHttpJsonOptions` sets for request bodies, and which carries the
response type into the document where `TypedResults.Json` carries nothing.
`wireFormat` stays for the `Results.Json` calls the operator endpoints still
make, and both paths apply the one policy. Neither the names nor the order
of the values on the wire changed.

## Addendum, 2026-09-30: the request pipeline, by name

Naming the same file in The Shed, Steve pointed out that the word pipeline can mean too many things to be clear. This
site alone serves ADR: The deploy pipeline beside this record. The middleware file is now
`Composition/RequestPipeline.cs` and the call `app.UseTheYardRequestPipeline(host)`, ASP.NET Core's own
name for it ("The ASP.NET Core request pipeline consists of a sequence of request delegates", Microsoft
Learn, ASP.NET Core Middleware). Nothing in the order changed; the file moved and its type was renamed.

## Addendum, 2026-09-30: documentation, spelled out

Naming the same kind of file in The Shed, Steve pointed out that "Docs" was an abbreviation and hid
what the file does. The endpoints that serve the records, their pictures and diagrams, the Bicep, the resume and
/about are now `Endpoints/DocumentationEndpoints.cs`, mapped by `app.MapDocumentationEndpoints()`.
The version is not among them here (`/api/version` is in `HealthEndpoints`), so the name says
documentation and nothing more. Routes and behaviour are unchanged. The catalog those endpoints
read (slug to file, plus the diagrams) is `DocumentationCatalog.cs` for the same reason, held by
`DocumentationCatalogTests`.

## Where it sits

This record is about the host Api alone: Program.cs is a table of contents, and Composition/ is the composition root, the one place where the outer rings' classes are bound to the ports the inner rings define. That binding is dependency inversion, with Application owning the interfaces while Infrastructure and Infrastructure.Cosmos supply them through constructors, and each registration file under Composition/ has one job, which is single responsibility. The cost is more files to open than a single Program.cs, so a newcomer follows a line to its file before reading any code. A service with a handful of endpoints and one store would read better as one short Program.cs.

## Files

- [`api/TheYard.Api/Program.cs`](https://github.com/SteveStout/TheYard/blob/main/api/TheYard.Api/Program.cs): the table of contents this record walks.
- [`api/TheYard.Api/Composition`](https://github.com/SteveStout/TheYard/tree/main/api/TheYard.Api/Composition): one file per registration step, the startup, the pipeline and the single-page fallback.
- [`api/TheYard.Api/Endpoints`](https://github.com/SteveStout/TheYard/tree/main/api/TheYard.Api/Endpoints): one static class per feature.
- [`api/TheYard.Api/YardComposition.cs`](https://github.com/SteveStout/TheYard/blob/main/api/TheYard.Api/YardComposition.cs): what the steps hand one another.
- [`api/TheYard.Api/TheYard.Api.csproj`](https://github.com/SteveStout/TheYard/blob/main/api/TheYard.Api/TheYard.Api.csproj): `net10.0`, nullable reference types on, implicit usings on (which is why the file has so few `using` lines).
- [`api/TheYard.Api/VehicleQueryParams.cs`](https://github.com/SteveStout/TheYard/blob/main/api/TheYard.Api/VehicleQueryParams.cs), [`api/TheYard.Api/Clocks.cs`](https://github.com/SteveStout/TheYard/blob/main/api/TheYard.Api/Clocks.cs), [`api/TheYard.Api/VehicleWire.cs`](https://github.com/SteveStout/TheYard/blob/main/api/TheYard.Api/VehicleWire.cs): binding, the clock anchor, and the outgoing shape.
- [`api/TheYard.Application/InventoryService.cs`](https://github.com/SteveStout/TheYard/blob/main/api/TheYard.Application/InventoryService.cs): the load held once and shared, which is what makes the singleton registration matter.
- [`api/TheYard.Api/DocumentationCatalog.cs`](https://github.com/SteveStout/TheYard/blob/main/api/TheYard.Api/DocumentationCatalog.cs), [`api/TheYard.Api/LiveSamples.cs`](https://github.com/SteveStout/TheYard/blob/main/api/TheYard.Api/LiveSamples.cs), [`api/TheYard.Api/Observability.cs`](https://github.com/SteveStout/TheYard/blob/main/api/TheYard.Api/Observability.cs): the pieces the host wires.
- [`api/TheYard.Tests/AdminEndpointTests.cs`](https://github.com/SteveStout/TheYard/blob/main/api/TheYard.Tests/AdminEndpointTests.cs) and the tests beside it: every one boots this file through `WebApplicationFactory<Program>`.
- [`Dockerfile`](https://github.com/SteveStout/TheYard/blob/main/Dockerfile): the port, the provenance arguments, the HEALTHCHECK, and the files copied for the walk to find.
- [`.github/workflows/deploy.yml`](https://github.com/SteveStout/TheYard/blob/main/.github/workflows/deploy.yml): the Verify step that asks `/readyz`.
- [`docs/app-architecture/PROJECTS.md`](https://github.com/SteveStout/TheYard/blob/main/docs/app-architecture/PROJECTS.md): the layers the services come from (served as Project structure under About).

## Addendum, 2026-10-03: endpoints ask the Application ring

The section on endpoints says a handler calls the service that owns the rule. A bidding or catalogue handler now asks one Application type, `Auction` (`api/TheYard.Application/Auction.cs`), which composes the catalogue, the bids and the room in the order the rules need; the three services stay on each `Backend` and no endpoint holds them. `HandleBid` takes a function of the auction, the vehicle and the clock. The relational context factory the 2026-09-08 addendum mentions moved from the host into the relational adapter (`api/TheYard.Infrastructure/ContextFactory.cs`), because it builds `YardDbContext`; the host still creates it before the container exists and only asks it for contexts. `OnionTests` holds both changes.

## Addendum, 2026-10-07 (1.0.3.89): the catalogue loads beside the server

"Started before it serves" above stopped being true of the catalogue. `StartTheYardAsync` still attaches the loggers and says a missing store out loud before anything is served, but the default store's bids and catalogue now load beside the server rather than before it, so a roll answers in seconds instead of minutes (ADR: The ports learn to wait, the addendum on listening first). A dataset that cannot be loaded still ends the process: the failure is logged and the host is stopped, and readiness, which now waits on the catalogue, keeps the deploy's Verify step from passing.


## Addendum, 2026-10-07 (1.0.3.92): the pipeline line, reordered

`Program.cs` did not change: `app.UseTheYardRequestPipeline(host)` is still one line, and the order still lives in `Composition/RequestPipeline.cs`. What that file holds did. The files now come straight after the problem shape and the error record, before routing, the user, the session and the store, where Microsoft's middleware order puts them; routing is called out loud instead of left to run at the top; the forwarded headers run before anything that reads the visitor's address or scheme; the health probes end at routing; and the HTTP logger, which never wrote a line, is gone. Every placement carries its reason beside its line (ADR: The order of the request pipeline).

```csharp
// What: matches the request to an endpoint. How: an explicit UseRouting; left
// out, ASP.NET Core adds one at the very top. Why here: after the files, which
// never need a route, and before the user and the endpoint. The health probes
// are marked ShortCircuit (HealthEndpoints.cs), so routing answers them right
// here and they skip reading the session cookie. That is all it saves them: they
// are not under /api, so the session's renewal and the store's warmth never
// applied to them, and the timing ring already left them out by path. Anything
// that needs the matched endpoint later (a CORS policy, a rate limit, an output
// cache policy) goes below this line.
app.UseRouting();
```
*Live from [`api/TheYard.Api/Composition/RequestPipeline.cs`](https://github.com/SteveStout/TheYard/blob/1d6b50b/api/TheYard.Api/Composition/RequestPipeline.cs#L209-L218), region `routing`, read from this build at 1d6b50b.*
