Validation, Explain and Debugging
A fluent chain always produces regex text, but that text is not always valid. Raw regex added with RegEx(...), text loaded from a settings file and patterns typed by users can all fail to compile. This page covers the members that help you find and understand those problems before they reach production:
| Member | Returns | Use it when |
|---|---|---|
IsValid() | bool | You only need a yes/no answer. |
TryValidate(out string errorMessage) | bool, plus the error message | You want to show or log why the pattern is invalid. |
GetGroupNames() | string[] | You need the names of the named groups, for example to check them before binding. |
GetGroupCount() | int | You need the number of capturing groups. |
Explain() | string | You want a plain-English, line-by-line description of the pattern. |
| Debugger display | Shown in the IDE | You are stepping through code and want to see what a Pattern variable holds. |
All of these members live on Pattern in the Masterly.FluentRegex namespace. They are available on every target framework of the package: netstandard2.1, net8.0 and net10.0.
You do not need these runtime checks for patterns written in your code. The package ships Roslyn analyzers that build constant fluent chains at compile time. An invalid one fails the build with FRX003, for example: The pattern produces an invalid regular expression '[unclosed': Invalid pattern '[unclosed' at offset 9. Unterminated [] set. Use the members on this page for regex text that only exists at run time. See Compile-time checks.
Checking that a pattern is valid
IsValid
IsValid() tries to compile the pattern and tells you whether it worked.
using System;
using Masterly.FluentRegex;
// regexText comes from your application; in this example it is "[unclosed"
Pattern digits = Pattern.With.Digit.Repeat.OneOrMore;
Pattern configured = Pattern.With.RegEx(regexText);
Console.WriteLine(digits.IsValid()); // True
Console.WriteLine(configured.IsValid()); // False
IsValid()never throws for a bad pattern. It catches theArgumentExceptionthatRegexthrows and returnsfalse.- An empty pattern (
Pattern.With) is valid. It compiles to the empty regex, which matches everywhere. - Each call compiles a new
Regex. It does not use or fill the cached regex thatIsMatchand the other matching members use. Validate once, for example when you load the configuration, not on every request.
TryValidate and the error message
TryValidate does the same check and also gives you the error message from the .NET regex parser.
using System;
using Masterly.FluentRegex;
// regexText comes from your application; in this example it is @"(?<year>\d{4}"
Pattern pattern = Pattern.With.RegEx(regexText);
if (!pattern.TryValidate(out string? error))
{
Console.WriteLine($"Invalid pattern: {error}");
}
// prints: Invalid pattern: Invalid pattern '(?<year>\d{4}' at offset 13. Not enough )'s.
errorisnullwhen the method returnstrue.- The message comes from the .NET runtime, not from this library. Its wording depends on the runtime your application runs on.
- The offset in the message counts characters from the start of the whole regex text, not from the start of the part you added with
RegEx(...).
These are the messages .NET 10 gives for common mistakes:
| Regex text | Message |
|---|---|
[unclosed | Invalid pattern '[unclosed' at offset 9. Unterminated [] set. |
(unclosed | Invalid pattern '(unclosed' at offset 9. Not enough )'s. |
a) | Invalid pattern 'a)' at offset 2. Too many )'s. |
a{2,1} | Invalid pattern 'a{2,1}' at offset 6. Illegal {x,y} with x > y. |
x** | Invalid pattern 'x**' at offset 3. Nested quantifier '*'. |
\ | Invalid pattern '\' at offset 1. Illegal \ at end of pattern. |
[z-a] | Invalid pattern '[z-a]' at offset 4. [x-y] range in reverse order. |
(?<1a>x) | Invalid pattern '(?<1a>x)' at offset 4. Invalid group name: Group names must begin with a word character. |
\k<nope> | Invalid pattern '\k<nope>' at offset 8. Reference to undefined group name 'nope'. |
\p{Nope} | Invalid pattern '\p{Nope}' at offset 8. Unknown property 'Nope'. |
The message repeats the regex text. If the text came from a user, treat the message as user input too: encode it before you put it in HTML, and think twice before you write it to a shared log.
Validating and wrapping in one step
When the regex text arrives as a string, Pattern.FromRegex(regex, options) validates it and wraps it in a Pattern in one call. Unlike RegEx(...), it fails immediately instead of returning a pattern that fails later.
using System;
using System.Text.RegularExpressions;
using Masterly.FluentRegex;
// regexText comes from your application; in this example it is "a{2,1}"
try
{
Pattern pattern = Pattern.FromRegex(regexText, RegexOptions.IgnoreCase);
}
catch (ArgumentException exception)
{
Console.WriteLine(exception.Message);
}
// prints: The regular expression is invalid: Invalid pattern 'a{2,1}' at offset 6. Illegal {x,y} with x > y. (Parameter 'regex')
FromRegexvalidates with the options you pass, so it catches options that do not combine.RegexOptions.ECMAScript | RegexOptions.SinglelinethrowsArgumentExceptionwith the messageThe regular expression is invalid: Specified argument was out of the range of valid values. (Parameter 'options') (Parameter 'regex').- A
nullregex throwsArgumentNullException. - It does not catch
NotSupportedException.RegexOptions.NonBacktrackingwith a backreference such as(a)\1throwsNotSupportedException: RegexOptions.NonBacktracking is not supported in conjunction with expressions containing: 'backreference (\ number)'.Catch both exception types when the options come from configuration. - See Regex to fluent code for what you can do with the wrapped pattern.
Finding the exact position of the error
TryValidate gives you only the message. On .NET 7 and later, the exception that Regex throws is the public RegexParseException, whose Error and Offset properties tell you what went wrong and where. Call ToRegex() and catch it when you want to point at the bad character, for example in an editor.
using System;
using System.Text.RegularExpressions;
using Masterly.FluentRegex;
// regexText comes from your application; in this example it is @"(?<year>\d{4}"
Pattern pattern = Pattern.With.RegEx(regexText);
try
{
Regex regex = pattern.ToRegex();
}
catch (RegexParseException exception)
{
Console.WriteLine($"{exception.Error} at offset {exception.Offset}");
}
// prints: InsufficientClosingParentheses at offset 13
RegexParseExceptionderives fromArgumentException, so acatch (ArgumentException)block catches it too.RegexParseExceptionis a public type of the .NET 7+ runtime. Code that targetsnetstandard2.1(or .NET 6 and earlier) cannot reference it. CatchArgumentExceptionthere.
Validation uses the pattern's options
IsValid() and TryValidate() compile the regex with the options you set with WithOptions(...) or UseNonBacktracking(), because options change what is valid:
using System;
using System.Text.RegularExpressions;
using Masterly.FluentRegex;
// regexText comes from your application; in this example it is "a # [ comment"
Pattern commented = Pattern.With.RegEx(regexText).WithOptions(RegexOptions.IgnorePatternWhitespace);
Console.WriteLine(commented.IsValid()); // True
Console.WriteLine(commented.IsMatch("a")); // True
Pattern backreference = Pattern.With.Group(Pattern.With.Word).Backreference(1).UseNonBacktracking();
Console.WriteLine(backreference.TryValidate(out string error)); // False
Console.WriteLine(error);
// prints: RegexOptions.NonBacktracking is not supported in conjunction with expressions containing: 'backreference (\ number)'.
- With
IgnorePatternWhitespace,# [ commentis a comment, so the pattern is valid. Without the option,[would open a character set that is never closed. - The non-backtracking engine does not support backreferences, lookarounds, atomic groups or conditionals, so such a pattern is invalid after
UseNonBacktracking().UseNonBacktracking()exists only onnet8.0andnet10.0. - Options that cannot be combined, such as
RegexOptions.ECMAScript | RegexOptions.Singleline, make the pattern invalid.TryValidatereportsSpecified argument was out of the range of valid values. (Parameter 'options'). - The timeout plays no part in validation, and neither does
RegexOptions.Compiled, which only changes how the regex runs. - To check text against other options than the pattern's own, compile it with
ToRegex(RegexOptions options)inside atryblock, and catch bothArgumentExceptionandNotSupportedException.
Validation checks syntax only. A valid pattern can still be slow or match the wrong text. Run AnalyzeSafety() on patterns that come from outside your code, and set a match timeout with WithTimeout(...).
What an invalid pattern does
Building an invalid pattern never throws. At run time, RegEx(regexText) with the text [unclosed returns a pattern, and ToString() returns its text. (Written as a constant, the same call is rejected at build time by FRX003.) The error shows up when a member needs to compile or parse it:
| Member | With an invalid pattern |
|---|---|
ToString() | Returns the text. Never throws. |
Explain() | Explains the text character by character. Never throws. See Explaining a pattern. |
IsValid(), TryValidate(...) | Return false. |
IsMatch, Match, Matches, Replace, Split, ToRegex() | Throw ArgumentException (a RegexParseException on .NET 7 and later) with the parser's message. The failed regex is not cached, so every call throws again. |
GetGroupNames(), GetGroupCount() | Throw the same ArgumentException. |
ToFluentCode(), Trace(...), AnalyzeSafety(), ToMermaid() and the other analysis members | Throw InvalidOperationException, for example The pattern is not valid regex syntax: Unterminated [] set (at position 0). |
Pattern.FromRegex(...) | Throws ArgumentException when you create the pattern. |
The analysis members use the library's own regex parser. Its message is shorter, and its position points at the start of the construct that is broken (here the [ at position 0), not at the place where the .NET parser gave up.
Inspecting groups
GetGroupNames
GetGroupNames() returns the names of the named groups, in the order they appear in the pattern.
using System;
using Masterly.FluentRegex;
Pattern phone = Pattern.With
.NamedGroup("area", Pattern.With.Digit.Repeat.Times(3))
.Literal("-")
.Group(Pattern.With.Digit.Repeat.Times(4))
.NonCapturingGroup(Pattern.With.Literal(" ext").Digit.Repeat.OneOrMore).Repeat.Optional;
string[] names = phone.GetGroupNames();
int count = phone.GetGroupCount();
Console.WriteLine(phone); // (?<area>\d{3})-(\d{4})(?: ext\d+)?
Console.WriteLine(string.Join(", ", names)); // area
Console.WriteLine(count); // 2
- Numbered groups created with
Group(...)have no name and are not listed. - A group with a numeric name, such as
(?<5>a)added withRegEx(...), is treated as a numbered group and is not listed either. - A name used twice, as in
(?<d>a)|(?<d>b), is listed once. - A pattern without named groups returns an empty array, never
null.
GetGroupCount
GetGroupCount() returns the number of capturing groups. It does not count group 0, which is always the whole match.
- Named groups and numbered groups count. In the sample above,
areaand(\d{4})give 2. - Non-capturing groups, lookaheads, lookbehinds and atomic groups do not count.
- A numbered group that shares its number with another group counts once.
(?<d>a)|(?<d>b)has 1 group.
Two cases often surprise people:
using System;
using System.Text.RegularExpressions;
using Masterly.FluentRegex;
Pattern choice = Pattern.With.Choice(Pattern.With.Literal("cat"), Pattern.With.Literal("dog"));
Pattern alternation = Pattern.With.Literal("cat") | Pattern.With.Literal("dog");
Console.WriteLine(choice); // (cat|dog)
Console.WriteLine(choice.GetGroupCount()); // 1
Console.WriteLine(alternation); // (?:cat|dog)
Console.WriteLine(alternation.GetGroupCount()); // 0
Pattern date = Pattern.With
.NamedGroup("year", Pattern.With.Digit.Repeat.Times(4))
.Group(Pattern.With.Literal("-"))
.NamedGroup("month", Pattern.With.Digit.Repeat.Times(2));
Console.WriteLine(string.Join(", ", date.GetGroupNames())); // year, month
Console.WriteLine(date.GetGroupCount()); // 3
Console.WriteLine(date.WithOptions(RegexOptions.ExplicitCapture).GetGroupCount()); // 2
Choice(...)wraps the alternatives in a capturing group, so it adds a group. The|operator builds a non-capturing group instead. See Composing patterns.- Unlike
IsValid(), the group members compile the pattern with its own options. WithRegexOptions.ExplicitCapture, unnamed groups do not capture, so they are not counted.
GetGroupNames() and GetGroupCount() throw ArgumentException when the pattern is invalid. Because they use the pattern's own options, they also throw NotSupportedException for a UseNonBacktracking() pattern with a backreference. Check the pattern with IsValid() or TryValidate() first when the regex text comes from outside your code (see Validation uses the pattern's options).
Checking groups before you bind them
Typed extraction fills properties from groups with the same name. When the pattern comes from configuration, check that the groups you need exist:
using System;
using System.Linq;
using Masterly.FluentRegex;
// pattern comes from your application; in this example it is ^(?<user>[\w.]+)@(?<domain>[\w.-]+)$
string[] required = { "user", "host" };
string[] missing = required.Except(pattern.GetGroupNames()).ToArray();
if (missing.Length > 0)
{
Console.WriteLine($"The pattern has no group named: {string.Join(", ", missing)}");
}
// prints: The pattern has no group named: host
Group names are case-sensitive, so Host and host are different groups. When the pattern is written in your code and you bind it to a type with Extract<T>, TryExtract<T> or ExtractAll<T>, the FRX008 analyzer warns at compile time about a target member that has no matching group. See Typed extraction.
Explaining a pattern
Explain() returns a plain-English description of the pattern, one line per construct. Use it in logs, in code review, or to understand a regex someone else wrote.
using System;
using Masterly.FluentRegex;
Pattern pattern = Pattern.With
.StartOfLine
.NamedGroup("digits", Pattern.With.Digit.Repeat.OneOrMore)
.Literal("-")
.Word.Repeat.ZeroOrMore.Lazy
.EndOfLine;
Console.WriteLine(pattern.Explain());
Output:
Pattern: ^(?<digits>\d+)-\w*?$
Explanation:
- Start of line/string
- Named capturing group 'digits'
- Digit (any Unicode decimal digit)
- One or more
- End of group
- Character '-'
- Word character (any Unicode letter, digit or _)
- Zero or more (lazy)
- End of line/string
- The first line is the regex text. A blank line and
Explanation:follow. - Each construct gets its own line. A quantifier gets its own line right after the thing it repeats.
- The contents of a group are indented two more spaces.
End of groupis printed at the indentation of the group's contents, and the lines after it move back out. - Every line ends with
Environment.NewLine, including the last one. - An empty pattern returns only the header:
Pattern:, a blank line andExplanation:. Explain()builds a new string on every call. It parses the pattern once withRegexto learn its groups, and still works when the pattern is invalid.
Groups nest as deeply as the pattern does:
using System;
using Masterly.FluentRegex;
Pattern quoted = Pattern.With
.NamedGroup("quote", Pattern.With.AnyOf("'\""))
.Anything.Repeat.ZeroOrMore.Lazy
.Backreference("quote")
.NegativeLookahead(Pattern.With.Word);
Console.WriteLine(quoted.Explain());
Output:
Pattern: (?<quote>['"]).*?\k<quote>(?!\w)
Explanation:
- Named capturing group 'quote'
- Any character in: ['"]
- End of group
- Any character (except newline)
- Zero or more (lazy)
- Backreference to named group 'quote'
- Negative lookahead (must NOT follow)
- Word character (any Unicode letter, digit or _)
- End of group
What each construct is called
| Regex | Explanation line |
|---|---|
^, $ | Start of line/string, End of line/string |
. | Any character (except newline) |
*, *? | Zero or more, Zero or more (lazy) |
+, +? | One or more, One or more (lazy) |
?, ?? | Optional (zero or one), Optional (lazy) |
{3} | Exactly 3 times |
{2,} | At least 2 times |
{2,5} | Between 2 and 5 times |
{...}? | The same text followed by (lazy), for example Between 2 and 5 times (lazy) |
Any other {, such as in {,3} | Character '{'. .NET reads it as a literal brace, not a quantifier. |
\d, \D | Digit (any Unicode decimal digit), Non-digit. With RegexOptions.ECMAScript: Digit [0-9], Non-digit [^0-9]. |
\w, \W | Word character (any Unicode letter, digit or _), Non-word character. With RegexOptions.ECMAScript: Word character [a-zA-Z0-9_]. |
\s, \S | Whitespace, Non-whitespace |
\b, \B | Word boundary, Non-word boundary |
\A, \z, \Z, \G | Start of string, End of string, End of string (or before a final newline), Where the previous match ended |
\t, \n, \r, \f, \v, \a, \e | Tab, Newline, Carriage return, Form feed, Vertical tab, Bell character, Escape character (U+001B) |
\x41, é, \101, \cM | Character 'A' (U+0041), Character 'é' (U+00E9), Character 'A' (U+0041), Control character Ctrl+M. Invisible characters show only the code point, such as Character U+000A. |
\p{Lu}, \P{Lu} | Any character in Unicode category or block 'Lu', Any character NOT in Unicode category or block 'Lu' |
\ followed by digits | Backreference to group 12 when that group exists. A single digit is always a backreference. Otherwise the digits are an octal escape, as in .NET: \12 with fewer than 12 groups is Character U+000A. \0 is always octal. |
\k<name>, \k'name' | Backreference to named group 'name' |
Any other escape, such as \. | Escaped literal '.' |
[a-z], [^a-z] | Any character in: [a-z], Any character NOT in: [a-z]. Escaped ], a ] right after [, and subtractions such as [a-z-[aeiou]] stay inside the set. |
( | Capturing group |
(?<name>, (?'name' | Named capturing group 'name' |
(?<close-open> | Balancing group 'close' (captures the text since the last capture of 'open' and removes it) |
(?: | Non-capturing group |
(?=, (?! | Positive lookahead (must follow), Negative lookahead (must NOT follow) |
(?<=, (?<! | Positive lookbehind (must precede), Negative lookbehind (must NOT precede) |
(?> | Atomic group (no backtracking) |
(?i:, (?i) | Group with inline options (on: ignore case), Inline options for the rest of the group (on: ignore case). Turned-off options are listed after off:. |
(?#note) | Comment: note |
(?(1), (?(name) | Conditional: if group 1 has matched, use the first branch. The | that starts the other branch is shown as Otherwise. |
(?(?=...) | Conditional: if the lookaround below matches, use the first branch, followed by the lookaround itself |
) | End of group |
| | OR (alternation) |
A letter or digit (any script, such as é) | Literal 'a' |
Any other character, including _ and space | Character '-' |
When a construct is cut off, Explain() falls back to a generic line: a \ at the end gives Escape character, \k without a name gives Backreference, a [ without a closing ] gives Character class start, (? at the end gives Group start, and an unfinished (?< gives Lookbehind or named group. The scan then continues with the next character.
Group-aware explanations
Explain() knows which groups the pattern has, so it can tell a backreference from an octal escape and a group condition from an expression condition. Here is a conditional that requires a closing parenthesis only when an opening one matched:
using System;
using Masterly.FluentRegex;
Pattern areaCode = Pattern.FromRegex(@"(\()?\d{3}(?(1)\))");
Console.WriteLine(areaCode.Explain());
Output:
Pattern: (\()?\d{3}(?(1)\))
Explanation:
- Capturing group
- Escaped literal '('
- End of group
- Optional (zero or one)
- Digit (any Unicode decimal digit)
- Exactly 3 times
- Conditional: if group 1 has matched, use the first branch
- Escaped literal ')'
- End of group
Anchors, Unicode categories and inline options
using System;
using System.Globalization;
using Masterly.FluentRegex;
Pattern pattern = Pattern.With
.StartOfInput
.Unicode(UnicodeCategory.UppercaseLetter)
.IgnoreCase(Pattern.With.Literal("ab"))
.Digit.Repeat.Times(2);
Console.WriteLine(pattern.Explain());
Output:
Pattern: \A\p{Lu}(?i:ab)\d{2}
Explanation:
- Start of string
- Any character in Unicode category or block 'Lu'
- Group with inline options (on: ignore case)
- Literal 'a'
- Literal 'b'
- End of group
- Digit (any Unicode decimal digit)
- Exactly 2 times
Limits of Explain
Explain() reads the regex text from left to right. Keep these limits in mind:
\dmatches more than 0-9. .NET's\dmatches every Unicode decimal digit, such as the Arabic-Indic٣, and the line says so. UseWithAsciiDigits()orRegexOptions.ECMAScriptwhen you mean 0-9 only. See Character classes.^and$do not show the mode. They are alwaysStart of line/stringandEnd of line/string. WithRegexOptions.Multilinethey match at every line; without it, only at the start and end of the input.- Whitespace mode is not applied. With
RegexOptions.IgnorePatternWhitespace, spaces and#comments in the pattern are still listed as characters. - It never validates.
Explain()never throws, even for an invalid pattern.(unclosedis explained asCapturing groupwith eightLiterallines and noEnd of group. Do not use it to check a pattern. UseTryValidate.
For a description built from a real parse, use Trace(input). It runs the pattern against an input and describes each construct with the library's parser, for example a digit, exactly 3 times, and shows which part of the input each construct matched or where the match failed. See Match tracing.
Explaining from the command line
The command-line tool has the same feature. It wraps the regex with Pattern.FromRegex, so it validates the regex first.
fluentregex explain '^(?<year>\d{4})-(?<month>\d{2})$'
Pattern: ^(?<year>\d{4})-(?<month>\d{2})$
Explanation:
- Start of line/string
- Named capturing group 'year'
- Digit (any Unicode decimal digit)
- Exactly 4 times
- End of group
- Character '-'
- Named capturing group 'month'
- Digit (any Unicode decimal digit)
- Exactly 2 times
- End of group
- End of line/string
--options IgnoreCase,MultilinepassesRegexOptions. Names are not case-sensitive. An unknown name printserror: Unknown regex option 'Bogus'.and exits with code 2.- An invalid regex prints the
FromRegexmessage and exits with code 2, for exampleerror: The regular expression is invalid: Invalid pattern '[unclosed' at offset 9. Unterminated [] set. (Parameter 'regex'). --ascii-digitsappliesWithAsciiDigits()before explaining, so\dis shown asAny character in: [0-9].
Seeing a pattern in the debugger
Pattern is marked with [DebuggerDisplay] and [DebuggerTypeProxy]. When you hover over a Pattern variable or add it to a watch window, the debugger shows the regex text instead of the type name. When you expand it, you see a summary instead of the private fields.
Take this pattern:
using System;
using System.Text.RegularExpressions;
using Masterly.FluentRegex;
Pattern phone = Pattern.With
.NamedGroup("area", Pattern.With.Digit.Repeat.Times(3))
.Literal("-")
.NamedGroup("number", Pattern.With.Digit.Repeat.Times(4))
.WithOptions(RegexOptions.CultureInvariant)
.WithTimeout(TimeSpan.FromSeconds(2));
The collapsed value is (?<area>\d{3})-(?<number>\d{4}), without quotes. Expanded, it shows:
| Row | Shows | For phone |
|---|---|---|
Regex | The regex text, the same as ToString(). | (?<area>\d{3})-(?<number>\d{4}) |
Options | The options set with WithOptions(...). | CultureInvariant |
Timeout | The match timeout. No timeout shows as -00:00:00.0010000 (Regex.InfiniteMatchTimeout). | 00:00:02 |
GroupNames | The result of GetGroupNames(). | area, number |
Explanation | The non-empty lines of Explain(), as an array. | Pattern: (?<area>\d{3})-(?<number>\d{4}), Explanation:, - Named capturing group 'area', ... |
Safety | The ReDoS verdict of AnalyzeSafety(). | None: no catastrophic backtracking risks found. |
- Each row is computed the first time you expand it, then cached. Expanding a pattern never changes it.
- No row ever throws. A row that fails shows
Unavailable:and the error. For(unclosed,GroupNamesshowsUnavailable: Invalid pattern '(unclosed' at offset 9. Not enough )'s.andSafetyshowsUnavailable: The pattern is not valid regex syntax: Not enough )'s (at position 0). Safetyshows the risk level and the most severe issue, with(+N more)when there are others. For(a+)+$it starts withHigh: High NestedQuantifier at 0 '(a+)+': The repetition 'a+' is nested inside the repetition '+'.It runs the static analysis only, without the timed confirmation. See ReDoS safety.- The view works in any debugger that honors these attributes, such as Visual Studio, JetBrains Rider and VS Code with the C# extension. You cannot create the view in code. Call the members listed above instead.
Expanding Safety runs the ReDoS analysis inside the debugger. For a very large pattern it can take a moment. The collapsed value only calls ToString(), which is cached.
Choosing the right tool
Explain() describes the pattern alone. Other members answer different questions:
| Question | Use | Page |
|---|---|---|
| Does this pattern compile? | IsValid(), TryValidate(...), ToRegex() | This page |
| What does each part of the pattern mean? | Explain() | This page |
| Why does this input match or not match? | Trace(input) | Match tracing |
| Where are the matches and groups in a text? | Highlight(...) | Highlighting |
| What does the pattern look like as a diagram? | ToRailroadSvg(), ToMermaid() | Diagrams |
| Can a hostile input make it hang? | AnalyzeSafety() | ReDoS safety |
| Where does matching spend its time? | Profile(input) | Backtracking profiler |
| How do I write this regex with the fluent API? | ToFluentCode() | Regex to fluent code |
| Which branches do my tests exercise? | Regex coverage | Regex coverage |
Common pitfalls
- Validating a constant. A sample like
Pattern.With.RegEx("[unclosed").IsValid()does not build: the analyzer reportsFRX003first. Runtime validation is for text that is not known at compile time. - Validating on every call.
IsValid(),TryValidate(),GetGroupNames()andGetGroupCount()each compile a newRegex. Validate once and keep the result. - Using
Explain()as a validator. It never throws and happily explains broken text. - Expecting
Choiceto be non-capturing. It adds a numbered group, which changesGetGroupCount()and the numbers of the groups after it. Use the|operator when you do not need the capture. - Parsing the error message. Its wording belongs to the .NET runtime and can change between versions. Show it to people, but branch on
RegexParseException.Errorin code.