A regular expression (regex) is a pattern that describes text: "four digits, a dash, two digits", "a word that starts with a capital letter", "anything between square brackets". In C#, the Regex class in System.Text.RegularExpressions finds, extracts, replaces and splits text with such patterns.
IsMatch, Match and Matches
Three methods cover most searches. IsMatch answers yes or no, Match returns the first match, and Matches returns all of them:
Output:
True
2026-03-14 at index 22
4 matches:
1042
2026
1043
2026
Success: False, Value: []
A failed Match does not return null: it returns a Match whose Success is false and whose Value is empty. Always test Success before using a match.
The four-digit search shows how patterns catch more than intended. \b\d{4}\b means "exactly four digits as a whole word", and the years inside the dates qualify, because - is a word boundary. Tighten the pattern to say what you mean, for example (?<=Order |order )\d+, or use groups, next.
Verbatim strings for patterns
Regex syntax uses backslashes everywhere (\d, \s, \b), and so do C# string escapes. In a normal string, "\d" does not even compile (CS1009, unrecognized escape sequence) and "\b" compiles into a backspace character, silently changing the pattern. Write patterns as verbatim strings with @, where a backslash is just a backslash:
var a = new Regex("\\d+\\.\\d{2}"); // escaped twice: hard to read
var b = new Regex(@"\d+\.\d{2}"); // verbatim: what the regex engine sees
In a verbatim string, a double quote is written "". C# 11 raw string literals ("""...""") avoid even that.
Groups: extracting parts of a match
Parentheses create a group, and each group's text is available after the match. Groups are numbered from 1 in order of their opening parenthesis; group 0 is the whole match. Named groups, (?<name>...), are easier to read and survive edits to the pattern:
Output:
2026-03-14
ERROR
payments
Card declined for order 1042
60 by 90, area 5400
[ and ] have a meaning in regex (a character class), so matching the literal brackets needs \[ and \]. The same goes for . * + ? ( ) { } ^ $ | \. To match a user-supplied string literally inside a pattern, pass it through Regex.Escape, which escapes the metacharacters for you: Regex.Escape("price (USD)") returns price\ \(USD\) (spaces are escaped too, which is harmless).
Parentheses that only group, without capturing, are written (?:...). They keep the group numbering clean and are slightly faster.
Replace: substitutions and lambdas
Regex.Replace replaces every match. In the replacement string, $1 inserts group 1, ${name} a named group and $0 the whole match. For anything a template cannot express, pass a function (usually a lambda) that receives each Match and returns its replacement:
Output:
Invoiced 14/03/2026, paid 02/04/2026.
too many spaces
Card **** **** **** 1234
Tea 2.75, Scone 3.52, Jam 1.10
The card mask uses a lookahead, (?=...): it matches a digit only if four more digits follow, without consuming them. Lookarounds ((?=...), (?!...), (?<=...), (?<!...)) test context without including it in the match, which is what lets a single Replace keep the last four digits intact.
For plain text with no pattern, string.Replace is simpler and faster; reach for Regex.Replace when the thing to replace varies.
Split
Regex.Split splits on every match of a pattern, which handles separators that vary:
Output:
csharp / dotnet / regex / tutorial / beginner
Order Shipped Event Handler
If the pattern contains capturing groups, Regex.Split includes the captured separators in the result; use (?:...) when you do not want them.
RegexOptions
Options change how the whole pattern behaves. Combine them with |:
Output:
1
2
error INFO Error
True
The ones you will use:
IgnoreCase: case-insensitive matching (inline form(?i)).Multiline:^and$match at the start and end of every line, not only of the whole string.Singleline:.also matches\n(by default it matches any character except a newline).IgnorePatternWhitespace: spaces in the pattern are ignored and#starts a comment, for long patterns written over several lines.CultureInvariant: withIgnoreCase, compare without the current culture's rules.Compiled: compile the pattern to IL once, for a regex used many times.
Validating input: anchors and the email question
For validation, anchor the pattern with ^ and $, otherwise it passes as soon as part of the input matches:
Output:
PT-1000 loose=True strict=True
pt-1000 loose=False strict=False
XPT-1000Y loose=True strict=False
PT-10 loose=False strict=False
ana@example.com True
ana@example False
ana @example.com False
ana@@example.com False
The email pattern checks the shape only: something without spaces or @, one @, a domain with a dot. That is deliberate. The real grammar for addresses (RFC 5322) allows quoted local parts, comments and IP-literal domains; patterns that try to cover it run to hundreds of characters and still reject valid addresses people really use. And a syntactically perfect address can still bounce. Check the shape, then send a confirmation email. System.Net.Mail.MailAddress offers another shape check if you prefer not to write a pattern.
Performance: static methods, instances, compiled
The static methods (Regex.IsMatch(input, pattern)) parse the pattern and keep it in a small cache (15 patterns by default), so repeated calls with the same pattern are cheap. For a pattern used in a hot loop, create one Regex instance and keep it in a static readonly field; add RegexOptions.Compiled if it runs thousands of times, trading a slower start for faster matching.
Timeouts and catastrophic backtracking
Some patterns take exponential time on certain inputs. The classic is a nested quantifier such as ^(a+)+$ against "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!": the engine tries every way of dividing the as between the two + before giving up. On a web server, one such input from a user can pin a CPU core (a "ReDoS" attack). Give any regex that handles untrusted input a timeout:
var pattern = new Regex(@"^(\w+\s?)*$", RegexOptions.None, TimeSpan.FromMilliseconds(100));
try
{
bool ok = pattern.IsMatch(userInput);
}
catch (RegexMatchTimeoutException)
{
// treat as invalid input
}
Every static method has an overload that takes a timeout too. .NET 7 also added RegexOptions.NonBacktracking, an engine that guarantees linear time at the cost of some features (lookarounds, backreferences).
GeneratedRegex (.NET 7)
.NET 7 added a source generator that turns a pattern into ordinary C# code at compile time. You get the speed of Compiled with no start-up cost, the generated code is readable in the IDE, and it works with trimming and Native AOT:
public static partial class Patterns
{
[GeneratedRegex(@"^[A-Z]{2}-\d{4}$", RegexOptions.IgnoreCase)]
public static partial Regex ProductCode();
}
bool ok = Patterns.ProductCode().IsMatch("pt-1000"); // True
On .NET 7 and later this is the recommended form for any pattern known at compile time.
Common mistakes
- Missing anchors in validation. Without
^...$,IsMatchaccepts input that merely contains a match. - Normal strings for patterns.
"\b"is a backspace, not a word boundary. Use@"...". - Unescaped special characters.
.matches any character;3.50as a pattern also matches3x50. Escape with\., or withRegex.Escapefor user input. - Greedy quantifiers.
<.*>on<b>bold</b>matches the whole string. Use the lazy.*?or a negated class[^>]*. - No timeout on untrusted input. A nested quantifier can hang a request.
- Regex for structured formats. HTML, JSON and CSV with quotes need a parser, not a pattern.
Frequently Asked Questions
How do I use regex in C#?
Add using System.Text.RegularExpressions; and call the static methods on Regex: Regex.IsMatch(input, pattern) returns a bool, Regex.Match returns the first match, Regex.Matches all of them, Regex.Replace substitutes and Regex.Split splits. Write patterns as verbatim strings, @"\d+", so backslashes reach the regex engine unchanged.
How do I get a group value from a regex match in C#?
Put parentheses around the part you want and read match.Groups[1].Value (groups are numbered from 1; group 0 is the whole match). Named groups are clearer: (?<year>\d{4}) is read with match.Groups["year"].Value. Check match.Success first, because a failed match has empty groups rather than null.
How do I replace text with regex in C#?
Regex.Replace(input, pattern, replacement) replaces every match. The replacement can refer to groups: $1 for a numbered group, ${name} for a named one, $0 for the whole match. For logic that a template cannot express, pass a lambda: Regex.Replace(text, @"\d+", m => (int.Parse(m.Value) * 2).ToString()).
How do I make a C# regex case-insensitive?
Pass RegexOptions.IgnoreCase: Regex.IsMatch(input, "error", RegexOptions.IgnoreCase). Options combine with |, for example RegexOptions.IgnoreCase | RegexOptions.Multiline. You can also switch it on inside the pattern with (?i).
How do I validate an email address with regex in C#?
A pattern such as ^[^@\s]+@[^@\s]+\.[^@\s]+$ catches obvious typos (missing @, spaces, no domain dot) and is usually all a form needs. A regex cannot really validate an address: the full grammar allows forms no practical pattern handles, and a syntactically valid address may not exist. Check the basic shape, then confirm by sending a verification email.