Skip to content
Kumar Chandrachooda
.NET

The Framework Before the Framework

DShop.Common is the shared library nine microservices were built on - and the rough draft of a framework its authors later shipped as Convey.

By Kumar Chandrachooda 22 Jul 2025 6 min read
One package radiating the eighteen concerns every service inherits

Every microservices tutorial hands you the services. The interesting artifact is almost always the thing underneath them — the shared library every service references so that none of them has to re-solve messaging, discovery, persistence and auth. The industry has a name for it now: the microservices chassis. In 2018 it mostly didn't, and teams wrote their own. This series reads one of those hand-rolled chassis end to end, because its authors did something unusual afterwards: they threw it away and rebuilt it as a real open-source framework.

The chassis is DShop.Common, the shared kernel of DevMentors' teaching estate DShop — an e-commerce shop built once as a monolith and once as nine microservices. The framework it became is Convey, which this blog already covers as its own source-read series. Reading DShop.Common first is like finding the sketchbook behind a finished painting: the same ideas, before they were named, packaged, and — in a few cases — fixed.

To be clear up front: I did not write DShop.Common. It is MIT-licensed code by DevMentors (Piotr Gankiewicz and Dariusz Pawlukiewicz), and I am reading it as a power user who has shipped this exact style of chassis and wants to know how theirs holds up. Where the implementation is the story I will quote it, attributed to the source and its era; everything else is fresh illustrative code. This is 2018–2019 code targeting netstandard2.0 and ASP.NET Core 2.2 — I will cite versions exactly, because half the lessons here are lessons about time.

One package, seventy-five dependencies

The first thing the .csproj tells you is that DShop.Common is one package, and it is not shy:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
    <VersionPrefix>2.0.5</VersionPrefix>
    <Authors>DevMentors.io</Authors>
    <PackageId>DShop.Common_</PackageId>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Autofac" Version="4.9.2" />
    <PackageReference Include="Consul" Version="0.7.2.6" />
    <PackageReference Include="Jaeger" Version="0.3.2" />
    <PackageReference Include="MongoDB.Driver" Version="2.8.1" />
    <PackageReference Include="RawRabbit" Version="2.0.0-rc5" />
    <!-- ...and roughly seventy more -->
  </ItemGroup>
</Project>

I trimmed the list to five of about seventy-five. The full manifest carries twenty App.Metrics.* packages, eleven RawRabbit.* packages, Consul, Jaeger, Mongo, Polly, RestEase, Serilog, Swashbuckle, VaultSharp, and the JWT stack. A service that references DShop.Common inherits every one of them, transitively, whether it publishes metrics to InfluxDB or not. This is the defining trade of the single-package chassis: you get the entire platform in one <PackageReference>, and you get all of it or none of it. There is no DShop.Common.RabbitMq, no DShop.Common.Mongo — the seams that would later become separately versioned Convey packages are, here, just folders.

Two smaller things in that file are worth pausing on, because they are the kind of detail that only reading the source surfaces. The version is set by VersionPrefix rather than a plain <Version>, which means the real package version is 2.0.5 plus whatever build number the CI appends — versioning as a side effect of the build, a thread I pull all the way in the final part. And the PackageId is DShop.Common_, with a trailing underscore. That underscore is not a typo you fix; it is a name chosen to avoid colliding with something on a feed, frozen into the identity of the package. Small tells, but they set the register for the whole estate: this is real teaching code, published for real, with the rough edges left on.

Eighteen concerns, one convention

Inside src/DShop.Common, the code is organised by concern — eighteen folders, each a slice of the platform:

Authentication  Consul     Dispatchers  Fabio
Handlers        Jaeger     Logging      MailKit
Messages        Metrics    Mongo        Mvc
RabbitMq        Redis      RestEase     Swagger
Types           Vault

plus five files at the root — AppOptions, Extensions, IInitializer, IStartupInitializer, StartupInitializer. That is the entire surface: eighteen concerns and a handful of composition roots. What makes it a framework rather than a folder of utilities is that every one of those concerns speaks the same two-word dialect. Each concern exposes an AddX to register itself and, where it plugs into the request pipeline, a UseX to activate it. The RabbitMq folder is typical:

