Skip to content
Kumar Chandrachooda
.NET

Pacts Through a Folder Six Levels Up

Orders and Parcels keep the estate's only cross-service contract honest with Pactify - a pact written to a relative path six directories above the repository, verified against a seeded Huge Weapon, and checked for shape only. Consumer-driven contracts with no broker, read from source.

By Kumar Chandrachooda 08 Feb 2026 7 min read
A contract handed sideways through a folder above every repo

Part 5 ended on the only two tests in the estate that CI actually executes: a Pactify consumer test in Orders and a Pactify provider test in Parcels, one each, passing a contract between them through the file path ../../../../../../pacts. This part reads that pair line by line, because between them they are the entire answer ten Pacco services give to the question “how do we stop quietly breaking each other” — and both the ambition and the shortfall of that answer are instructive.

The seam under contract is real. Orders' ParcelsServiceClient calls GET {parcels}/parcels/{id} over HTTP and deserialises the response into its own ParcelDto — a copied, four-property shadow of a type it does not share with Parcels, because nothing in this estate is shared. If Parcels renames a property, Orders finds out at runtime, in production, as a null. Consumer-driven contract testing exists precisely for this, and the estate uses Pactify — the small MIT-licensed contract-testing library by Dariusz Pawlukiewicz, one half of DevMentors, version 1.1.0 in both test projects — to pin the seam down.

The consumer writes a promise

Here is Orders' side, complete — tests/Pacco.Services.Orders.PactConsumerTests/PACT/ParcelsApiPactConsumerTests.cs, the only test in the repository:

public class ParcelsApiPactConsumerTests
{
    private const string ParcelId = "c68a24ea-384a-4fdc-99ce-8c9a28feac64";

    [Fact]
    public async Task Given_Valid_Parcel_Id_Parcel_Should_Be_Returned()
    {
        var options = new PactDefinitionOptions
        {
            IgnoreCasing = true,
            IgnoreContractValues = true
        };

        await PactMaker
            .Create(options)
            .Between("orders", "parcels")
            .WithHttpInteraction(b => b
                .Given("Existing parcel")
                .UponReceiving("A GET request to retrieve parcel details")
                .With(request => request
                    .WithMethod(HttpMethod.Get)
                    .WithPath($"/parcels/{ParcelId}"))
                .WillRespondWith(response => response
                    .WithHeader("Content-Type", "application/json")
                    .WithStatusCode(HttpStatusCode.OK)
                    .WithBody<ParcelDto>()))
            .PublishedAsFile("../../../../../../pacts")
            .MakeAsync();
    }
}

Read it slowly, because every line teaches something.

No server starts. A conventional Pact consumer test drives your real client code against a mock provider. Pactify's PactMaker does something more modest: it records the definition — method, path, expected status, and a body shape derived from ParcelDto by reflection via WithBody<ParcelDto>() — and publishes it. The consumer test cannot fail because Orders' client code is wrong; it can only fail if the pact cannot be written. It is less a test than a declaration with a green tick attached.

The id is a constant. WithPath($"/parcels/{ParcelId}") bakes the literal GUID c68a24ea-… into the contract's path. That constant is now load-bearing: whoever verifies this pact must be able to serve that specific parcel. Hold the thought.

And the destination is a relative path. PublishedAsFile("../../../../../../pacts") resolves against the test runner's working directory, which is the test assembly's output folder — tests/Pacco.Services.Orders.PactConsumerTests/bin/Debug/netcoreapp3.1. Count the climb: out of netcoreapp3.1, Debug, bin, the project folder, tests, and finally out of the Orders repository itself. Six levels up lands in the directory where the thirteen Pacco repos are cloned side by side — the layout the umbrella repo's clone script produces — and Pactify's own source names the file it writes there: $"{localPath}/{consumer}-{provider}.json", so pacts/orders-parcels.json, sitting above every repository, belonging to none of them.

There is no Pact broker. Pactify supports one — PublishedViaHttp(url, method, apiKey) sits right next to PublishedAsFile in the source — but the estate chose the folder. And the folder is not committed anywhere: grep all thirteen repositories and you will find no pacts directory and no pact JSON. The contract file exists only on a machine where the consumer test has already run.

The provider seeds a Huge Weapon

Parcels' side — tests/Pacco.Services.Parcels.PactProviderTests/PACT/ParcelsApiPactProviderTests.cs — is the estate's most complete test after Availability's pyramid, and its arrange region repays attention:

[Fact]
public async Task Pact_Should_Be_Verified()
{
    await _mongoDbFixture.InsertAsync(Parcel);

    await PactVerifier
        .Create(_httpClient)
        .Between("orders", "parcels")
        .RetrievedFromFile(@"../../../../../../pacts")
        .VerifyAsync();
}

private readonly ParcelDocument Parcel = new ParcelDocument
{
    Id = new Guid("c68a24ea-384a-4fdc-99ce-8c9a28feac64"),
    Name = "Product",
    Size = Size.Huge,
    Variant = Variant.Weapon,
    CreatedAt = DateTime.Now
};

