Newsletter Archive
Browse through our collection of past newsletters. Each edition is packed with C# and .NET insights.
Page 7 of 19 (56 editions)
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 everywherevar delimiters = SearchValues.Create([',', ';', '|', '\t', ' ']);
var input = "hello,world;this|is\ta test with spaces";
// Find the first delimitervar 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.
Don't miss the next tip 💧
Get a .NET tip and curated links delivered to your inbox every week.
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.
Don't miss the next tip 💧
Get a .NET tip and curated links delivered to your inbox every week.
[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;
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.
Don't miss the next tip 💧
Get a .NET tip and curated links delivered to your inbox every week.