public static IBusSubscriber UseRabbitMq(this IApplicationBuilder app)
    => new BusSubscriber(app);

public static void AddRabbitMq(this ContainerBuilder builder)
{
    builder.Register(context =>
    {
        var configuration = context.Resolve<IConfiguration>();
        var options = configuration.GetOptions<RabbitMqOptions>("rabbitMq");
        return options;
    }).SingleInstance();
    // ...register handlers, the publisher, the bus...
}

A service's Startup becomes a legible list of intentions — AddRabbitMq(), AddMongo(), AddConsul(), AddJaeger() — and its Configure a matching list of UseRabbitMq(), UseErrorHandler(). This is the ASP.NET Core builder idiom applied uniformly to everything, three years before “chassis” was common vocabulary. If you have read the Convey series, AddConvey() and its Add-per-plugin fluent chain will feel familiar; this is where that muscle memory was formed.

Configuration as a two-line ritual

Every concern reads its own configuration through one tiny extension, the most-used method in the whole library:

public static TModel GetOptions<TModel>(this IConfiguration configuration, string section)
    where TModel : new()
{
    var model = new TModel();
    configuration.GetSection(section).Bind(model);
    return model;
}

Twelve words of C# that appear in nearly every AddX. It binds a named appsettings.json section to a strongly-typed options object and hands it back. There is no IOptions<T>, no validation, no reload-on-change — just bind and go. And paired with it is a convention that turns the whole platform into a set of switches: most of the optional concerns carry an Enabled flag on their options class.

public class JaegerOptions
{
    public bool Enabled { get; set; }
    // endpoint, service name, sampler...
}

Eight of the option classes do this — Consul, Fabio, Jaeger, Vault, Swagger, Metrics, and both logging sinks (Seq and ELK). The pattern is: AddX binds the options, checks Enabled, and either wires the concern in or quietly does nothing. It means a developer configures the estate by flipping booleans in JSON rather than editing Startup, which is genuinely pleasant — and it means the real wiring of a service lives in its config file, not its code, which is genuinely a place bugs hide. Both halves of that are true, and this series is going to keep finding both halves.

Where the series goes

DShop.Common is small enough to read completely and rich enough to be worth it. Over the next fifteen parts I read it concern by concern, and every part pairs one good idea with the honest ledger of how it was actually implemented:

  1. The Framework Before the Framework — you are here.
  2. Queue Names Are the Whole Topology — how one naming class encodes the entire message routing scheme.
  3. Command or Event, Enforced by Convention — the publisher that treats the two as one thing.
  4. The Envelope That Scrambled Itself — a correlation context with a real, invisible argument-order bug.
  5. Rejected Events — a Two-Lane Error Taxonomy — domain failure versus infrastructure failure, decided by exception type.
  6. The Retry Path Nobody Took — a fully written broker-requeue path, left dead in the file.
  7. What the Framework Doesn't Have — the absence audit: no outbox, no inbox, no dead-letter.
  8. Hand-Rolled MediatR in Thirty Lines — the in-process dispatcher and its dynamic double-dispatch.
  9. The Fluent Handler That Explodes on Successawait null and the NRE that eats your domain exception.
  10. Three Load Balancers Behind One Config String — Consul, Fabio, or static, chosen by a string.
  11. A Load Balancer With Amnesia — a registry that forgets everything it learns, per resolution.
  12. Writing to the Backing Field — reflecting into <Id>k__BackingField to mutate an immutable command.
  13. Every Error Is a 400 and Says Too Much — the middleware that computes a safe message and then discards it.
  14. Revoking the Irrevocable — a JWT deny-list in Redis and a per-request round-trip.
  15. BuildServiceProvider Nine Times — three DI containers per service and the containers built to throw away.
  16. The Rough Draft of Convey — the retrospective: what Convey kept, renamed, and finally fixed.

If your team has ever written its own Add-everything shared library — the one internal NuGet package that every service references and nobody dares to split — the next fifteen parts are a mirror. DShop.Common is that package, published, MIT-licensed, and honest about its scars. Next, the naming class that quietly decides how every message in the estate finds its way home.