Skip to main content

Newsletter Archive

Browse through our collection of past newsletters. Each edition is packed with C# and .NET insights.

Page 4 of 19 (56 editions)

August 9, 2026

For two decades, the pattern has been the same: declare a private readonly object _lock = new(), wrap your critical section in lock(_lock), and hope nobody accidentally locks on this, a string literal, or some other shared reference. It works, but it’s a pattern held together by convention, not by the type system.

.NET 9 introduces System.Threading.Lock, a purpose-built type that the compiler recognizes and optimizes. No more locking on arbitrary objects.

The Old Way

public class ConnectionPool
{
private readonly object _lock = new();
private readonly List<Connection> _connections = [];
public Connection Acquire()
{
lock (_lock)
{
var conn = _connections.FirstOrDefault(c => !c.InUse);
if (conn is not null)
conn.InUse = true;
return conn ?? CreateNew();
}
}
}

The compiler turns that lock into Monitor.Enter / Monitor.Exit. It works, but there’s no type safety — you could accidentally pass _lock to something that locks on it elsewhere, or some junior dev writes lock(this) in a code review at 5 PM.

The New Way

using System.Threading;
public class ConnectionPool
{
private readonly Lock _lock = new();
private readonly List<Connection> _connections = [];
public Connection Acquire()
{
lock (_lock)
{
var conn = _connections.FirstOrDefault(c => !c.InUse);
if (conn is not null)
conn.InUse = true;
return conn ?? CreateNew();
}
}
}

Looks almost identical, right? The difference is under the hood. When the compiler sees lock on a System.Threading.Lock, it emits a call to Lock.EnterScope() which returns a ref struct that releases the lock on Dispose. This is more efficient than Monitor.Enter/Exit because the JIT can optimize the scope-based pattern better.

Scoped Locking

You can also use the scope explicitly when you need more control:

public void TransferAll(ConnectionPool other)
{
using (_lock.EnterScope())
{
foreach (var conn in _connections)
{
other.Add(conn);
}
_connections.Clear();
}
}

The Lock.Scope is a ref struct, so it can’t escape the method or get boxed. The lock is guaranteed to release when the scope ends, even if an exception fires.

Why Bother?

  1. Type safety. You can’t accidentally lock on a string, a type, or this. The type system prevents misuse.
  2. Performance. EnterScope() avoids the overhead of Monitor’s thread-affinity tracking in the uncontended case.
  3. Intent. Seeing Lock in a field declaration screams “synchronization primitive.” Seeing object screams nothing.

Key Takeaway

Replace private readonly object _lock = new() with private readonly Lock _lock = new(). Same lock keyword, same syntax, better codegen, and the type system finally prevents the classic locking mistakes. One find-and-replace, zero behavior changes.

Read the full tip + 7 curated links

Don't miss the next tip 💧

Get a .NET tip and curated links delivered to your inbox every week.

August 2, 2026

MapStaticAssets: Static Files That Actually Cache Properly

UseStaticFiles() has been the default for serving CSS, JS, and images since ASP.NET Core 1.0. It works. But it’s dumb: no content-based ETags, no precompressed variants, no fingerprinted cache headers. You end up with stale CSS in your users’ browsers, or you manually configure response caching middleware, or you append ?v=2 query strings like it’s 2010.

.NET 9’s MapStaticAssets() fixes all of this with one line.

The Swap

// Before: basic file serving, no smart caching
app.UseStaticFiles();
// After: content-hashed ETags, precompressed files, immutable cache headers
app.MapStaticAssets();

What You Get for Free

Content-based ETags. The ETag is a hash of the file content, not the last-modified timestamp. Deploy a new version with an identical file? No unnecessary download. Change one byte? Cache invalidates automatically.

Precompressed responses. If you have app.css.br (Brotli) or app.css.gz (gzip) alongside app.css, MapStaticAssets serves the compressed variant automatically based on Accept-Encoding. Blazor and the .NET build system generate these for you.

Immutable cache headers. For fingerprinted files (like Blazor’s app.abc123.css), it sets Cache-Control: max-age=31536000, immutable. The browser caches it forever and never asks again. When the content changes, the filename changes, and the old cache entry is simply abandoned.

Blazor Integration

