A Service in Thirty Lines
The fair part of the ledger - how UseDispatcherEndpoints, self-identifying commands, afterDispatch hooks and snake_case conventions let every Pacco service put its whole HTTP surface in one readable file.
Newest first, grouped by year. Search or filter by topic.
The fair part of the ledger - how UseDispatcherEndpoints, self-identifying commands, afterDispatch hooks and snake_case conventions let every Pacco service put its whole HTTP surface in one readable file.
Retention as a first-class column: how ArchiveKit stamps ExpiresAt at archive time from a TtlDays attribute, hard-deletes expired rows in bounded batches, and runs the whole thing on Lambda with the hosted services switched off. Also, the rows TTL doesn't clean yet. Part 8 of the Archive-on-Delete with ArchiveKit series.
MessageBroker.cs exists in seven Pacco repositories and hashes identically once you substitute the service name - this part inventories the copied glue, finds the double registrations hiding in seventeen-call chains, and weighs copy-paste against a shared kernel.
FeedR's weather feed is the pattern for every external integration: poll a REST API inside an IAsyncEnumerable, guard it with a typed HttpClient and Polly retries, translate its JSON at the boundary, and republish it as a push. Plus the retry clause I can't defend and a hard-coded API key with a lesson in it. Part 6 of the FeedR deep dive.
Convey sells a microservice in thirty lines, and Pacco builds ten of them - this series reads the whole estate to price what the framework compresses and what every consumer pays for by hand. Part 1 of The Convey Consumer Tax.
Convey.WebApi.CQRS wires HTTP verbs straight to command and query dispatchers - one line per endpoint - and publishes your message catalog at /_contracts. A source read of the thinnest HTTP layer I've shipped to production.
A point lookup that might hit any of three tables, a list query that should span them all, and writes that must keep a registry honest - this part walks VersionKit's version-aware reader and writer, and the generated UNION view that makes every schema era queryable as if it were current. Part 8 of the Entity Versioning with VersionKit series.
When a Chronicle saga rejects, the saga log is replayed backwards and every handled message gets its CompensateAsync called. Tracing both rejection paths through the source reveals a surprise - only one of them actually compensates - plus a ForEach that never awaits.
FastEndpoints' processors are its cross-cutting hook - but their exact pipeline position, the shared state trick, and a captured exception that waits in a finally block are what make them genuinely useful. Part 6 of FastEndpoints in Depth.
Shipping a migration to one database is a deploy step. Shipping it to every schema in every group database is an orchestration problem — cross-database MigrateAsync, an HTTP migration surface, a per-tenant audit ledger, and the honest gap where the fan-out loop should be.
The estate's transaction logging wrote to blob storage through a dedicated logging function - which consumers located through the same Key Vault registry the logger belongs to. Discovery consuming discovery, the transaction-id thread that made it useful, and the bootstrap questions self-reference always raises. Part 6 of Service Discovery Without a Service Mesh.
The provider side of Pactify - how CreateFor<Startup> hosts your whole API in memory with TestServer, why interactions verify concurrently, the SmartFormat trick behind endpoint templating, and the discovery that Given(...) never runs.
ArchiveKit's snapshot service assembles a full JSON tree of a deleted object graph using nothing but archive rows - the primary data is already gone. Job tickets, a four-state machine, retries with a terminal Failed state, and the single-table assembly limitation I still owe a fix for. Part 7 of the Archive-on-Delete with ArchiveKit series.
FeedR pushes live prices to clients through a gRPC server stream: a four-message proto contract, fixed-point decimals, dual Kestrel endpoints, and an event-to-stream bridge that busy-spins - plus the channel-based rewrite it's asking for. Part 5 of the FeedR deep dive.
Convey.WebApi threw away controllers, replaced every MVC formatter with its own, and bound route values into immutable commands by writing to compiler-generated backing fields. A source read of a routing layer built years before .NET shipped the same idea.
JSON payloads can migrate lazily because the reader controls the shape - relational rows cannot, because the table is the shape. VersionKit's EF Core answer is table-per-version - archive Orders as Orders_v2 on a schema bump, track every row's home in a registry, and let old rows stay old until something moves them. Part 7 of the Entity Versioning with VersionKit series.
Seeker, initializer, processor, post-processor - Chronicle's whole runtime is four internal classes you can read in ten minutes. A source walk through the pipeline, including the gate that drops messages, the finally block that always persists, and the state that isn't as final as it looks.
The series retrospective - Pacco's domain teaches real rules (state machines, priority expropriation, loyalty ladders, compensations) on top of cardboard data (bare-decimal prices, one vehicle per order, size-cube parcels, no payment, and a capacity check that ships in two halves nobody ever connects). What to steal, what to distrust, and what fourteen parts of source-reading add up to.
AssignVehicleToOrder is the one command in Pacco that cannot finish alone - Orders calls Vehicles, then Pricing, and Pricing calls Customers, a synchronous enrichment chain on the write path of a messaging estate. At the end of it sits a deliberately bus-less calculator with a loyalty ladder, an unassigned Id and a discount logged in dollars.
FastEndpoints wires FluentValidation into the pipeline by request-DTO type, runs validators as singletons, and shares one failure list between binder, validator and handler. The lifetime rule is the part that bites. Part 5 of FastEndpoints in Depth.
Availability guards every save with a version-filtered ReplaceOneAsync - and discards the result, so a lost update is indistinguishable from a successful one and the integration event ships either way. Three folders away, Orders and Customers inherit the same AggregateRoot name with the versioning quietly deleted.
In Pacco the CQRS split is not a diagram - it is wiring you can read. Every command arrives through two front doors, RabbitMQ subscription and controller-less HTTP dispatch, while queries ride HTTP only; commands assign their own ids in the constructor, and a marker attribute turns each service into its own runtime contract registry.
Deleting a tenant is the most dangerous button in the platform — behind it sit DROP SCHEMA CASCADE and DROP DATABASE. A two-step lifecycle with global query filters, a SaveChanges interception and an explicit purge keeps the blast radius survivable.
Every Pacco service that enables Convey's Mongo outbox also sets disableTransactions to true - the aggregate write and the outbox write are separate operations, the pattern's one guarantee switched off in configuration. Meanwhile the inbox that would absorb the resulting duplicates sits beside three handlers that throw on them.
One static registration method wires the whole discovery subsystem into an Azure Functions host, and one CI pipeline packs it as a private NuGet package. The lifetime choices in those few lines quietly decide the caching semantics of the entire estate - and a duplicate registration teaches how DI forgives. Part 5 of Service Discovery Without a Service Mesh.