# CQRS, the practices

TheYard is a used-vehicle auction platform built in the open: a .NET 10 API, a React front end, and
two databases underneath it. Every request its API answers is either a query or a command, and the
record of that decision, with what was weighed and what was left out, is
[ADR: A request is a query or a command](https://theyard.stevenstout.biz/?doc=adr-a-request-is-a-query-or-a-command).
This page is the practical half: the shape, shown with whole classes read from the running build, the
counts this build has, and what this codebase deliberately does not do.

## In plain words

A request either asks a question or asks for something to change. CQRS, command query responsibility
segregation, means writing those two kinds of request as two kinds of code. Here a question is a
*query* and a change is a *command*; each has exactly one piece of code that answers it, a *handler*,
and a query's handler is never handed anything that can write.

What that is worth: a developer can tell from one word whether a piece of code can change anything,
and can test a query without fearing it saves something; the organization gets that held by the build
on every push rather than by a reviewer reading carefully.

### The words this page uses

- **Query**: a request that reads and changes nothing. `ReadFacetsQuery`, `FindVehicleQuery`.
- **Command**: a request that changes something and may answer what it did. `PlaceBidCommand`,
  `ResetBidsCommand`.
- **Handler**: the one class that answers one request. Its name is the request's with `Handler` in
  place of the last word.
- **Port**: an interface the Application ring declares and an adapter implements, such as a store.
  A *reader* port only reads; a *store* or *writer* port can write.

## The two interfaces, and nothing else

The whole mechanism is two interfaces. There is no bus, no mediator library and no pipeline: the slice's
`Module` maps a route to its handler, the compiler checks the types, and "go to definition" on a route
lands on the code that runs.

[`api/TheYard.Application/Shared/Requests.cs`](https://github.com/SteveStout/TheYard/blob/main/api/TheYard.Application/Shared/Requests.cs):

```csharp
/// <summary>Answers one query: reads, and writes nothing. Its constructor takes reader ports only (CqrsTests).</summary>
/// <typeparam name="TQuery">The query, a record named <c>&lt;Verb&gt;&lt;Noun&gt;Query</c>.</typeparam>
/// <typeparam name="TResult">What the query answers.</typeparam>
public interface IQueryHandler<in TQuery, TResult>
{
    /// <summary>The answer to one query.</summary>
    /// <param name="query">What is asked.</param>
    /// <param name="cancellation">The request's cancellation.</param>
    Task<TResult> HandleAsync(TQuery query, CancellationToken cancellation);
}

/// <summary>Carries out one command: changes something, and answers what it did. It may take reader and writer ports.</summary>
/// <typeparam name="TCommand">The command, a record named <c>&lt;Verb&gt;&lt;Noun&gt;Command</c>.</typeparam>
/// <typeparam name="TResult">What the command answers: the outcome, or whether it did anything.</typeparam>
public interface ICommandHandler<in TCommand, TResult>
{
    /// <summary>Carries out one command.</summary>
    /// <param name="command">What is to be done.</param>
    /// <param name="cancellation">The request's cancellation.</param>
    Task<TResult> HandleAsync(TCommand command, CancellationToken cancellation);
}
```
*Live from [`api/TheYard.Application/Shared/Requests.cs`](https://github.com/SteveStout/TheYard/blob/085b3b4/api/TheYard.Application/Shared/Requests.cs#L9-L29), region `requests`, read from this build at 085b3b4.*

Greg Young's documents trace the idea to Bertrand Meyer's command-query separation, a method that
either does something or answers something, lifted from methods to objects
([CQRS Documents](https://cqrs.wordpress.com/wp-content/uploads/2010/11/cqrs_documents.pdf)). That
lift is all this codebase takes.

## A query is handed the reading side only

The catalogue's three queries. Each takes `StandingReader` or `IStockReader`, neither of which has a
method that writes, so a listing cannot place a bid however it is changed later.

[`api/TheYard.Application/Features/Vehicles/VehicleHandlers.cs`](https://github.com/SteveStout/TheYard/blob/main/api/TheYard.Application/Features/Vehicles/VehicleHandlers.cs):

```csharp
/// <summary>
/// A page of the catalogue (ADR: The onion cut into vertical slices, rule 7): filtered, sorted and
/// paged in the store, each vehicle at its standing price with its sold flag, so a listing never
/// composes the bids, the room or the sale itself.
/// </summary>
/// <param name="standings">The request store's vehicles as they stand, and the caller's badges: the reading side only.</param>
public sealed class SearchVehiclesHandler(StandingReader standings) : IQueryHandler<SearchVehiclesQuery, StandingPage>
{
    /// <summary>One page.</summary>
    /// <param name="request">The filter, the clock it was read on, the sort and the page.</param>
    /// <param name="cancellation">The request's cancellation.</param>
    public Task<StandingPage> HandleAsync(SearchVehiclesQuery request, CancellationToken cancellation) =>
        standings.SearchAsync(request.Filter, request.Clock, request.Sort, request.Limit, request.Offset, cancellation);
}

/// <summary>The distinct values behind the filters, across the whole catalogue.</summary>
/// <param name="inventory">The catalogue, reading side.</param>
public sealed class ReadFacetsHandler(IStockReader inventory) : IQueryHandler<ReadFacetsQuery, InventoryFacets>
{
    /// <summary>The makes, body styles, title statuses and provinces.</summary>
    /// <param name="query">The facets, which take nothing.</param>
    /// <param name="cancellation">The request's cancellation.</param>
    public Task<InventoryFacets> HandleAsync(ReadFacetsQuery query, CancellationToken cancellation) => inventory.FacetsAsync(cancellation);
}

/// <summary>The values behind the filters; there is nothing to ask beyond the asking.</summary>
public sealed record ReadFacetsQuery;

/// <summary>One vehicle at its standing price, on the server's clock.</summary>
/// <param name="standings">The request store's vehicles as they stand, and the caller's badges: the reading side only.</param>
/// <param name="clock">The server's clock.</param>
public sealed class FindVehicleHandler(StandingReader standings, Func<AuctionClock> clock) : IQueryHandler<FindVehicleQuery, FoundVehicle>
{
    /// <summary>The vehicle and the clock its facts are derived on, or no vehicle.</summary>
    /// <param name="query">Which vehicle.</param>
    /// <param name="cancellation">The request's cancellation.</param>
    public async Task<FoundVehicle> HandleAsync(FindVehicleQuery query, CancellationToken cancellation) =>
        new(await standings.FindAsync(query.Id, cancellation), clock());
}

/// <summary>One vehicle by its id.</summary>
/// <param name="Id">The vehicle's id.</param>
public sealed record FindVehicleQuery(string Id);
```
*Live from [`api/TheYard.Application/Features/Vehicles/VehicleHandlers.cs`](https://github.com/SteveStout/TheYard/blob/085b3b4/api/TheYard.Application/Features/Vehicles/VehicleHandlers.cs#L10-L52), region `vehicle-handlers`, read from this build at 085b3b4.*

`StandingReader` is how every query reads a vehicle: the catalogue, then everybody's bids, then the
simulated room, in the one order the price rules need, from the reading side of each.

[`api/TheYard.Application/Shared/Auction/StandingReader.cs`](https://github.com/SteveStout/TheYard/blob/main/api/TheYard.Application/Shared/Auction/StandingReader.cs):

```csharp
// A vehicle as every query reads it: the dataset's figures, then everybody's bids, then the room's,
// with its sold flag beside it, and one buyer's bids with everybody else's answer folded in. It is
// the reading half of the auction's three parts (ADR-115: A request is a query or a command): it is
// handed the three parts' reading sides only, so nothing that reads through it can
// place a bid or move the room, and the price a bid is measured against still has one copy.
using TheYard.Data;
using TheYard.Domain.Shared.Auction;

namespace TheYard.Application.Shared.Auction;

/// <summary>
/// One store's vehicles at the price they stand at, and one buyer's badges, read for the queries and
/// for the commands that answer with what they left.
/// </summary>
/// <param name="inventory">The store's catalogue, read side.</param>
/// <param name="bids">Everybody's bids on that catalogue, read side.</param>
/// <param name="room">The simulated competing bidders on that catalogue, read side.</param>
public sealed class StandingReader(IStockReader inventory, IBidReader bids, IRoomReader room)
{
    // #region overlays
    /// <summary>
    /// The overlay a listing applies to every row: everybody's bids, then the room's. Each one only
    /// raises the price (StandingRules.RaisedTo), so the page shows the highest of the dataset, the
    /// bids and the room, and a competitor who has gone higher is what the buyer sees. It names the
    /// vehicles it moves, because the store holds the dataset's figures and a listing that filters or
    /// sorts by price has to work those vehicles out from the moved ones (ADR: The store does the
    /// work). Null when nobody has bid at all, so a cold listing pays for neither. IsEmpty rather
    /// than a snapshot count, because a snapshot copies the whole dictionary just to ask whether it
    /// is empty.
    /// </summary>
    private ListingOverlay? Overlay()
    {
        if (bids.IsEmpty && room.IsEmpty)
        {
            return null;
        }
        var moved = new HashSet<string>(StringComparer.Ordinal);
        if (!bids.IsEmpty)
        {
            moved.UnionWith(bids.Standing().Keys);
        }
        if (!room.IsEmpty)
        {
            moved.UnionWith(room.Snapshot().Keys);
        }
        return new ListingOverlay(moved, AsItStands);
    }

    /// <summary>The vehicle at the price it stands at: the dataset, then everybody's bids, then the room's.</summary>
    private Vehicle AsItStands(Vehicle vehicle) => room.Apply(bids.Apply(vehicle));
    // #endregion overlays

    /// <summary>One vehicle by id, standing at its price and with its sold flag, or null when the catalogue has no such id.</summary>
    public async Task<StandingVehicle?> FindAsync(string vehicleId, CancellationToken cancellation) =>
        await inventory.FindAsync(vehicleId, cancellation) is { } vehicle ? new StandingVehicle(AsItStands(vehicle), bids.IsSold(vehicleId)) : null;

    /// <summary>One page of the catalogue, filtered and sorted, each vehicle at the price it stands at and with its sold flag.</summary>
    public async Task<StandingPage> SearchAsync(VehicleFilter filter, AuctionClock clock, VehicleSort sort, int limit, int offset, CancellationToken cancellation)
    {
        var page = await inventory.SearchAsync(filter, clock, sort, limit, offset, Overlay(), cancellation);
        return new StandingPage(page.Total, [.. page.Vehicles.Select(vehicle => new StandingVehicle(vehicle, bids.IsSold(vehicle.Id)))]);
    }

    // #region views
    /// <summary>
    /// One buyer's bids, each with everybody else's answer folded in. A vehicle bought outright is
    /// never outbid, whatever anyone does afterwards: the sale already happened.
    /// <para>The buyer's own bids are read first and everybody else's second. The reads are not
    /// atomic, so a bid placed between them shows in one and not the other; reading the others
    /// second means the stale answer is "you have been outbid" rather than "you are winning", and
    /// only the second of those can cost somebody an auction.</para>
    /// </summary>
    public IReadOnlyDictionary<string, BidView> BidsOf(string userId)
    {
        var mine = bids.SnapshotFor(userId);
        var standing = bids.Standing();
        var against = room.Snapshot();
        var views = new Dictionary<string, BidView>(StringComparer.Ordinal);
        foreach (var (id, bid) in mine)
        {
            standing.TryGetValue(id, out var held);
            against.TryGetValue(id, out var answered);

            int highest = Math.Max(held?.Amount ?? bid.Amount, answered?.Amount ?? 0);
            bool someoneElseHolds = held is not null && held.Amount > bid.Amount;
            bool roomHolds = answered is not null && answered.Amount > bid.Amount;
            bool outbid = !bid.WonBuyNow && (someoneElseHolds || roomHolds);

            views[id] = new BidView(
                bid.Amount,
                // The count everybody has added to, not one buyer's, so the count never falls
                // when a buyer retakes a lead somebody else raised.
                Math.Max(Math.Max(bid.BidCount, held?.BidCount ?? 0), answered?.BidCount ?? 0),
                bid.WonBuyNow,
                bid.AtMs,
                outbid,
                answered?.Amount,
                highest);
        }
        return views;
    }

    /// <summary>
    /// One buyer's history for the account page, newest first: each vehicle's title beside the
    /// buyer's standing on it, or "(withdrawn)" when the vehicle has left the catalogue.
    /// </summary>
    public async Task<BidHistory> HistoryOfAsync(string userId, CancellationToken cancellation)
    {
        var mine = BidsOf(userId);
        var titles = (await inventory.FindManyAsync(mine.Keys.ToList(), cancellation))
            .ToDictionary(v => v.Id, v => $"{v.Year} {v.Make} {v.Model}", StringComparer.Ordinal);
        var entries = mine
            .OrderByDescending(entry => entry.Value.AtMs)
            .Select(entry => new BidHistoryEntry(
                entry.Key,
                titles.TryGetValue(entry.Key, out var title) ? title : "(withdrawn)",
                entry.Value))
            .ToList();
        return new BidHistory(entries.Count, entries);
    }
    // #endregion views
}
```
*Live from [`api/TheYard.Application/Shared/Auction/StandingReader.cs`](https://github.com/SteveStout/TheYard/blob/085b3b4/api/TheYard.Application/Shared/Auction/StandingReader.cs#L1-L122), region `*`, read from this build at 085b3b4.*

## A command takes the side it writes through

The bid. It reads the vehicle as it stands through the same reader the queries use, then writes
through the bids' writing side, and nothing else.

[`api/TheYard.Application/Features/Bids/PlaceBidHandler.cs`](https://github.com/SteveStout/TheYard/blob/main/api/TheYard.Application/Features/Bids/PlaceBidHandler.cs):

```csharp
/// <summary>
/// Places a bid, or buys now, for one signed-in bidder (ADR: The onion cut into vertical slices,
/// rule 7). The order of the three questions is the contract the browser relies on: the session's
/// store first, then the vehicle, then the rules. The vehicle is read as it stands and the bid is
/// written through the bids' writing side, each handed in by the slice's Module; the clock is the server's.
/// </summary>
/// <param name="standings">The request store's vehicles as they stand, and the caller's badges: the reading side only.</param>
/// <param name="bids">Everybody's bids, writing side: the bid and the buy-now purchase.</param>
/// <param name="clock">The server's clock, read once per request.</param>
public sealed class PlaceBidHandler(StandingReader standings, IBidWriter bids, Func<AuctionClock> clock) : ICommandHandler<PlaceBidCommand, BidAnswer>
{
    /// <summary>Answers one bid or Buy Now.</summary>
    /// <param name="request">Who, on which vehicle, and the amount or none for Buy Now.</param>
    /// <param name="cancellation">The request's cancellation.</param>
    public async Task<BidAnswer> HandleAsync(PlaceBidCommand request, CancellationToken cancellation)
    {
        // A session bids where its account is. The token names the store that opened it, so
        // this check costs no lookup; a bid on the other store would be a row under an id that
        // store has no account for.
        if (!request.SessionOnThisStore)
        {
            return BidAnswer.Refused(BidAnswerKind.SessionElsewhere, null);
        }
        var now = clock();
        cancellation.ThrowIfCancellationRequested();
        if (await standings.FindAsync(request.VehicleId, cancellation) is not { } vehicle)
        {
            return BidAnswer.Refused(BidAnswerKind.NoSuchVehicle, null);
        }
        // The bid is measured against the vehicle as it stands, the room's price included: measuring
        // against the dataset's figure alone would let a buyer retake the lead below the going rate.
        var outcome = request.Amount is { } amount
            ? await bids.PlaceBidAsync(vehicle.Vehicle, amount, now, request.UserId, cancellation)
            : await bids.BuyNowAsync(vehicle.Vehicle, now, request.UserId, cancellation);
        if (outcome.Kind == BidOutcomeKind.Rejected)
        {
            return BidAnswer.Refused(BidAnswerKind.Rejected, outcome.Reason);
        }
        // The room's answer rides back with the bid, so the badge is right the moment the
        // response lands. TryGetValue, because a reset from the same account in a second tab can
        // land between the bid and this read. The vehicle is read again after the bid, so the
        // price and the sold flag are the ones it left.
        return new BidAnswer(
            BidAnswerKind.Placed,
            outcome,
            null,
            standings.BidsOf(request.UserId).TryGetValue(request.VehicleId, out var standing) ? standing : null,
            // Read with no token: the bid has been taken and this is the answer describing
            // what it left, so a caller who has gone must not turn a placed bid into an error.
            await standings.FindAsync(request.VehicleId, CancellationToken.None) ?? vehicle,
            now);
    }
}
```
*Live from [`api/TheYard.Application/Features/Bids/PlaceBidHandler.cs`](https://github.com/SteveStout/TheYard/blob/085b3b4/api/TheYard.Application/Features/Bids/PlaceBidHandler.cs#L10-L62), region `place-bid`, read from this build at 085b3b4.*

## Split a port where a query needs half of it

This is Interface Segregation from SOLID, made a rule by CQRS. A port that carries reads and writes in
one interface is split so the query can be handed the read half. The blog's store, the read half:

[`api/TheYard.Application/Features/Blog/BlogStore.cs`](https://github.com/SteveStout/TheYard/blob/main/api/TheYard.Application/Features/Blog/BlogStore.cs):

```csharp
/// <summary>
/// The read side of the blog's store: what a query may ask, and nothing that writes (ADR-115: A request is a
/// query or a command, Interface Segregation). The query handlers take this; the command handlers take the
/// whole store.
/// </summary>
public interface IBlogReader
{
    /// <summary>Whether the store can keep posts and reads now, and why not when it cannot.</summary>
    Task<BlogAvailability> AvailabilityAsync(CancellationToken cancellation);

    /// <summary>Every published post, newest first.</summary>
    Task<IReadOnlyList<BlogPost>> PostsAsync(CancellationToken cancellation);

    /// <summary>One published post by its slug, or null.</summary>
    Task<BlogPost?> PostAsync(string slug, CancellationToken cancellation);

    /// <summary>Each published post's numbers, bots left out, newest post first.</summary>
    Task<IReadOnlyList<BlogStat>> StatsAsync(DateTimeOffset now, CancellationToken cancellation);

    /// <summary>Reads by source since an instant, bots left out.</summary>
    Task<IReadOnlyList<BlogSourceCount>> SourcesAsync(DateTimeOffset since, CancellationToken cancellation);
}
```
*Live from [`api/TheYard.Application/Features/Blog/BlogStore.cs`](https://github.com/SteveStout/TheYard/blob/085b3b4/api/TheYard.Application/Features/Blog/BlogStore.cs#L10-L31), region `blog-reader`, read from this build at 085b3b4.*

And the whole store, which extends it with the three writes and is handed to the commands:

```csharp
/// <summary>Where the blog's posts and its two logs are kept: the reader, and the three writes.</summary>
public interface IBlogStore : IBlogReader
{
    /// <summary>Adds a post, or replaces the one with the same slug; returns its id.</summary>
    Task<int> SaveAsync(BlogPost post, DateTimeOffset at, CancellationToken cancellation);

    /// <summary>Keeps one read.</summary>
    Task KeepReadAsync(BlogRead read, CancellationToken cancellation);

    /// <summary>Keeps one event.</summary>
    Task KeepEventAsync(BlogEvent blogEvent, CancellationToken cancellation);
}
```
*Live from [`api/TheYard.Application/Features/Blog/BlogStore.cs`](https://github.com/SteveStout/TheYard/blob/085b3b4/api/TheYard.Application/Features/Blog/BlogStore.cs#L35-L46), region `blog-store`, read from this build at 085b3b4.*

The auction's three parts are split the same way, one reading and one writing interface each:

[`api/TheYard.Application/Shared/Auction/AuctionParts.cs`](https://github.com/SteveStout/TheYard/blob/main/api/TheYard.Application/Shared/Auction/AuctionParts.cs):

```csharp
/// <summary>The catalogue as a query reads it: one vehicle, several, the first live ones, the filters' values and a page.</summary>
public interface IStockReader
{
    /// <summary>One vehicle by id, at the dataset's figures, or null when the catalogue has no such id.</summary>
    Task<Vehicle?> FindAsync(string id, CancellationToken cancellation);

    /// <summary>The vehicles with these ids that the catalogue holds, in no promised order.</summary>
    Task<IReadOnlyList<Vehicle>> FindManyAsync(IReadOnlyCollection<string> ids, CancellationToken cancellation);

    /// <summary>The first live auctions in the catalogue's own order, at most <paramref name="take"/>.</summary>
    Task<IReadOnlyList<Vehicle>> FirstLiveAsync(AuctionClock clock, int take, CancellationToken cancellation);

    /// <summary>The values behind the catalogue's filters.</summary>
    Task<InventoryFacets> FacetsAsync(CancellationToken cancellation);

    /// <summary>One page of the catalogue, filtered and sorted, with the overlay laid on before either.</summary>
    Task<SearchResult> SearchAsync(VehicleFilter filter, AuctionClock clock, VehicleSort sort = VehicleSort.EndingSoonest, int limit = int.MaxValue, int offset = 0, ListingOverlay? overlay = null, CancellationToken cancellation = default);
}

/// <summary>Everybody's bids as a query reads them: the standing on each vehicle, one buyer's bids, and whether a vehicle is sold.</summary>
public interface IBidReader
{
    /// <summary>Whether nobody has bid on anything, so a cold listing pays for no overlay.</summary>
    bool IsEmpty { get; }

    /// <summary>One buyer's own bids, by vehicle id.</summary>
    IReadOnlyDictionary<string, BidState> SnapshotFor(string userId);

    /// <summary>Every vehicle's standing, by id.</summary>
    IReadOnlyDictionary<string, VehicleStanding> Standing();

    /// <summary>Whether anybody has bought this vehicle outright.</summary>
    bool IsSold(string vehicleId);

    /// <summary>The standing in the shape the room reads it, one bid per vehicle.</summary>
    IReadOnlyDictionary<string, BidState> StandingAsBids();

    /// <summary>The vehicle with everybody's bids laid on its dataset figures.</summary>
    Vehicle Apply(Vehicle vehicle);
}

/// <summary>The writes that move everybody's bids: a bid, a buy-now purchase and one buyer's start-over.</summary>
public interface IBidWriter
{
    /// <summary>One bid from one buyer, measured against the vehicle as it stands.</summary>
    /// <remarks>The token is honoured up to the gate and not past it: once a bid's turn comes
    /// the store is written and memory follows, and that pair finishes (ADR: The ports learn to
    /// wait, the addendum on the token).</remarks>
    Task<BidOutcome> PlaceBidAsync(Vehicle vehicle, int amount, AuctionClock clock, string userId, CancellationToken cancellation);

    /// <summary>A buy-now purchase from one buyer, at the vehicle's buy-now price.</summary>
    /// <remarks>Cancellable up to the gate, for the reason a bid is.</remarks>
    Task<BidOutcome> BuyNowAsync(Vehicle vehicle, AuctionClock clock, string userId, CancellationToken cancellation);

    /// <summary>One buyer's bids removed; answers the vehicles nobody else is bidding on any more.</summary>
    /// <remarks>Cancellable up to the gate, for the reason a bid is.</remarks>
    Task<IReadOnlyList<string>> ResetAsync(string userId, CancellationToken cancellation);
}

/// <summary>The room's bids as a query reads them.</summary>
public interface IRoomReader
{
    /// <summary>Whether the room has bid on nothing yet.</summary>
    bool IsEmpty { get; }

    /// <summary>The room's standing bid on every vehicle it has bid on, by id.</summary>
    IReadOnlyDictionary<string, MarketBid> Snapshot();

    /// <summary>The vehicle with the room's bid laid on it.</summary>
    Vehicle Apply(Vehicle vehicle);
}

/// <summary>The writes that move the room: one round of bidding, and forgetting the vehicles a reset left alone.</summary>
public interface IRoomWriter
{
    /// <summary>One round over these candidates against everybody's standing; answers the ids it raised.</summary>
    IReadOnlyList<string> Tick(IReadOnlyList<Vehicle> candidates, IReadOnlyDictionary<string, BidState> buyerBids, AuctionClock clock, int maxBids = 3);

    /// <summary>The room's answers on these vehicles dropped.</summary>
    void Forget(IEnumerable<string> vehicleIds);
}
```
*Live from [`api/TheYard.Application/Shared/Auction/AuctionParts.cs`](https://github.com/SteveStout/TheYard/blob/085b3b4/api/TheYard.Application/Shared/Auction/AuctionParts.cs#L12-L92), region `auction-parts`, read from this build at 085b3b4.*

## No services

A class named `BidService` is a bag of use cases behind one name: it placed bids, reset them and read
standings, and anything handed it could do all three. Each use case is a handler now, and the shared
state the handlers need is a part with two sides. Jimmy Bogard's vertical slices make the same move:
organise by request, and the request is already a command or a query, so shared layers of services and
repositories mostly fall away
([Vertical Slice Architecture](https://www.jimmybogard.com/vertical-slice-architecture/)).

## The test that holds it

[`api/TheYard.Tests/CqrsTests.cs`](https://github.com/SteveStout/TheYard/blob/main/api/TheYard.Tests/CqrsTests.cs):

```csharp
/// <summary>
/// A request is a query or a command (ADR-115). A request is a record named <c>&lt;Verb&gt;&lt;Noun&gt;Query</c> or
/// <c>&lt;Verb&gt;&lt;Noun&gt;Command</c> in its slice's Application folder, with exactly one handler of the same name,
/// and every handler is one of the two interfaces in Shared/Requests.cs. A query handler is handed nothing that
/// writes: no parameter of its constructor is a store or a writer, only a reader. There is no bus: the slice's
/// Module maps a route to its one handler, and the route takes that handler and nothing else this codebase defines.
/// </summary>
public sealed class CqrsTests(WebApplicationFactory<Program> factory) : IClassFixture<WebApplicationFactory<Program>>
{
    /// <summary>The Application ring, where every request and its handler live.</summary>
    private static readonly Assembly Application = typeof(IQueryHandler<,>).Assembly;

    /// <summary>The slices' namespace in the Application ring.</summary>
    private const string Features = "TheYard.Application.Features";

    /// <summary>Every request in the slices whose name ends in this word: Query or Command.</summary>
    private static List<Type> Requests(string word) =>
        [.. Application.GetTypes().Where(type => type.IsClass && type.Namespace?.StartsWith(Features, StringComparison.Ordinal) == true && type.Name.EndsWith(word, StringComparison.Ordinal))];

    /// <summary>Every class in the Application ring's root or its slices whose name ends in Service.</summary>
    private static List<string> Services() =>
        [.. Application.GetTypes()
            .Where(type => type.IsClass && !type.Name.StartsWith('<') && type.Name.EndsWith("Service", StringComparison.Ordinal)
                && (type.Namespace == "TheYard.Application" || type.Namespace?.StartsWith(Features, StringComparison.Ordinal) == true))
            .Select(type => type.Name)];

    /// <summary>Every handler in the Application ring: a class whose name ends in Handler.</summary>
    private static List<Type> HandlerTypes() =>
        [.. Application.GetTypes().Where(type => type.IsClass && !type.IsAbstract && type.Name.EndsWith("Handler", StringComparison.Ordinal) && type.Namespace?.StartsWith(Features, StringComparison.Ordinal) == true)];

    /// <summary>The query or command interfaces a type implements, with the request each names.</summary>
    private static List<(Type Contract, Type Request, bool Query)> Contracts(Type handler) =>
        [.. handler.GetInterfaces()
            .Where(contract => contract.IsGenericType && (contract.GetGenericTypeDefinition() == typeof(IQueryHandler<,>) || contract.GetGenericTypeDefinition() == typeof(ICommandHandler<,>)))
            .Select(contract => (contract, contract.GetGenericArguments()[0], contract.GetGenericTypeDefinition() == typeof(IQueryHandler<,>)))];

    [Fact]
    public void Every_handler_is_exactly_one_query_or_command_handler_named_for_its_request()
    {
        var handlers = HandlerTypes();
        Assert.True(handlers.Count >= 27, $"only {handlers.Count} handlers were found");
        var wrong = new List<string>();
        foreach (var handler in handlers)
        {
            var contracts = Contracts(handler);
            if (contracts.Count != 1)
            {
                wrong.Add($"{handler.Name} implements {contracts.Count} of IQueryHandler and ICommandHandler; it implements exactly one");
                continue;
            }
            var (_, request, query) = contracts[0];
            string expected = handler.Name[..^"Handler".Length] + (query ? "Query" : "Command");
            if (request.Name != expected)
            {
                wrong.Add($"{handler.Name} handles {request.Name}; its request is named {expected}");
            }
        }
        Assert.True(wrong.Count == 0, string.Join(Environment.NewLine, wrong));
    }

    [Fact]
    public void Every_query_and_command_has_exactly_one_handler()
    {
        var requests = Requests("Query").Concat(Requests("Command")).ToList();
        Assert.True(requests.Count >= 27, $"only {requests.Count} requests were found");
        var handled = HandlerTypes().SelectMany(Contracts).GroupBy(contract => contract.Request).ToDictionary(group => group.Key, group => group.ToList());
        var wrong = new List<string>();
        foreach (var request in requests)
        {
            int count = handled.TryGetValue(request, out var found) ? found.Count : 0;
            if (count != 1)
            {
                wrong.Add($"{request.Name} has {count} handlers; it has exactly one");
            }
            else if (request.Name.EndsWith("Query", StringComparison.Ordinal) != found![0].Query)
            {
                wrong.Add($"{request.Name} is handled as a {(found[0].Query ? "query" : "command")}; its name says otherwise");
            }
            if (!request.IsSealed)
            {
                wrong.Add($"{request.Name} is not sealed");
            }
        }
        Assert.True(wrong.Count == 0, string.Join(Environment.NewLine, wrong));
    }

    [Fact]
    public void A_query_handler_is_handed_no_store_and_no_writer()
    {
        // Interface Segregation as a rule: a query reads, so the ports it takes end in Reader or Catalogue,
        // or are none of the two writing kinds; a store or a writer handed to a query is a write it could make.
        static string Bare(Type type) => (Nullable.GetUnderlyingType(type) ?? type).Name;
        var wrong = HandlerTypes()
            .Where(handler => Contracts(handler).Any(contract => contract.Query))
            .SelectMany(handler => handler.GetConstructors().SelectMany(constructor => constructor.GetParameters())
                .Where(parameter => Bare(parameter.ParameterType).EndsWith("Store", StringComparison.Ordinal) || Bare(parameter.ParameterType).EndsWith("Writer", StringComparison.Ordinal))
                .Select(parameter => $"{handler.Name} takes {Bare(parameter.ParameterType)} {parameter.Name}; a query takes the reader"))
            .ToList();
        Assert.True(wrong.Count == 0, string.Join(Environment.NewLine, wrong));
    }

    // #region a-reader-is-known-by-what-it-can-do
    [Fact]
    public void A_query_handler_is_handed_no_port_that_can_write()
    {
        // The rule above reads a port's name, so a port named ...Reader that also writes passes it. This one
        // reads what the port can do: no type a query handler takes by constructor, among this codebase's own,
        // has a member whose name starts with a writing verb, counting every member of every interface it
        // inherits. A port that both reads and writes is split, and the query takes the reading half.
        var wrong = HandlerTypes()
            .Where(handler => Contracts(handler).Any(contract => contract.Query))
            .SelectMany(handler => handler.GetConstructors().SelectMany(constructor => constructor.GetParameters())
                .Select(parameter => (handler, parameter, writes: WritingMembers(Nullable.GetUnderlyingType(parameter.ParameterType) ?? parameter.ParameterType)))
                .Where(found => found.writes.Count > 0)
                .Select(found => $"{found.handler.Name} takes {found.parameter.ParameterType.Name} {found.parameter.Name}, which can {string.Join(", ", found.writes)}; a query takes a port that only reads"))
            .Where(line => !ExemptReaders.Any(exempt => line.StartsWith(exempt.Key + " ", StringComparison.Ordinal)))
            .ToList();
        Assert.True(wrong.Count == 0, string.Join(Environment.NewLine, wrong));
    }

    /// <summary>The verbs a member that writes starts with; a reader has none of them.</summary>
    private static readonly Regex WritingVerb = new(@"^(Keep|Write|Save|Upsert|Delete|Add|Remove|Record|Put|Set)(?=[A-Z_]|$)", RegexOptions.Compiled);

    /// <summary>
    /// Query handlers that still take a port that writes, each with why and the lane that splits it; the
    /// list only shrinks.
    /// </summary>
    private static readonly Dictionary<string, string> ExemptReaders = new(StringComparer.Ordinal);

    /// <summary>Every member of one of this codebase's types, and of every interface it inherits, whose name starts with a writing verb.</summary>
    private static List<string> WritingMembers(Type port)
    {
        if (port.Namespace?.StartsWith("TheYard.", StringComparison.Ordinal) != true)
        {
            return [];
        }
        IEnumerable<Type> contracts = port.IsInterface ? port.GetInterfaces().Prepend(port) : [port];
        return [.. contracts
            .SelectMany(contract => contract.GetMembers(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly))
            .Where(member => member is not MethodInfo { IsSpecialName: true } && WritingVerb.IsMatch(member.Name))
            .Select(member => member.Name)
            .Distinct(StringComparer.Ordinal)
            .Order(StringComparer.Ordinal)];
    }
    // #endregion a-reader-is-known-by-what-it-can-do

    [Fact]
    public void No_type_in_the_Application_ring_s_slices_or_root_is_a_service()
    {
        // A service is a bag of use cases behind one name; a request has one handler instead. BidService,
        // InventoryService and MarketService were the last three, and Auction composed them: they are
        // BidBook, Stock and Room behind a reading and a writing interface each, and the handlers.
        var services = Services();
        Assert.True(services.Count == 0, "types named *Service: " + string.Join(", ", services));
    }

    [Fact]
    public void Every_route_that_asks_a_handler_asks_a_query_or_a_command_handler()
    {
        // HandlerRuleTests holds a route to one handler; this holds that handler to one of the two shapes, so a
        // route can never ask a bag of use cases. The routes HandlerRuleTests names as exempt read the host's own
        // state and ask no handler yet; the Admin reads among them become queries in ADR-115's fourth rung.
        var routes = factory.Services.GetRequiredService<EndpointDataSource>().Endpoints.OfType<RouteEndpoint>()
            .Select(endpoint => endpoint.Metadata.GetMetadata<MethodInfo>())
            .OfType<MethodInfo>()
            .Where(answer => answer.DeclaringType?.Namespace?.StartsWith("TheYard.Api.Features.", StringComparison.Ordinal) == true)
            .ToList();
        Assert.NotEmpty(routes);
        var wrong = routes
            .SelectMany(answer => answer.GetParameters()
                .Where(parameter => parameter.ParameterType.Namespace?.StartsWith(Features, StringComparison.Ordinal) == true && parameter.ParameterType.Name.EndsWith("Handler", StringComparison.Ordinal))
                .Where(parameter => Contracts(parameter.ParameterType).Count != 1)
                .Select(parameter => $"{answer.DeclaringType!.Name}.{answer.Name} asks {parameter.ParameterType.Name}, which is not a query or a command handler"))
            .ToList();
        Assert.True(wrong.Count == 0, string.Join(Environment.NewLine, wrong));
    }

    [Fact]
    public void The_practices_page_quotes_the_counts_this_build_has()
    {
        // A number in prose goes stale silently, so the page's table is read back against the build, the
        // way SealedByDefaultTests reads SEALED.md.
        string page = File.ReadAllText(Path.Combine(Repo.Root(), "docs", "best-practices", "CQRS.md"));
        var counted = new Dictionary<string, int>(StringComparer.Ordinal)
        {
            ["queries"] = Requests("Query").Count,
            ["commands"] = Requests("Command").Count,
            ["handlers"] = HandlerTypes().Count,
            ["services"] = Services().Count,
        };
        var wrong = new List<string>();
        foreach (var (shape, actual) in counted)
        {
            var quoted = Regex.Match(page, $@"^\| {shape} \| (\d+) \|", RegexOptions.Multiline);
            if (!quoted.Success)
            {
                wrong.Add($"docs/best-practices/CQRS.md has no row for {shape}");
            }
            else if (int.Parse(quoted.Groups[1].Value, System.Globalization.CultureInfo.InvariantCulture) != actual)
            {
                wrong.Add($"docs/best-practices/CQRS.md says {quoted.Groups[1].Value} {shape} and this build has {actual}");
            }
        }
        Assert.True(wrong.Count == 0, string.Join(Environment.NewLine, wrong));
    }
}
```
*Live from [`api/TheYard.Tests/CqrsTests.cs`](https://github.com/SteveStout/TheYard/blob/085b3b4/api/TheYard.Tests/CqrsTests.cs#L11-L216), region `cqrs-tests`, read from this build at 085b3b4.*

## How the rule reads a port

A port's name is a promise and nothing checks a promise. From 1.0.4.68 the rule reads what a port can do: every type a query handler takes by constructor, among this codebase's own, is read by reflection with every interface it inherits, and a member whose name starts with a writing verb (Keep, Write, Save, Upsert, Delete, Add, Remove, Record, Put or Set) makes it a port that writes. A query handler handed one is red, whatever the port is called. The name check stays beside it.

```csharp
[Fact]
public void A_query_handler_is_handed_no_port_that_can_write()
{
    // The rule above reads a port's name, so a port named ...Reader that also writes passes it. This one
    // reads what the port can do: no type a query handler takes by constructor, among this codebase's own,
    // has a member whose name starts with a writing verb, counting every member of every interface it
    // inherits. A port that both reads and writes is split, and the query takes the reading half.
    var wrong = HandlerTypes()
        .Where(handler => Contracts(handler).Any(contract => contract.Query))
        .SelectMany(handler => handler.GetConstructors().SelectMany(constructor => constructor.GetParameters())
            .Select(parameter => (handler, parameter, writes: WritingMembers(Nullable.GetUnderlyingType(parameter.ParameterType) ?? parameter.ParameterType)))
            .Where(found => found.writes.Count > 0)
            .Select(found => $"{found.handler.Name} takes {found.parameter.ParameterType.Name} {found.parameter.Name}, which can {string.Join(", ", found.writes)}; a query takes a port that only reads"))
        .Where(line => !ExemptReaders.Any(exempt => line.StartsWith(exempt.Key + " ", StringComparison.Ordinal)))
        .ToList();
    Assert.True(wrong.Count == 0, string.Join(Environment.NewLine, wrong));
}

/// <summary>The verbs a member that writes starts with; a reader has none of them.</summary>
private static readonly Regex WritingVerb = new(@"^(Keep|Write|Save|Upsert|Delete|Add|Remove|Record|Put|Set)(?=[A-Z_]|$)", RegexOptions.Compiled);

/// <summary>
/// Query handlers that still take a port that writes, each with why and the lane that splits it; the
/// list only shrinks.
/// </summary>
private static readonly Dictionary<string, string> ExemptReaders = new(StringComparer.Ordinal);

/// <summary>Every member of one of this codebase's types, and of every interface it inherits, whose name starts with a writing verb.</summary>
private static List<string> WritingMembers(Type port)
{
    if (port.Namespace?.StartsWith("TheYard.", StringComparison.Ordinal) != true)
    {
        return [];
    }
    IEnumerable<Type> contracts = port.IsInterface ? port.GetInterfaces().Prepend(port) : [port];
    return [.. contracts
        .SelectMany(contract => contract.GetMembers(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly))
        .Where(member => member is not MethodInfo { IsSpecialName: true } && WritingVerb.IsMatch(member.Name))
        .Select(member => member.Name)
        .Distinct(StringComparer.Ordinal)
        .Order(StringComparer.Ordinal)];
}
```
*Live from [`api/TheYard.Tests/CqrsTests.cs`](https://github.com/SteveStout/TheYard/blob/085b3b4/api/TheYard.Tests/CqrsTests.cs#L113-L154), region `a-reader-is-known-by-what-it-can-do`, read from this build at 085b3b4.*

## The counts this build has

Read back by `CqrsTests` on every build, so a number here cannot go stale.

| shape | count |
| --- | --- |
| queries | 39 |
| commands | 19 |
| handlers | 58 |
| services | 0 |

## The Function finds its read-only use cases rather than being told them

The job hunt's Function answers no request, so it has no query to name and no handler to count. The half
of this page that does carry over is the half that matters: a use case that only reads cannot write. Its
ports are split for it - `IMergeStore` extends `IShareReader`, `IInbox` extends `IInboxReader` - and the
reading use cases take the reading halves.

Until 1.0.4.126 that rule ran over a dictionary of two names kept by hand in the Function's `CqrsTests`,
which made it exactly as good as somebody's memory: a new use case that reads the share and is handed the
writing half of its port was never asked the question, because it was not in the dictionary. A rule whose
subject is a list only covers the list.

So the subject is read from the build. `tests/rules/UseCaseRule.cs` asks the ring which types are handed a
port, and asks the compiled code which of them write; the rule runs over the ones that do not. It finds the
same two the dictionary named, which is the point - the number did not change, the way of arriving at it
did, and the next read-only use case is covered the moment it is written.

Whether a use case writes is read from its code and never from its name, and two hops make that harder
than scanning one method body. The first is the state machine: an `async` method's own body is builder
plumbing, and the calls a reader sees in the source live in the `MoveNext` of a nested compiler-generated
type that the builder starts generically, so no call instruction joins the two. The second is delegation:
`TrackerMerger` is handed the writing port and calls nothing that writes, because it makes a `MergeContext`
and the merge steps and lets them write. Measured one method deep it reads as a use case that only reads
holding a port that writes - this rule's own failure message, aimed at the type whose whole job is writing.
So writes travel backwards along the calls a method makes, over methods rather than types: collapsed to
types, `MergerHealth` would read as a writer for calling `TrackerMerger.IsSpoolFile`, a static that tests a
file name.

```csharp
/// <summary>
/// One rule, one body, compiled into both test projects by a link (ADR-075, the addendum of 1.0.4.70):
/// a use case that only reads is handed reading ports only. The ring is asked which use cases it holds
/// and which of them write, so the rule covers every one of them and not the ones somebody remembered
/// to write down.
/// </summary>
/// <remarks>
/// <para>The question this answers is "does this use case hold a write it never makes", and a list of
/// names cannot answer it. Until 1.0.4.126 the job hunt's Function held its read-only use cases to
/// reading ports through a dictionary of two, so the rule was exactly as good as somebody's memory: a
/// new use case that reads the share and takes the writing half of its port was never looked at,
/// because it was not in the dictionary. What makes the rule self-maintaining is reading which use
/// cases exist, and then asking the build itself which of them write.</para>
/// <para>Whether a use case writes is read from its compiled code and not from its name, because a
/// name is a promise and nothing checks a promise. A use case writes when the code it owns reaches a
/// call to a member whose name starts with a writing verb on one of this family's types - and
/// "reaches" is the whole difficulty. Two hops are invisible to a scan of one method body, and both of
/// them are in this codebase.</para>
/// <para>The first is the state machine. An <c>async</c> method's own body is builder plumbing: the
/// calls a reader sees in the source live in the <c>MoveNext</c> of a nested compiler-generated type,
/// which the builder starts generically, so no call instruction joins the two. The edge is taken from
/// <c>AsyncStateMachineAttribute</c> (and the iterator's equivalent), which names that type.</para>
/// <para>The second is delegation. <c>TrackerMerger</c> is handed the writing port and calls nothing
/// that writes: it makes a <c>MergeContext</c> and the merge steps and lets them write. Measured one
/// method deep it reads as a use case that only reads and holds a port that writes, which is this
/// rule's own failure message, on the type whose whole job is writing. So writes travel along the
/// calls a method makes, and the walk is over methods rather than types on purpose: collapsed to
/// types, <c>MergerHealth</c> would read as a writer for calling <c>TrackerMerger.IsSpoolFile</c>, a
/// static that tests a file name.</para>
/// <para>What is a port is read narrowly, for the same reason. A record is never a port: its
/// compiler-generated equality takes its own type, so every record in a ring would read as a type
/// handed one of this family's types, and a property that happens to be a noun spelled like a verb
/// (<c>TrackerFrame.Record</c>) would read as a member that writes. A port is one of this family's
/// interfaces, or one of its classes that is not a record - which is how <c>MergeContext</c>, a class
/// holding the share's writing half behind two safe ways to change the master, is a port.</para>
/// <para>The writing verbs are the site's (<c>CqrsTests</c>, ADR-115's addendum of 1.0.4.68) widened
/// by the Function's own, and the caller passes them, so one ring's vocabulary does not silently
/// become another's.</para>
/// </remarks>
public static class UseCaseRule
{
    /// <summary>
    /// One of this family's types handed to a use case, with what it can do: the port, the parameter
    /// that carries it, where that parameter comes in, and the names of its members that write.
    /// </summary>
    /// <param name="Port">The port's type.</param>
    /// <param name="Where">The constructor or method that takes it, so a reader can find it.</param>
    /// <param name="Name">The parameter's name, as the code spells it.</param>
    /// <param name="Writes">Its members whose names start with a writing verb, counting every interface it inherits; empty for a reading port.</param>
    public sealed record Handed(Type Port, string Where, string Name, IReadOnlyList<string> Writes);

    /// <summary>
    /// One use case the ring holds: a type handed at least one port, the ports it is handed, and
    /// whether its own code ever reaches a write.
    /// </summary>
    /// <param name="Type">The use case's type.</param>
    /// <param name="Ports">Every port it is handed, by constructor or by a method of its own.</param>
    /// <param name="Writes">True when the code it owns reaches a writing member, directly or through the calls it makes.</param>
    public sealed record UseCase(Type Type, IReadOnlyList<Handed> Ports, bool Writes);

    /// <summary>
    /// Every use case in a ring, each with its ports and whether it writes. The list is the rule's
    /// subject: a caller holds the read-only ones to reading ports and asserts the count is a count
    /// and not nothing, because a discovery that finds no use cases would satisfy every rule written
    /// over it.
    /// </summary>
    /// <param name="ring">The ring assembly whose use cases and ports are wanted.</param>
    /// <param name="writingVerbs">The verbs a writing member's name starts with, as alternatives for a regular expression.</param>
    public static List<UseCase> Discover(Assembly ring, string writingVerbs)
    {
        var verb = new Regex("^(" + writingVerbs + ")(?=[A-Z_]|$)", RegexOptions.Compiled);
        string family = Family(ring);
        var writers = Writers(ring.Location, family, verb);
        return [.. ring.GetTypes()
            .Where(type => !type.IsInterface && !type.IsEnum && type.DeclaringType is null && !type.Name.StartsWith('<'))
            .Select(type => new UseCase(type, Ports(type, family, verb), writers.Contains(type.FullName ?? type.Name)))
            .Where(found => found.Ports.Count > 0)
            .OrderBy(found => found.Type.Name, StringComparer.Ordinal)];
    }

    /// <summary>
    /// Every read-only use case that is handed a port it could write through, as a line naming the use
    /// case, the parameter and what that parameter can do. Empty is the rule holding.
    /// </summary>
    /// <param name="ring">The ring assembly to read.</param>
    /// <param name="writingVerbs">The verbs a writing member's name starts with.</param>
    public static List<string> ReadOnlyCasesHandedAWrite(Assembly ring, string writingVerbs) =>
        [.. Discover(ring, writingVerbs)
            .Where(found => !found.Writes)
            .SelectMany(found => found.Ports
                .Where(port => port.Writes.Count > 0)
                .Select(port => $"{found.Type.Name} only reads and takes {port.Port.Name} {port.Name} by {port.Where}, which can {string.Join(", ", port.Writes)}; a use case that only reads takes the reading half"))
            .Distinct(StringComparer.Ordinal)
            .Order(StringComparer.Ordinal)];

    /// <summary>The family a ring belongs to, as a namespace prefix: everything before the first dot of its assembly name, so TheYard.Application reads TheYard types and JobHunt.Application reads JobHunt ones.</summary>
    private static string Family(Assembly ring) => (ring.GetName().Name ?? "").Split('.')[0] + ".";

    /// <summary>Whether a type is one of this family's, by its namespace.</summary>
    private static bool Ours(string? space, string family) => space?.StartsWith(family, StringComparison.Ordinal) == true;

    /// <summary>
    /// Whether a type may be handed as a port: one of this family's interfaces, or one of its classes
    /// that is not a record. A record is a value a use case is given to work on, never a seam it works
    /// through, and its compiler-generated equality would make every record in the ring look like one.
    /// </summary>
    private static bool IsPort(Type type, string family) =>
        Ours(type.Namespace, family) && !type.IsEnum
        && (type.IsInterface || (type.IsClass && type.GetMethod("<Clone>$", BindingFlags.Public | BindingFlags.Instance) is null));

    /// <summary>
    /// Every port a type is handed: by its constructors, and by the methods it declares itself, which
    /// is how a use case written as a record with a static reader is caught beside one written as a
    /// class. Compiler-generated members and operators are left out, because a record's equality takes
    /// its own type and is not a port being handed anywhere.
    /// </summary>
    private static List<Handed> Ports(Type type, string family, Regex verb)
    {
        static Type Bare(Type type) => Nullable.GetUnderlyingType(type) ?? type;
        bool Written(MemberInfo member) =>
            member is not MethodInfo { IsSpecialName: true }
            && member.GetCustomAttribute<CompilerGeneratedAttribute>() is null
            && verb.IsMatch(member.Name);
        List<string> Writes(Type port)
        {
            IEnumerable<Type> contracts = port.IsInterface ? port.GetInterfaces().Prepend(port) : [port];
            return [.. contracts
                .SelectMany(contract => contract.GetMembers(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly))
                .Where(Written)
                .Select(member => member.Name)
                .Distinct(StringComparer.Ordinal)
                .Order(StringComparer.Ordinal)];
        }

        var taken = new List<(Type Port, string Where, string Name)>();
        foreach (var maker in type.GetConstructors())
        {
            taken.AddRange(maker.GetParameters().Select(parameter => (Bare(parameter.ParameterType), "its constructor", parameter.Name ?? "?")));
        }
        foreach (var method in type.GetMethods(BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance | BindingFlags.DeclaredOnly)
            .Where(method => !method.IsSpecialName && method.GetCustomAttribute<CompilerGeneratedAttribute>() is null))
        {
            taken.AddRange(method.GetParameters().Select(parameter => (Bare(parameter.ParameterType), method.Name, parameter.Name ?? "?")));
        }
        return [.. taken
            .Where(found => IsPort(found.Port, family))
            .DistinctBy(found => found.Port.FullName + " " + found.Name, StringComparer.Ordinal)
            .Select(found => new Handed(found.Port, found.Where, found.Name, Writes(found.Port)))];
    }

    /// <summary>
    /// The full name of every outermost type in the ring whose code reaches a writing member. Read from
    /// the compiled module: a method writes directly when it calls a writing member of this family's,
    /// and then a write travels backwards along every call, so a use case that delegates its writing is
    /// a writer. The async and iterator state machines are joined to the methods they belong to,
    /// because a generic builder start leaves no call instruction between them.
    /// </summary>
    private static HashSet<string> Writers(string module, string family, Regex verb)
    {
        using var read = ModuleDefinition.ReadModule(module);
        var all = read.GetTypes().SelectMany(type => type.Methods).ToList();
        var calls = all.ToDictionary(method => method, _ => new List<MethodDefinition>());
        var writes = new HashSet<MethodDefinition>();

        MethodDefinition? Here(MethodReference called)
        {
            try
            {
                var found = called.Resolve();
                return found is not null && found.Module == read ? found : null;
            }
            catch (AssemblyResolutionException)
            {
                return null;
            }
        }

        foreach (var method in all)
        {
            foreach (var built in method.CustomAttributes
                .Where(attribute => attribute.AttributeType.Name is "AsyncStateMachineAttribute" or "IteratorStateMachineAttribute")
                .Where(attribute => attribute.ConstructorArguments.Count > 0)
                .Select(attribute => (attribute.ConstructorArguments[0].Value as TypeReference)?.Resolve())
                .OfType<TypeDefinition>())
            {
                calls[method].AddRange(built.Methods.Where(calls.ContainsKey));
            }
            if (!method.HasBody)
            {
                continue;
            }
            foreach (var called in method.Body.Instructions.Select(instruction => instruction.Operand).OfType<MethodReference>())
            {
                if (Ours(called.DeclaringType.Namespace, family) && verb.IsMatch(called.Name))
                {
                    writes.Add(method);
                }
                if (Here(called) is { } target)
                {
                    calls[method].Add(target);
                }
            }
        }

        var owners = new HashSet<string>(StringComparer.Ordinal);
        foreach (var start in all.Where(method => !owners.Contains(Outermost(method.DeclaringType))))
        {
            var seen = new HashSet<MethodDefinition> { start };
            var waiting = new Queue<MethodDefinition>([start]);
            while (waiting.Count > 0)
            {
                var at = waiting.Dequeue();
                if (writes.Contains(at))
                {
                    owners.Add(Outermost(start.DeclaringType));
                    break;
                }
                foreach (var next in calls[at].Where(seen.Add))
                {
                    waiting.Enqueue(next);
                }
            }
        }
        return owners;
    }

    /// <summary>The full name of the type a nested type sits in, however deep: the type a reader of the source would say owns the code.</summary>
    private static string Outermost(TypeReference type)
    {
        var owner = type;
        while (owner.DeclaringType is { } holder)
        {
            owner = holder;
        }
        return owner.FullName;
    }
}
```
*Live from [`tests/rules/UseCaseRule.cs`](https://github.com/SteveStout/TheYard/blob/085b3b4/tests/rules/UseCaseRule.cs#L9-L245), region `use-case-rule`, read from this build at 085b3b4.*

| shape | count |
| --- | --- |
| the Function's use cases | 18 |
| of them that only read | 2 |

Read back by the Function's `CqrsTests` on every build, so these two cannot go stale either.

## What this codebase does not do, and why

No second store for reads, no events between a write side and a read side, no event sourcing and no
bus. Martin Fowler's page warns that for most systems the full pattern adds risky complexity and keeps
it to the parts of a system that need it ([CQRS](https://martinfowler.com/bliki/CQRS.html)), and
Microsoft's pattern page is plain that the basic form shares one store and that separate stores bring
synchronisation and data that can be behind
([CQRS pattern](https://learn.microsoft.com/en-us/azure/architecture/patterns/cqrs)). Here each store
answers a page of a hundred thousand vehicles inside the site's budget, and the bid that changes a
price has to be read back at once by the buyer who placed it. Two stores would buy nothing this site
needs and cost the one thing a bid must have, a price that is current. The Admin tab's 24 reads became
queries in 1.0.4.43; what is still exempt is the host serving its own files, its health and its error
rings, fifteen routes named in `HandlerRuleTests` with the reason for each.

## Sources

- Martin Fowler, [CQRS](https://martinfowler.com/bliki/CQRS.html), 2011. Read 2026-10-23.
- Microsoft Learn, [CQRS pattern](https://learn.microsoft.com/en-us/azure/architecture/patterns/cqrs), updated 2025-02-21. Read 2026-10-23.
- Greg Young, [CQRS Documents](https://cqrs.wordpress.com/wp-content/uploads/2010/11/cqrs_documents.pdf). Read 2026-10-23.
- Jimmy Bogard, [Vertical Slice Architecture](https://www.jimmybogard.com/vertical-slice-architecture/), 2018. Read 2026-10-23.