That fixture document is my favourite artefact in the estate: a parcel named “Product”, sized Huge, of variant Weapon — the domain's cheerful any-cargo-welcome policy compressed into one seeded row. But notice why it exists: its Id is the same GUID the consumer test hard-coded into the pact's path. The provider must insert exactly this document into a real MongoDB at mongodb://localhost:27017 (database test-parcels-service, dropped again on dispose) before verification, or the pact's GET /parcels/c68a24ea-… returns nothing and the contract fails for reasons that have nothing to do with the contract. The consumer's constant and the provider's fixture are a hidden pact about the pact, coordinated by copy-paste across two repositories — the consumer tax, collected even here.

The constructor holds the other half of the arrangement:

var testServer = new TestServer(Program.GetWebHostBuilder(new string[0]));
testServer.AllowSynchronousIO = true;
_httpClient = testServer.CreateClient();

This is real verification — the actual Parcels service pipeline, hosted in-memory, receiving the pact's request. And it needed a source change to be possible: Parcels' Program.cs is the only one in the estate refactored into Main plus a public static IWebHostBuilder GetWebHostBuilder(string[] args), precisely so a test can host the app. Availability solved the same problem the other way, with a three-line WebApplicationFactory<TEntryPoint> subclass that forces a tests environment. Two repositories, two testability idioms, no shared answer — each service that ever wants a hosted test will pick one, alone, again. The provider test also carries a full copied appsettings.json — a hundred and thirty lines of configuration duplicated into the test project so the TestServer can boot, with Mongo pointed at the test database and consul, fabio, jaeger and the outbox switched off.

What “verified” means when values are ignored

Both IgnoreCasing and IgnoreContractValues are set, and it is worth being precise about what survives. In Pactify 1.1.0's HttpInteractionVerifier, body verification walks the expected body's top-level properties and, for each one, finds the matching property in the actual response (case-insensitively, thanks to IgnoreCasing). If the property is absent — or present with a null value, which the check cannot distinguish from absence — that is a failure. And then:

if (options.IgnoreContractValues)
{
    continue;
}

Values are never compared. The contract is a shape: these property names exist, with something non-null in them. Rename Size to ParcelSize and the pact catches it. Change Size from "Huge" to 37, or start returning sizes in a different unit, and the pact is satisfied. To be fair, for a GET-by-id read model, shape is most of the contract — and shape-only is an honest choice for a demo, since value-matching against a seeded fixture mostly tests the fixture. But it is worth saying plainly that the strongest test in this estate asserts the presence of four property names, once, over one interaction.

And the scope is narrower still: this pact covers one HTTP seam. The far larger contract surface between these same two services is the bus — Orders holds copied classes for Parcels' events, and events cannot appear in a Pactify HTTP interaction at all. The domain series showed what those copies cost when the parcel wouldn't leave; no pact in this estate could have caught it.

The folder is the broker

Now assemble the operational picture, because this is where the design stops being charming.

The pact file is born when Orders' consumer test runs, six levels above the repo, on whatever machine ran it. It is consumed when Parcels' provider test runs — if that machine has the same sibling-checkout layout, if the consumer test ran first, and if a MongoDB is listening on localhost. A Pact broker exists to answer exactly the questions this arrangement cannot: which pact version is current, which consumer published it, whether the provider verified it, and when. The folder answers none of them. There is no versioning — each consumer run overwrites orders-parcels.json in place — no record of verification, and no way for Parcels' CI to obtain the pact at all, because Travis clones one repository into an empty workspace. Run the provider job on a clean runner and RetrievedFromFile finds nothing and the Mongo insert has nowhere to land; the one repository pair whose dotnet test step tests anything real is also the pair whose tests cannot pass anywhere except a developer's fully-cloned machine. Whether they ever passed on Travis is unknowable now — the badges died with travis-ci.org — but the configuration as committed could not have supported it.

A contract exchanged through a filesystem path is a contract with whoever shares your disk, not with your provider. The whole point of consumer-driven contracts is to catch the break between two teams' machines; routing the contract through a folder that only exists when both codebases are checked out side by side quietly reduces “consumer-driven contract testing” to “integration testing on the author's laptop”, with extra steps.

To be fair to the authors: this is a teaching estate, the sibling-checkout layout is its documented mode of use, and standing up a Pact broker for a two-service demo would be infrastructure for infrastructure's sake. The pair of tests genuinely demonstrates the shape of the practice — consumer declares, provider verifies against the real pipeline — and Pactify's file mode makes the demonstration runnable in one afternoon. But the gap between the shape of the practice and its substance is exactly what this series is pricing, and here the gap is the broker: the estate has consumer-driven contracts, and no way to deliver them.

The estate does have one more contract artefact, though — bigger than this pact by an order of magnitude, covering every message on the bus, and maintained entirely by hand. Next: a shadow registry of every message, where one JSON file subscribes a service to eighty contracts and nothing on earth checks that any of them are real.