Skip to main content

API Reference

This page lists the public API of every Masterly.FluentRegex package: each type, each member, its signature, what it returns and when it throws. Use it to look up a member you already know about. To learn how a feature works, follow the links to the guide pages.

Each package uses one namespace that matches its name. The only exception is the AddFluentPatterns registration method of the ASP.NET Core package, which lives in Microsoft.Extensions.DependencyInjection, as ASP.NET Core extensions usually do.

PackageNamespaceTarget frameworksContents
Masterly.FluentRegexMasterly.FluentRegexnetstandard2.1, net8.0, net10.0The fluent builder, matching, extraction, templates, streaming, redaction, common patterns, algebra, optimizer, safety analysis, debugging tools, test-data generation, coverage, property testing and export. Also contains the analyzers and the [GeneratePattern] source generator.
Masterly.FluentRegex.EntityFrameworkCoreMasterly.FluentRegex.EntityFrameworkCorenet8.0 (EF Core 8), net10.0 (EF Core 10)Translate patterns to SQL in LINQ queries, CHECK constraints and raw SQL conditions.
Masterly.FluentRegex.AspNetCoreMasterly.FluentRegex.AspNetCorenet8.0, net10.0A pattern registry and the pattern route constraint.
Masterly.FluentRegex.XunitMasterly.FluentRegex.Xunitnet8.0, net10.0Theory data attributes for xUnit v3.
Masterly.FluentRegex.AIMasterly.FluentRegex.AInet8.0, net10.0Draft patterns from plain English with Claude, verified locally.
Masterly.FluentRegex.Tool(command-line tool)net8.0The fluentregex command.
note

Signatures on this page are copied from the 3.1.0 assemblies. Parameter names are the real ones, so you can use them as named arguments.

Masterly.FluentRegex​

The core package. Everything in it lives in the Masterly.FluentRegex namespace.

Pattern​

Pattern is the fluent builder and the object you match with. Start a chain with Pattern.With and add elements one property or method at a time.

using System;
using Masterly.FluentRegex;

Pattern digits = Pattern.With.Digit.Repeat.OneOrMore;
Pattern code = Pattern.With.StartOfLine.Then(digits).EndOfLine;

Console.WriteLine(digits); // \d+
Console.WriteLine(code); // ^\d+$
Console.WriteLine(code.IsMatch("2024")); // True
Console.WriteLine(digits); // \d+ (digits is unchanged)
  • Pattern is immutable. Every member that adds an element returns a new Pattern and leaves the original unchanged, so you can share patterns between threads and reuse one pattern as a part of many others.
  • A pattern compiles its Regex once, on first use, and caches it on that instance. Keep patterns you match often in static readonly fields so the cached regex is reused.
  • Pattern is a class that implements IEquatable<Pattern>. It does not overload ==. See Pattern: configuration and conversion.

The members of Pattern are grouped by task in the sections below.

Pattern: creating patterns​

MemberReturnsDescription
static Pattern With { get; }PatternA new, empty pattern to start a chain.
Pattern()Creates an empty pattern. Pattern.With reads better.
static Pattern FromRegex(string regex, RegexOptions options = RegexOptions.None)PatternWraps an existing regex after validating it. Throws ArgumentNullException for null and ArgumentException for invalid syntax. The result can be chained, analyzed, traced and converted like any other pattern.
static Pattern Learn(params string[] match)PatternInfers an anchored ^...$ pattern that fully matches every example. See Learning from examples.
static Pattern Learn(IEnumerable<string> match, IEnumerable<string>? reject = null)PatternThe same, and the result must also reject every example in reject. Throws ArgumentNullException when match or an example is null, and ArgumentException when match is empty or a reject example cannot be separated (it is also a match example).
bool IsEmpty { get; }booltrue when the pattern has no elements.
using System;
using System.Text.RegularExpressions;
using Masterly.FluentRegex;

Pattern empty = Pattern.With;
Pattern wrapped = Pattern.FromRegex(@"^[A-Z]{3}-\d{4}$", RegexOptions.IgnoreCase);
Pattern learned = Pattern.Learn("INV-0001", "INV-0042", "INV-1234");

Console.WriteLine(empty.IsEmpty); // True
Console.WriteLine(wrapped.Options); // IgnoreCase
Console.WriteLine(wrapped.IsMatch("abc-1234")); // True
Console.WriteLine(learned); // ^INV-\d{4}$

Pattern: anchors​

Anchors match a position, not a character. All of them are properties that return Pattern.

MemberRegexDescription
StartOfLine^The start of the string, or of any line under RegexOptions.Multiline.
EndOfLine$The end of the string or the position before a final \n, or the end of any line under Multiline.
StartOfInput\AThe start of the input, whatever the options.
EndOfInput\zThe very end of the input, whatever the options. Does not accept a trailing \n.
WordBoundary\bA position between a word character and a non-word character.
NonWordBoundary\BA position that is not a word boundary.
Pattern line = Pattern.With.StartOfLine.Word.Repeat.OneOrMore.EndOfLine;
Pattern input = Pattern.With.StartOfInput.Word.Repeat.OneOrMore.EndOfInput;

Console.WriteLine(line); // ^\w+$
Console.WriteLine(input); // \A\w+\z
Console.WriteLine(line.IsMatch("hello\n")); // True
Console.WriteLine(input.IsMatch("hello\n")); // False
tip

Use StartOfInput and EndOfInput to validate a whole value. EndOfLine also accepts a value that ends with a newline, which is rarely what a validator wants.

Pattern: character matchers​

Each matcher is a property that returns Pattern and matches one character. Several of them render differently inside a Set(...), so they combine into one character class.

MemberRegex on its ownInside Set(...)Matches
Digit\d\dAny Unicode decimal digit, not only 0-9. See Unicode.
NonDigit\D\DAny character that is not a digit.
AsciiDigit[0-9]0-9Only the ASCII digits.
HexDigit[0-9a-fA-F]0-9a-fA-FA hexadecimal digit.
Word\w\wA Unicode letter, digit, connector punctuation or combining mark.
NonWord\W\WAny character that is not a word character.
Whitespace\s\sAny whitespace character.
NonWhitespace\S\SAny non-whitespace character.
Letter[a-zA-Z]a-zA-ZAn ASCII letter. Use Unicode(UnicodeGroup.Letter) for letters of every script.
LowercaseLetter[a-z]a-zA lowercase ASCII letter.
UppercaseLetter[A-Z]A-ZAn uppercase ASCII letter.
Anything.. (a literal dot)Any character except \n (any character under RegexOptions.Singleline). Inside a set the dot loses its meaning and matches only ..
Tab\t\tA tab.
Newline\n\nA line feed.
CarriageReturn\r\rA carriage return.
CodePoint(?:...)Not allowedOne Unicode scalar value: a surrogate pair, such as most emoji, or one non-surrogate character. Lone surrogates do not match.
Grapheme(?:...)Not allowedOne user-perceived character, an approximation of an extended grapheme cluster.

CodePoint and Grapheme can match more than one UTF-16 code unit, so they render as a non-capturing group and a quantifier applies to the whole sequence. Inside Set(...) or NegatedSet(...) they throw InvalidOperationException when the pattern is rendered; combine them with | or OneOf(...) instead.

Pattern alone = Pattern.With.Letter;
Pattern inSet = Pattern.With.Set(Pattern.With.Letter.AsciiDigit.Literal("_"));
Pattern hex = Pattern.With.Literal("0x").HexDigit.Repeat.OneOrMore;

Console.WriteLine(alone); // [a-zA-Z]
Console.WriteLine(inSet); // [a-zA-Z0-9_]
Console.WriteLine(hex); // 0x[0-9a-fA-F]+

Pattern: literal and raw text​

MemberReturnsDescription
Literal(string literalChars)PatternMatches the text exactly. Regex special characters are escaped, inside and outside a set. Throws ArgumentNullException for null.
Literal(params char[] characters)PatternMatches the characters exactly, escaped the same way.
RegEx(string regex)PatternAdds raw regex text without escaping. Use it for constructs the builder does not cover.
Pattern price = Pattern.With.Literal("$").Digit.Repeat.OneOrMore.Literal(".").Digit.Repeat.Times(2);

Console.WriteLine(price); // \$\d+\.\d{2}
  • RegEx does not validate its text when you call it. An invalid fragment fails when the pattern is first used, or at build time if the analyzer can see it (diagnostic FRX003). Check it yourself with IsValid() or TryValidate.

Pattern: character classes and Unicode​

MemberReturnsDescription
Set(Pattern pattern)PatternOne character from the set the inner pattern describes: [...]. Literal characters are escaped, so Literal("a-z") inside a set matches a, - and z. Use Range for ranges.
NegatedSet(Pattern pattern)PatternOne character not in the set: [^...].
Range(char from, char to)PatternAn inclusive range. Renders [a-f] on its own and a-f inside a set. Throws ArgumentException when from is greater than to.
AnyOf(string characters)PatternOne of the listed characters, all taken literally, including -, ] and ^. Throws ArgumentException for an empty string.
NoneOf(string characters)PatternAny character except the listed ones: [^...]. Throws ArgumentException for an empty string.
Unicode(UnicodeCategory category)PatternA character in a System.Globalization.UnicodeCategory, such as \p{Lu}. Works inside and outside a set.
NotUnicode(UnicodeCategory category)PatternA character not in the category: \P{Lu}.
Unicode(UnicodeGroup group)PatternA character in a general group, such as \p{L} for any letter in any script.
NotUnicode(UnicodeGroup group)PatternA character not in the group: \P{L}.
UnicodeBlock(string blockName)PatternA character in a named .NET block, such as \p{IsGreek}. The Is prefix is optional: UnicodeBlock("Greek") also renders \p{IsGreek}. The prefix may be written in any case. Throws ArgumentException for an empty name, one with characters other than letters, digits, - and _, or a block .NET does not know.
Script(UnicodeScript script)PatternAny character of a writing system, rendered as a class of code-point ranges (for Greek, [\u0300-\u036F\u0370-\u03E1\u03F0-\u03FF\u1F00-\u1FFF]). Works inside and outside a set.
LetterIn(UnicodeScript script)PatternA letter (category L) of the script. Marks, digits and punctuation are excluded.
DigitIn(UnicodeScript script)PatternA decimal digit of the script. Latin gives 0-9.
using System;
using System.Globalization;
using Masterly.FluentRegex;

Console.WriteLine(Pattern.With.Range('a', 'f')); // [a-f]
Console.WriteLine(Pattern.With.Set(Pattern.With.Range('a', 'f').Range('0', '9'))); // [a-f0-9]
Console.WriteLine(Pattern.With.AnyOf("-+]")); // [\-\+\]]
Console.WriteLine(Pattern.With.NoneOf("aeiou")); // [^aeiou]
Console.WriteLine(Pattern.With.Unicode(UnicodeCategory.UppercaseLetter)); // \p{Lu}
Console.WriteLine(Pattern.With.NotUnicode(UnicodeGroup.Letter)); // \P{L}
Console.WriteLine(Pattern.With.UnicodeBlock("IsGreek")); // \p{IsGreek}
Console.WriteLine(Pattern.With.DigitIn(UnicodeScript.Arabic)); // [\u0660-\u0669\u06F0-\u06F9]

See Character classes and Unicode for details.

Pattern: quantifiers​

A quantifier applies to the element just before it.

MemberReturnsDescription
Repeat { get; }RepeatStarts a quantifier for the last element. See Repeat.
Lazy { get; }PatternMakes the preceding quantifier lazy: +?, *?, ??, {n,m}?.
Possessive { get; }PatternMakes the preceding quantifier possessive by wrapping it in an atomic group: (?>\d+). The engine never gives characters back, which removes catastrophic backtracking for that element.
Console.WriteLine(Pattern.With.Anything.Repeat.OneOrMore.Lazy); // .+?
Console.WriteLine(Pattern.With.Digit.Repeat.OneOrMore.Possessive); // (?>\d+)
  • Lazy and Possessive must directly follow a quantifier. Otherwise they throw InvalidOperationException with a message such as Lazy must directly follow a quantifier, for example Pattern.With.Digit.Repeat.OneOrMore.Lazy.. The analyzer reports this mistake at build time as FRX006.
  • See Quantifiers.

