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:
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:
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:
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
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}:
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
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):
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:
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
For frequently used patterns, compile the regex:
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
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