DEV Community

Cover image for Stop Writing Unreadable Regex - Build Patterns the Fluent Way in C#
Ahmad Al-Freihat
Ahmad Al-Freihat

Posted on

Stop Writing Unreadable Regex - Build Patterns the Fluent Way in C#

The Problem Every Developer Knows

We've all been there. You're reviewing code and encounter something like this:

var regex = new Regex(@"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*()_+\-=]).{8,}$");
Enter fullscreen mode Exit fullscreen mode

What does it do? πŸ€”

You squint at it for 5 minutes, open regex101.com, and finally realize it's password validation. Now imagine coming back to this code 6 months later...

There's a Better Way

What if you could write regex like this instead?

Pattern password = Pattern.With
    .StartOfLine
    .PositiveLookahead(Pattern.With.Anything.Repeat.ZeroOrMore.Set(Pattern.With.LowercaseLetter))
    .PositiveLookahead(Pattern.With.Anything.Repeat.ZeroOrMore.Set(Pattern.With.UppercaseLetter))
    .PositiveLookahead(Pattern.With.Anything.Repeat.ZeroOrMore.Digit)
    .PositiveLookahead(Pattern.With.Anything.Repeat.ZeroOrMore.Set(Pattern.With.Literal("!@#$%^&*()_+-=")))
    .Anything.Repeat.AtLeast(8)
    .EndOfLine;
Enter fullscreen mode Exit fullscreen mode

Self-documenting. Readable. Maintainable.

This is Masterly.FluentRegex - and v2.0 just dropped with some killer features! πŸš€

What's New in v2.0

1. Named Groups & Backreferences

Extract data with meaningful names:

Pattern phone = Pattern.With
    .NamedGroup("area", Pattern.With.Digit.Repeat.Times(3))
    .Literal("-")
    .NamedGroup("exchange", Pattern.With.Digit.Repeat.Times(3))
    .Literal("-")
    .NamedGroup("number", Pattern.With.Digit.Repeat.Times(4));

Match match = phone.Match("555-123-4567");
Console.WriteLine(match.Groups["area"].Value);     // "555"
Console.WriteLine(match.Groups["exchange"].Value); // "123"
Console.WriteLine(match.Groups["number"].Value);   // "4567"
Enter fullscreen mode Exit fullscreen mode

Find duplicate words with backreferences:

Pattern duplicates = Pattern.With
    .WordBoundary
    .NamedGroup("word", Pattern.With.Word.Repeat.OneOrMore)
    .Whitespace.Repeat.OneOrMore
    .Backreference("word")
    .WordBoundary;

duplicates.IsMatch("the the");     // true
duplicates.IsMatch("hello world"); // false
Enter fullscreen mode Exit fullscreen mode

2. Lookahead & Lookbehind

Zero-width assertions made simple:

// Match digits only if preceded by '$'
Pattern price = Pattern.With
    .PositiveLookbehind(Pattern.With.Literal("$"))
    .Digit.Repeat.OneOrMore;

var match = price.Match("Price: $100");
Console.WriteLine(match.Value); // "100" (not "$100"!)

// Match 'q' only if NOT followed by 'u'
Pattern qNotU = Pattern.With
    .Literal("q")
    .NegativeLookahead(Pattern.With.Literal("u"));

qNotU.IsMatch("iraq");  // true
qNotU.IsMatch("queen"); // false
Enter fullscreen mode Exit fullscreen mode

3. Full Matching API

No need to call ToRegex() anymore:

Pattern digits = Pattern.With.Digit.Repeat.OneOrMore;

// Direct matching
digits.IsMatch("abc123");           // true
digits.Match("abc123def456");       // First match: "123"
digits.Matches("abc123def456");     // All matches: ["123", "456"]

// Replace & Split
digits.Replace("a1b2c3", "X");      // "aXbXcX"
digits.Replace("a1b2", m => (int.Parse(m.Value) * 2).ToString()); // "a2b4"

