The Microservices Chassis You Were Going to Write Anyway
Every team's fifth microservice ships with the same four hundred lines of bootstrap code as the first four. Opening a deep-dive series on Convey, the open-source .NET toolkit that turned that boilerplate into thirty composable packages.
The second microservice a team builds is a copy of the first one. Not the domain logic — the other stuff. The Serilog wiring, the Swagger registration, the health-check endpoint, the RabbitMQ connection factory with its retry loop, the JWT validation block, the correlation-ID middleware somebody got working at 11pm and nobody has touched since. By service five, that bootstrap code is four hundred lines long, exists in five slightly divergent copies, and fixing a bug in it means five pull requests.
This is such a predictable failure that it has a pattern name: the microservice chassis. Like a car chassis, it is the frame every vehicle in the fleet shares — you build many different services on top of it, but the suspension, the wiring loom and the crash structure are engineered once. Some companies buy a chassis (Spring Boot filled this role for the JVM world). Some inherit one from a platform team. Most .NET teams I have worked with ended up accreting one, copy by copy, without ever deciding to.
This series is a deep dive into a team that did decide to: Convey, the open-source .NET microservices toolkit from the DevMentors ecosystem. I did not write Convey and I don't maintain it — credit belongs to its authors. What I bring is years of working with it, reading it, and stealing from it: I have shipped services on it, debugged it under load, and been through essentially all of its source. It is one of the best codebases I know for learning how a chassis is actually engineered, because it is small enough to read end-to-end and honest enough to show its trade-offs.
What a chassis buys you
Strip the buzzwords and a chassis is three promises:
- Every cross-cutting concern is a library, not a snippet. Logging, metrics, tracing, discovery, messaging, auth — each one is a package you reference, not a paragraph you paste.
- Configuration follows one convention. You should be able to guess what the config section for a new capability looks like before you read its docs.
- Composition is explicit. One place in the service tells you everything the service participates in — and adding a capability is one line, not an afternoon.
Convey delivers all three with a mechanism so simple it is almost disappointing: a builder interface and a pile of extension methods. Here is the Program.cs of the Orders service from Convey's own samples, lightly trimmed:
services
.AddConvey()
.AddErrorHandler<ExceptionToResponseMapper>()
.AddConsul()
.AddFabio()
.AddJaeger()
.AddMongo()
.AddMongoRepository<Order, Guid>("orders")
.AddCommandHandlers()
.AddEventHandlers()
.AddQueryHandlers()
.AddInMemoryCommandDispatcher()
.AddInMemoryEventDispatcher()
.AddInMemoryQueryDispatcher()
.AddPrometheus()
.AddRedis()
.AddRabbitMq(plugins: p => p.AddJaegerRabbitMqPlugin())
.AddMessageOutbox(o => o.AddMongo())
.AddWebApi()
.AddSwaggerDocs()
.Build();
Read that slowly, because it is the whole thesis of the toolkit in one screenful. Service discovery via Consul, load balancing via Fabio, distributed tracing via Jaeger, MongoDB persistence with a typed repository, a full CQRS spine, Prometheus metrics, Redis, RabbitMQ messaging with tracing propagated through the broker, a transactional outbox, an HTTP API and generated docs. Twenty lines. Every one of those Add* calls is a separate NuGet package, and every one follows the same internal pattern — which means once you have read one, you can navigate all thirty-three.
The application pipeline is the same story (UseConvey(), UseRabbitMq().SubscribeEvent<DeliveryStarted>(), UseDispatcherEndpoints(...)), and I will take it apart over the coming parts.
The trick underneath: one pattern, thirty-three packages
Convey's packages don't share a base class or a plugin manifest. They share a discipline, and it has three ingredients.
An IConveyBuilder. AddConvey() wraps your IServiceCollection in a small builder that adds two things vanilla DI doesn't have: a named registry so each module registers exactly once no matter how many times you call it, and a queue of post-build actions so modules can do work after the container exists — like registering the service with Consul, or declaring RabbitMQ exchanges. Part 2 is entirely about this class; it is barely a hundred lines and it carries the whole ecosystem.
An options section per package. Every package binds a plain options class from a conventionally named configuration section — "consul", "rabbitmq", "mongo", "jaeger", "logger". Your appsettings.json becomes a table of contents for the service's capabilities. There is no magic binder; it is IConfiguration.GetSection(name).Bind(model) behind one shared GetOptions<T>() helper.
An extension method as the front door. Each package exposes Add<Thing>(this IConveyBuilder builder, string sectionName = "...") and, where it participates in the request pipeline, Use<Thing>(this IApplicationBuilder app). That's the entire public contract. If you have written an ASP.NET Core library in the last five years, this looks obvious — but Convey applied it uniformly across an entire microservices stack, and uniformity is the part teams fail at.
None of this is exotic. That is precisely why it is worth studying: the gap between “we have shared libraries” and “we have a chassis” is not technology, it is consistency.
An honest note about the era
The Convey source I will be reading targets .NET 6. Parts of it predate things the platform later absorbed: its controller-less endpoint DSL predates Minimal APIs, its Jaeger client predates OpenTelemetry becoming the obvious default, its hand-rolled HTTP retries predate everyone agreeing on IHttpClientFactory + Polly as the idiom. I am not going to pretend otherwise, and the final part of this series is an explicit retrospective: what aged well, what modern .NET ate, and what I would still use a chassis for in 2026.
That historical layer is a feature of the series, not a bug. Watching a toolkit solve a problem before the platform did is the best way I know to understand what the platform's solution is actually for.
Where the series goes
The plan, one part a week:
- The chassis argument — this post.
- One builder, thirty packages —
IConveyBuilder, the registry, build actions and startup initializers. - Three marker interfaces and a service scope — the CQRS spine: commands, queries, events, and in-memory dispatch.
- The decorator seam — cross-cutting logging injected around handlers you didn't change.
- Minimal APIs before Minimal APIs existed — the controller-less endpoint DSL.
- From route to handler with no controller in between — dispatcher endpoints and the public contracts catalog.
- Documenting an API that has no controllers — synthesising OpenAPI from an endpoint registry.
- A message broker behind an interface — the messaging abstractions and naming conventions.
- RabbitMQ by convention — connections, publishing, subscribing.
- Retries, dead letters, and the plugin chain — what happens when a message fails.
- Commands and events on the wire — CQRS over the broker.
- The outbox that keeps your events honest — transactional outbox and inbox deduplication.
- Persistence as a registration — the Mongo repository and Redis.
- Typed clients over one HTTP door — the HTTP client and RestEase integration.
- Discovery and load balancing before the mesh — Consul and Fabio.
- Secrets before configuration — Vault: KV, dynamic credentials, PKI.
- JWTs and the blacklist problem — auth, and revoking tokens in a stateless world.
- Service-to-service trust — mTLS and certificate ACLs.
- The logger you configure once — Serilog, Seq, and correlation.
- Metrics two ways — AppMetrics and Prometheus.
- Tracing a message through the chassis — Jaeger, and spans that survive a broker hop.
- What Convey got right — and what modern .NET ate — the retrospective.
If your team is three services into the copy-paste spiral, this series is the map of what the fifth copy was trying to become. Start with the builder — everything else in the toolkit stands on it.