What Convey Got Right — and What Modern .NET Ate
Closing the series with an honest audit of the chassis - which of Convey's ideas still earn their keep in 2026, which ones the platform absorbed, and what I would build differently today.
Twenty-one parts ago I claimed that Convey — DevMentors' open-source .NET microservices toolkit — was one of the best codebases you could read to learn how a service chassis is actually engineered. Having now walked every package from the builder to the broker to the Vault integration, the closing question is the one I would ask about any infrastructure investment: if I were starting a fleet of services today, how much of this would I still want?
The honest answer is: less of the code, almost all of the ideas.
What aged well
The composition discipline. The single best thing in Convey is not a feature, it is the uniformity: every capability is Add<Thing>() on IConveyBuilder, every capability reads one predictably named configuration section, every capability that touches the pipeline has a matching Use<Thing>(). Twenty-two weeks in, I never once had to guess where something was wired. Modern .NET gives you better primitives than the hand-rolled builder — but it still does not give you the discipline. Teams have to impose that themselves, and Convey remains the cleanest worked example I can point at.
Conventions as contract. The messaging layer's insistence that exchange names, routing keys and queue names derive mechanically from type names (part 9) looked like a convenience feature. In production it turned out to be the governance feature: nobody debates naming, and the wire topology is diffable from the code alone. The same is true of the options sections — an appsettings.json in a Convey service is a legible manifest of everything the service participates in.
The outbox and inbox (part 12). The dual-write problem has not gone anywhere, and Convey's IMessageOutbox — store the event with the state change, publish from a background loop, deduplicate on the consumer — is the same shape you will find in MassTransit, Wolverine, or anything you roll yourself. The implementation has real warts (the fire-and-forget timer tick with no re-entrancy guard, the EF store that never cleans up), but the model is exactly what I still deploy.
The /_contracts endpoint (part 6). Serving a machine-readable catalog of every command and event a service understands, straight from the types themselves, is an idea I have re-implemented on services that never used Convey. It is async-API documentation for the price of one middleware.
The decorator seam (part 4). Registering handlers while excluding [Decorator]-marked types, then wrapping closed generic interfaces one by one, is still the cheapest way I know to add cross-cutting behavior without a mediator framework. The reflection-into-Scrutor wiring was too clever; the seam itself was right.
What modern .NET ate
This is the longer list, and it should be — Convey's whole reason to exist was to fill gaps in the platform circa .NET Core 2–6. Platforms close gaps.
The endpoint DSL → Minimal APIs. Part 5's controller-less UseEndpoints(e => e.Get<GetOrder, OrderDto>(...)) predates Minimal APIs and reads eerily like them. Today app.MapGet("/orders/{id}", ...) with TypedResults, endpoint filters and [AsParameters] binding does everything the DSL did — without reflecting into <name>k__BackingField to hydrate init-only properties. The DSL is the clearest case of the platform absorbing the idea wholesale.
OpenTracing + Jaeger client → OpenTelemetry. Part 21's tracing story is archaeology now: OpenTracing is archived, the C# Jaeger client is deprecated, and System.Diagnostics.ActivitySource plus OTLP export is the answer to every question the Jaeger package answered — including the broker-hop propagation the RabbitMQ plugin hand-stitched through a span_context header, which W3C traceparent context now standardizes. And crucially, an OTel instrumentation library that swallowed handler exceptions the way Convey's Jaeger plugin did would be treated as a critical bug. Observability must never change reliability semantics; that plugin is the series' sharpest cautionary tale.
The HTTP client → IHttpClientFactory + Polly/resilience handlers. Part 14's IHttpClient with its config-driven retries retried every exception — including non-idempotent POSTs — and its typed GetAsync<T> returned default on a non-2xx. AddHttpClient().AddStandardResilienceHandler() in .NET 8+ gives you retry classification, circuit breaking and timeout budgets that took Convey a page of hand-rolled code, and does it per-named-client instead of globally.
Consul + Fabio → platform networking. On Kubernetes, service DNS plus a mesh (or plain kube-proxy) made client-side registries redundant for most teams; .NET 8's Microsoft.Extensions.ServiceDiscovery and Aspire's service bindings cover the local-dev story Consul filled. The pattern knowledge from part 15 still matters — health-gated registration, deregistration on shutdown — but I have not deployed a new Consul agent for service discovery in years.
Vault config loading → provider-native config. UseVault() hooking secrets into IConfiguration before the host builds (part 16) is exactly how Azure Key Vault and AWS provider packages work today, minus the bespoke lease-renewal service. Vault itself is still excellent; the integration code is no longer something you should write.
Sequential in-memory dispatch → source-generated mediators or none at all. The CQRS spine (part 3) still reads beautifully, but reflection-based dispatch (MakeGenericType per query) and AppDomain-wide assembly scans are exactly the things AOT-friendly .NET is moving away from. Today I would use a source-generated mediator — or, more often, admit that a Minimal API endpoint calling an application service is enough and skip the bus entirely.
RabbitMQ plumbing → MassTransit or Wolverine. Part 9–10's connection management, channel-per-thread pooling and DLX wiring are educational precisely because mature libraries now hide them — with publisher confirms, which Convey never enabled, and exponential backoff, which its fixed-interval Polly policy never did.
What I would build differently — and what I would keep
If I were assembling a chassis in 2026, the inventory looks like this:
- Keep the one-line-per-capability composition and the section-per-package configuration convention. Implement it as thin extension methods over the standard builder —
IHostApplicationBuildernow gives youAddServiceDefaults()-style composition for free; Aspire's defaults project is, structurally,AddConvey()with a Microsoft badge. - Keep conventions-derived messaging topology, the outbox/inbox pair, and the contracts endpoint — as policy over MassTransit/Wolverine rather than as raw RabbitMQ code.
- Replace every observability package with OpenTelemetry, every HTTP nicety with resilience handlers, the endpoint DSL with Minimal APIs, and discovery with whatever the platform gives you.
- Drop the reflection glue: backing-field binding, Scrutor-by-reflection decoration, throwaway
BuildServiceProvider()calls. Every one of these was a pragmatic 2019 answer that trimming and AOT have since turned into a liability.
The deeper lesson of the series is about where a chassis's value lives. Almost none of it was in the clever code — the clever code is what rotted. The value was in the decisions the chassis made once so that fifty services didn't make them fifty ways: names of config sections, shapes of handlers, what happens to a failed message, how a trace crosses a broker. Those decisions outlived every implementation detail, and several of them outlived the toolkit itself and re-appeared in the platform.
So: should you adopt Convey today? For a new system — no; the platform and the current OSS generation cover it. Should you read it? Absolutely, and that has been the point of these twenty-two parts. It is a complete, honest, production-shaped answer to "what does a .NET microservice actually need?", small enough to hold in your head, and old enough that you can check its bets against how history went. Codebases that let you grade their predictions are rare. Convey got the important ones right.
My thanks to the DevMentors team for building it in the open. If you have a fleet of services and no chassis, start where this series started: not with a framework, but with the list of decisions you are currently making more than once.