Regex pattern definition implementation
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.