Menu

C# Regex: Match, Replace, Split and Groups with Examples

How to use regular expressions in C# with System.Text.RegularExpressions: IsMatch, Match and Matches, numbered and named groups, Replace with substitutions and a lambda, Split, RegexOptions, verbatim pattern strings, validating input, timeouts, and the .NET 7 GeneratedRegex attribute.

This page includes runnable editors - edit, run, and see output instantly.

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: with IgnoreCase, 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 ^...$, IsMatch accepts 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.50 as a pattern also matches 3x50. Escape with \., or with Regex.Escape for 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.

Coddy programming languages illustration

Learn to code with Coddy

GET STARTED