Pattern comma = Pattern.With.Literal(",");
comma.Split("a,b,c");               // ["a", "b", "c"]
Enter fullscreen mode Exit fullscreen mode

4. 20+ Pre-built Common Patterns

Stop reinventing the wheel:

// Email
CommonPatterns.Email().IsMatch("user@example.com"); // true

// URLs
CommonPatterns.Url().IsMatch("https://dev.to"); // true

// IP Addresses
CommonPatterns.IPv4().IsMatch("192.168.1.1"); // true
CommonPatterns.IPv4().IsMatch("256.1.1.1");   // false (validates 0-255!)

// And many more...
CommonPatterns.Guid();
CommonPatterns.HexColor();
CommonPatterns.CreditCard();
CommonPatterns.PhoneNumber();
CommonPatterns.StrongPassword();
CommonPatterns.DateIso();
CommonPatterns.Time24();
// ... and more!
Enter fullscreen mode Exit fullscreen mode

5. Debugging Tools

Understand any pattern instantly:

Pattern pattern = Pattern.With
    .StartOfLine
    .NamedGroup("digits", Pattern.With.Digit.Repeat.OneOrMore)
    .EndOfLine;

Console.WriteLine(pattern.Explain());
Enter fullscreen mode Exit fullscreen mode

Output:

Pattern: ^(?<digits>\d+)$

Explanation:
  - Start of line/string
  - Named capturing group 'digits'
    - Digit [0-9]
    - One or more
  - End of group
  - End of line/string
Enter fullscreen mode Exit fullscreen mode

Validate before runtime errors:

Pattern invalid = Pattern.With.RegEx("[unclosed");

if (!invalid.TryValidate(out string error))
{
    Console.WriteLine($"Invalid: {error}");
}
Enter fullscreen mode Exit fullscreen mode

Real-World Example: Log Parser

Pattern logEntry = Pattern.With
    .StartOfLine
    .Literal("[")
    .NamedGroup("date", Pattern.With.Digit.Repeat.Times(4)
        .Literal("-").Digit.Repeat.Times(2)
        .Literal("-").Digit.Repeat.Times(2))
    .Whitespace
    .NamedGroup("time", Pattern.With.Digit.Repeat.Times(2)
        .Literal(":").Digit.Repeat.Times(2)
        .Literal(":").Digit.Repeat.Times(2))
    .Literal("]")
    .Whitespace
    .Literal("[")
    .NamedGroup("level", Pattern.With.Word.Repeat.OneOrMore)
    .Literal("]")
    .Whitespace
    .NamedGroup("message", Pattern.With.Anything.Repeat.OneOrMore)
    .EndOfLine;

string log = "[2024-01-15 10:30:45] [ERROR] Connection timeout";
Match m = logEntry.Match(log);

Console.WriteLine($"Level: {m.Groups["level"].Value}");   // "ERROR"
Console.WriteLine($"Message: {m.Groups["message"].Value}"); // "Connection timeout"
Enter fullscreen mode Exit fullscreen mode

Get Started

dotnet add package Masterly.FluentRegex
Enter fullscreen mode Exit fullscreen mode
using Masterly.FluentRegex;

// Start building!
Pattern myPattern = Pattern.With
    .StartOfLine
    .Digit.Repeat.OneOrMore
    .EndOfLine;
Enter fullscreen mode Exit fullscreen mode

Links


What regex patterns do you find yourself writing repeatedly? Drop a comment below - maybe we can add them to the CommonPatterns library! πŸ‘‡

If this helps you write better regex, consider giving it a ⭐ on GitHub!

Top comments (2)

Collapse
 
xwero profile image
david duymelinck

I used your post and library as inspiration for a post of mine; dev.to/xwero/php-fun-regex-builder.... I hope you don't mind.

Collapse
 
a7mdfre7at profile image
Ahmad Al-Freihat

No problem.