Skip to content
Kumar Chandrachooda
.NET

The Rough Draft of Convey

A retrospective on DShop.Common - the frozen shared-contracts tombstone, a release candidate pinned forever, and a concordance of what Convey kept, renamed, and fixed.

By Kumar Chandrachooda 29 Oct 2025 6 min read
A sketch and its finished portrait side by side

Fifteen parts ago I claimed DShop.Common was worth reading because its authors threw it away and rebuilt it as Convey — that it was the sketchbook behind a finished painting. This part earns that claim. I want to do three things: date the moment the estate changed its mind about a fundamental decision and left a tombstone to prove it, walk the package archaeology that fixes this code precisely in time, and lay DShop.Common next to Convey to see what survived the rewrite, what got a new name, and what finally got fixed. A good retrospective is a concordance, and this one writes itself because both books exist.

The tombstone: DShop.Messages

The estate began with a shared-contracts package. DShop.Messages is a single netstandard2.0 library holding every command and event in the system — CreateOrder, CustomerCreated, SignedUp, thirty-nine message types across Customers, Identity, Orders, Products and Operations, plus its own ICommand/IEvent/IRejectedEvent markers. It is the shared-kernel answer to “how do services agree on message shapes”: one package, referenced by all, single source of truth.

And then the estate changed its mind. Its last commit is dated 29 July 2018 — “Merge pull request #3 from devmentors/develop” — and nothing has touched it since, while DShop.Common rolled on to version 2.0.5 and other repos lived into 2019. DShop.Messages is a fossil, frozen the moment the estate pivoted from shared contracts to copied ones: each service re-declaring the events it consumes in its own namespace, with the [MessageNamespace] attribute from part two existing precisely to route those local copies back onto the right exchange. The whole apparatus of convention-derived topology is downstream of this reversal.

The tombstone is not quite dead, and that is the best part: exactly one service — Storage — still references the package, while everyone else copies. So the estate contains both answers to the shared-kernel question, side by side, one of them a stopped clock. That single dangling reference becomes a version-skew time bomb the second series takes apart in detail; here it is enough to see it for what it is — a strategic decision reversed mid-flight, with the evidence left in the git history and one <ProjectReference> nobody removed. When you inherit a codebase, find its tombstones. They tell you what the team learned the hard way.

Package archaeology

The dependencies date this code to the month, and a few are worth exhuming:

  • RawRabbit 2.0.0-rc5. The entire messaging core — everything in parts two through seven — is built on a release candidate. Not a stable release; an rc5. Eleven RawRabbit packages, all pinned to a prerelease the estate never moved off. Building production messaging on an RC is a bet that it will go stable soon; when it doesn't, you are frozen on a version that will never get a fix, and every quirk of the bus is now permanent. That is a real risk this estate took and, reading it years later, plainly lost.
  • Build-number versioning. DShop.Common's version is <VersionPrefix>2.0.5</VersionPrefix> — a prefix, completed at pack time by a CI-appended build number (the pack.sh and Travis config tell that story for the sibling estate too). The real version of any given build is 2.0.5.{whatever the build counter was}, which means “what version is running?” has no answer you can reconstruct from source alone. Versioning as a side effect of the build is versioning you cannot reason about.
  • The DShop.Common_ PackageId, trailing underscore intact — a name chosen to dodge a feed collision and then frozen into the package's identity, exactly as I flagged in part one. Small, permanent, and honest about the rough-draft nature of the whole thing.
  • Seventy-five dependencies in one package, Travis CI rather than anything you'd recognise today, an old Newtonsoft in the messages tombstone — the estate is a 2018 amber, and every dependency is a fly in it.

The concordance: kept, renamed, fixed

Here is the payoff. Lay the sketch beside the painting.

What Convey kept. The core ideas were right and survived intact. The AddX/UseX self-registration convention from part one is Convey's entire ergonomic identity — AddConvey() and a fluent chain of Add-per-plugin. The marker-interface ICommand/IEvent/IQuery vocabulary (parts three and eight) carried straight over. Convention-derived message routing (part two) stayed the design centre. The two-lane domain-versus-infrastructure error taxonomy (part five) — genuinely the best thing here — is recognisable in Convey's message handling. Even the Redis deny-list revocation (part fourteen) survived, cost and all. The instincts were sound; that is why the rewrite is a refinement, not a repudiation.

What Convey renamed. DShop.Common became Convey — the shared-kernel-you-hand-rolled became the framework-you-install. The one fat package split along its eighteen folder-seams into separately versioned packages, so a service takes Convey.MessageBrokers.RabbitMQ without inheriting App.Metrics and Swagger it never uses — the package granularity part one wished for. GetOptions<T> and the BuildServiceProvider-to-read-config dance (part fifteen) became IConfiguration threaded cleanly through the registration chain — the one-appsettings-file-read-top-to-bottom shape the Convey series describes.

What Convey finally fixed. The absences from part seven became packages: the missing transactional outbox is Convey's two-outboxes-one-interface; idempotent-receiver and dead-letter handling grew up. The broker-side retry that was written-but-dead (part six) came back, re-enabled with routing that targets the right consumer — resolving the exact wildcard hazard the abandoned method's comment described. The scrambled correlation context (part four) slimmed to far fewer positional strings. And the concerns that were half-wired here — the error middleware that meant to map status codes, the two disagreeing token validators — consolidated into single, correct implementations.

When not to read this as a model

An honest retrospective has to say where the sharp edges are, so: do not copy DShop.Common. Copy what it learned. Its live defects are real — a correlation envelope that transposes its fields on every request, an error handler that 400s everything and leaks raw exception text, a load-balancer registry that only remembers when it is also unsafe, a JWT iat a thousand-fold too large. Its dead code is real too — a retry path, a fluent handler, a From<T> overload, all present and unreachable. If you inherited this as a production system you would have a fortnight of careful work ahead. That is not a criticism of teaching code; it is the difference between a sketch and a shipped framework, and pretending otherwise would waste the whole exercise.

What it teaches is worth more than most finished code: that self-registering concerns are a good idea, that convention-derived topology trades inspectability for consistency, that DI lifetime is a correctness decision, that await x?.M() and long positional argument lists and compute-then-discard are bugs waiting for a maintenance edit, and above all that the gaps in a framework are its roadmap. Everything DShop.Common lacked, its authors went and built. You are reading the requirements document for Convey, written in the shape of its own absences.

That is the framework. The second series, Nine Services and a Message Bus, turns from the chassis to the cars — the nine services stamped from one template, the contract that drifts between two of them, the sagas that orchestrate a checkout, and the test folder that only pretends to test. If this series was how the messaging core works, the next is what nine teams did with it. It starts with the topology, and the first thing it finds is an OrderCreated that means two different things depending on who is listening.