Pattern: groups, backreferences and conditionals​

MemberRegexDescription
Group(Pattern pattern)(...)A numbered capturing group.
NamedGroup(string name, Pattern pattern)(?<name>...)A named capturing group. The name may contain only letters, digits and underscores; otherwise ArgumentException.
NonCapturingGroup(Pattern pattern)(?:...)Groups without capturing.
AtomicGroup(Pattern pattern)(?>...)Once matched, the engine never backtracks into it.
Backreference(string name)\k<name>Matches the text a named group captured.
Backreference(int groupNumber)\1Matches the text a numbered group captured. Throws ArgumentOutOfRangeException when groupNumber is less than 1.
Conditional(string groupName, Pattern truePattern, Pattern falsePattern)(?(name)yes|no)Uses truePattern when the named group took part in the match, otherwise falsePattern.
Conditional(int groupNumber, Pattern truePattern, Pattern falsePattern)(?(1)yes|no)The same for a numbered group. Group numbers start at 1; a smaller number throws ArgumentOutOfRangeException.
Pattern word = Pattern.With.Word.Repeat.OneOrMore;

Pattern doubled = Pattern.With.NamedGroup("word", word).Whitespace.Backreference("word");

Pattern bracketed = Pattern.With
.NamedGroup("open", Pattern.With.Literal("<")).Repeat.Optional
.Word.Repeat.OneOrMore
.Conditional("open", Pattern.With.Literal(">"), Pattern.With.EndOfLine);

Console.WriteLine(doubled); // (?<word>\w+)\s\k<word>
Console.WriteLine(doubled.Match("the the end").Value); // the the
Console.WriteLine(bracketed); // (?<open><)?\w+(?(open)>|$)

See Groups and backreferences.

Pattern: assertions​

Lookarounds check what comes before or after the current position without consuming it.

MemberRegexDescription
PositiveLookahead(Pattern pattern)(?=...)The pattern must follow.
NegativeLookahead(Pattern pattern)(?!...)The pattern must not follow.
PositiveLookbehind(Pattern pattern)(?<=...)The pattern must precede.
NegativeLookbehind(Pattern pattern)(?<!...)The pattern must not precede.
Pattern price = Pattern.With
.PositiveLookbehind(Pattern.With.Literal("$"))
.Digit.Repeat.OneOrMore
.NegativeLookahead(Pattern.With.Literal("%"));

Console.WriteLine(price); // (?<=\$)\d+(?!%)
Console.WriteLine(price.Match("Cost: $120").Value); // 120
Console.WriteLine(price.IsMatch("Rate: 15%")); // False

See Assertions.

Pattern: composition​

MemberReturnsDescription
Then(Pattern pattern)PatternAppends every element of another pattern. A quantifier that follows applies to its last element, as if it were written inline.
operator +(Pattern left, Pattern right)PatternConcatenation. The same as left.Then(right).
operator |(Pattern left, Pattern right)PatternNon-capturing alternation. Chains flatten: a | b | c renders (?:a|b|c).
OneOf(params Pattern[] alternatives)Pattern(?:a|b|c). Throws ArgumentException when no alternative is given.
OneOf(params string[] words)PatternLiteral alternatives, escaped: (?:cat|dog).
Choice(Pattern firstChoice, Pattern secondChoice, params Pattern[] additionalChoices)PatternAlternation inside a numbered capturing group: (a|b|c).
IgnoreCase(Pattern pattern)PatternMatches the inner pattern case-insensitively: (?i:...).
CaseSensitive(Pattern pattern)PatternMatches the inner pattern case-sensitively: (?-i:...).
InlineOptions(RegexOptions enable, Pattern pattern, RegexOptions disable = RegexOptions.None)Pattern(?imnsx-imnsx:...). Only IgnoreCase, Multiline, ExplicitCapture, Singleline and IgnorePatternWhitespace can be applied inline; others throw ArgumentException.
using System;
using System.Text.RegularExpressions;
using Masterly.FluentRegex;

Pattern year = Pattern.With.Digit.Repeat.Times(4);
Pattern month = Pattern.With.Digit.Repeat.Times(2);

Pattern date = year + Pattern.With.Literal("-") + month;
Pattern unit = Pattern.With.Literal("kg") | Pattern.With.Literal("lb") | Pattern.With.Literal("g");

Console.WriteLine(date); // \d{4}-\d{2}
Console.WriteLine(unit); // (?:kg|lb|g)
Console.WriteLine(Pattern.With.OneOf("c++", "c#")); // (?:c\+\+|c#)
Console.WriteLine(Pattern.With.Choice(year, month)); // (\d{4}|\d{2})
Console.WriteLine(Pattern.With.IgnoreCase(Pattern.With.Literal("id")).Digit); // (?i:id)\d
Console.WriteLine(Pattern.With.InlineOptions(RegexOptions.Multiline, Pattern.With.StartOfLine, RegexOptions.IgnoreCase)); // (?m-i:^)

See Composing patterns.

Pattern: configuration and conversion​

Settings travel with the pattern. Each With... member returns a copy.

MemberReturnsDescription
Options { get; }RegexOptionsThe options used when the pattern runs. Default RegexOptions.None.
Timeout { get; }TimeSpanThe match timeout. Default Regex.InfiniteMatchTimeout.
WithOptions(RegexOptions options)PatternA copy that uses these options. Replaces the previous options; it does not add to them.
WithTimeout(TimeSpan timeout)PatternA copy that throws RegexMatchTimeoutException when a match runs longer. The runtime defense against ReDoS. Throws ArgumentOutOfRangeException unless the value is positive and below about 24 days, or Regex.InfiniteMatchTimeout.
UseNonBacktracking()PatternA copy that runs on the linear-time RegexOptions.NonBacktracking engine. Patterns with backreferences, lookarounds, atomic groups or conditionals then throw NotSupportedException when they run. net8.0 and net10.0 only.
WithAsciiDigits()PatternA copy in which \d matches only 0-9 and \D everything else, including inside sets and lookarounds. \w and explicit categories such as \p{Nd} are left as written. Returns the same instance when there is nothing to rewrite.
WithDigits(DigitMode digits)PatternUnchanged for DigitMode.Unicode; WithAsciiDigits() for DigitMode.Ascii. Throws ArgumentOutOfRangeException for an undefined value.
IgnoreDiacritics()PatternA copy that matches text with or without diacritics, in precomposed (NFC) and decomposed (NFD) form: each literal letter becomes a class of its accented variants followed by a run of combining marks. Options and timeout are kept. Throws InvalidOperationException for a pattern that uses RegexOptions.ECMAScript.
ToString()stringThe regex text. Computed once and cached.
ToRegex()RegexA new Regex with the pattern's options and timeout.
ToRegex(RegexOptions options)RegexA new Regex with explicit options. The pattern's timeout still applies.
Equals(Pattern? other)booltrue when both render the same regex with the same options and timeout. GetHashCode() agrees with it.
using System;
using System.Text.RegularExpressions;
using Masterly.FluentRegex;

Pattern sku = Pattern.With.StartOfLine.Letter.Repeat.Times(3).Literal("-").Digit.Repeat.Times(4).EndOfLine;

Pattern configured = sku
.WithOptions(RegexOptions.IgnoreCase)
.WithTimeout(TimeSpan.FromMilliseconds(250));

Console.WriteLine(configured.Options); // IgnoreCase
Console.WriteLine(configured.Timeout); // 00:00:00.2500000
Console.WriteLine(sku.Timeout == Regex.InfiniteMatchTimeout); // True
Console.WriteLine(sku.IsMatch("ABC-١٢٣٤")); // True: \d accepts Arabic-Indic digits
Console.WriteLine(sku.WithAsciiDigits().IsMatch("ABC-١٢٣٤")); // False
Console.WriteLine(sku.WithAsciiDigits()); // ^[a-zA-Z]{3}-[0-9]{4}$
Console.WriteLine(sku.Equals(Pattern.FromRegex(sku.ToString()))); // True
Console.WriteLine(sku == Pattern.FromRegex(sku.ToString())); // False: == compares references
Console.WriteLine(Pattern.With.Literal("café").IgnoreDiacritics().IsMatch("cafe")); // True
  • WithAsciiDigits() and IgnoreDiacritics() return a pattern built from the rewritten regex text, as FromRegex would. Call them at the end of a chain.
  • sku.Equals(...) is true because both render ^[a-zA-Z]{3}-\d{4}$ with the same options and timeout. == is not overloaded, so it compares references.
warning

ToRegex() and IsMatch(input, options) build a new Regex on every call. The other matching members reuse one cached Regex per pattern instance. Call ToRegex() once and keep the result if you need a Regex object.

Pattern: matching​

These members match like the Regex members of the same name, using the pattern's options and timeout.

MemberReturnsDescription
IsMatch(string input)booltrue when the pattern matches anywhere in the input.
IsMatch(string input, RegexOptions options)boolThe same with explicit options. Builds a new Regex each call.
Match(string input)MatchThe first match. Check Match.Success.
Match(string input, int startAt)MatchThe first match at or after startAt.
Matches(string input)MatchCollectionAll non-overlapping matches.
Count(string input)intThe number of matches.
Replace(string input, string replacement)stringReplaces every match. The replacement supports $1, ${name} and $$.
Replace(string input, string replacement, int count)stringReplaces at most count matches.
Replace(string input, MatchEvaluator evaluator)stringReplaces every match with the evaluator's result.
Replace(string input, MatchEvaluator evaluator, int count)stringReplaces at most count matches.
Replace(string input, Func<Replacement, Replacement> build)stringReplaces every match with a fluent Replacement. Group names and numbers are checked before anything is replaced.
Replace(string input, Func<Replacement, Replacement> build, int count)stringThe same for at most count matches.
Split(string input)string[]Splits the input at every match.
Split(string input, int count)string[]Splits into at most count parts.
IsMatch(ReadOnlySpan<char> input)boolAllocation-free test on a span. net8.0 and net10.0 only.
Count(ReadOnlySpan<char> input)intAllocation-free count on a span. net8.0 and net10.0 only.
EnumerateMatches(ReadOnlySpan<char> input)Regex.ValueMatchEnumeratorMatch positions without allocating Match objects. net8.0 and net10.0 only.
EnumerateSplits(ReadOnlySpan<char> input)Regex.ValueSplitEnumeratorThe ranges between matches, without allocating strings. net10.0 only.
Pattern number = Pattern.With.Digit.Repeat.OneOrMore;
string text = "3 apples, 12 pears, 7 plums";

Console.WriteLine(number.Match(text).Value); // 3
Console.WriteLine(number.Match(text, 2).Value); // 12
Console.WriteLine(number.Count(text)); // 3
Console.WriteLine(number.Replace(text, "#", 1)); // # apples, 12 pears, 7 plums
Console.WriteLine(number.Replace(text, m => (int.Parse(m.Value) * 2).ToString())); // 6 apples, 24 pears, 14 plums
Console.WriteLine(string.Join("|", Pattern.With.Literal(", ").Split(text))); // 3 apples|12 pears|7 plums

See Matching and Fluent replacements.

Pattern: typed extraction and streaming​