In a Blazor app, your App.razor references static assets, and the build system fingerprints them:

<link rel="stylesheet" href="@Assets["app.css"]" />
<script src="@Assets["app.js"]"></script>

@Assets["app.css"] resolves to something like app.a1b2c3d4.css at build time. Combined with MapStaticAssets(), the browser gets an immutable, precompressed file with a year-long cache lifetime. Deploy a new version and the filename changes — instant cache bust, zero stale content.

Razor Pages / MVC

Works the same way with tag helpers:

<link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />

With MapStaticAssets, the version append uses the same content hash, and responses include proper cache headers.

Response Headers Comparison

HeaderUseStaticFiles()MapStaticAssets()
ETagFile modified timeContent hash
Cache-ControlNot setmax-age=31536000, immutable (fingerprinted)
Content-EncodingManual middlewareAutomatic (br/gz)
VaryNot setAccept-Encoding

Key Takeaway

Replace app.UseStaticFiles() with app.MapStaticAssets() and get content-hashed ETags, automatic Brotli/gzip serving, and immutable cache headers, all without configuration. Your CSS will never be stale again, and your users download fewer bytes. One line, done.

Read the full tip + 7 curated links

Don't miss the next tip 💧

Get a .NET tip and curated links delivered to your inbox every week.

July 26, 2026

CompositeFormat: Stop Re-Parsing Format Strings on Every Call

string.Format("{0} processed {1} items in {2:F2}ms", name, count, elapsed) looks innocent enough. But every time you call it, the runtime parses that format string from scratch — finding the {0}, {1:F2} placeholders, validating them, and building the output. Call it once? Fine. Call it 100,000 times in a logging or reporting hot path? That parsing adds up.

CompositeFormat lets you parse the format string once and reuse the compiled representation forever.

The Before

// In a high-throughput logger or report generator
for (int i = 0; i < records.Length; i++)
{
// Parses the format string on EVERY iteration
var line = string.Format(
"[{0:yyyy-MM-dd HH:mm:ss}] {1}: processed {2:N0} records ({3:P1} complete)",
records[i].Timestamp,
records[i].Name,
records[i].Count,
records[i].Progress);
output.AppendLine(line);
}

The After

using System.Text;
// Parse once at startup
private static readonly CompositeFormat LogFormat = CompositeFormat.Parse(
"[{0:yyyy-MM-dd HH:mm:ss}] {1}: processed {2:N0} records ({3:P1} complete)");
// Use the pre-parsed format in the hot path
for (int i = 0; i < records.Length; i++)
{
var line = string.Format(
null, // IFormatProvider (null = current culture)
LogFormat, // pre-parsed, no re-parsing
records[i].Timestamp,
records[i].Name,
records[i].Count,
records[i].Progress);
output.AppendLine(line);
}

The CompositeFormat.Parse() call does the format string analysis once. Every subsequent string.Format call with that CompositeFormat skips the parsing step entirely and goes straight to writing output.

Works with StringBuilder Too

private static readonly CompositeFormat RowFormat =
CompositeFormat.Parse("| {0,-20} | {1,10:C2} | {2,8:N0} |");
var sb = new StringBuilder();
sb.AppendLine("| Product | Price | Quantity |");
sb.AppendLine("|----------------------|------------|----------|");
foreach (var item in inventory)
{
sb.AppendFormat(null, RowFormat, item.Name, item.Price, item.Quantity);
sb.AppendLine();
}

When to Use It

  • Logging pipelines where the same format pattern is applied to millions of entries
  • Report generators that format table rows, CSV lines, or export records
  • Template engines that apply the same pattern repeatedly with different data
  • Any loop where string.Format with a constant format string shows up in your profiler

When NOT to Use It

If your format string is used once or rarely, the overhead of pre-parsing isn’t worth it. CompositeFormat shines specifically in high-repetition scenarios.

Key Takeaway

CompositeFormat.Parse() compiles your format string once. Every subsequent string.Format(provider, compositeFormat, args) call skips re-parsing and goes straight to output. It’s a one-line change that eliminates redundant string parsing on hot paths. If your profiler highlights string.Format, this is your fix.

Read the full tip + 10 curated links

Don't miss the next tip 💧

Get a .NET tip and curated links delivered to your inbox every week.