Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

C# & .NET (C# 14, .NET 10 LTS)  Module 4 – Strings and text

Regular expressions with source generation

Match and parse text with .NET regular expressions: named groups, [GeneratedRegex] code made at build time, and patterns that cannot take exponential time.

  • Intermediate
  • 15 minutes
  • Examples run with .NET 10.0.401 and Python 3.14.8
  • By MySmartCoPilot
Free core, Pro depth: 6 of 15 minutes free
Free: Named groups pull the parts out of a match · [GeneratedRegex] turns the pattern into code at build time · Some patterns take exponential time on inputs that almost match. With a pass: 3 in-depth parts and 3 interview answers, about 9 minutes.

What you will learn

  • Write .NET regular expressions with groups and named captures, and read the parts of a match
  • Generate a regex at build time with [GeneratedRegex] on a partial property
  • Recognise a pattern that can backtrack exponentially, and name the two ways to bound it

Before you start

The log line 18:30:05 WARN [payments] retry 3 of 5 has four parts, and one regular expression with named groups pulls all four out in a single match. Written with [GeneratedRegex], the same pattern becomes C# code when the program is built, while a carelessly written pattern can keep the engine busy for hours on a short input.

You will write .NET regular expressions with groups and named captures, generate them at build time, and recognise the patterns whose running time can explode, with the two ways .NET offers to bound it.

Named groups pull the parts out of a match

A regular expression: A pattern that describes text, such as \d{2}:\d{2} for a time: .NET's Regex class finds, checks, splits or replaces the parts of a string that match it. is a small language for describing text: \d is a digit, [A-Z] a capital letter, + one or more of what comes before it, {2} exactly two, ^ the start of the text and $ its end or the point just before a final line break. Parentheses make a group, and (?<name>…) a named group: A part of a regular expression written (?<name>…): the text it matches is kept in the match under that name, as Match.Groups["name"].Value., whose text the Match keeps under that name:

logs.cs: three lines parsed with named groups, then Matches and Replace C# · logs/logs.cs
using System.Text.RegularExpressions;

// Three log lines, and one pattern whose named groups pull out the parts.
string[] lines =
[
    "18:30:05 WARN  [payments] retry 3 of 5",
    "18:30:07 INFO  [orders] order A-17 placed",
    "not a log line at all",
];

var pattern = new Regex(@"^(?<time>\d{2}:\d{2}:\d{2}) (?<level>[A-Z]+) +\[(?<area>[a-z]+)\] (?<message>.+)$");

foreach (string line in lines)
{
    Match m = pattern.Match(line);
    if (!m.Success)
    {
        Console.WriteLine($"no match: \"{line}\"");
        continue;
    }
    Console.WriteLine($"time {m.Groups["time"].Value}  level {m.Groups["level"].Value,-5} area {m.Groups["area"].Value,-9} message \"{m.Groups["message"].Value}\"");
}

// Every number in a text, with where each one starts.
foreach (Match n in Regex.Matches("retry 3 of 5, waited 250 ms", @"\d+"))
{
    Console.WriteLine($"number {n.Value} at index {n.Index}");
}

// Replace with a group: put the area first.
Console.WriteLine(pattern.Replace(lines[0], "${area}: ${message} (${level})"));

Output

time 18:30:05  level WARN  area payments  message "retry 3 of 5"
time 18:30:07  level INFO  area orders    message "order A-17 placed"
no match: "not a log line at all"
number 3 at index 6
number 5 at index 11
number 250 at index 21
payments: retry 3 of 5 (WARN)

Recorded with .NET SDK 10.0.401 (runtime 10.0.12) on macOS 26 arm64. To run it yourself: dotnet run logs.cs

Step through how the pattern consumed the first line:

Interactive · stage by stage One log line matched piece by piece Step through the pattern against the first line of logs.cs and watch each named group take its part.
One log line matched piece by piece, step 5 of 5. Showing (?<time>…) (time = 18:30:05), (?<level>[A-Z]+) (level = WARN), \[(?<area>[a-z]+)\] (area = payments), (?<message>.+)$ (message = retry 3 of 5), The Match (4 named groups filled). In focus: The Match.(?<time>…)time = 18:30:05(?<level>[A-Z]+)level = WARN\[(?<area>[a-z]+)\]area = payments(?<message>.+)$message = retry 3 of 5The Match4 named groups filled

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. Anchor and time. ^ pins the match to the start of the text. Then \d{2}:\d{2}:\d{2} needs two digits, a colon, two digits, a colon and two digits, and the group named time keeps them.
  2. Level. After one space, [A-Z]+ takes one or more capital letters as the group level. It stops at the first space, because a space is not a capital letter.
  3. Area. A space and a + allow the two spaces that line the levels up. \[ and \] are the brackets themselves, escaped, and [a-z]+ inside them is the group area.
  4. Message. After one more space, .+ takes everything up to the end of the line, and $ checks that the text ends there. The group message holds the rest.
  5. The result. m.Success is true and m.Groups["level"].Value is WARN. The third line, not a log line at all, failed at the first digit, so Success was false.

