Skip to main content

Newsletter Archive

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

Page 1 of 10 (28 editions)

March 29, 2026

Extension Members: C# 14 Breaks the ‘this’ Barrier

For over 15 years, extension methods have been one of C#‘s most beloved features. But they’ve always had a frustrating limitation in that you can only add methods. Want an extension property? An extension operator? A static helper that feels like it belongs on the type? Sorry, best we can do is another this parameter.

C# 14 changes that. The new extension block syntax lets you add properties, operators, and static members to types you don’t own.

The Old Way

We’ve all written utility classes like this:

public static class StringExtensions
{
public static bool IsNullOrEmpty(this string? s)
=> string.IsNullOrEmpty(s);
public static string Truncate(this string s, int maxLength)
=> s.Length <= maxLength ? s : s[..maxLength];
}

It works, but IsNullOrEmpty should be a property. There’s no argument, no verb, it’s clearly a state check. And if you wanted a static helper like string.Join but custom? Impossible through extensions.

The New Way

C# 14 introduces the extension block. You declare it inside a static class, specify the receiver type, and then define members as if you were writing them inside the type itself:

public static class StringExtensions
{
extension(string? s)
{
// Extension property — no more parentheses for simple checks
public bool IsNullOrEmpty => string.IsNullOrEmpty(s);
// Extension method — works just like before, cleaner grouping
public string Truncate(int maxLength)
=> s is null || s.Length <= maxLength ? s! : s[..maxLength];
}
}

Now you call IsNullOrEmpty the way it always should have been:

string? name = GetName();
if (name.IsNullOrEmpty)
Console.WriteLine("No name provided");
string preview = name.Truncate(50);

No parentheses, no “is this a method or a property?” confusion. It just reads like a member.

Static Extension Members

Want to add a factory method or constant that lives on the type itself? Use the parameterless receiver syntax:

public static class GuidExtensions
{
extension(Guid)
{
public static Guid CreateFrom(string input)
{
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(input));
return new Guid(hash.AsSpan(0, 16));
}
}
}

Now it’s called like a real static member:

var userId = Guid.CreateFrom("[email protected]");

No GuidHelper.CreateFrom(...). No hunting for which utility class has the method. It’s right where you’d expect it.

Generics Work Too

Extension blocks support generic type parameters and constraints:

public static class EnumerableExtensions
{
extension<T>(IEnumerable<T> source)
{
public bool IsEmpty => !source.Any();
public IEnumerable<T> WhereGreaterThan(T value)
where T : IComparable<T>
=> source.Where(x => x.CompareTo(value) > 0);
}
}
var numbers = new List<int> { 1, 2, 3, 4, 5 };
if (!numbers.IsEmpty)
{
var big = numbers.WhereGreaterThan(3); // [4, 5]
}

What You Can’t Do (Yet)

  • No backing fields - extension properties are computed only, no stored state.
  • No constructors or events - you’re extending the surface, not the internals.
  • Old-style extensions still work - the classic this parameter syntax isn’t going anywhere. You can mix both in the same project.

The Bottom Line

Extension members take the most-used workaround in C# and make them real. Properties feel like properties. Static helpers live on the type. Your code reads more naturally, and your consumers don’t need to know the difference.

If you maintain a library or a shared “Extensions” folder, C# 14 just made it a lot more expressive.

Read the full tip + 9 curated links

Don't miss the next tip 💧

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

March 22, 2026

Ditch the Double Lookup: CollectionsMarshal.GetValueRefOrAddDefault

Here’s a pattern every C# developer has written a hundred times for counting occurrences, aggregating values, or building up a cache:

var wordCounts = new Dictionary<string, int>();
foreach (var word in words)
{
if (wordCounts.TryGetValue(word, out var count))
wordCounts[word] = count + 1;
else
wordCounts[word] = 1;
}

It works. It’s clear. But it hashes the key twice on every iteration, once for TryGetValue and once for the indexer set. On a hot path with millions of entries, that adds up fast.

Since .NET 6, System.Runtime.InteropServices.CollectionsMarshal gives you a way to do this in a single lookup. The GetValueRefOrAddDefault method returns a ref directly into the dictionary’s internal storage:

using System.Runtime.InteropServices;
var wordCounts = new Dictionary<string, int>();
foreach (var word in words)
{
ref int count = ref CollectionsMarshal.GetValueRefOrAddDefault(
wordCounts, word, out bool exists);
// 'count' is a ref to the slot — mutate it in place
count++;
}

That’s it. One hash, one probe, one update. If the key already existed, exists is true and count points at the current value. If it didn’t, the runtime inserts a default entry (0 for int) and count points at that new slot. Either way, count++ does the right thing.

This works beautifully for any “upsert” operation. Building a grouped lookup? Same idea:

