Skip to main content

Newsletter Archive

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

Page 7 of 19 (56 editions)

June 7, 2026

SearchValues<T>: Turbocharged String Scanning

Searching for characters in a string sounds like a solved problem, right? Just call IndexOfAny(char[]) and move on. But if you’re doing it in a parser, a tokenizer, or anything that processes megabytes of text, that naive approach leaves a lot of performance on the table. SearchValues<T> is .NET’s way of saying, “Give me your search set once, and I’ll vectorize the heck out of it.”

Here’s the basic setup: create a SearchValues<char> once and reuse it:

using System.Buffers;
// Create once, reuse everywhere
var delimiters = SearchValues.Create([',', ';', '|', '\t', ' ']);
var input = "hello,world;this|is\ta test with spaces";
// Find the first delimiter
var index = input.AsSpan().IndexOfAny(delimiters);
Console.WriteLine($"First delimiter at index {index}: '{input[index]}'");
// Split manually using spans (zero allocation!)
var remaining = input.AsSpan();
while (remaining.Length > 0)
{
var delimIndex = remaining.IndexOfAny(delimiters);
if (delimIndex < 0)
{
Console.WriteLine($"Token: \"{remaining}\"");
break;
}
Console.WriteLine($"Token: \"{remaining[..delimIndex]}\"");
remaining = remaining[(delimIndex + 1)..];
}

When you pass a SearchValues<char> to IndexOfAny, the runtime picks the optimal SIMD strategy based on your hardware and the search set. Small sets might use a bitmap lookup. Larger sets might use AVX2 or ARM NEON vector comparisons. You don’t need to know the details; just know that it’s often 3-10x faster than passing a plain char[].

It also works with SearchValues<string> for multi-value string searching in .NET 9, which is fantastic for things like scanning for keywords or finding HTML tags:

var keywords = SearchValues.Create(
["async", "await", "Task", "ValueTask"],
StringComparison.Ordinal);
var code = "The async method returns a Task<int> that you can await";
var span = code.AsSpan();
var pos = span.IndexOfAny(keywords);
Console.WriteLine($"First keyword found at position {pos}: starts with '{span[pos..][..5]}'");

The key insight is that SearchValues<T> is a precomputed search structure. The upfront cost of Create() is tiny, and every subsequent search benefits from the optimized representation. If you have any hot-path code doing IndexOfAny with a static set of characters or strings, this is the single easiest performance win you’ll find.

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.

May 31, 2026

Primary Constructors Everywhere: Cleaner DI in Three Seconds

Remember when you’d open an ASP.NET Core service class and the first 20 lines were just field declarations and a constructor that assigned them? We all pretended that was fine. It wasn’t fine. C# 12 primary constructors fix this for classes (not just records), and your DI code has never looked this clean.

Here’s the classic “before” that we’ve all copy-pasted a thousand times:

public class OrderService
{
private readonly IOrderRepository _repo;
private readonly ILogger<OrderService> _logger;
private readonly IEmailSender _email;
public OrderService(
IOrderRepository repo,
ILogger<OrderService> logger,
IEmailSender email)
{
_repo = repo;
_logger = logger;
_email = email;
}
public async Task PlaceOrderAsync(Order order)
{
_logger.LogInformation("Placing order {Id}", order.Id);
await _repo.SaveAsync(order);
await _email.SendConfirmationAsync(order);
}
}

And here’s the “after” with a primary constructor:

public class OrderService(
IOrderRepository repo,
ILogger<OrderService> logger,
IEmailSender email)
{
public async Task PlaceOrderAsync(Order order)
{
logger.LogInformation("Placing order {Id}", order.Id);
await repo.SaveAsync(order);
await email.SendConfirmationAsync(order);
}
}

That’s it. No fields, no constructor body, no ceremony. The parameters are captured and available throughout the class. You just slashed 10+ lines of boilerplate without sacrificing readability (arguably improving it).

One thing to keep in mind: primary constructor parameters are not automatically readonly fields. They’re captured variables, so technically you could reassign repo = null somewhere in the class (please don’t). If that keeps you up at night, you can still assign them to readonly fields explicitly, but in practice the convention is simple: treat them as read-only and move on.

Register your service the same way you always have (builder.Services.AddScoped<OrderService>()), and the DI container handles the rest. Less plumbing, more shipping. That’s the dream.

Read the full tip + 8 curated links

Don't miss the next tip 💧

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

May 24, 2026

[GeneratedRegex]: Your Regex, but Make It Fast

Regex in .NET has always been powerful, but let’s be honest, every time you write new Regex(pattern) you feel a tiny pang of guilt about the runtime compilation cost. And if you’re using Regex.IsMatch() with a string literal in a loop? That’s a performance review waiting to happen. [GeneratedRegex] fixes all of this by source-generating your regex at compile time.

The syntax is minimal. Slap the attribute on a partial method that returns Regex, and the compiler does the rest:

using System.Text.RegularExpressions;
var inputs = new[] { "[email protected]", "not-an-email", "[email protected]" };
foreach (var input in inputs)
{
Console.WriteLine(EmailRegex().IsMatch(input)
? $"✅ {input}"
: $"❌ {input}");
}
partial class Program
{
[GeneratedRegex(@"^[^@\s]+@[^@\s]+\.[^@\s]+$", RegexOptions.IgnoreCase)]
private static partial Regex EmailRegex();
}

What happens behind the scenes is pretty slick. The source generator analyzes your pattern and emits a custom Regex-derived class with a hand-optimized TryFindNextPossibleStartingPosition and TryMatchAtCurrentPosition. No interpretation, no runtime compilation. Just raw, ahead-of-time matching logic. This means it’s also fully Native AOT compatible, which new Regex() is not.

You also get free IDE diagnostics. If your pattern has a syntax error, you’ll see it as a compile-time warning, not a runtime ArgumentException thrown at 2 AM in production. The Roslyn analyzer even catches common pitfalls like catastrophic backtracking patterns.

Performance-wise, benchmarks consistently show [GeneratedRegex] matching 2-5x faster than interpreted regex, with zero allocation overhead on the matching path. If you have any static readonly Regex fields in your codebase, migrating them is a five-second refactor with a massive payoff. Your patterns deserve the VIP treatment.

Read the full tip + 6 curated links

Don't miss the next tip 💧

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