MemberReturnsDescription
Extract<T>(string input, IFormatProvider provider = null)TMatches the input and binds the named groups to a new T. Throws FormatException when the input does not match.
TryExtract<T>(string input, out T result, IFormatProvider provider = null)boolThe same, returning false instead of throwing.
ExtractAll<T>(string input, IFormatProvider provider = null)IEnumerable<T>Binds every match. Evaluated lazily.
Matches(TextReader reader, StreamMatchOptions? options = null)IEnumerable<StreamMatch>Finds every match in text read from a reader, keeping only a bounded window in memory.
MatchesAsync(TextReader reader, StreamMatchOptions? options = null, CancellationToken cancellationToken = default)IAsyncEnumerable<StreamMatch>The async version.
MatchesAsync(Stream stream, Encoding? encoding = null, StreamMatchOptions? options = null, CancellationToken cancellationToken = default)IAsyncEnumerable<StreamMatch>Decodes a stream (UTF-8 by default) and finds every match. The stream is left open.
using System;
using System.IO;
using Masterly.FluentRegex;

Pattern version = Pattern.With
.NamedGroup("major", Pattern.With.Digit.Repeat.OneOrMore).Literal(".")
.NamedGroup("minor", Pattern.With.Digit.Repeat.OneOrMore).Literal(".")
.NamedGroup("patch", Pattern.With.Digit.Repeat.OneOrMore);

Console.WriteLine(version.Extract<Version3>("v3.1.0")); // Version3 { Major = 3, Minor = 1, Build = 0 }
Console.WriteLine(version.TryExtract("no version", out Version3 _)); // False

Pattern errorCode = Pattern.With.Literal("ERR-").NamedGroup("code", Pattern.With.Digit.Repeat.Times(3));

using StringReader reader = new StringReader("ok\nERR-404 missing\nok\nERR-500 crash\n");
foreach (StreamMatch match in errorCode.Matches(reader))
{
Console.WriteLine($"{match.Index} {match.Value} {match.Groups["code"]}"); // 3 ERR-404 404, then 22 ERR-500 500
}

public sealed record Version3(int Major, int Minor, [RegexGroup("patch")] int Build);
  • Groups bind to constructor parameters and settable properties by name, ignoring case. [RegexGroup("name")] binds a member to a group with another name. Types beyond the built-in ones need a converter from ExtractionConverters.
  • Streaming throws NotSupportedException for RegexOptions.RightToLeft and for the \G anchor, and InvalidOperationException for a match longer than StreamMatchOptions.MaxMatchLength that is not at the end of the input.
  • See Typed extraction and Streaming.

Pattern: validation and semantic rules​

MemberReturnsDescription
IsValid()booltrue when the pattern compiles to a valid regex.
TryValidate(out string errorMessage)boolThe same, with the parser's error message when it is invalid.
GetGroupNames()string[]The names of the named capturing groups.
GetGroupCount()intThe number of capturing groups, not counting group 0 (the whole match).
Explain()stringA plain-English, line-by-line description of the pattern. Conditionals, inline options, Unicode categories and escapes such as \x41 are described, and \d/\w are described as Unicode-aware unless the pattern uses RegexOptions.ECMAScript.
Must(Func<string, bool> rule, string? description = null)ConstrainedPatternAdds a rule the whole match must also pass, such as a checksum. See ConstrainedPattern.
Must(string groupName, Func<string, bool> rule, string? description = null)ConstrainedPatternAdds a rule for the value of a named group. Skipped when the group did not take part in the match. Throws ArgumentException when the pattern has no such group.
// regexText comes from your application, for example "(unclosed"
Pattern userPattern = Pattern.With.RegEx(regexText);

Console.WriteLine(userPattern.IsValid()); // False
Console.WriteLine(userPattern.TryValidate(out string error)); // False
Console.WriteLine(error); // Invalid pattern '(unclosed' at offset 9. Not enough )'s.

Pattern card = Pattern.With.StartOfLine.NamedGroup("number", Pattern.With.Digit.Repeat.Times(16)).EndOfLine;
Console.WriteLine(card.GetGroupCount()); // 1

Console.WriteLine(Pattern.With.Digit.Repeat.Times(3).Explain());
// prints:
// Pattern: \d{3}
//
// Explanation:
// - Digit (any Unicode decimal digit)
// - Exactly 3 times

See Validation and debugging and Semantic rules.

Pattern: language algebra and facts​

These members treat a pattern as the set of strings it matches in full, and prove facts about it with automata. They work for the regular subset of .NET regex. A pattern with backreferences, lookarounds or conditionals throws NotSupportedException, and a pattern too large to analyze throws InvalidOperationException. GetLanguageFacts is the exception: for non-regular constructs it falls back to sound approximations (IsExact is false) instead of throwing.

MemberReturnsDescription
static Compare(Pattern left, Pattern right)PatternComparisonHow the two languages relate, with the shortest strings that show it.
IsEquivalentTo(Pattern other)boolBoth match exactly the same strings.
IsSubsetOf(Pattern other)boolEvery string this pattern matches, other matches too.
Overlaps(Pattern other)boolAt least one string matches both.
MatchesNothing()boolNo string matches this pattern in full.
ShortestMatch()string?The shortest string the pattern matches in full, preferring readable characters, or null when it matches nothing.
Intersect(Pattern other)PatternAn anchored pattern that matches the strings both patterns match. Keeps this pattern's options and timeout.
Except(Pattern other)PatternAn anchored pattern that matches the strings this pattern matches and other rejects.
Union(Pattern other)PatternA pattern that matches the strings either pattern matches.
GetLanguageFacts(MatchScope scope = MatchScope.FullString)LanguageFactsLength bounds, the required prefix and suffix, and whether the empty string is accepted. Never throws for a valid pattern, except ArgumentOutOfRangeException for an undefined scope.
Pattern twoDigits = Pattern.With.Digit.Repeat.Times(2);
Pattern oneToThree = Pattern.With.Digit.Repeat.Times(1, 3);

PatternComparison comparison = Pattern.Compare(oneToThree, twoDigits);
Console.WriteLine(comparison.Relation); // ProperSuperset
Console.WriteLine(comparison.OnlyLeftExample); // 0
Console.WriteLine(twoDigits.IsSubsetOf(oneToThree)); // True
Console.WriteLine(twoDigits.Intersect(Pattern.With.Literal("x")).MatchesNothing()); // True

LanguageFacts facts = Pattern.With.Literal("INV-").Digit.Repeat.Times(4, 6).GetLanguageFacts();
Console.WriteLine(facts); // FullString: length 8..10, prefix "INV-", suffix "", accepts empty: false (exact)

Pattern doubled = Pattern.FromRegex(@"(a)\1");
Console.WriteLine(doubled.GetLanguageFacts()); // FullString: length 1..unbounded, prefix "a", suffix "", accepts empty: false (approximate)
// doubled.IsEquivalentTo(twoDigits) throws NotSupportedException: Language algebra does not support backreferences ...

See Pattern algebra and Language facts.

Pattern: optimization and safety​

MemberReturnsDescription
Optimize()OptimizationResultRewrites the pattern into a simpler one with the same .NET match behavior: the same leftmost-first matches, groups, captures and options. Branch order never changes.
AnalyzeSafety(bool confirm = false, TimeSpan? confirmationBudget = null)SafetyReportFinds constructs that can backtrack catastrophically (ReDoS). With confirm: true, runs each attack input with a timeout to prove the slowdown; confirmationBudget defaults to 250 ms per finding and must be positive.
OptimizationResult optimized = Pattern.FromRegex("(?:a|b|c)(?:x)+").Optimize();
Console.WriteLine(optimized.Optimized); // [abc]x+
Console.WriteLine(optimized.VerifiedEquivalent); // True

SafetyReport report = Pattern.FromRegex("^(a+)+$").AnalyzeSafety();
Console.WriteLine(report.Risk); // High
Console.WriteLine(report.Issues[0].Kind); // NestedQuantifier
Console.WriteLine(report.Issues[0].AttackInput?.Length); // 33

See Optimizer and ReDoS safety.

Pattern: debugging and visualization​

MemberReturnsDescription
Trace(string input)MatchTraceMatches the input and explains which construct consumed which characters, or where the match failed. Throws ArgumentNullException for null.
Highlight(string text, HighlightFormat format = HighlightFormat.Ansi)stringThe text with every match and group capture marked. Throws ArgumentNullException for null text and ArgumentOutOfRangeException for an undefined format.
Profile(string input, ProfileOptions? options = null)ProfileReportMatches with a step-counting backtracking interpreter and reports the cost of each construct. Throws NotSupportedException for conditionals, balancing groups, \G or RightToLeft, and InvalidOperationException when the pattern is not valid regex syntax.
ToRailroadSvg()stringA railroad diagram as a self-contained SVG document.
ToMermaid()stringA Mermaid flowchart, starting with flowchart LR.
Pattern phone = Pattern.With.NamedGroup("area", Pattern.With.Digit.Repeat.Times(3)).Literal("-").Digit.Repeat.Times(4);

MatchTrace miss = phone.Trace("555-12");
Console.WriteLine(miss.IsMatch); // False
Console.WriteLine(miss.Failure.Construct); // \d{4}
Console.WriteLine(miss.Failure.Description); // a digit, exactly 4 times

Console.WriteLine(phone.Highlight("Call 555-1234 now", HighlightFormat.Brackets)); // Call [[area:555]-1234] now

ProfileReport profile = Pattern.FromRegex("^(a+)+$").Profile("aaaaaaaab");
Console.WriteLine(profile.Steps); // 1543
Console.WriteLine(profile.Backtracks); // 510

See Match tracing, Highlighting, Diagrams and Backtracking profiler.

Pattern: testing​

MemberReturnsDescription
GenerateMatching(int count = 10, int? seed = null)string[]Distinct strings the pattern matches, each checked with the real .NET engine. Pass a seed for reproducible output; without one each call differs. May return fewer than count when the pattern has fewer distinct strings. Throws ArgumentOutOfRangeException for a negative count.
GenerateNonMatching(int count = 10, int? seed = null)string[]Distinct strings the pattern does not match, mostly near misses one edit away from a valid sample.
MeasureCoverage(IEnumerable<string> inputs)CoverageReportMatches every input and reports which branches, optional parts and repetition limits they exercised. Throws ArgumentNullException when inputs or an input is null, and InvalidOperationException when the pattern is not valid regex syntax.
TrackCoverage()PatternCoverageA thread-safe recorder you match through during a test run, then ask for a report.
Pattern title = Pattern.With.OneOf("Mr", "Mrs", "Ms").Literal(".").Repeat.Optional;

Console.WriteLine(string.Join(", ", title.GenerateMatching(count: 3, seed: 1))); // Mr., Mrs., Mr
Console.WriteLine(string.Join(", ", title.GenerateNonMatching(count: 3, seed: 1))); // M ., rM, M!s
Console.WriteLine(title.MeasureCoverage(new[] { "Mr", "Mrs." }).Percentage); // 25
Console.WriteLine(title.MeasureCoverage(new[] { "Mr", "Mrs." }));
// prints:
// Coverage for (?:Mr|Mrs|Ms)\.?: 25% (1/4), 2 inputs
// ✓ branch "Mr" (2 hits)
// ✗ branch "Mrs" was never matched
// ✗ branch "Ms" was never matched
// ✗ optional part "\.?" was never taken
  • "Mrs." counts as a hit for the Mr branch: the unanchored pattern finds Mr inside it, so only one of four items is covered.

See Test data generation, Regex coverage and Property testing.

Pattern: code generation and export​

MemberReturnsDescription
ToFluentCode(int indentSize = 4)stringC# code that rebuilds the pattern with the fluent API. Throws ArgumentOutOfRangeException for a negative indent and InvalidOperationException when the pattern is not valid regex syntax.
ToJavaScript()stringA JavaScript regex literal with exact .NET semantics. Throws NotSupportedException for constructs JavaScript cannot express.
ToFlavor(RegexFlavor flavor, FlavorOptions? options = null)FlavorConversionTranslates the pattern, including its options, into another dialect and lists every difference. Throws NotSupportedException when the dialect cannot express a construct; the message lists them all with positions. Throws ArgumentOutOfRangeException for an undefined flavor. options is ignored by the SQL flavors.
ToSqlCondition(string columnSql, RegexFlavor flavor)stringA boolean SQL condition for raw SQL or Dapper, using the database's regex operator. columnSql is inserted verbatim, so quote it yourself. Throws ArgumentNullException for a null column, and ArgumentException when flavor is not a SQL flavor (JavaScript is not a SQL flavor; use ToFlavor for it.).
ToLike(LikeDialect dialect, MatchScope scope = MatchScope.Substring, LikeCollation collation = LikeCollation.Binary)LikeConversionA SQL LIKE pattern that accepts every string the pattern accepts, with a proof of whether it accepts exactly those. Never throws for a valid pattern, except ArgumentOutOfRangeException for an undefined enum value.
Pattern digits = Pattern.With.StartOfLine.Digit.Repeat.OneOrMore.EndOfLine;

