C# Regex Guide

.NET's System.Text.RegularExpressions.Regex is a full backtracking engine — it supports backreferences, all four lookaround forms, and famously variable-length lookbehind, which most engines reject. The trade-off is that a careless pattern can backtrack catastrophically, so .NET also gives you match timeouts to defend against ReDoS. This guide covers the static-vs-instance split, RegexOptions, the newer source-generated [GeneratedRegex], and the gotchas that bite developers. Pair it with the regex tester and the explainer to confirm any pattern you write.

Static vs Instance vs Generated

.NET gives you three ways to run a pattern, and choosing the right one is mostly a performance decision:

using System.Text.RegularExpressions;

// 1. Static — convenient for one-offs; uses a small internal cache (size 15).
bool ok = Regex.IsMatch(input, @"\d+");

// 2. Instance — compile once, reuse everywhere. Best for a hot path.
private static readonly Regex Digits = new(@"\d+");
bool ok2 = Digits.IsMatch(input);

// 3. Source-generated (.NET 7+) — compiled at build time, zero startup cost.
[GeneratedRegex(@"\d+")]
private static partial Regex DigitsGen();

The static methods are fine for scripts and rare calls, but their cache is only 15 entries by default (Regex.CacheSize) — use many distinct patterns and you silently recompile. For anything in a loop or request path, use a cached instance or, on modern .NET, a generated regex.

Use Verbatim Strings

C# string literals interpret \ as an escape, so "\d+" is a compile error and "\\d+" is noisy. Prefix the string with @ to make it verbatim — the regex sees the backslashes untouched:

var re1 = new Regex("\\b\\d{4}\\b");   // works, but hard to read
var re2 = new Regex(@"\b\d{4}\b");      // verbatim — the idiomatic form

This is the C# equivalent of Python's raw strings. Use @"..." for every pattern. In a verbatim string a literal double quote is written "".

The Core Methods

The same handful of operations exist as both static and instance methods:

var re = new Regex(@"\d+");

re.IsMatch("abc 123")              // true
re.Match("abc 123 45")             // first Match: "123" (check .Success)
re.Matches("abc 123 45")           // MatchCollection of all matches
re.Replace("a1 b2", "#")           // "a# b#"
re.Split("a1b22c")                 // ["a", "b", "c"] when splitting on \d+

Match always returns a Match object — it is never null. Check match.Success to know whether it actually matched:

Match m = Regex.Match("no digits here", @"\d+");
if (m.Success)
    Console.WriteLine(m.Value);   // not reached

Iterate all matches with a foreach, or in .NET 7+ use the allocation-free EnumerateMatches which yields ValueMatch structs over a ReadOnlySpan<char>.

RegexOptions

Behaviour flags are passed as the RegexOptions enum, combined with |:

