Regex pattern definition implementation My requirement is to design a part of a solution in such matter that specific placeholders are replaces dynamically with a defined logic and new rules can be easily added.

Simple example:


  Today is {today}. The year is {year}. Last year was {year-1}.


Should output something like:


  Today is 26 Jan 2018. The year is 2018. Last year was 2017.


There are several more business and context specific placeholders and new rules can follow frequently.

As such I came up with this class:

public class PlaceholderDefinition
{
    public string Name { get; }
    public string Pattern { get; }
    private readonly Func&lt;Match, string&gt; _logic;

    public PlaceholderDefinition(string name, string pattern, Func&lt;Match, string&gt; logic)
    {
        Name = name;
        Pattern = pattern;
        _logic = logic;
    }

    public string Apply(Match match) =&gt; _logic.Invoke(match);
}


To fulfill the above example, I register the following PlaceholderDefinitions in the service configuration:

RegisteredPlaceholders.Add(
    new PlaceholderDefinition(
        "Today",
        @"{[tT][oO][dD][aA][yY]}",
        (_) =&gt; DateTime.Today.ToShortDateString()
    ));

RegisteredPlaceholders.Add(
    new PlaceholderDefinition(
        "Year",
        @"{[yY][eE][aA][rR]}",
        (_) =&gt; DateTime.Today.Year.ToString()
    ));

RegisteredPlaceholders.Add(
    new PlaceholderDefinition(
        "YearAddition",
        @"(?:{[yY][eE][aA][rR])([+-])(\d+)}",
        (m) =&gt;
        {
            var operation = m.Groups[1].Value;
            int.TryParse(m.Groups[2].Value, out int value);
            return (operation == "+" ? DateTime.Today.Year + value : DateTime.Today.Year - value).ToString();
        }
    ));


The RegisteredPlaceholders is an enumerable of PlaceholderDefinition held in the container.

The logic of these patterns/placeholders is applied like this:

foreach (var placeholder in RegisteredPlaceholders)
 {
     var match = new Regex(placeholder.Pattern).Match(content);
     while (match.Success)
     {
         content = content.Remove(match.Index, match.Length).Insert(match.Index, placeholder.Apply(match));
         match = match.NextMatch();
     }
 }


I decided to take this approach instead of using Regex.Replace() to not reveal the content or text to the class defining the pattern logic. Maybe there is a more elegant solution, that I didn't come up with. 

Every critique, improvement proposal, code smells are welcome.
Try to use named group. As your pattern becomes more complex, it will get harder to maintain.

@"(?:{[yY][eE][aA][rR])([+-])(\d+)}"
var operation = m.Groups[1].Value;
int.TryParse(m.Groups[2].Value, out int value);

PlaceholderDefinition implementation requires way too much boilerplate code to use:

 foreach (var placeholder in RegisteredPlaceholders)
  {
      var match = new Regex(placeholder.Pattern).Match(content);
      while (match.Success)
      {
          content = content.Remove(match.Index, match.Length).Insert(match.Index, placeholder.Apply(match));
          match = match.NextMatch();
      }
  }


It could be boiled down to: content = placeholder.Apply(text);
There is no need to use Match.NextMatch &amp; string.Remove, when you can use Regex.Replace
_logic seems to be a poorly chosen name. While it describes the essence, but not its concrete job. You should name Func with "Selector, factory, builder, ...", and prefix it with a noun ("result, replacement, ...") and the private member prefix _ (if this is the convention, you choose to follow).




Modified PlaceholderDefinition class:

public class PlaceholderDefinition
{
    public string Name { get; }
    public Regex Pattern { get; }

    private readonly Func&lt;GroupCollection, object&gt; _replacementSelector;

    public PlaceholderDefinition(string name, string pattern, Func&lt;GroupCollection, object&gt; replacementSelector)
         : this(name, new Regex(pattern), replacementSelector)
    {
    }
    public PlaceholderDefinition(string name, string pattern, RegexOptions options, Func&lt;GroupCollection, object&gt; replacementSelector)
         : this(name, new Regex(pattern, options), replacementSelector)
    {
    }
    public PlaceholderDefinition(string name, Regex pattern, Func&lt;GroupCollection, object&gt; replacementSelector)
    {
        this.Name = name;
        this.Pattern = pattern;
        this._replacementSelector = replacementSelector;
    }

    public string Apply(string input) =&gt; Pattern.Replace(input, m =&gt; _replacementSelector(m.Groups).ToString());
}


There is various overloads of ctor to accommodate different needs, like the one suggested by @t3chb0t. Feels free to add more, if it help you to keep the declarations as clean as possible.

And, the rest of the code:

var text = "Today is {today}. The year is {year}. Last year was {year-1}.";
var placeholders = new List&lt;PlaceholderDefinition&gt;();
placeholders.Add(new PlaceholderDefinition("Today", @"{today}", RegexOptions.IgnoreCase, _ =&gt; DateTime.Today.ToShortDateString()));
placeholders.Add(new PlaceholderDefinition("Year", @"{year}", RegexOptions.IgnoreCase, _ =&gt; DateTime.Today.Year));
placeholders.Add(new PlaceholderDefinition(
    "YearAddition", 
    @"{year(?&lt;sign&gt;[+-])(?&lt;value&gt;\d+)}",
    RegexOptions.IgnoreCase, 
    g =&gt; DateTime.Today.Year + int.Parse(g["sign"].Value + g["value"].Value)
));

foreach (var placeholder in placeholders)
{
    text = placeholder.Apply(text);
}





The _replacementSelector takes in GroupCollection, so we can skip writing m =&gt; m.Group..., and the object TResult allows us to skip the necessary .ToString() or a parenthesis on binary operation.
int.Parse will parse number prefixed by a + sign as positive number, allowing us to skip the ternary operation.