m.Groups["level"].Value gave WARN without counting positions, and the line that is not a log line came back with Success false instead of an exception. Regex.Matches found every number in a sentence with its index, and Replace reused the groups by name with ${area}. Brackets, dots and the other characters that mean something in a pattern are escaped with a backslash when you mean the character itself, as \[ and \] were.

Regex Tester It tests JavaScript patterns, which write named groups as (?<name>…) like .NET; paste the log pattern and the three lines.

Checkpoint

Check: Groups and named captures

  1. Question 1 of 2 Which pattern keeps the level of "18:30:05 WARN" under the name level?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: ^\d{2}:\d{2}:\d{2} (?<level>[A-Z]+)

    (?<name>subpattern) captures the text the subpattern matches and keeps it under the name, so code reads parts by name instead of by position.

  2. Question 2 of 2 pattern.Match("not a log line at all") in logs.cs did what?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: It returned a Match whose Success was false

    Check Success before reading groups: the groups of a failed match are empty strings.

[GeneratedRegex] turns the pattern into code at build time

new Regex(pattern) parses the pattern when the program runs and then interprets it. Since .NET 7, the source generator: A component that runs inside the C# compiler and adds code to the program while it is built. The regex source generator turns each [GeneratedRegex] pattern into C# matching code. for regular expressions does that work while the program is built: mark a partial method, or since .NET 9 and C# 13 a partial property, with [GeneratedRegex(pattern)], and the build writes a class with C# code that matches exactly that pattern:

generated.cs: the log pattern as a [GeneratedRegex] property C# · generated/generated.cs
using System.Text.RegularExpressions;

// The same pattern as logs.cs, generated at build time by the regex source generator.
string line = "18:30:05 WARN  [payments] retry 3 of 5";
Match m = Patterns.LogLine.Match(line);
Console.WriteLine($"level {m.Groups["level"].Value}, area {m.Groups["area"].Value}");
Console.WriteLine($"IsMatch on a span, no string made: {Patterns.LogLine.IsMatch(line.AsSpan())}");
Console.WriteLine($"The generated matcher is a class of its own, derived from {Patterns.LogLine.GetType().BaseType?.Name}");

static partial class Patterns
{
    // C# 13 and later: a partial property; a partial method returning Regex works too.
    [GeneratedRegex(@"^(?<time>\d{2}:\d{2}:\d{2}) (?<level>[A-Z]+) +\[(?<area>[a-z]+)\] (?<message>.+)$")]
    public static partial Regex LogLine { get; }
}

Output

level WARN, area payments
IsMatch on a span, no string made: True
The generated matcher is a class of its own, derived from Regex

Recorded with .NET SDK 10.0.401 (runtime 10.0.12) on macOS 26 arm64. To run it yourself: dotnet run generated.cs

The generated matcher found the same groups, and IsMatch on a span of the line matched without making a new string. The property returns one cached instance of a class derived from Regex, so there is nothing to store in a field. Microsoft’s guide to the generator lists what it buys: no parsing at start-up, the speed of RegexOptions.Compiled without compiling at run time, code that works with Native AOT and trimming, and generated code you can read and step through. The analyzer SYSLIB1045 points at Regex calls that could be converted.

Engine Pattern prepared Speed of matching Native AOT
new Regex(p) parsed at run time, interpreted the slowest works
RegexOptions.Compiled parsed and compiled at run time fast ignored, so the pattern is interpreted
[GeneratedRegex(p)] turned into C# when you build fast, and no start-up cost works

Checkpoint

Check: [GeneratedRegex] at build time

  1. Question 1 of 2 When is the matching code of a [GeneratedRegex] pattern produced?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: When the program is built, by a source generator

    Source generators run inside the C# compiler, so the pattern is checked and turned into code with the rest of the program.

  2. Question 2 of 2 Which declaration compiles with C# 14 and .NET 10 for a generated regex?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: [GeneratedRegex(@"^\d{6}$")] public static partial Regex Pin { get; }

    The generator implements a partial member that returns Regex, from a constant pattern. Patterns known only at run time still need new Regex.

Some patterns take exponential time on inputs that almost match

.NET’s default engines backtracking: How .NET's default regex engines search: when part of a pattern fails, they go back and try another way to match the text before it. Nested quantifiers can make the number of tries exponential.: when part of a pattern fails, they go back and try another way to match what came before. For most patterns that costs little, but a quantifier inside a quantifier, such as (a+)+$, lets a run of a be split into groups in exponentially many ways, and an input that almost matches makes the engine try them all. Step through ^(a+)+$ on aaa!, three letters and a char it never accepts:

