.dev notes
Practical posts on .NET, C#, Azure and building backend integrations.
A new post each week, usually on something I ran into on a real project, with the code to go with it.
October 2026
-
Agentless logging from .NET to Datadog
You don't need to install the Datadog Agent to get your .NET logs into Datadog. Two agentless options send them straight over HTTPS, structured and ready to search.
September 2026
-
Pass your CancellationToken all the way down
When a caller gives up on a request, your code should stop working on it too. It only takes a parameter, but you have to pass it through every layer.
-
Make your Service Bus message handlers idempotent
Azure Service Bus delivers every message at least once, and duplicates can arrive at the same moment. Here's how to make handling the same message twice harmless, even when two copies race.
-
Pull requests that get good reviews
The quality of a code review depends a lot on the pull request. Small, focused changes with a clear description get faster and better reviews, and a template makes that the default.
-
CQRS without the ceremony
CQRS often arrives with separate databases, event sourcing and a mediator library. The core idea is much simpler and useful on its own: write through your domain model, read with plain queries.
-
How retries really work with the Service Bus trigger
When a Service Bus-triggered function throws, the message is retried immediately, ten times in a row, and then dead-lettered. If you need a delay between attempts, you have to build it.
August 2026
-
Where does validation belong?
Request validation, business rules and data constraints are different kinds of checks. Putting each one in the right layer avoids both duplication and gaps.
-
Stop copying pipeline YAML between repos
Ten services with ten slightly different build pipelines means ten places to fix every change. Azure Pipelines templates let you define the steps once and reuse them everywhere.
-
Changing a message without breaking its consumers
Once other services consume your messages, the message format is a public contract. Additive changes are easy; breaking ones need a plan. Here's how to evolve message contracts safely.
-
Parse text without allocating with Span<T>
Splitting a line with
string.Splitcreates a new string for every field. For large files,ReadOnlySpan<char>lets you parse the same data with almost no allocations. -
MongoDB indexes from C#, and how to know you need one
A query that's fast with a thousand documents can scan millions in production. Here's how to create indexes from the C# driver, order their fields, and check that queries actually use them.
July 2026
-
Replace legacy systems one route at a time
Big-bang rewrites of legacy apps tend to run late and launch with surprises. The strangler fig pattern moves functionality to the new system piece by piece, with a working system at every step.
-
Every outbound call needs a timeout
The default HttpClient timeout is 100 seconds, far longer than anyone waiting on you. Set timeouts deliberately, layer them so they make sense together, and pass cancellation through.
-
Pattern matching that reads like the business rule
Switch expressions, property patterns and list patterns can turn a page of nested ifs into a few lines that read like the requirements. Here are the forms worth knowing.
-
Ship code dark with feature flags
Feature flags separate deploying code from releasing it. With Azure App Configuration, you can turn a feature on for 10% of users, or off in an emergency, without a deployment.
June 2026
-
Keep other systems' models out of yours
When a third-party API's field names and status codes spread through your code, every change on their side becomes a change on yours. An anti-corruption layer stops it at the boundary.
-
Test time-dependent code with TimeProvider
Code that calls
DateTime.UtcNowdirectly is hard to test. Since .NET 8,TimeProvidergives you a standard abstraction, and a fake one that you can move forward on demand. -
Zero-downtime deployments with slots
Deploying straight to production means a cold start and no quick way back. Deployment slots let you deploy, warm up and check a release first, then swap it live in seconds.
-
Domain events and integration events aren't the same thing
Both describe something that happened, but one stays inside your service and the other is a public contract. Mixing them up couples services you meant to keep apart.
May 2026
-
Route messages with Service Bus topic filters
With a topic, every subscription gets every message by default. Filters let each consumer receive only what it needs, without the publisher knowing who's listening.
-
Do you still need ConfigureAwait(false)?
In ASP.NET Core application code, no. In libraries, yes. Here's why the answer depends on where your code runs.
-
Share blobs with user delegation SAS tokens
A SAS token signed with the storage account key can't be revoked without rotating the key. A user delegation SAS is signed with Entra ID credentials, expires sooner and fits with managed identity.
-
Integration tests against a real database
The EF Core in-memory provider doesn't behave like SQL Server, so tests pass and production fails. WebApplicationFactory plus Testcontainers lets you test the real thing.
-
Add idempotency keys to your POST endpoints
When a client times out and retries a POST, did the first one work? An idempotency key lets your API recognize the retry and return the original result instead of doing the work twice.
April 2026
-
Primary constructors and their surprises
Primary constructors on classes save a lot of boilerplate for dependency injection. Their parameters aren't readonly fields, though, and that has consequences.
-
Fan-out and fan-in with Durable Functions
Processing 5,000 records one after another takes too long, and running them all at once in one function times out. Durable Functions runs them in parallel and collects the results.
-
Organize code by feature, not by layer
Controllers, Services, Repositories: a layered folder structure spreads one feature across five places. Vertical slices keep everything for a feature together.
-
Paging through data without skipping records
Offset paging quietly skips or repeats records when data changes mid-sync. Cursor and keyset paging don't, whether you're calling an API or building one.
March 2026
-
Faster JSON with System.Text.Json source generation
By default, System.Text.Json uses reflection to work out how to serialize your types. A source generator can do that work at compile time instead, for faster startup and trimming support.
-
Exceptions or results for expected failures?
"Customer not found" isn't exceptional, but most .NET code throws for it anyway. A simple result type makes expected failures part of the method's signature.
-
Health checks that tell the truth
A health endpoint that always returns 200 doesn't help anyone. Separate "is the process alive" from "can it do its job", and let App Service act on the answer.
-
Follow one request across every service
When a request passes through an API, a queue and two background workers, you need one ID that ties all the logs together. .NET already creates it for you.
-
Stream results with IAsyncEnumerable
Loading 100,000 rows into a list before sending the first one wastes memory and time.
IAsyncEnumerable<T>lets you process and return items as they arrive.
February 2026
-
Upgrading from .NET 8 to .NET 10
.NET 8 support ends in November 2026, and .NET 10 is the next long-term support release. Here's a checklist for moving production services without surprises.
-
Circuit breakers stop you hammering a failing service
Retries help with brief failures. When a dependency is down for minutes, retries just pile on. A circuit breaker notices and fails fast until it recovers.
-
Moving Azure Functions to the isolated worker model
Support for the in-process model ends in November 2026. The isolated worker model is where new .NET versions and features land, and the move is mostly mechanical.
-
Stop using async void and fire-and-forget
async voidcan crash your process, and_ = Task.Run(...)in a web request quietly loses errors and uses disposed services. Here's what to do instead.
January 2026
-
Value objects with C# records
Replacing a
decimaland astringwith aMoneytype removes a whole class of bugs. Records make value objects short to write, with one catch aroundwithexpressions. -
Receiving webhooks safely
A webhook endpoint is a public URL that anyone can call. Verify the signature, reject replays, respond quickly and expect duplicates.
-
Structured logging that you can actually query
A log message built with string interpolation is just text. A message template turns every value into a field you can filter and chart in Application Insights.
-
Turning on nullable reference types in an existing codebase
Nullable reference types catch null reference exceptions at compile time. Switching them on in a large, older project produces thousands of warnings. Here's how to adopt them one step at a time.
December 2025
-
Don't ignore your dead-letter queue
Every Service Bus queue and subscription has a dead-letter queue. Messages that land there are silently waiting for someone to notice. Here's how to watch it, inspect it and replay it.
-
Write down your architecture decisions
Six months from now, nobody will remember why you chose Service Bus over Event Grid. An architecture decision record takes ten minutes to write and answers that question for good.
-
ValueTask: when to use it and when not to
ValueTask<T>can save an allocation on hot paths. It also comes with rules thatTaskdoesn't have, and breaking them causes bugs that are hard to trace. -
Key Vault references in App Service and Functions
Keep secrets in Key Vault without changing a line of code. App settings can point at a secret, and your app reads it like any other setting.
November 2025
-
Handling 429 Too Many Requests
When an API says you're sending too much, it usually tells you how long to wait. Respect that, slow yourself down, and your integration stops getting throttled.
-
IOptions, IOptionsSnapshot or IOptionsMonitor?
The options pattern has three interfaces that look almost identical. Picking the wrong one means settings that never reload, or a startup crash.
-
Start with a modular monolith
Microservices solve problems most new systems don't have yet. A modular monolith gives you clean boundaries now and an easy path to split later, if you ever need to.
-
The transactional outbox pattern
Saving to the database and publishing a message are two separate operations, and one of them will eventually fail. The outbox pattern makes them succeed or fail together.
-
DI lifetimes and the captive dependency trap
A singleton that holds on to a scoped service keeps it alive forever. It's one of the easiest bugs to create in ASP.NET Core and one of the hardest to spot.
October 2025
-
Use managed identity instead of connection strings
Connection strings with keys end up in config files, pipelines and screenshots. Managed identity lets your Azure app authenticate with no secret at all.
-
Retries with exponential backoff and jitter
Retrying a failed call is easy. Retrying it without making the outage worse takes backoff, jitter and some care about which requests are safe to repeat.
-
Records vs classes in C#
Records give you value equality,
withexpressions and a one-line declaration. Here's when that helps, and the two places it quietly bites.