Console.WriteLine(digits.ToJavaScript()); // /^\p{Nd}+(?=\n?$)/u
Console.WriteLine(digits.ToFlavor(RegexFlavor.JsonSchema).ToLiteral()); // "^\\p{Nd}+(?=\\n?$)"
Console.WriteLine(digits.WithAsciiDigits().ToSqlCondition("\"Code\"", RegexFlavor.PostgreSql)); // "Code" ~ '\A[0-9]+(?=\n?\Z)'

Pattern invoice = Pattern.With.StartOfInput.Literal("INV-").AsciiDigit.Repeat.Times(4).EndOfInput;
Console.WriteLine(invoice.ToLike(LikeDialect.SqlServer, MatchScope.FullString)); // INV-[0-9][0-9][0-9][0-9] (exact)

Console.WriteLine(Pattern.FromRegex(@"\d{3}-\d{4}").ToFluentCode());
// prints:
// Pattern.With
// .Digit.Repeat.Times(3)
// .Literal("-")
// .Digit.Repeat.Times(4)

See Export, SQL dialects, SQL LIKE and Regex to fluent code.

Repeat​

Repeat applies a quantifier to the last element of a pattern. You reach it through Pattern.Repeat. Every member returns the parent Pattern with the quantifier applied.

MemberRegexDescription
Repeat(Pattern parentPattern)Constructor. Throws ArgumentNullException for null. You rarely call it directly.
OneOrMore+One or more times.
ZeroOrMore*Zero or more times.
Optional?Zero or one time.
Times(int exactly){n}Exactly n times.
Times(int min, int max){min,max}Between min and max times.
AtLeast(int x){x,}At least x times.
AtMost(int x){0,x}At most x times.
Console.WriteLine(Pattern.With.Digit.Repeat.Times(2, 4)); // \d{2,4}
Console.WriteLine(Pattern.With.Digit.Repeat.AtLeast(2)); // \d{2,}
Console.WriteLine(Pattern.With.Digit.Repeat.AtMost(2)); // \d{0,2}
  • Negative counts throw ArgumentOutOfRangeException, and so does max below min.
  • Calling a quantifier on an empty pattern throws InvalidOperationException: Repeat must follow an element to repeat, for example Pattern.With.Digit.Repeat.OneOrMore.. The Repeat property itself does not throw; the quantifier member does.
  • A quantifier on a quantifier wraps the first one in a non-capturing group, because \d+{2} is not valid .NET syntax: Pattern.With.Digit.Repeat.OneOrMore.Repeat.Times(2) renders (?:\d+){2}.
  • Follow a quantifier with .Lazy or .Possessive (members of Pattern).

Replacement​

A fluent, validated replacement for Pattern.Replace(input, r => ...). Group names and numbers are checked against the pattern before anything is replaced, and a missing group throws ArgumentException.