Interactive · stage by stage Every way to split aaa, tried before no match Step through the ways ^(a+)+$ splits the run aaa when a ! follows it, and count the splits it tries.
Every way to split aaa, tried before no match, step 5 of 5. Showing Try (aaa) (fails at the !), Try (aa)(a) (fails at the !), Try (a)(aa) (fails at the !), Try (a)(a)(a) (fails at the !), No match (4 for aaa, 8 for aaaa). In focus: No match.Try (aaa)fails at the !Try (aa)(a)fails at the !Try (a)(aa)fails at the !Try (a)(a)(a)fails at the !No match4 for aaa, 8 for aaaa

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. One group. The group a+ takes all three a, and the + around it finds no second group. $ then needs the end of the text, but ! comes next, so this way fails.
  2. Give one back. The engine goes back: the first group gives up its last a, and a second group takes it. ! still comes next, so $ fails again.
  3. The other split. It goes back further: the first group keeps one a and the second takes the other two. The text still goes on with !, so this split fails too.
  4. Every a apart. The last way puts each a in a group of its own. It fails like the others, and no way to split three a is left.
  5. Then no match. Only now does the engine report no match, after all 4 ways to split 3 letters. Four letters can be split 8 ways, and every extra a doubles the count.

The engine tried every split before it gave up: 4 for three letters, 8 for four, and each extra a doubles the count, so a run of 20 has 524,288. Microsoft’s guide calls the result catastrophic backtracking.

Two settings bound the damage. A match timeout makes a match that runs too long throw RegexMatchTimeoutException. RegexOptions.NonBacktracking runs in time linear in the length of the input, but it supports no backreferences and no lookarounds. Often the pattern can simply be rewritten without the nesting: ^a+$ matches the same texts as ^(a+)+$ and has nothing to try twice.

Key idea

Name groups for the parts you want back, generate patterns that are known when you build, and never let a quantifier repeat a group that can itself match the same text in several ways.

Checkpoint

Check: Patterns that backtrack exponentially

  1. One question Which pattern risks catastrophic backtracking on an input that almost matches?

    Choose one answer.

    Show the answer: One question

    Answer: ^(\w+\s?)*$

    Look for a quantifier on a group whose content can itself match a run in several ways: (a+)+, (\w+\s?)* and (a|aa)+ are the usual shapes.

Catastrophic backtracking, reproduced and bounded Pro: opens with a Pro, Premium or Ultimate pass

A pattern with a quantifier inside a quantifier can take time that doubles with every extra character of input. The run below measures it on inputs from 15 to 23 chars, stops it with a time limit, and runs the same pattern with the engine that cannot backtrack.

In this part:

  • Doubling time on inputs that almost match
  • Two ways to bound it, and the better fix

