.NET
Regex
named capturing groups
C#
regular expressions

How do I access named capturing groups in a .NET Regex?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

In .NET, named capturing groups use the syntax (?<name>pattern) in a regex pattern, and you access them through match.Groups["name"] on the Match object. Named groups make regex code more readable than numbered groups (\1, \2) because you reference matches by descriptive names instead of position indices. The System.Text.RegularExpressions.Regex class supports named groups in Match(), Matches(), and Replace() operations.

Defining Named Groups

Use (?<name>...) to define a named capturing group:

csharp
1using System.Text.RegularExpressions;
2
3string pattern = @"(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})";
4string input = "Date: 2025-03-15";
5
6Match match = Regex.Match(input, pattern);
7
8if (match.Success)
9{
10    string year = match.Groups["year"].Value;
11    string month = match.Groups["month"].Value;
12    string day = match.Groups["day"].Value;
13
14    Console.WriteLine($"Year: {year}, Month: {month}, Day: {day}");
15    // Year: 2025, Month: 03, Day: 15
16}

Alternative Syntax

.NET also supports (?'name'...) with single quotes:

csharp
string pattern = @"(?'year'\d{4})-(?'month'\d{2})-(?'day'\d{2})";
// Functionally identical to (?<year>...) syntax

Accessing Groups by Name vs Index

Named groups are also accessible by their numeric index:

csharp
1string pattern = @"(?<first>\w+)\s(?<last>\w+)";
2string input = "John Smith";
3
4Match match = Regex.Match(input, pattern);
5
6// By name (preferred)
7Console.WriteLine(match.Groups["first"].Value); // John
8Console.WriteLine(match.Groups["last"].Value);  // Smith
9
10// By index (still works)
11Console.WriteLine(match.Groups[1].Value); // John
12Console.WriteLine(match.Groups[2].Value); // Smith
13
14// Groups[0] is always the entire match
15Console.WriteLine(match.Groups[0].Value); // John Smith

Iterating Over Multiple Matches

csharp
1string pattern = @"(?<key>\w+)=(?<value>[^&]+)";
2string queryString = "name=Alice&age=30&city=NYC";
3
4MatchCollection matches = Regex.Matches(queryString, pattern);
5
6foreach (Match m in matches)
7{
8    string key = m.Groups["key"].Value;
9    string value = m.Groups["value"].Value;
10    Console.WriteLine($"{key} => {value}");
11}
12// name => Alice
13// age => 30
14// city => NYC

Named Groups in Replace

Reference named groups in replacement strings with ${name}:

csharp
1string pattern = @"(?<month>\d{2})/(?<day>\d{2})/(?<year>\d{4})";
2string input = "Today is 03/15/2025";
3
4// Reformat from MM/DD/YYYY to YYYY-MM-DD
5string result = Regex.Replace(input, pattern, "${year}-${month}-${day}");
6Console.WriteLine(result); // Today is 2025-03-15

Named Groups with LINQ

csharp
1string pattern = @"(?<name>\w+)\s*:\s*(?<score>\d+)";
2string input = "Alice: 95, Bob: 87, Carol: 92";
3
4var scores = Regex.Matches(input, pattern)
5    .Select(m => new
6    {
7        Name = m.Groups["name"].Value,
8        Score = int.Parse(m.Groups["score"].Value)
9    })
10    .OrderByDescending(x => x.Score)
11    .ToList();
12
13foreach (var s in scores)
14    Console.WriteLine($"{s.Name}: {s.Score}");
15// Alice: 95
16// Carol: 92
17// Bob: 87

Checking if a Group Matched

A named group may not participate in every match (e.g., optional groups):

csharp
1string pattern = @"(?<protocol>https?)://(?<domain>[^/:]+)(:(?<port>\d+))?";
2string input = "https://example.com:8080";
3
4Match match = Regex.Match(input, pattern);
5
6// Check if the optional port group matched
7if (match.Groups["port"].Success)
8{
9    Console.WriteLine($"Port: {match.Groups["port"].Value}"); // 8080
10}
11else
12{
13    Console.WriteLine("No port specified");
14}
15
16// With a URL without port
17string input2 = "https://example.com";
18Match match2 = Regex.Match(input2, pattern);
19Console.WriteLine(match2.Groups["port"].Success); // False

Named Groups with Repeated Captures

When a named group matches multiple times (inside a quantifier), .Captures contains all matches:

csharp
1string pattern = @"(?<word>\w+)\s*";
2string input = "Hello World Foo";
3
4Match match = Regex.Match(input, pattern + "+");
5
6// Groups["word"].Value only returns the LAST capture
7Console.WriteLine(match.Groups["word"].Value); // Foo
8
9// Captures contains ALL matches
10foreach (Capture capture in match.Groups["word"].Captures)
11{
12    Console.WriteLine(capture.Value);
13}
14// Hello
15// World
16// Foo

Compiled Regex for Performance

For frequently used patterns, compile the regex:

csharp
1// Compiled regex — slower to create, faster to execute
2var regex = new Regex(
3    @"(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})",
4    RegexOptions.Compiled
5);
6
7// Use the same instance for multiple matches
8foreach (string line in lines)
9{
10    Match match = regex.Match(line);
11    if (match.Success)
12    {
13        ProcessDate(match.Groups["year"].Value,
14                    match.Groups["month"].Value,
15                    match.Groups["day"].Value);
16    }
17}

.NET 7+ Source Generators

csharp
1// .NET 7+ — compile-time regex with source generators
2[GeneratedRegex(@"(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})")]
3private static partial Regex DateRegex();
4
5// Usage
6Match match = DateRegex().Match("2025-03-15");
7string year = match.Groups["year"].Value;

Source-generated regex provides the best performance with zero runtime compilation overhead.

Common Pitfalls

  • Accessing a group that does not exist: match.Groups["nonexistent"] returns an empty Group object (not null) with Success = false and Value = "". Always check .Success before using the value to avoid silent empty-string bugs.
  • Confusing .Value with .Captures: For a group inside a quantifier (e.g., (?<word>\w+)+), .Value returns only the last capture. Use .Captures to access all individual matches of that group.
  • Forgetting ${} syntax in Replace: In replacement strings, named groups use ${name}, not $name or \name. Using the wrong syntax inserts a literal string instead of the captured value.
  • Not escaping special regex characters in group patterns: Characters like ., (, ), [, + have special meaning in regex. Escape them with \ (or \\ in C# strings) or use Regex.Escape() for dynamic patterns.
  • Performance with uncompiled regex in loops: Creating a new Regex object inside a loop recompiles the pattern each time. Use RegexOptions.Compiled, a static field, or [GeneratedRegex] (.NET 7+) for patterns used repeatedly.

Summary

  • Define named groups with (?<name>pattern) and access with match.Groups["name"].Value
  • Use ${name} in replacement strings for Regex.Replace()
  • Check match.Groups["name"].Success for optional groups that may not match
  • Use .Captures instead of .Value when a named group matches multiple times inside a quantifier
  • Use RegexOptions.Compiled or [GeneratedRegex] (.NET 7+) for performance-critical patterns

Course illustration
Course illustration

All Rights Reserved.