# SOLID Principles

SOLID is five principles for arranging code so that it can change without breaking. The rings on the
[Onion and Clean Architecture](https://theyard.stevenstout.biz/?doc=clean-architecture) page rest on
them. This page takes the five one at a time: what each one means in plain words, where it came
from, and where it shows in TheYard, with the code read from the running build. The record behind
the page is [ADR: Onion and SOLID, how this codebase holds them](https://theyard.stevenstout.biz/?doc=adr-onion-and-solid),
which also lists where a principle was deliberately not pushed further.

## In plain words

Five habits. Give each piece of code one job. Add new behaviour by adding code, not by editing what
already works. Make sure anything that stands in for something else behaves the same way. Keep
interfaces small and shaped to the code that uses them. Have the important code depend on
interfaces it owns, never on the database or the framework underneath.

What that is worth: a developer can add a second database, a new endpoint or a new page without
reopening code that already works, and the organization gets a codebase where a change lands in one
place, is covered by the tests that already exist, and does not surprise the code around it.

### The words this page uses

- **Principle**: a rule of thumb about shape, not a law. Each one below says where TheYard stops
  applying it.
- **Responsibility**: one reason for a piece of code to change, which usually means one group of
  people who would ask for the change.
- **Interface**: in C#, a contract a class promises to keep: these methods, with these meanings.
- **Abstraction**: an interface or a base type that says what is done without saying how.
- **Substitution**: putting one implementation where another was, and nothing around it noticing.
- **Port and adapter**: the interface the inner ring declares, and the class outside that implements
  it (the words are explained on the [Onion and Clean Architecture](https://theyard.stevenstout.biz/?doc=clean-architecture#the-words-this-page-uses) page).
- **Composition root**: the one place, at startup, where the real classes are created and wired.
- **Sealed**: a C# class nothing can inherit from, so no derived type can change its meaning.

## Where the five came from

Robert C. Martin collected the five in the second half of the 1990s, starting from a 1995 newsgroup
post, and published them as the principles of object-oriented design; the letters were later lined
up into one word. Two of the five are older than the collection: Bertrand Meyer stated the open-closed
principle in 1988, and Barbara Liskov described the substitution idea in a 1987 keynote. The other
three are Martin's own.

## S, single responsibility

**What it means.** A class, or a file, has one reason to change. Martin's later restatement is the
useful one: a reason to change is a group of people, so a module should answer to one group. When
two groups can each force a change to the same file, their changes collide.

**In TheYard.** A rule lives in `TheYard.Domain` and nowhere else. `BidRules` decides whether a bid
stands. `StandingRules` holds the one rule for raising a vehicle's shown price, and three callers
(the buyer's bids, the room's bids and the room's price to beat) all raise a price through it, so
the rule has one copy and the three cannot drift apart:

```csharp
extension(Vehicle vehicle)
{
    /// <summary>
    /// The vehicle with a bid layered over it, and only when the bid is higher than the
    /// price it already shows. A lower bid leaves the vehicle as it was, so laying the
    /// buyer's bids and then the room's over the dataset always ends on the highest of the
    /// three. The bid count keeps the larger of the two counts, because both count the same
    /// auction from different sides.
    /// </summary>
    public Vehicle RaisedTo(int amount, int bidCount) =>
        amount > (vehicle.CurrentBid ?? 0)
            ? vehicle with { CurrentBid = amount, BidCount = Math.Max(vehicle.BidCount, bidCount) }
            : vehicle;

    /// <summary>Whether the reserve is met at the price this vehicle shows.</summary>
    public ReserveStatus Reserve => ReserveOf(vehicle.ReservePrice, vehicle.CurrentBid);
}
```
*Live from [`api/TheYard.Domain/StandingRules.cs`](https://github.com/SteveStout/TheYard/blob/1d6b50b/api/TheYard.Domain/StandingRules.cs#L26-L42), region `raised-to`, read from this build at 1d6b50b.*

The host keeps the same discipline at the top. `Program.cs` is a table of contents: each line is
one call, and the comment beside it names the file in `Composition/` that holds the details, one
file per job (the stores, the sessions, the activity counters, the API's shape, the Admin tab)
([ADR: The composition root, split by job](https://theyard.stevenstout.biz/?doc=adr-composition-root)):

```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.*

The front end holds it as a rule the build reads. Every file in `src/app` and `src/library` opens
with three lines, what it does, what it does not do and who uses it, and no file runs past three
hundred lines. A file that has to is named in the test with the reason it is allowed, which keeps
the reason where a reader of the rule will find it. The test reads the headers back, and checks
"Used by" against the files that really import it:

```csharp
/// <summary>The two folders the rule holds for code and stylesheets alike, relative to the repository root.</summary>
private static readonly string[] Folders = ["src/app", "src/library"];

/// <summary>What a file in them can be: code, and the stylesheets beside it.</summary>
private static readonly string[] Kinds = [".ts", ".tsx", ".css"];

/// <summary>And every stylesheet anywhere under this folder: a sheet grows a job at a time the way code does.</summary>
private const string Sheets = "src";

/// <summary>The three labels, in the order a header gives them.</summary>
private static readonly string[] Labels = ["Does:", "Does not:", "Used by:"];

/// <summary>
/// Past three hundred lines, each with why. One job per file keeps a file
/// short. The four component sheets named here each hold one
/// component's look, shared by that component and the parts split out of
/// it, and are over the line because the look is one piece; each comes
/// off this list when its sheet is split by job.
/// </summary>
private static readonly Dictionary<string, string> LongAllowed = new(StringComparer.Ordinal)
{
    ["src/components/landing/Landing/Landing.module.css"] = "the landing page's one look (hero, featured tiles, section tiles); split with Landing.tsx in the component split lane",
    ["src/components/layout/SideNav/SideNav.module.css"] = "the side rail's two shapes, one set of rows: one sheet shared by SideNav.tsx and the rows split out of it",
    ["src/components/admin/AdminPanel/AdminPanel.module.css"] = "the Admin workbench's frame: one sheet shared by AdminPanel.tsx and the parts split out of it",
    ["src/components/vehicle/VehicleDetail/VehicleDetail.module.css"] = "a vehicle's own page; split with VehicleDetail.tsx in the component split lane",
};
```
*Live from [`api/TheYard.Tests/FileHeaderTests.cs`](https://github.com/SteveStout/TheYard/blob/1d6b50b/api/TheYard.Tests/FileHeaderTests.cs#L19-L44), region `the-header-rule`, read from this build at 1d6b50b.*

**Where it stops.** `Program.cs` lists ten calls, one per area of the application, rather than ten
classes with one method each, because a list a reader can scan is the job. The record's section
[What is kept on purpose](https://theyard.stevenstout.biz/?doc=adr-onion-and-solid#what-is-kept-on-purpose)
names the others.

## O, open and closed

**What it means.** Code should be open for extension and closed for modification: when a new case
arrives, add a class behind an interface the existing code already calls, instead of editing the
class that already works. Meyer stated it for classes in 1988; in practice it is about where a new
case lands.

**In TheYard.** The second database is the proof. Azure Cosmos DB arrived as new adapter classes
behind the same three ports the relational store already implemented, and the rules and the use
cases did not change
([ADR: A second store on Cosmos DB](https://theyard.stevenstout.biz/?doc=adr-second-store)). The
composition root grew a second branch, which is where a new case is supposed to land. The relational
backend, then the document backend, as the build wires them:

```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.*

The same shape holds the documents you are reading. A new page is one row in the server's
catalogue and one entry in the front end's list of pages, and a test fails if the two ever
disagree. This page arrived that way: two rows, no change to the code that serves documents.

**Where it stops.** The ports themselves changed once for the second store: every member became a
`Task`, because the document store has no synchronous driver. That one change touched every caller
once, and the record says so plainly rather than pretending the door was never opened
([ADR: The ports learn to wait](https://theyard.stevenstout.biz/?doc=adr-ports-wait)).

## L, Liskov substitution

**What it means.** If code works with a type, it must keep working with anything that stands in for
that type, without checking which one it got. Liskov's 1987 keynote put it as a property of types:
a subtype must behave the way the supertype's users expect. For interfaces, that means any
implementation can replace any other and the callers cannot tell.

**In TheYard.** The gate is the proof. The same xUnit suite runs on both stores: once with the
relational store underneath, and again with Cosmos DB, by pointing the process at the account and
running `dotnet test` again. Not a second suite written for the second store: the same facts, the
other adapter, the same answers. The `if` in the block below is why the second pass is not run on
every version: it runs when the version touched the API, the Cosmos infrastructure or the three
specs that read both stores, and is carried forward from the version that last ran it otherwise.
Between the two passes sit the seven tests that do need the live account, which are the one set
written for the second store. From the gate's own workflow:

```yaml
read code s < <(t bash -c 'dotnet test api --no-build --filter "Store!=cosmos" --logger "trx;LogFileName=xunit-sqlite.trx" --results-directory "$RAW/trx" > "$RAW/xunit-sqlite.txt" 2>&1')
echo "xunit=$code" >> "$r"; echo "xunit_s=$s" >> "$r"
export Cosmos__AccountEndpoint="$COSMOS_ENDPOINT"
read code s < <(t bash -c 'dotnet test api --no-build --filter "Store=cosmos" --logger "trx;LogFileName=xunit-live.trx" --results-directory "$RAW/trx" > "$RAW/xunit-live.txt" 2>&1')
echo "live=$code" >> "$r"; echo "live_s=$s" >> "$r"
export Cosmos__ContainerPrefix="tests-"
if [ "$COSMOS_NEEDED" = 1 ]; then
  read code s < <(t bash -c 'dotnet test api --no-build --filter "Store!=cosmos" --logger "trx;LogFileName=xunit-cosmos.trx" --results-directory "$RAW/trx" > "$RAW/xunit-cosmos.txt" 2>&1')
  echo "oncosmos=$code" >> "$r"; echo "oncosmos_s=$s" >> "$r"
else echo "oncosmos=skipped" >> "$r"; echo "oncosmos_s=0" >> "$r"; fi
```
*Live from [`.github/workflows/gate.yml`](https://github.com/SteveStout/TheYard/blob/1d6b50b/.github/workflows/gate.yml#L123-L132), region `both-stores`, read from this build at 1d6b50b.*

`NullBidStore` is the smaller case. It implements `IBidStore` and keeps nothing, and the tests of
the use cases hand it in where a real store would go, so a use case can be tested with no database
and no mocking framework. It stands in for a real store in production too: a backend whose store has
not come up yet bids against it, and the bids are forgotten at restart. That is a deliberate limit,
written down in [ADR: The relational store](https://theyard.stevenstout.biz/?doc=adr-store).

**Where it stops.** Substitution is not complete everywhere, and the record says where: the Cosmos
DB user store does not move the email claim when an address changes, and nothing in the site changes
an address today ([Known gaps in substitution](https://theyard.stevenstout.biz/?doc=adr-onion-and-solid#what-is-kept-on-purpose)).
Sealing classes helps hold the principle from the other side: a class nothing can inherit from is a
class no derived type can change the meaning of, which is why TheYard seals by default
([Sealed by default](https://theyard.stevenstout.biz/?doc=sealed)).

## I, interface segregation

**What it means.** No class should be forced to depend on methods it does not use. Shape an interface
to the code that calls it, and prefer several small interfaces to one wide one.

**In TheYard.** The auction has three ports, not one repository, because the catalogue, the photos
and the bids are read by different code at different times:

```csharp
// The three seams the layers meet at. Application declares what it needs and
// never learns where the data lives; Infrastructure implements these against
// EF Core and SQLite, against Cosmos DB, the tests against in-memory arrays,
// and the 100,000-record scale-up is a decorator over IVehicleSource that
// nothing above it can see.
//
// Every member returns a Task, because the document store has no synchronous
// driver, and a port that held a request thread while a cloud store answered
// would stall a one-vCPU container (ADR: The ports learn to wait).
/// <summary>Port: where the vehicle dataset comes from.</summary>
public interface IVehicleSource
{
    Task<IReadOnlyList<Vehicle>> LoadAsync();
}

/// <summary>Port: where the photo manifest comes from.</summary>
public interface IPhotoManifestSource
{
    Task<IReadOnlyList<PhotoEntry>> LoadAsync();
}

/// <summary>
/// Port: where everybody's bids are kept between one run of this process and
/// the next. Read once at startup and written through on every accepted bid,
/// which is the shape the bidding path can afford (ADR: The relational store).
/// A bid belongs to a user (ADR: Accounts and per-user bids), so the key is the
/// pair and not the vehicle.
/// </summary>
public interface IBidStore
{
    Task<IReadOnlyList<StoredBid>> LoadAsync();

    Task SaveAsync(string userId, string vehicleId, BidState state);

    /// <summary>
    /// Forget one person's bids. Not everybody's: this is what the reset button
    /// on a page a stranger can also be looking at is allowed to do
    /// (ADR: Reset is one person's start-over).
    /// </summary>
    Task ClearAsync(string userId);
}

/// <summary>
/// The port wired to nothing: bids live in memory and end with the process.
/// The unit tests use it, and so does a backend whose store has not come up
/// yet; those bids are dropped when the store attaches.
/// </summary>
public sealed class NullBidStore : IBidStore
{
    public static readonly NullBidStore Instance = new();

    private NullBidStore()
    {
    }

    public Task<IReadOnlyList<StoredBid>> LoadAsync() => Task.FromResult<IReadOnlyList<StoredBid>>([]);

    public Task SaveAsync(string userId, string vehicleId, BidState state) => Task.CompletedTask;

    public Task ClearAsync(string userId) => Task.CompletedTask;
}
```
*Live from [`api/TheYard.Application/Ports.cs`](https://github.com/SteveStout/TheYard/blob/1d6b50b/api/TheYard.Application/Ports.cs#L6-L66), region `ports`, read from this build at 1d6b50b.*

`IStoreExperiment`, the port behind the Admin tab's performance proof, has one method, because the
Admin tab asks one question.

**Where it stops.** `IActivityStore` and `ILogStore` each carry one writer beside the handful of
reads their callers ask for, and are not split, because splitting them would double the interfaces
and allow no new swap. They stay whole
until a caller needs only half
([What is kept on purpose](https://theyard.stevenstout.biz/?doc=adr-onion-and-solid#what-is-kept-on-purpose)).

## D, dependency inversion

**What it means.** The important code should not depend on the details underneath it; both should
depend on an abstraction, and the abstraction should belong to the important code. In the rings,
that is the move that makes the whole shape possible: the inner ring declares the interface it
needs, the outer ring implements it, and at compile time every arrow points inward.

[![The inner ring owns the interface: Application declares IBidStore and uses it, EfBidStore and CosmosBidStore implement it from the outer ring, and the composition root decides which one each store gets](/api/docs/images/port-adapter.svg#1400x860)](https://theyard.stevenstout.biz/api/docs/diagrams/port-adapter)

*A preview. [Open the port and its adapters in a new page](https://theyard.stevenstout.biz/api/docs/diagrams/port-adapter) to zoom in and follow it.*

**In TheYard.** `TheYard.Application` declares `IBidStore`. `EfBidStore` in the relational adapter and
`CosmosBidStore` in the document adapter implement it. `Composition/StoreRegistration.cs` decides
which one a store gets, and every class receives what it needs through its constructor. Four tests
read the compiled host and fail the build if a handler reaches a database, a Cosmos DB client or the
service container, composes the bid services itself, queries a database, or registers a service
anywhere but the composition root:

```csharp
[Fact]
public void Endpoints_never_reach_a_store_or_the_service_container()
{
    // An endpoint reads the request, asks the Application ring, and writes the answer. A
    // database context, a Cosmos DB client or a service looked up by hand would make it a
    // second place where the rules and the wiring live.
    var result = Types.InAssembly(typeof(BidEndpoints).Assembly)
        .That().MeetCustomRule(IsPartOfAnEndpointClass)
        .ShouldNot().HaveDependencyOnAny(
            "Microsoft.EntityFrameworkCore",
            "Microsoft.Azure.Cosmos",
            "Microsoft.Data.SqlClient",
            "TheYard.Infrastructure.Cosmos",
            "TheYard.Infrastructure.YardDbContext",
            "TheYard.Infrastructure.ContextFactory",
            "System.IServiceProvider",
            "Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions")
        .GetResult();
    Assert.True(result.IsSuccessful, Explain(result));
}

[Fact]
public void Endpoints_reach_the_auction_only_through_the_Application_ring()
{
    // The order bids are composed in (the buyer's first, the room's second) and the price a
    // bid is measured against are Application's, in Auction. An endpoint that held the
    // services itself could compose them in another order.
    var result = Types.InAssembly(typeof(BidEndpoints).Assembly)
        .That().MeetCustomRule(IsPartOfAnEndpointClass)
        .ShouldNot().HaveDependencyOnAny(
            "TheYard.Application.BidService",
            "TheYard.Application.MarketService",
            "TheYard.Application.InventoryService")
        .GetResult();
    Assert.True(result.IsSuccessful, Explain(result));
}

[Fact]
public void The_host_never_queries_a_database_itself()
{
    // A statement against a store belongs to the adapter for that store; the host asks the
    // adapter. Entity Framework's design-time factory and the composition root may name the
    // context type, and neither runs a query.
    var result = Types.InAssembly(typeof(BidEndpoints).Assembly)
        .ShouldNot().HaveDependencyOnAny(
            "Microsoft.EntityFrameworkCore.RelationalDatabaseFacadeExtensions",
            "Microsoft.EntityFrameworkCore.RelationalQueryableExtensions",
            "Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions",
            "Microsoft.EntityFrameworkCore.DbSet",
            "Microsoft.Data.SqlClient",
            "Microsoft.Azure.Cosmos.QueryDefinition",
            "Microsoft.Azure.Cosmos.Container")
        .GetResult();
    Assert.True(result.IsSuccessful, Explain(result));
}

[Fact]
public void Services_are_registered_only_in_the_composition_root()
{
    // Registration classes in Composition/ and Program.cs are where the container is filled.
    // Anywhere else, a registration would be wiring hidden inside behaviour.
    var result = Types.InAssembly(typeof(BidEndpoints).Assembly)
        .That().MeetCustomRule(type => !IsCompositionRoot(type))
        .ShouldNot().HaveDependencyOnAny(
            "Microsoft.Extensions.DependencyInjection.IServiceCollection",
            "Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions",
            "Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions")
        .GetResult();
    Assert.True(result.IsSuccessful, Explain(result));
}
```
*Live from [`api/TheYard.Tests/OnionTests.cs`](https://github.com/SteveStout/TheYard/blob/1d6b50b/api/TheYard.Tests/OnionTests.cs#L109-L178), region `host`, read from this build at 1d6b50b.*

**Where it stops.** The server's clock is a static, read once at the edge and handed inward as a
value, rather than a `TimeProvider` injected into every handler: the rules take the clock as an
argument, which is what lets their tests fix time, and the extra parameter would buy the tests
nothing ([ADR: Onion and SOLID](https://theyard.stevenstout.biz/?doc=adr-onion-and-solid#what-is-kept-on-purpose)).

## Five principles, one table

| Letter | The principle in one line | Where it shows in TheYard | What holds it |
| --- | --- | --- | --- |
| S | One reason to change. | A rule lives in Domain once; `Program.cs` is a list; one job per front-end file. | `FileHeaderTests`, `OnionTests` |
| O | Add a class, do not edit the core. | Cosmos DB arrived as adapters behind the same ports; a page is a catalogue row. | `DocumentationCatalogTests` |
| L | A stand-in behaves the same. | The same suite runs on both stores; `NullBidStore`. | The gate's second xUnit pass |
| I | Small interfaces, shaped to the caller. | Three auction ports; a one-method experiment port. | Review, and the record |
| D | Depend on an abstraction the inner ring owns. | `IBidStore` declared inside, implemented outside, wired in `Composition/`. | `OnionTests` |

## How the build holds them

```readouts
Architecture tests | 10
File header tests | 3
Sealed-by-default tests | 5
Files with the header | 88 of 88
```

Numbers read from the build when the page is opened. The architecture tests, by name:

```text
Data_depends_on_nothing_outside_the_BCL
Domain_depends_on_nothing_outside_the_BCL_and_Data
Application_depends_only_on_Domain_Data_and_the_BCL
Domain_never_reaches_the_disk_the_network_or_the_clock
Application_never_reaches_the_disk_or_the_network
The_document_store_adapter_borrows_only_the_shared_user_from_the_relational_one
Endpoints_never_reach_a_store_or_the_service_container
Endpoints_reach_the_auction_only_through_the_Application_ring
The_host_never_queries_a_database_itself
Services_are_registered_only_in_the_composition_root
```

## Read next

- [Onion and Clean Architecture](https://theyard.stevenstout.biz/?doc=clean-architecture): the
  rings these principles produce, and TheYard's projects beside the textbook.
- [Onion architecture, the practices](https://theyard.stevenstout.biz/?doc=onion): each practice
  with a whole file to copy.
- [ADR: Onion and SOLID, how this codebase holds them](https://theyard.stevenstout.biz/?doc=adr-onion-and-solid):
  the record, the ten rules, and what is kept on purpose.

## References

- [Robert C. Martin, The Principles of OOD](http://butunclebob.com/ArticleS.UncleBob.PrinciplesOfOod):
  the five principles in one line each, from the author who collected them, with the 1995 origin.
- [Robert C. Martin, The Single Responsibility Principle](https://blog.cleancoder.com/uncle-bob/2014/05/08/SingleReponsibilityPrinciple.html)
  (2014): the restatement this page uses, a reason to change is a group of people.
- [Bertrand Meyer, Object-Oriented Software Construction](https://bertrandmeyer.com/OOSC2/)
  (1988; second edition 1997, online): the book that stated the open-closed principle.
- [Barbara Liskov, Data Abstraction and Hierarchy](https://doi.org/10.1145/62139.62141)
  (keynote 1987, published 1988): the origin of the substitution principle.
- [Architectural principles, .NET architecture guides](https://learn.microsoft.com/en-us/dotnet/architecture/modern-web-apps-azure/architectural-principles):
  Microsoft's version of the same ground, with dependency inversion and single responsibility as
  principles for a .NET application.