1 runnable example (C#) · about 4 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Validating input with an allowlist pattern, with its traps Pro: opens with a Pro, Premium or Ultimate pass

A pattern that checks the shape of input is only as strict as its anchors, its character classes and its options. The run below checks eight inputs against a first attempt and a fixed version, and three of them pass the first attempt although no clerk would accept them.

In this part:

  • A simplified registration number, checked two ways
  • Why each fix works

1 runnable example (C#) · about 3 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

SYSLIB1045 and the code the generator writes Pro: opens with a Pro, Premium or Ultimate pass

The regex source generator also ships an analyzer that finds patterns worth converting, and it writes readable C# with comments that explain the pattern. The session below turns the analyzer’s suggestion into a build warning, fixes it, and prints the comments the generator wrote.

In this part:

  • The suggestion as a warning, and the fix
  • What the generated code says

3 runnable examples (C#, Python) · about 3 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Practise: parse a log line with a generated pattern

Add a request id to the log pattern

Copy generated.cs, add a request id such as req=7f3a to the end of the log line, and extend the pattern with a named group for it, req=(?<req>[0-9a-f]+). Print m.Groups["req"].Value and check that the old lines without an id now fail to match, then make the group optional with (?: req=(?<req>[0-9a-f]+))?.

These questions go back over the log pattern, the generated property and the nested quantifier. Your least certain idea is asked first, and each choice explains what it gets right or misses:

Practice that adapts to you

Practise this lesson

8 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 8 logs.cs called pattern.Replace(line, "${area}: ${message} (${level})"). What does ${area} refer to?

    Choose one answer.

    Show the answer: Question 1 of 8

    Answer: The text the named group area matched

    Named groups work in replacements too, which keeps a reordering readable even when the pattern changes.

  2. Question 2 of 8 Regex.Matches("retry 3 of 5, waited 250 ms", @"\d+") found 250 at which index?

    Type a number.

    Show the answer: Question 2 of 8

    Answer: 21

    Each Match carries its Value and its Index in the input: 3 at 6, 5 at 11 and 250 at 21, as logs.cs printed.

  3. Question 3 of 8 A pattern must match the text [payments] with its brackets. Which part does it?

    Choose one answer.

    Show the answer: Question 3 of 8

    Answer: \[[a-z]+\]

    Characters such as [ ] ( ) . * + ? { } \ ^ $ | mean something in a pattern; a backslash before one matches the character itself.

  4. Question 4 of 8 A tool is published with Native AOT and uses one fixed pattern on every line of its input. Which choice fits best?

    Choose one answer.

    Show the answer: Question 4 of 8

    Answer: [GeneratedRegex] on a partial property

    For a pattern known at build time, the source generator gives the speed of Compiled without run-time code generation, which is exactly what AOT needs.

  5. Question 5 of 8 What did Patterns.LogLine.GetType().BaseType report in generated.cs?

    Choose one answer.

    Show the answer: Question 5 of 8

    Answer: Regex: the generator writes its own class derived from Regex

    The generated class overrides how matching runs while keeping the whole Regex API, so code that used new Regex changes only where the instance comes from.

  6. Question 6 of 8 Which two settings bound the time a pattern such as ^(a+)+$ can spend on one input? Choose both.

    Choose every answer that is right.

    Show the answer: Question 6 of 8

    Answer:

    • A match timeout
    • RegexOptions.NonBacktracking

    A timeout stops a match that runs too long, and NonBacktracking keeps the time linear in the length of the input. Rewriting the pattern without the nesting, as ^a+$, removes the problem instead of bounding it.

  7. Question 7 of 8 What does RegexOptions.NonBacktracking give up in return for linear time?

    Choose one answer.

    Show the answer: Question 7 of 8

    Answer: Constructs such as backreferences and lookarounds

    NonBacktracking matches in time linear in the input, for patterns that avoid the constructs only backtracking can run.

  8. Question 8 of 8 Which pattern matches the same texts as ^(a+)+$ without the risk?

    Choose one answer.

    Show the answer: Question 8 of 8

    Answer: ^a+$

    Rewriting the pattern removes the exponential paths at the root; timeouts and NonBacktracking only bound them.

Interview questions

Fresher · What does a named group give you in a .NET regular expression?

A name for a part of the match. (?<level>[A-Z]+) captures what that part matched and keeps it under the name, so code reads m.Groups["level"].Value instead of counting parentheses to find group 2. The name keeps working when someone adds a group earlier in the pattern, which would shift every number, and it can be reused in a replacement as ${level}. Check m.Success before reading groups: a failed match has empty groups rather than an exception. A pattern with many groups reads better with names, and RegexOptions.ExplicitCapture makes the unnamed parentheses plain groupings that capture nothing.

  1. Mid to senior

    After a release, one API endpoint now and then pins a CPU core for seconds. Profiles point at a Regex.IsMatch call that validates a free-text field with a new pattern. How do you confirm the cause, contain it in production and fix it properly?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Fresher to mid

    A validator uses Regex.IsMatch(input, @"^\d{6}$") for a six-digit postal code. A tester finds that "411014" followed by a line break passes, and so does the same code written in Devanagari digits. Explain both and fix the pattern.

    Model answer, Pro. First step and follow-ups

  3. Mid to senior

    A service creates about forty Regex objects with RegexOptions.Compiled in static fields, and its start-up is slow. The team wants to publish it with Native AOT. What would you change, how, and what would you check before and after?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • A named group (?<name>…) keeps its part of the text in Match.Groups["name"], and a failed match returns Success false rather than throwing.
  • [GeneratedRegex] on a partial method or property turns a pattern into C# code at build time: no parsing at start-up, and code that works with Native AOT.
  • A quantifier inside a quantifier can make the backtracking engines take exponential time; a match timeout or RegexOptions.NonBacktracking bounds it, and a simpler pattern removes it.

Terms in this lesson

backtracking
How .NET's default regex engines search: when part of a pattern fails, they go back and try another way to match the text before it. Nested quantifiers can make the number of tries exponential.
named group
A part of a regular expression written (?<name>…): the text it matches is kept in the match under that name, as Match.Groups["name"].Value.
regular expression
A pattern that describes text, such as \d{2}:\d{2} for a time: .NET's Regex class finds, checks, splits or replaces the parts of a string that match it.
source generator
A component that runs inside the C# compiler and adds code to the program while it is built. The regex source generator turns each [GeneratedRegex] pattern into C# matching code.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.