MemberReturnsDescription
static With { get; }ReplacementAn empty replacement. Inside Replace(input, r => ...) you get one as r.
Group(string name)ReplacementThe text a named group captured.
Group(string name, Func<string, string> transform)ReplacementThe same, transformed.
Group(int number)ReplacementThe text a numbered group captured.
Group(int number, Func<string, string> transform)ReplacementThe same, transformed.
Literal(string text)ReplacementLiteral text. Dollar signs are escaped for you.
WholeMatch(Func<string, string> transform = null)ReplacementThe whole match, optionally transformed: $&.
BeforeMatch()ReplacementThe input before the match: $`.
AfterMatch()ReplacementThe input after the match: $'.
LastGroup()ReplacementThe last captured group: $+.
EntireInput()ReplacementThe entire input: $_.
Computed(Func<Match, string> compute)ReplacementText computed from the match.
ToString()stringThe equivalent .NET replacement string. Transformed and computed parts appear as {computed}.
Pattern date = Pattern.With
.NamedGroup("day", Pattern.With.Digit.Repeat.Times(2)).Literal("/")
.NamedGroup("month", Pattern.With.Digit.Repeat.Times(2)).Literal("/")
.NamedGroup("year", Pattern.With.Digit.Repeat.Times(4));

string iso = date.Replace("Due 17/05/2024.", r => r.Group("year").Literal("-").Group("month").Literal("-").Group("day"));

Console.WriteLine(iso); // Due 2024-05-17.
Console.WriteLine(Replacement.With.Group("year").Literal("-").Group(2)); // ${year}-${2}

CommonPatterns​

Ready-made patterns. Each method returns a new Pattern. Unless noted, the pattern is anchored with ^...$ for whole-value validation.

MethodMatches
Email(), Email(PatternStrictness strictness)An e-mail address. Strict follows the standard closely; Loose accepts anything with the right shape.
Url(), Url(PatternStrictness strictness)HTTP and HTTPS URLs. Loose accepts any scheme:// followed by non-whitespace.
PhoneNumber()US phone numbers: (555) 123-4567, 555-123-4567, 555.123.4567 or 5551234567.
PhoneNumberE164()An international number in E.164 format: +, a non-zero digit, then 1 to 14 more digits, captured in the group number.
IPv4()An IPv4 address with octets 0-255. Leading zeros are accepted.
IPv6()An IPv6 address in full notation: 8 groups of 1 to 4 hex digits. The :: shorthand is not accepted.
MacAddress()Six pairs of hex digits separated consistently by : or -.
Guid()A hyphenated GUID or UUID.
HexColor()A CSS hex color: #RGB, #RGBA, #RRGGBB or #RRGGBBAA.
DateIso()The shape yyyy-MM-dd. It does not check the calendar; pair it with Validators.IsoDate.
DateTimeIso()An ISO 8601 date and time, such as 2024-05-17T10:30:00.123+02:00.
Date()Common US and ISO date formats.
Time24()HH:mm or HH:mm:ss.
Time12()12-hour time with AM/PM.
CreditCard()16-digit card numbers with optional separators. Pair it with Validators.Luhn.
Iban()The shape of an IBAN, compact or in groups of four. Pair it with Validators.IbanChecksum.
SocialSecurityNumber()A US SSN.
ZipCode()A US ZIP code, 5-digit or ZIP+4.
Username()Letters, digits and underscores, 3 to 20 characters.
StrongPassword()A password with several character-type requirements.
Slug()Lowercase words separated by single hyphens.
SemVer()A Semantic Versioning 2.0.0 version, using the official grammar.
Jwt()A compact JSON Web Token: three base64url segments; header and payload start with eyJ.
Base64()Non-empty standard Base64 with correct padding.
FilePath()Windows and Unix file paths.
HtmlTag()An HTML or XML tag with optional quoted attributes. Not anchored: it finds tags inside text.
MarkdownLink()A Markdown inline link. Not anchored: use it with Matches or ExtractAll.
Integer()An integer with an optional leading -. A + sign is not accepted.
Decimal()A number with an optional leading - and an optional fraction, such as -12.5.
ScientificNotation()A number such as 1.5e10 or -2E-5.

Every method also has an overload that takes a DigitMode as its last parameter, such as Integer(DigitMode digits) and Email(PatternStrictness strictness, DigitMode digits). The parameterless overloads use DigitMode.Unicode, so \d in them matches every Unicode decimal digit.

Console.WriteLine(CommonPatterns.Email().IsMatch("alice@example.com")); // True
Console.WriteLine(CommonPatterns.Integer().IsMatch("١٢٣")); // True: \d is Unicode
Console.WriteLine(CommonPatterns.Integer(DigitMode.Ascii).IsMatch("١٢٣")); // False
Console.WriteLine(CommonPatterns.Integer(DigitMode.Ascii)); // ^-?[0-9]+$
Console.WriteLine(CommonPatterns.SemVer().IsMatch("3.1.0-docs")); // True

See Common patterns.

PatternStrictness and DigitMode​

EnumValues
PatternStrictnessLoose (0): accepts anything with the right overall shape. Standard (1): the balanced default of the parameterless overloads. Strict (2): follows the relevant standard closely.
DigitModeUnicode (0): \d matches any Unicode decimal digit, the .NET default. Ascii (1): \d matches only 0-9 and \D everything else.

ConstrainedPattern and ConstraintResult​

A ConstrainedPattern is a Pattern plus rules a regex cannot express, such as a Luhn checksum or a real calendar date. A match counts only when every rule accepts it. Create one with pattern.Must(...) or new ConstrainedPattern(pattern). It is immutable: Must returns a copy.

MemberReturnsDescription
ConstrainedPattern(Pattern pattern)A constrained pattern with no rules yet. Throws ArgumentNullException for null.
Pattern { get; }PatternThe underlying pattern.
RuleDescriptions { get; }IReadOnlyList<string>The rule descriptions, in evaluation order. A rule added without a description is listed by position, as rule 1, or rule 1 on group 'number' for a group rule.
Must(Func<string, bool> rule, string? description = null)ConstrainedPatternA copy with a rule for the whole match.
Must(string groupName, Func<string, bool> rule, string? description = null)ConstrainedPatternA copy with a rule for a named group. Skipped when the group did not take part.
IsMatch(string input)boolSome match in the input passes every rule.
Match(string input)Match?The first match that passes every rule, or null when there is none. Unlike Pattern.Match, it never returns an unsuccessful Match.
Matches(string input)IEnumerable<Match>Every match that passes every rule, scanned lazily. After a rejected candidate the scan resumes one character later, so an overlapping valid value is still found.
IsValid(string input)boolThe whole input matches and passes every rule. The cheap form of Check.
Check(string input)ConstraintResultValidates the whole input and explains a failure.

Matches, IsValid and Check throw ArgumentNullException for a null input.

ConstraintResult members:

MemberReturnsDescription
IsValidboolThe input matched and passed every rule.
PatternFailedboolThe pattern itself rejected the input.
FailedRulestring?The description of the first rule that rejected the input, or null.
TraceMatchTrace?When the pattern rejected the input, the trace that shows where; otherwise null.
ToString()stringA one-line summary that does not include the input.
Pattern card = Pattern.With.StartOfLine.NamedGroup("number", Pattern.With.Digit.Repeat.Times(16)).EndOfLine;
ConstrainedPattern luhn = card.Must("number", Validators.Luhn, "Luhn checksum");

Console.WriteLine(luhn.IsValid("4111111111111111")); // True
ConstraintResult result = luhn.Check("4111111111111112");
Console.WriteLine(result.FailedRule); // Luhn checksum
Console.WriteLine(result); // Invalid: the rule 'Luhn checksum' rejected the value
Console.WriteLine(luhn.Check("abc")); // Invalid: the value does not match the pattern at position 0: expected (?<number>\d{16}) (group 'number')
Console.WriteLine(luhn.Match("4111111111111112") == null); // True

Validators​

Ready-made rules for Must. Every validator returns false for null instead of throwing.

MethodReturns true when
static bool Luhn(string value)The value has at least two digits and passes the Luhn (mod 10) checksum, as used by card numbers and IMEIs. Spaces and dashes are ignored; any other non-digit makes it invalid.
static bool IbanChecksum(string value)The ISO 13616 mod-97 check digits are right. Spaces are ignored; letters may be in either case.
static bool IsoDate(string value)The value is exactly yyyy-MM-dd and names a real date: 2024-02-29 is valid, 2026-02-30 is not.
static bool IPv4(string value)The value is a dotted quad with each octet 0-255. Leading zeros are accepted.
Console.WriteLine(Validators.Luhn("4111 1111 1111 1111")); // True
Console.WriteLine(Validators.IbanChecksum("GB82 WEST 1234 5698 7654 32")); // True
Console.WriteLine(Validators.IsoDate("2026-02-30")); // False

MatchesPatternAttribute​

A data-annotations attribute that validates a string property against a Pattern or ConstrainedPattern exposed by a public static property, field or parameterless method. net8.0 and net10.0 only, because System.ComponentModel.DataAnnotations is not part of netstandard2.1.

MemberDescription
MatchesPatternAttribute(Type patternType, string memberName)Reads the pattern from memberName on patternType. Throws ArgumentNullException for a null argument and ArgumentException for a blank member name. Applies to properties, fields and parameters.
PatternType, MemberNameThe constructor arguments.
IsValid(object? value, ValidationContext? validationContext)Protected. The whole value must match, and every rule must pass. null and the empty string are valid; add [Required] to forbid them. A value that is not a string fails. Throws InvalidOperationException when the pattern member cannot be resolved.
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Masterly.FluentRegex;

ProductInput input = new ProductInput { Sku = "abc-12" };
List<ValidationResult> errors = new List<ValidationResult>();

Console.WriteLine(Validator.TryValidateObject(input, new ValidationContext(input), errors, true)); // False
Console.WriteLine(errors[0].ErrorMessage); // The field Sku does not match the required pattern.

public sealed class ProductInput
{
[MatchesPattern(typeof(Patterns), nameof(Patterns.Sku))]
public string? Sku { get; set; }
}

Patterns.Sku is the field declared in the GeneratePatternAttribute sample below. The EF Core package can turn the same attribute into a CHECK constraint (AddCheckConstraintsFromAttributes).

RegexGroupAttribute and ExtractionConverters​

MemberDescription
RegexGroupAttribute(string name)Put it on a constructor parameter or a property to bind it to a group with a different name. Name returns the group name.
ExtractionConverters.Register<T>(Func<string, IFormatProvider, T> converter)Registers a culture-aware converter for T, replacing any existing one. Global and thread-safe. Throws ArgumentNullException for null.
ExtractionConverters.Register<T>(Func<string, T> converter)Registers a culture-independent converter.
ExtractionConverters.Unregister<T>()Removes the converter for T. Returns true when there was one.

Built in, without a converter: string, object, the integer types, float, double, decimal, bool, char, enums (names match ignoring case), DateTime, DateTimeOffset, TimeSpan, Guid, Uri, Version, and Nullable<T> of any of them (an empty group gives null). DateOnly and TimeOnly work on net8.0 and net10.0. Numbers written with Unicode digits, such as ٢٠٢٦, are converted to ASCII first, because \d matches them. bool also accepts yes/no, y/n, on/off and 1/0. A registered converter wins over the built-in one. A type with neither throws ArgumentException that tells you to register a converter.

Template and Template<T>​

Typed templates: string interpolation in reverse. A template such as "{Date:yyyy-MM-dd} [{Level}] {Message}" parses text into a typed value and formats the value back.

MemberReturnsDescription
static Template.Of<T>(string template, TemplateOptions? options = null)Template<T>Compiles a template for T. Placeholders are {Name} or {Name:format}; {{ and }} are literal braces. Throws ArgumentNullException for null and ArgumentException for an empty or malformed template, an unknown member (the message lists the available ones), an unbound required constructor parameter or an unsupported format.
Template<T>.Parse(string input)TParses an input that must match the whole template. Throws FormatException with an explanation when it does not.
Template<T>.TryParse(string input, out T value)boolThe same, returning false instead of throwing.
Template<T>.ParseAll(string text)IEnumerable<T>Every occurrence of the template in the text. Evaluated lazily. Throws FormatException when an occurrence matches but a value cannot be converted.
Template<T>.Format(T value)stringRenders a value with the same formats and culture, so Parse(Format(x)) round-trips. Throws ArgumentException when a value cannot be written so that it reads back.
Template<T>.PatternPatternThe unanchored pattern for one occurrence, with a named group per placeholder.
Template<T>.ToString()stringThe template text.

TemplateOptions has three init-only properties: Culture (IFormatProvider, default CultureInfo.InvariantCulture), IgnoreCase (bool, for literal text; enum names and booleans always ignore case) and Timeout (TimeSpan?, null for no timeout). Templates are immutable and thread-safe.

Template<LogEntry> log = Template.Of<LogEntry>("{Date:yyyy-MM-dd} [{Level}] {Message}");

LogEntry entry = log.Parse("2024-05-17 [WARN] Disk almost full");
Console.WriteLine(entry.Level); // WARN
Console.WriteLine(log.Format(entry)); // 2024-05-17 [WARN] Disk almost full
Console.WriteLine(log.Pattern); // (?<Date>\d{4}-\d{2}-\d{2}) \[(?<Level>[^\r\n]*?)\] (?<Message>[^\r\n]*)

public sealed record LogEntry(DateTime Date, string Level, string Message);

See Templates.

StreamMatch and StreamMatchOptions​

MemberTypeDescription
StreamMatch.IndexlongPosition of the match in the whole input, in characters.
StreamMatch.LengthintLength of the match.
StreamMatch.ValuestringThe matched text.
StreamMatch.GroupsIReadOnlyDictionary<string, string>Every participating group, keyed by name or by number as a string ("1"). Group 0 is not included.
StreamMatchOptions.MaxMatchLengthintThe longest match guaranteed to be found exactly. Default 4096.
StreamMatchOptions.LookbehindContextintCharacters kept before the scan position for lookbehinds, \b and ^. Default 256.
StreamMatchOptions.BufferSizeintCharacters read at a time. Default 65536.

The three options throw ArgumentOutOfRangeException for zero or negative values. StreamMatch.ToString() returns the position and text, such as 3: "ERR-404".

Redactor, RedactionStyle and RedactionReport​

A Redactor finds sensitive values in free text and replaces them. It is immutable: Add returns a new redactor. Anchors that wrap a whole validation pattern (^...$, \A...\z) are turned into word-boundary checks, so CommonPatterns work in free text.

MemberReturnsDescription
static Redactor.For(params Pattern[] patterns)RedactorA redactor that masks every value the patterns find with RedactionStyle.Mask. Throws ArgumentNullException when the array or an element is null.
Add(Pattern pattern, RedactionStyle? style = null)RedactorAlso redacts the values this pattern finds.
Add(ConstrainedPattern pattern, RedactionStyle? style = null)RedactorAlso redacts the values that pass all of its rules.
Mask(string text)stringThe redacted text.
Redact(string text)RedactionReportThe redacted text and where each value was.
PatternCountintThe number of registered patterns.
RedactionStyle memberDescription
MaskReplaces every letter and digit with * and keeps separators: 555-123-4567 becomes ***-***-****.
EmailKeeps the first character and the domain: a***@example.com. Values without @ are masked like Mask.
KeepLast(int count)Masks letters and digits except the last count. A value with count or fewer letters and digits is masked completely. Throws ArgumentOutOfRangeException for a negative count.
KeepFirst(int count)Masks letters and digits except the first count, with the same rules.
Label(string label)Replaces the whole value with the label, such as [EMAIL]. Throws ArgumentNullException for null and ArgumentException for an empty label.
Custom(Func<Match, string> replacement)Computes the replacement from the match. The function must not return null.
Name, LabelTextThe style name as reported, such as Mask, KeepFirst(2), Label or Custom, and the label of a Label style (otherwise null). ToString() returns Name.

Mask and Email are static properties; the others are static factory methods. When hits of different patterns overlap, the earliest start wins, then the longest hit, then the pattern registered first.

RedactionReport has Text, Spans (ordered by position) and HasRedactions. Each RedactedSpan has Start, Length, RedactedStart, RedactedLength, Style, Label and PatternIndex. Neither keeps the original values, so a report is safe to log.

Redactor redactor = Redactor
.For(CommonPatterns.SocialSecurityNumber())
.Add(CommonPatterns.Email(), RedactionStyle.Email)
.Add(CommonPatterns.CreditCard(), RedactionStyle.KeepLast(4));

string text = "SSN 123-45-6789, mail alice@example.com, card 4111-1111-1111-1111.";

Console.WriteLine(redactor.Mask(text)); // SSN ***-**-****, mail a***@example.com, card ****-****-****-1111.
Console.WriteLine(redactor.Redact(text)); // Redacted 3 span(s): [4..15) Mask, [22..39) Email, [46..65) KeepLast(4)

See PII redaction.

PatternComparison and PatternRelation​

The result of Pattern.Compare(left, right). Every example string is checked with the real .NET engine.

MemberTypeDescription
RelationPatternRelationHow the two languages relate.
AreEquivalentboolBoth match exactly the same strings.
OnlyLeftExamplestringThe shortest string only the left pattern matches, or null.
OnlyRightExamplestringThe shortest string only the right pattern matches, or null.
CommonExamplestringThe shortest string both match, or null.
ToString()stringFor example Not equivalent (left is a proper superset of right). Shortest counterexample: "0" (left accepts it, right rejects it). Both accept "00".

PatternRelation values: Equivalent (0), ProperSubset (1), ProperSuperset (2), Overlapping (3), Disjoint (4).

LanguageFacts and MatchScope​

The result of GetLanguageFacts(scope). The facts are sound: they hold for every string the pattern accepts.

MemberTypeDescription
ScopeMatchScopeThe set of strings the facts describe.
MinLengthintThe shortest accepted length. A lower bound when IsExact is false.
MaxLengthint?The longest accepted length, or null when unbounded.
RequiredPrefixstringText every accepted string starts with; empty when none.
RequiredSuffixstringText every accepted string ends with. A pattern ending in $ requires no suffix, because it also accepts a trailing newline.
AcceptsEmptyboolThe empty string is accepted.
IsEmptyLanguageboolNo string is accepted.
IsExactboolComputed from the automaton (true) or approximated from the syntax tree for non-regular constructs (false).

MatchScope values: FullString (0), the strings the pattern matches entirely; Substring (1), the strings for which IsMatch returns true.

Pattern invoice = Pattern.With.Literal("INV-").Digit.Repeat.Times(4, 6);

Console.WriteLine(invoice.GetLanguageFacts(MatchScope.Substring)); // Substring: length 8..unbounded, prefix "", suffix "", accepts empty: false (exact)

OptimizationResult​

MemberTypeDescription
OriginalPatternThe pattern that was optimized.
OptimizedPatternThe rewritten pattern, or Original when nothing applied.
IsChangedboolAt least one rewrite applied.
AppliedRewritesIReadOnlyList<string>A description of each rewrite, in order, such as Merged alternation 'a|b|c' into '[abc]'.
VerifiedEquivalentboolLanguage algebra proved both match the same strings. false for non-regular constructs, which were checked on sample inputs only.
ToString()stringA summary line followed by one line per rewrite.
Console.WriteLine(Pattern.FromRegex("(?:a|b|c)(?:x)+").Optimize());
// prints:
// Optimized '(?:a|b|c)(?:x)+' to '[abc]x+' (proven equivalent):
// - Merged alternation 'a|b|c' into '[abc]'
// - Unwrapped redundant group '(?:[abc])'
// - Unwrapped redundant group '(?:x)'

SafetyReport, SafetyIssue, SafetyIssueKind and RiskLevel​

MemberTypeDescription
SafetyReport.PatternstringThe analyzed regex.
SafetyReport.RiskRiskLevelThe highest risk among all issues.
SafetyReport.IsSafeboolThe risk is Low or lower.
SafetyReport.IssuesIReadOnlyList<SafetyIssue>Every issue, most severe first.
SafetyIssue.Kind, RiskSafetyIssueKind, RiskLevelWhat the construct is and how dangerous.
SafetyIssue.Position, Fragmentint, stringWhere the construct is in the regex text, and its text.
SafetyIssue.Description, SuggestionstringA plain-language explanation and a suggested fix.
SafetyIssue.AttackInputstringAn input crafted to trigger the backtracking, or null.
SafetyIssue.Confirmedbool?Whether running the attack input exceeded the budget. null unless confirm: true.
SafetyIssue.ConfirmationTimeTimeSpan?How long the confirmation run took.
SafetyReport.ToString(), SafetyIssue.ToString()stringA multi-line report with the fix and the attack input, and a one-line summary of one issue.
EnumValues
RiskLevelNone (0), Low (1): not exploitable in practice, Medium (2): polynomial backtracking, High (3): exponential backtracking.
SafetyIssueKindNestedQuantifier (0): (a+)+. OverlappingAlternation (1): (\w|\d)+. AdjacentOverlappingQuantifiers (2): \d+\d+.

MatchTrace, TraceStep and TraceFailure​

MemberTypeDescription
MatchTrace.Pattern, InputstringThe regex and the traced input.
MatchTrace.IsMatchboolThe pattern matched.
MatchTrace.Index, Length, Valueint, int, stringThe match; Index is -1 and Value is null when there is none.
MatchTrace.StepsIReadOnlyList<TraceStep>The top-level steps of a successful match.
MatchTrace.FailureTraceFailureWhy the match failed, or null on success.
TraceStep.Construct, DescriptionstringThe regex text of the construct and a plain-language description.
TraceStep.Index, Length, Valueint, int, stringWhat the construct consumed.
TraceStep.ChildrenIReadOnlyList<TraceStep>The steps of its parts. Repeated groups have one child per iteration.
TraceFailure.Position, Construct, Descriptionint, string, stringWhere the match died, the construct that failed and what it expected.
TraceFailure.MatchedSoFar, FoundstringThe text matched before the failure, and the quoted input at the failure position (or the end of the input).
TraceFailure.MatchedStepsIReadOnlyList<TraceStep>The constructs that matched before the failure.

HighlightFormat​

ValueOutput
Ansi (0)ANSI terminal colors: matches get a background color, groups a foreground color, and a one-line legend follows.
Html (1)Matches in <mark class="frx-match">, groups in <span class="frx-group frx-group-{name}" title="{name}">. All text is HTML-encoded.
Brackets (2)Plain text for tests and logs: [...] for matches and [name:...] for groups.

ProfileReport, ProfileHotspot and ProfileOptions​

MemberTypeDescription
ProfileReport.IsMatchboolThe interpreter found a match. false when the budget ran out.
ProfileReport.MatchIndex, MatchLengthintThe match, or -1 and 0.
ProfileReport.Steps, BacktrackslongTotal attempts and total returns to try another alternative.
ProfileReport.BudgetExhaustedboolThe interpreter hit StepBudget: a sign of catastrophic backtracking on this input.
ProfileReport.AgreesWithEngineboolThe real .NET engine found the same result. false when the budget ran out.
ProfileReport.HotspotsIReadOnlyList<ProfileHotspot>Constructs that did work, most expensive first.
ProfileReport.ToString()stringA text heatmap with a table of the top hotspots and a verdict.
ProfileHotspot.Fragment, Start, Lengthstring, int, intThe construct and where it is in the regex text.
ProfileHotspot.Steps, Backtracks, Sharelong, long, doubleThe work spent on it, and its fraction of all steps (0 to 1).
ProfileOptions.StepBudgetlongThe most steps before giving up. Default 1,000,000. Throws ArgumentOutOfRangeException for zero or a negative value.

CoverageReport, CoverageItem, CoverageItemKind and PatternCoverage​

MemberTypeDescription
CoverageReport.PercentagedoubleItems exercised at least once, 0 to 100. A pattern without decision points is 100% covered.
CoverageReport.Items, UncoveredIReadOnlyList<CoverageItem>Every item in pattern order, and the ones never exercised.
CoverageReport.InputsRecordedintInputs recorded, matched or not.
CoverageReport.ToString()stringA summary line and one line per item, with ✓ or ✗.
CoverageItem.Kind, DescriptionCoverageItemKind, stringWhat the decision point is, such as branch "Mrs".
CoverageItem.Start, LengthintWhere the construct is in the regex text.
CoverageItem.HitCount, IsCoveredint, boolHow many matches exercised it.
PatternCoverage.PatternPatternThe pattern being recorded.
PatternCoverage.IsMatch(string input), Match(string input)bool, MatchMatch through the recorder; every match is recorded.
PatternCoverage.Record(string input)voidRecords every match without returning a result.
PatternCoverage.Report()CoverageReportThe coverage recorded so far.

CoverageItemKind values: AlternationBranch (0), OptionalPartTaken (1), UpperBoundReached (2).

PatternProperty, PropertyOptions and PropertyResult​

Property-based testing driven by a pattern.

MemberReturnsDescription
static PatternProperty.ForMatching(Pattern pattern, Func<string, bool> property, PropertyOptions? options = null)PropertyResultChecks the property for strings the pattern matches.
static PatternProperty.ForNonMatching(Pattern pattern, Func<string, bool> property, PropertyOptions? options = null)PropertyResultChecks the property for strings it does not match, mostly near misses.
PropertyOptions.SamplesintSamples to check. Default 100. Throws ArgumentOutOfRangeException below 1.
PropertyOptions.SeedintThe generation seed. Default 0, so runs are reproducible.
PropertyOptions.MaxShrinkStepsintCandidates tried while shrinking a counterexample. Default 1000; 0 disables shrinking. Throws ArgumentOutOfRangeException for a negative value.
PropertyResult.PassedboolThe property held for every sample.
PropertyResult.SamplesTested, Seed, ShrinkStepsintHow many samples ran, the seed used and the shrink candidates tried.
PropertyResult.Counterexample, OriginalCounterexamplestring?The shrunk and the first failing input, or null when it passed.
PropertyResult.ExceptionException?What the property threw for the counterexample, or null.
PropertyResult.ThrowIfFailed()voidThrows PropertyFailedException when the property failed.
PropertyResult.ToString()stringA readable summary with the counterexample.
PropertyFailedException.ResultPropertyResult?The failed result. The exception also has the three standard exception constructors.
Pattern sku = Pattern.With.StartOfLine.Letter.Repeat.Times(3).Literal("-").Digit.Repeat.Times(4).EndOfLine;

PropertyResult result = PatternProperty.ForMatching(sku, s => !s.Contains('0'), new PropertyOptions { Samples = 200, Seed = 7 });

Console.WriteLine(result.Passed); // False
Console.WriteLine(result.Counterexample); // aaa-0000
Console.WriteLine(result.OriginalCounterexample); // URB-5409

FlavorConversion, FlavorOptions, RegexFlavor and PatternExport​

FlavorConversion is the result of ToFlavor.

MemberTypeDescription
FlavorRegexFlavorThe target dialect.
PatternstringThe translated source, without delimiters or flags.
FlagsstringFlags to compile with, such as iu. Always empty for JSON Schema and SQL.
IsExactboolAccepts exactly the strings .NET accepts.
WarningsIReadOnlyList<string>Every difference in which strings match. Empty when exact.
PreservesMatchExtentboolEvery match and group captures the same text as in .NET.
MatchExtentWarningsIReadOnlyList<string>Differences in what is matched or captured, which matter for extraction and replacement.
GroupNumbersIReadOnlyDictionary<string, int>The number of every .NET group in the translation, keyed by name and by .NET number. Numbers can differ because .NET numbers unnamed groups first.
IsSqlboolThe flavor is a SQL dialect.
ToLiteral()stringSource code: /.../u for JavaScript, a quoted JSON string, or a single-quoted SQL literal. ToString() returns the same.

FlavorOptions has two settable properties, both true by default: PreserveUnicodeSemantics (keep the Unicode meaning of \d, \w, \s, \b) and PreserveLineSemantics (keep .NET's $, . and multiline anchors). Turning them off gives shorter, idiomatic output and adds a warning.

FlavorConversion ascii = digits.ToFlavor(
RegexFlavor.JavaScript,
new FlavorOptions { PreserveUnicodeSemantics = false, PreserveLineSemantics = false });

Console.WriteLine(ascii.ToLiteral()); // /^\d+$/u
Console.WriteLine(ascii.IsExact); // False
Console.WriteLine(ascii.Warnings.Count); // 2

digits is the pattern from Pattern: code generation and export.

RegexFlavor valueTarget
JavaScript (0)ECMAScript 2018+ with the u flag.
JsonSchema (1)The pattern keyword of JSON Schema and OpenAPI: no flags, valid with u.
PostgreSql (2)PostgreSQL advanced regular expressions (~). Options are written into the pattern.
MySql (3)MySQL 8.0+ (ICU, REGEXP_LIKE). The pattern starts with (?-i).
MariaDb (4)MariaDB (PCRE2, REGEXP). The pattern starts with (?-i).
SqlServer (5)SQL Server 2025 and Azure SQL REGEXP_LIKE (RE2): no lookarounds, backreferences or atomic groups.
Sqlite (6)SQLite through the EF Core provider, whose regexp() runs .NET's non-backtracking engine.

PatternExport.ToTypeScript(IReadOnlyDictionary<string, Pattern> patterns, string? header = null) returns a TypeScript (and JavaScript) module with one exported regex constant per pattern, in ordinal name order, each documented with its .NET source and any warnings.

LikeConversion, LikeDialect and LikeCollation​

LikeConversion is the result of ToLike. A LIKE pattern is only returned when it accepts every string the regex accepts, so it can always pre-filter rows.

MemberTypeDescription
ExpressionstringThe LIKE pattern, unquoted.
EscapeCharacterchar?The ESCAPE character, or null when none is needed.
IsExactboolLIKE accepts exactly the regex's strings in Scope under Collation, proven by language algebra.
IsSupersetboolAlways true.
Dialect, Scope, CollationThe settings the proof assumed.
WarningsIReadOnlyList<string>Why it is not exact, plus remarks.
ToSql(string columnSql)stringA condition for raw SQL, such as [Number] LIKE N'INV-[0-9][0-9][0-9][0-9]'.
EnumValues
LikeDialectStandard (0): % and _ only. SqlServer (1): adds [set], [^set] and ranges. PostgreSql (2). MySql (3), also for MariaDB. Sqlite (4).
LikeCollationBinary (0): characters compare by code point. CaseInsensitive (1): letters match regardless of case.
Pattern anchoredByLine = Pattern.With.StartOfLine.Literal("INV-").AsciiDigit.Repeat.Times(4).EndOfLine;

Console.WriteLine(anchoredByLine.ToLike(LikeDialect.SqlServer, MatchScope.FullString)); // INV-[0-9][0-9][0-9][0-9]% (superset)
Console.WriteLine(anchoredByLine.ToLike(LikeDialect.Standard, MatchScope.FullString)); // INV-____% (superset)
  • The $ in this pattern also accepts a trailing newline, so LIKE needs a final % and is only a superset. The \A...\z pattern in Pattern: code generation and export gives an exact translation.

UnicodeScript and UnicodeGroup​

EnumValues
UnicodeScriptLatin, Greek, Cyrillic, Armenian, Hebrew, Arabic, Syriac, Thaana, Devanagari, Bengali, Gurmukhi, Gujarati, Tamil, Telugu, Kannada, Malayalam, Thai, Lao, Tibetan, Georgian, Hangul, Ethiopic, Khmer, Mongolian, Hiragana, Katakana, Han (0 to 26, in this order). Used by Script, LetterIn and DigitIn.
UnicodeGroupLetter (\p{L}), Mark (\p{M}), Number (\p{N}), Punctuation (\p{P}), Symbol (\p{S}), Separator (\p{Z}), Other (\p{C}).

GeneratePatternAttribute​

Evaluates a static Pattern field or property at build time and generates a {Member}Source string constant and a method that returns a cached Regex. The containing type must be partial (FRX002) and the member static (FRX009). The attribute applies to fields and properties, once per member.

MemberDescription
GeneratePatternAttribute()The method is named after the member with a Regex suffix, such as SkuRegex().
GeneratePatternAttribute(string methodName)Uses the given method name.
MethodNameThe custom name, or null for the default.
using System;
using System.Text.RegularExpressions;
using Masterly.FluentRegex;

Regex sku = Patterns.SkuRegex();

Console.WriteLine(Patterns.SkuSource); // ^[A-Z]{3}-\d{4}$
Console.WriteLine(sku.IsMatch("ABC-1234")); // True
Console.WriteLine(Patterns.IsInvoice().IsMatch("INV-000042")); // True
Console.WriteLine(ReferenceEquals(sku, Patterns.SkuRegex())); // True

public static partial class Patterns
{
[GeneratePattern]
public static readonly Pattern Sku =
Pattern.With.StartOfLine.UppercaseLetter.Repeat.Times(3).Literal("-").Digit.Repeat.Times(4).EndOfLine;

[GeneratePattern("IsInvoice")]
public static Pattern Invoice => Pattern.With.StartOfLine.Literal("INV-").Digit.Repeat.Times(6).EndOfLine.WithAsciiDigits();
}
warning

The build-time evaluator supports the core builder (anchors, matchers, literals, sets, ranges, Unicode categories and blocks, quantifiers, groups, assertions, composition, FromRegex, WithOptions, WithTimeout, WithAsciiDigits, WithDigits) and CommonPatterns. Members such as AsciiDigit, Script, LetterIn, DigitIn, CodePoint, Grapheme, IgnoreDiacritics and UseNonBacktracking cannot be evaluated: the analyzer reports FRX001 (for example 'UseNonBacktracking' cannot be evaluated at compile time) and no method is generated. In the sample above, Digit with WithAsciiDigits() is used instead of AsciiDigit for that reason.

Compile-time diagnostics​

The core package includes Roslyn analyzers and a code fix. They run for every target framework. See Compile-time checks.

IDSeverityReported when
FRX001WarningA [GeneratePattern] member cannot be evaluated at compile time.
FRX002ErrorThe type containing a [GeneratePattern] member is not partial.
FRX003ErrorA pattern produces an invalid regular expression.
FRX004WarningA pattern is vulnerable to exponential backtracking (ReDoS, high risk).
FRX005InfoA pattern may backtrack polynomially (medium risk).
FRX006ErrorBuilding the pattern throws, for example .Lazy without a quantifier.
FRX007InfoA compile-time constant pattern can be backed by a [GeneratedRegex] method. A code fix generates it.
FRX008WarningAn Extract<T> constructor parameter has no matching group. The message says what happens at run time: extracting throws, or the value is always null.
FRX009ErrorA [GeneratePattern] member is not static.

Target framework availability​

The core package targets netstandard2.1, net8.0 and net10.0. Almost everything is available on all three. These members are not:

Membernetstandard2.1net8.0net10.0
UseNonBacktracking()NoYesYes
IsMatch(ReadOnlySpan<char>), Count(ReadOnlySpan<char>), EnumerateMatches(ReadOnlySpan<char>)NoYesYes
EnumerateSplits(ReadOnlySpan<char>)NoNoYes
MatchesPatternAttributeNoYesYes
DateOnly and TimeOnly in typed extraction and templatesNoYesYes

Count(string) works on all three. On netstandard2.1 it counts by walking the matches.

Masterly.FluentRegex.EntityFrameworkCore​

Translates patterns to the database's regex dialect in LINQ queries, CHECK constraints and raw SQL. Namespace Masterly.FluentRegex.EntityFrameworkCore. See Entity Framework Core.

UseFluentRegex and FluentRegexOptionsBuilder​

MemberReturnsDescription
UseFluentRegex(this DbContextOptionsBuilder builder, Action<FluentRegexOptionsBuilder>? configure = null)DbContextOptionsBuilderEnables translation and CHECK constraints. The dialect is detected from the provider: Npgsql, Pomelo MySQL/MariaDB, SQL Server or SQLite.
UseFluentRegex<TContext>(this DbContextOptionsBuilder<TContext> builder, Action<FluentRegexOptionsBuilder>? configure = null)DbContextOptionsBuilder<TContext>The generic version.
UseDialect(SqlDialect dialect)FluentRegexOptionsBuilderOverrides detection. Needed for unknown providers and to choose SqlServer2025 when the provider's compatibility level is not set. Like UseLikeFallback and UseLikeCollation, it throws ArgumentOutOfRangeException for an undefined enum value.
UseIndexHints(bool enabled = true)FluentRegexOptionsBuilderAdds proven conditions such as column LIKE 'prefix%' and a length bound next to the regex, so a B-tree index can narrow the scan. On by default.
UseLikeFallback(LikeFallback fallback)FluentRegexOptionsBuilderHow patterns translate on SQL Server before 2025. Default ExactOnly.
UseLikeCollation(LikeCollation collation)FluentRegexOptionsBuilderStates whether SQL Server columns use a binary collation (_BIN2, _BIN). With the default, CaseInsensitive, LIKE comparisons are wrapped in COLLATE Latin1_General_100_BIN2 and the index-hint prefix is omitted. Declare Binary to drop the COLLATE and get the prefix hint. Other databases ignore this setting.
AddCheckConstraintsFromAttributes(bool enabled = true)FluentRegexOptionsBuilderCreates a CHECK constraint for every string property with [MatchesPattern]. A ConstrainedPattern contributes only its pattern; its rules run in .NET only.
using Masterly.FluentRegex;
using Masterly.FluentRegex.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;

// options is the DbContextOptionsBuilder from OnConfiguring or AddDbContext; connectionString comes from your configuration
options
.UseSqlServer(connectionString, sql => sql.UseCompatibilityLevel(160))
.UseFluentRegex(fluent => fluent
.UseDialect(SqlDialect.SqlServer)
.UseLikeFallback(LikeFallback.AllowSuperset)
.UseLikeCollation(LikeCollation.Binary)
.AddCheckConstraintsFromAttributes());

Query functions​

MemberReturnsDescription
PatternFunctions.Matches(string? value, Pattern pattern)booltrue when the pattern matches anywhere in the value; false for null. Translated to the dialect's regex predicate.
PatternFunctions.Group(string? value, Pattern pattern, string groupName)string?The named group in the first match, or null. Translated on PostgreSQL and SQL Server 2025.
PatternFunctions.Group(string? value, Pattern pattern, int groupNumber)string?The numbered group in the first match, or null.
PatternFunctions.Replace(string? value, Pattern pattern, string replacement)string?Replaces every match; .NET replacement syntax. null stays null. Translated on PostgreSQL, MySQL, MariaDB and SQL Server 2025.
EF.Functions.Matches(...), EF.Functions.Group(...), EF.Functions.Replace(...)The same functions as DbFunctions extensions, declared in FluentRegexDbFunctionsExtensions. They work only inside a query; evaluated in .NET they throw InvalidOperationException.

PatternFunctions also run in .NET outside a query, with the same results, so one expression works against a database and in memory.

Queryable extensions​

MemberReturnsDescription
WhereMatches<T>(this IQueryable<T> source, Expression<Func<T, string?>> selector, Pattern pattern)IQueryable<T>Filters entirely in SQL, with index hints. Throws like the translator when the dialect cannot express the pattern.
MatchingAsync<T>(this IQueryable<T> source, Expression<Func<T, string?>> selector, Pattern pattern, CancellationToken cancellationToken = default)IAsyncEnumerable<T>Works on every database: SQL applies a proven superset (exact regex, or a LIKE and length bound, or only a NULL check) and .NET applies the exact pattern.
MatchingAsync<T>(..., ConstrainedPattern pattern, CancellationToken cancellationToken = default)IAsyncEnumerable<T>The same; the rules run in .NET.
ToMatchingListAsync<T>(..., Pattern pattern, CancellationToken cancellationToken = default)Task<List<T>>Loads the matching rows into a list.
ToMatchingListAsync<T>(..., ConstrainedPattern pattern, CancellationToken cancellationToken = default)Task<List<T>>The same with rules.

All five throw ArgumentNullException for a null argument. Rows come back in query order.

HasPattern, SqlRegex and the enums​

MemberReturnsDescription
HasPattern<TProperty>(this PropertyBuilder<TProperty> builder, Pattern pattern, string? constraintName = null)PropertyBuilder<TProperty>Adds a CHECK constraint that every non-null value must pass. Same semantics as IsMatch, so anchor the pattern to constrain the whole value. The default name is CK_{Table}_{Column}_Pattern. Requires UseFluentRegex(). Throws InvalidOperationException for a property that is not a string; on SqlDialect.SqlServer model building throws when the pattern has no exact LIKE form.
SqlRegex.Condition(string columnSql, Pattern pattern, SqlDialect dialect)stringA raw SQL condition for Dapper, ADO.NET or migrations, true when the column contains a match. NULL never matches. On SqlServer it returns the exact LIKE translation under COLLATE Latin1_General_100_BIN2, or throws InvalidOperationException when there is none. Throws ArgumentException for a blank column and NotSupportedException when the dialect cannot express the pattern.
SqlRegex.Condition(string columnSql, Pattern pattern, SqlDialect dialect, LikeCollation collation)stringThe same, stating the column's collation. With Binary on SQL Server, no COLLATE is added, so an index can be used.
Enum or classValues
SqlDialectPostgreSql (0), MySql (1), MariaDb (2), SqlServer2025 (3), SqlServer (4): before 2025, no regex, LIKE only, Sqlite (5).
LikeFallbackExactOnly (0): LIKE only when exact, otherwise throw (the default). AllowSuperset (1): a superset LIKE plus a logged warning. Never (2): every pattern throws.
FluentRegexEventIdLikeSupersetFallback (id 79000) and TranslationWarning (id 79001, [Obsolete]: never raised), both static readonly EventId named Microsoft.EntityFrameworkCore.Query.LikeSupersetFallback and Microsoft.EntityFrameworkCore.Query.TranslationWarning, for ConfigureWarnings.
using System;
using System.Linq;
using Masterly.FluentRegex;
using Masterly.FluentRegex.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;

// db is a ShopContext configured with UseSqlite(connection).UseFluentRegex()
IQueryable<Product> query = db.Products.Where(p => PatternFunctions.Matches(p.Sku, ShopPatterns.Prefix));
Console.WriteLine(query.ToQueryString());
// prints:
// SELECT "p"."Id", "p"."Sku"
// FROM "Products" AS "p"
// WHERE "p"."Sku" REGEXP '\AABC-' AND "p"."Sku" LIKE 'ABC-%' ESCAPE '\'

Console.WriteLine(SqlRegex.Condition("\"Sku\"", ShopPatterns.Sku, SqlDialect.PostgreSql)); // "Sku" ~ '\A[A-Z]{3}-[0-9]{4}\Z'
Console.WriteLine(SqlRegex.Condition("[Sku]", ShopPatterns.Prefix, SqlDialect.SqlServer)); // [Sku] COLLATE Latin1_General_100_BIN2 LIKE N'ABC-%'
Console.WriteLine(SqlRegex.Condition("[Sku]", ShopPatterns.Prefix, SqlDialect.SqlServer, LikeCollation.Binary)); // [Sku] LIKE N'ABC-%'

public static class ShopPatterns
{
public static readonly Pattern Sku =
Pattern.With.StartOfInput.UppercaseLetter.Repeat.Times(3).Literal("-").AsciiDigit.Repeat.Times(4).EndOfInput;

public static readonly Pattern Prefix = Pattern.With.StartOfInput.Literal("ABC-");
}

public sealed class Product
{
public int Id { get; set; }

public string Sku { get; set; } = "";
}

public sealed class ShopContext : DbContext
{
public ShopContext(DbContextOptions<ShopContext> options)
: base(options)
{
}

public DbSet<Product> Products => Set<Product>();

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// SQLite: CONSTRAINT "CK_Product_Sku" CHECK ("Sku" REGEXP '\A[A-Z]{3}-[0-9]{4}\z')
modelBuilder.Entity<Product>()
.Property(p => p.Sku)
.HasPattern(ShopPatterns.Sku, constraintName: "CK_Product_Sku");
}
}
  • The LIKE 'ABC-%' in the query is an index hint from UseIndexHints. It never changes the result.
  • On SQLite, saving a Product with Sku = "bad" fails with SQLite Error 19: 'CHECK constraint failed: CK_Product_Sku'..
  • PatternFunctions.Replace uses .NET group numbering, where unnamed groups come before named ones. In (?<prefix>[A-Z]{3})-([0-9]{4}), $1 is the digits and $2 is prefix.

Masterly.FluentRegex.AspNetCore​

Named patterns for ASP.NET Core and the pattern route constraint. See ASP.NET Core.

MemberReturnsDescription
AddFluentPatterns(this IServiceCollection services, Action<PatternRegistry> configure)IServiceCollectionRegisters a PatternRegistry singleton and the pattern route constraint. Calling it again adds to the same registry. In Microsoft.Extensions.DependencyInjection.
FluentPatternsServiceCollectionExtensions.ConstraintNameconst string"pattern", as in {id:pattern(sku)}.
PatternRegistry.Add(string name, Pattern pattern)PatternRegistryRegisters a pattern. Route values must match it completely. Throws ArgumentException for a blank or already registered name (compared ignoring case).
PatternRegistry.Add(string name, ConstrainedPattern pattern)PatternRegistryRegisters a pattern with rules; every rule must also pass.
PatternRegistry.Get(string name)ConstrainedPatternThe registered pattern. Throws InvalidOperationException that lists the registered names when it is missing.
PatternRegistry.TryGet(string name, out ConstrainedPattern? pattern)boolThe same without throwing.
PatternRegistry.Contains(string name)boolA pattern with the name exists.
PatternRegistry.NamesIReadOnlyList<string>Registered names in ordinal order.
PatternRouteConstraint(PatternRegistry registry, string name)The constraint behind pattern(name), an IRouteConstraint. Throws InvalidOperationException when no pattern has that name, so a typo fails when the route table is built. Name and Pattern (a ConstrainedPattern) return its settings; Match(...) accepts a route value only when it matches the whole pattern and every rule passes.

Names are case-insensitive. The registry is thread-safe. A route value is validated like ConstrainedPattern.IsValid, so even a pattern anchored with ^...$ rejects a value with a trailing newline (/products/ABC-1234%0A returns 404).

using System;
using Masterly.FluentRegex;
using Masterly.FluentRegex.AspNetCore;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;

// builder is your WebApplicationBuilder
builder.Services.AddFluentPatterns(registry => registry
.Add("sku", ShopPatterns.Sku)
.Add("card", ShopPatterns.Card.Must(Validators.Luhn, "Luhn checksum")));

WebApplication app = builder.Build();

app.MapGet("/products/{sku:pattern(sku)}", (string sku) => $"Product {sku}");
app.MapGet("/cards/{number:pattern(card)}", (string number) => "Valid card");

// GET /products/ABC-1234 -> 200
// GET /products/abc -> 404
// GET /cards/4111111111111111 -> 200
// GET /cards/4111111111111112 -> 404 (fails the Luhn rule)

PatternRegistry registry = app.Services.GetRequiredService<PatternRegistry>();
Console.WriteLine(string.Join(",", registry.Names)); // card,sku
Console.WriteLine(registry.Contains("SKU")); // True

public static class ShopPatterns
{
public static readonly Pattern Sku =
Pattern.With.StartOfLine.UppercaseLetter.Repeat.Times(3).Literal("-").AsciiDigit.Repeat.Times(4).EndOfLine;

public static readonly Pattern Card = Pattern.With.StartOfLine.AsciiDigit.Repeat.Times(16).EndOfLine;
}

Masterly.FluentRegex.Xunit​

Theory data attributes for xUnit v3 that generate test strings from a pattern. The pattern is read from a public static property, field or parameterless method. See xUnit.

MemberDescription
MatchingDataAttribute(Type patternSource, string memberName)Supplies strings the pattern matches.
NonMatchingDataAttribute(Type patternSource, string memberName)Supplies strings the pattern does not match, mostly near misses.
PatternDataAttributeThe abstract base, derived from xUnit's DataAttribute. PatternSource, MemberName, Count (default 25; fewer rows when the pattern has fewer distinct samples) and Seed (default 0). To supply your own samples, derive from it with a protected constructor call base(patternSource, memberName) and override both protected abstract string SampleKind { get; } (the word used in error messages, such as "matching") and protected abstract string[] Generate(Pattern pattern, int count, int seed).
using Masterly.FluentRegex;
using Masterly.FluentRegex.Xunit;
using Xunit;

public static class TestPatterns
{
public static Pattern Sku =>
Pattern.With.StartOfLine.UppercaseLetter.Repeat.Times(3).Literal("-").AsciiDigit.Repeat.Times(4).EndOfLine;
}

public class SkuTests
{
[Theory]
[MatchingData(typeof(TestPatterns), nameof(TestPatterns.Sku), Count = 10, Seed = 42)]
public void Sku_GeneratedValidValue_IsAccepted(string sku)
{
Assert.True(TestPatterns.Sku.IsMatch(sku));
}

[Theory]
[NonMatchingData(typeof(TestPatterns), nameof(TestPatterns.Sku), Count = 10)]
public void Sku_NearMiss_IsRejected(string sku)
{
Assert.False(TestPatterns.Sku.IsMatch(sku));
}
}
  • Each attribute supplies one string per row, so the theory's first parameter must accept a string.
  • A null type or a null or blank member name makes GetData throw InvalidOperationException with a message that names the attribute. The constructors do not throw, so xUnit can show that message.
  • When xUnit asks for data, the attributes throw InvalidOperationException with a clear message when Count is below 1, the theory's first parameter does not accept a string, the member is missing, is not a Pattern or returns null, or no samples could be generated.
  • Rows are deterministic, so they are listed during test discovery. Both attributes can be applied more than once.

Masterly.FluentRegex.AI​

Drafts a pattern from a plain-English description with Claude, then verifies every draft locally with the real .NET engine. See AI drafting.

MemberReturnsDescription
PatternAssistant(HttpClient httpClient, PatternAssistantOptions options)The options are copied, so later changes have no effect, and the HttpClient is not disposed. Throws ArgumentNullException for null arguments and ArgumentException or ArgumentOutOfRangeException for invalid options, such as a missing API key.
DraftAsync(string description, IEnumerable<string>? shouldMatch = null, IEnumerable<string>? shouldNotMatch = null, CancellationToken cancellationToken = default)Task<PatternDraft>Asks for a regex, verifies it and sends failures back as feedback, up to MaxAttempts drafts. Returns the first draft that passes, or the best failed one. Positive examples must match the whole string; negative examples must not match anywhere. Throws ArgumentException for an empty description or a null example, PatternAssistantException when the API returns an error, times out, is unreachable or declines, and OperationCanceledException when cancelled.
PatternAssistantOptions propertyDefaultDescription
ApiKeynoneThe Anthropic API key. Required. Never included in messages or ToString().
Modelclaude-opus-5-5 (DefaultModel)The Claude model id.
Effortmedium (DefaultEffort)low, medium, high, xhigh or max.
MaxAttempts3How many drafts the model may produce.
MaxTokens16000The response token limit per request.
Timeout5 minutesThe time limit per request, or Timeout.InfiniteTimeSpan.
BaseAddresshttps://api.anthropic.com/The API root.
UseServerSideFallbacktrueRetry a declined request on Anthropic's fallback model. Turn it off for gateways that do not support it.

DefaultModel and DefaultEffort are public constants. ToString() describes the settings with the key hidden:

using System;
using Masterly.FluentRegex.AI;

Console.WriteLine(new PatternAssistantOptions());
// Model = claude-opus-5-5, MaxAttempts = 3, BaseAddress = https://api.anthropic.com/, Timeout = 00:05:00, MaxTokens = 16000, Effort = medium, ApiKey = <not set>
TypeMembers
PatternDraftPattern (null when no valid syntax was produced), Regex, Explanation, Passed, Attempts, Verification. When no draft passed, this is the best failed attempt.
PatternVerificationIsValidSyntax, SyntaxError, SafetyReport, HasSafetyFindings, RejectedPositives, AcceptedNegatives, Samples (up to five generated matches), Passed. Passed means valid syntax, no safety findings (any SafetyReport issue, even Low), no rejected positives and no accepted negatives.
PatternAssistantExceptionStatusCode, ErrorType, RequestId, RetryAfter, IsTransient (rate limits, overload, server errors, timeouts and network failures).
using System;
using System.Net.Http;
using Masterly.FluentRegex;
using Masterly.FluentRegex.AI;

// http is an HttpClient from your application
PatternAssistant assistant = new PatternAssistant(http, new PatternAssistantOptions
{
ApiKey = Environment.GetEnvironmentVariable("ANTHROPIC_API_KEY") ?? "",
});

try
{
PatternDraft draft = await assistant.DraftAsync(
"a UK postcode such as SW1A 1AA",
shouldMatch: new[] { "SW1A 1AA", "M1 1AE" },
shouldNotMatch: new[] { "12345", "SW1A1AA1" });

Console.WriteLine(draft.Passed ? draft.Regex : draft.Verification.ToString());
}
catch (PatternAssistantException ex) when (ex.IsTransient)
{
Console.WriteLine($"Try again after {ex.RetryAfter}: {ex.Message}");
}

Masterly.FluentRegex.Tool​

The fluentregex command-line tool. Install it with dotnet tool install -g Masterly.FluentRegex.Tool. See Command-line tool.

CommandWhat it does
convert <regex> / convert --file <path>Prints fluent C# for a regex, or for every regex literal in C# files.
analyze <regex> [--confirm]Reports ReDoS risks.
scan <path> [--fail-on high|medium|none] [--confirm]Scans C# files for risky regex literals. CI friendly.
explain <regex>Explains a regex in plain English.
generate <regex> [--count N] [--seed N] [--non-matching]Generates matching or near-miss strings.
trace <regex> <input>Shows which part matched what, or where it failed.
diagram <regex> [--format svg|mermaid] [--output <file>]Draws a railroad diagram or a Mermaid flowchart.
compare <regexA> <regexB>Proves equivalence or shows the shortest counterexample.
optimize <regex>Simplifies a regex with verified rewrites.
learn <example>... [--not <example>...]Infers a regex from examples, or --from-file <path>.
profile <regex> <input>Counts backtracking steps and shows hotspots.
highlight <regex> <file|-> [--format ansi|html|brackets]Shows matches and groups in a text file.
coverage <regex> <file|->Reports which branches and limits the lines of a file exercise.
export <regex> [--to js|json-schema|typescript|postgresql|mysql|mariadb|sqlserver|sqlite] [--column <sql>] [--name <identifier>]Translates to another dialect, or a full WHERE condition with --column.
export <regex> --to like [--dialect ...] [--scope substring|full] [--case-insensitive] [--column <sql>]Compiles to SQL LIKE and proves whether it is exact.
facts <regex> [--scope full|substring]Proves length bounds and the required prefix and suffix.

Most commands also accept --options <list> (comma-separated RegexOptions) and --ascii-digits. --help and --version print help and the version.

fluentregex compare '\d{2}' '\d{1,3}'
# Not equivalent (left is a proper subset of right). Shortest counterexample: "0" (right accepts it, left rejects it). Both accept "00".
# exit code 1

Exit codes: 0 for success, 2 for a usage or input error, and 1 when a command reports a finding: analyze (high risk), scan (at or above --fail-on), compare (not equivalent), profile (step budget exhausted) and coverage (something uncovered).