var grouped = new Dictionary<string, List<Order>>();
foreach (var order in orders)
{
ref List<Order>? list = ref CollectionsMarshal.GetValueRefOrAddDefault(
grouped, order.Region, out bool exists);
if (!exists)
list = new List<Order>();
list!.Add(order);
}

No TryGetValue. No ContainsKey. No double hashing. Just a direct ref into the slot.

A couple of things to keep in mind:

  • Don’t modify the dictionary while holding a ref. Adding or removing keys can resize the internal storage and invalidate your reference. Use the ref, then move on.
  • It lives in CollectionsMarshal for a reason. This is a low-level API. It trades a bit of safety for performance. For casual code, the classic TryGetValue pattern is perfectly fine. Reach for this when profiling tells you dictionary access is a bottleneck.
  • It only works with Dictionary<TKey, TValue>, not ConcurrentDictionary, not FrozenDictionary, not custom implementations.

For hot-path aggregation, counting, grouping, or caching, CollectionsMarshal.GetValueRefOrAddDefault is a one-line upgrade that cuts your hashing work in half.

Read the full tip + 11 curated links

Don't miss the next tip 💧

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

March 15, 2026

Stop Mocking the Clock: TimeProvider Makes Time Testable

We’ve all been there. You write a unit test that depends on DateTime.UtcNow, and suddenly you’re building a custom IClock interface, or wrapping things in a static ambient context, or just… hoping the test doesn’t run at midnight on a leap year. TimeProvider puts all of that to rest. It’s a first-class abstraction for time, and it lives right in the base class library.

The concept is straightforward. Instead of calling DateTime.UtcNow or DateTimeOffset.UtcNow directly, you inject a TimeProvider and call GetUtcNow(). In production, you register TimeProvider.System, which just returns the real clock. In tests, you swap in a FakeTimeProvider that lets you set, freeze, and advance time whenever you want.

Let’s look at a practical example. Say you have a service that checks whether a coupon code has expired:

public class CouponService(TimeProvider timeProvider)
{
public bool IsValid(Coupon coupon)
{
var now = timeProvider.GetUtcNow();
return now < coupon.ExpiresAt;
}
public async Task WaitAndExpire(Coupon coupon, CancellationToken ct)
{
var remaining = coupon.ExpiresAt - timeProvider.GetUtcNow();
if (remaining > TimeSpan.Zero)
{
await Task.Delay(remaining, timeProvider, ct);
}
coupon.IsActive = false;
}
}

Notice that Task.Delay overload. It accepts a TimeProvider, so even delays become fully controllable in tests. Registering it in your DI container works the same way as any other service:

builder.Services.AddSingleton(TimeProvider.System);

Now for the fun part. Install the testing companion package:

Terminal window
dotnet add package Microsoft.Extensions.TimeProvider.Testing

And write tests that are completely deterministic. No flaky timing, no Thread.Sleep, no crossing your fingers:

using Microsoft.Extensions.Time.Testing;
public class CouponServiceTests
{
[Fact]
public void IsValid_ReturnsFalse_AfterExpiration()
{
// Start at a known point in time
var fake = new FakeTimeProvider(
new DateTimeOffset(2025, 6, 1, 12, 0, 0, TimeSpan.Zero));
var coupon = new Coupon
{
ExpiresAt = fake.GetUtcNow().AddHours(2)
};
var service = new CouponService(fake);
Assert.True(service.IsValid(coupon));
// Jump forward 3 hours, instantly
fake.Advance(TimeSpan.FromHours(3));
Assert.False(service.IsValid(coupon));
}
[Fact]
public async Task WaitAndExpire_DeactivatesCoupon_WhenTimeAdvances()
{
var fake = new FakeTimeProvider(
new DateTimeOffset(2025, 6, 1, 12, 0, 0, TimeSpan.Zero));
var coupon = new Coupon
{
ExpiresAt = fake.GetUtcNow().AddMinutes(30),
IsActive = true
};
var service = new CouponService(fake);
var task = service.WaitAndExpire(coupon, CancellationToken.None);
// Time hasn't moved, coupon is still active
Assert.True(coupon.IsActive);
// Advance past expiration
fake.Advance(TimeSpan.FromMinutes(31));
await task;
Assert.False(coupon.IsActive);
}
}
public class Coupon
{
public DateTimeOffset ExpiresAt { get; set; }
public bool IsActive { get; set; }
}

The FakeTimeProvider.Advance() call is the secret sauce. It moves the clock forward instantly, and any pending Task.Delay calls waiting on that TimeProvider will resolve right away. No real waiting, no race conditions, no flaky CI runs.

If you’re still maintaining a homegrown IClock or IDateTimeProvider interface, it might be time to retire it. TimeProvider is baked into the framework, works with Task.Delay, PeriodicTimer, and CancellationTokenSource.CancelAfter, and has a battle-tested fake ready to go. That’s one less abstraction you have to carry around.

Read the full tip + 4 curated links

Don't miss the next tip 💧

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