var re = new Regex(@"^\d+", RegexOptions.IgnoreCase | RegexOptions.Multiline);
  • IgnoreCase — case-insensitive matching.
  • Multiline^ and $ match at line boundaries.
  • Singleline. matches newlines (this is .NET's name for "dotall").
  • IgnorePatternWhitespace — verbose mode: ignore unescaped whitespace and allow # comments.
  • ExplicitCapture — only named groups capture; bare (...) become non-capturing.
  • Compiled — compile the pattern to IL for faster matching at the cost of startup time.
  • CultureInvariant — case folding ignores the current culture (important for security).
  • NonBacktracking (.NET 7+) — use a linear-time engine that cannot backtrack, immune to ReDoS (no backreferences/lookaround in this mode).

The inline equivalents (?i), (?m), (?s), (?x) work too, and can be scoped: (?i:hello)World.

Source-Generated Regex

On .NET 7 and later, [GeneratedRegex] is the best option for any pattern known at compile time. You declare a partial method and the source generator writes the matcher:

using System.Text.RegularExpressions;

public partial class Validators
{
    [GeneratedRegex(@"^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$",
                    RegexOptions.IgnoreCase)]
    public static partial Regex Email();
}

// usage
bool ok = Validators.Email().IsMatch(address);

Because the code is generated at build time, there is no runtime compilation and no startup cost, it works with ahead-of-time compilation and trimming, and you can step into the generated matcher in the debugger. Prefer it over RegexOptions.Compiled whenever the pattern is a constant.

Named Groups

.NET uses (?<name>...) (or the equivalent (?'name'...)). Access by name through the Groups indexer:

var re = new Regex(@"(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})");
Match m = re.Match("2026-06-10");

m.Groups["year"].Value     // "2026"
m.Groups["month"].Value    // "06"
m.Groups[0].Value          // "2026-06-10" — the whole match

A useful .NET-specific feature: a named group can appear more than once and .Captures records every capture, which is handy for repeated structures. With RegexOptions.ExplicitCapture, only named groups capture, so you can use bare parentheses purely for grouping without consuming group numbers.

Replacement Syntax

Regex.Replace uses $-style substitutions in the replacement string:

var re = new Regex(@"(?<y>\d{4})-(?<m>\d{2})-(?<d>\d{2})");

re.Replace("2026-06-10", "${d}/${m}/${y}");   // "10/06/2026"
re.Replace("2026-06-10", "$1 is the year");   // "2026 is the year"
  • $1, ${name} — numbered and named group references.
  • $& — the entire match; $$ — a literal dollar sign.
  • $` and $' — text before and after the match.

For computed replacements, pass a MatchEvaluator delegate instead of a string:

string shouted = Regex.Replace("hello world", @"\w+", m => m.Value.ToUpper());
// "HELLO WORLD"

Lookaround, Including Variable-Length Lookbehind

.NET's backtracking engine supports all four lookaround forms, and unusually it allows variable-length lookbehind — a pattern many engines reject:

// Lookahead / negative lookahead
Regex.Matches("100 dollars 50 euros", @"\d+(?= dollars)");  // 100
Regex.Matches("100 dollars 50 euros", @"\d+(?! dollars)");  // partials + 50

// Fixed-width lookbehind
Regex.Matches("$50 and $100", @"(?<=\$)\d+");                // 50, 100

// Variable-length lookbehind — valid in .NET, rejected in Python/Java/JS
Regex.Matches("abc: 42", @"(?<=\w+:\s*)\d+");                // 42

This expressiveness comes from .NET evaluating lookbehind by running the inner pattern right-to-left, which removes the fixed-width restriction. The same engine is what gives you backreferences and full lookaround. For a deeper treatment, see the lookahead and lookbehind guide.

Defend Against ReDoS with a Timeout

Because the engine backtracks, a pattern like (a+)+b can take exponential time on a crafted input — the core of a ReDoS denial-of-service attack. Always pass a matchTimeout when matching untrusted input:

var re = new Regex(@"^(\w+)*$", RegexOptions.None, TimeSpan.FromSeconds(1));

try
{
    re.IsMatch(userSuppliedInput);
}
catch (RegexMatchTimeoutException)
{
    // reject the input rather than hanging the thread
}

On .NET 7+ you can sidestep the problem entirely for suitable patterns by passing RegexOptions.NonBacktracking, which uses a linear-time automaton (at the cost of backreferences and lookaround). For untrusted input, a timeout, the non-backtracking engine, or both is the responsible default.

Common Gotchas

  • Forgetting @"..." on patterns with backslashes. Use verbatim strings everywhere.
  • Treating Match as nullable. It is never null — always check match.Success.
  • Relying on the static-method cache. It holds only 15 patterns by default; use a cached instance or [GeneratedRegex] for hot paths.
  • No timeout on untrusted input. Backtracking plus a hostile string equals ReDoS. Pass a matchTimeout or use NonBacktracking.
  • Culture-sensitive case folding. IgnoreCase respects the current culture; add CultureInvariant for predictable, security-relevant comparisons.
  • Using $ for backreferences inside the pattern. In the pattern itself, a backreference is \1 or \k<name>; the $1 form is only for replacement strings.

Try Patterns Live

The regex tester uses JavaScript syntax. Most .NET patterns transfer directly, but remember that .NET supports variable-length lookbehind and named groups with (?<name>...) that the JS engine handles slightly differently. Check the differences in the regex cheat sheet, and use the regex explainer for a token-by-token breakdown of any unfamiliar pattern.

Frequently Asked Questions

Should I use static Regex methods or create a Regex instance in C#?

Use a cached instance (or a source-generated regex) when the same pattern runs repeatedly, and static methods for one-off matches. The static methods like Regex.IsMatch(input, pattern) keep an internal cache of recently compiled patterns, but that cache is small (Regex.CacheSize defaults to 15) and is evicted if you use many distinct patterns, causing silent recompilation. For a pattern used in a hot path, create one static readonly Regex instance, or in .NET 7+ use a [GeneratedRegex] partial method, so the pattern is compiled exactly once.

What is [GeneratedRegex] in .NET and why use it?

[GeneratedRegex] is a source generator introduced in .NET 7 that turns a regex into compile-time generated C# code via a partial method. You write [GeneratedRegex(@"\d+")] private static partial Regex MyRegex(); and the generator emits an optimized, fully compiled implementation at build time. It is the fastest option for a known, constant pattern because there is no startup cost and no runtime compilation, it works ahead-of-time and trimming-friendly, and you can step through the generated matcher in the debugger. Prefer it over RegexOptions.Compiled for any pattern known at build time.

How do I make a C# regex case-insensitive?

Pass RegexOptions.IgnoreCase to the method or constructor: Regex.IsMatch(input, @"hello", RegexOptions.IgnoreCase), or embed the inline flag (?i) at the start of the pattern. Combine options with the bitwise OR operator, for example RegexOptions.IgnoreCase | RegexOptions.Multiline. For culture-sensitive matching pitfalls, also consider RegexOptions.CultureInvariant so that case folding does not depend on the current thread culture, which matters for security-sensitive comparisons like the Turkish dotless-i problem.

Does .NET regex support variable-length lookbehind?

Yes. .NET is one of the few regex engines that supports variable-length lookbehind, so (?<=\w+)foo is valid where Python, Java, and JavaScript would reject it (they require fixed-width lookbehind). This makes .NET lookbehind unusually expressive. It comes from .NET using a backtracking engine that evaluates lookbehind by running the inner pattern right-to-left, which removes the fixed-width restriction. The same engine choice is why .NET also supports backreferences and full lookaround.

How do I protect a C# regex against catastrophic backtracking?

Set a match timeout. Because .NET uses a backtracking engine, a pattern with nested quantifiers like (a+)+b can take exponential time on certain inputs, which is the basis of ReDoS denial-of-service attacks. Pass a TimeSpan as the matchTimeout argument to the Regex constructor or the static methods, for example new Regex(pattern, options, TimeSpan.FromSeconds(1)). When the timeout elapses the engine throws a RegexMatchTimeoutException instead of hanging. You can also set a process-wide default with AppDomain.SetData("REGEX_DEFAULT_MATCH_TIMEOUT"), and prefer non-backtracking constructs or RegexOptions.NonBacktracking in .NET 7+ for untrusted input.