I don't think using the ${groupname} in the manner you suggested is feasible unless I am misunderstanding the exact replacement that's being performed. The reason is the replacement string has to be constructed in such a way that it accounts for each group name. Since they are dynamically generated this doesn't make it possible. In other words how, in 1 statement, do you design a replacement string that would cover c0...cn and substitute their respective capture values? You could loop through the names but how would you keep the modified text intact to perform 1 replacement per groupname?
I do have a possible solution for you though. It still uses the MatchEvaluator overload, but with some lambda expressions and LINQ you can get this down to 1 line. However, I'll format it for clarity below. Perhaps this will fit your needs or point you in the right direction.
string text = @"The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog.";
string[] searchKeywords = { "quick", "fox", "lazy" };
// build pattern based on keywords - you probably had a routine in place for this
var patternQuery = searchKeywords
.Select((s, i) =>
String.Format("(?<c{0}>{1})", i, s) +
(i < searchKeywords.Length - 1 ? "|" : ""))
.Distinct();
string pattern = String.Join("", patternQuery.ToArray());
Console.WriteLine("Dynamic pattern: {0}\n", pattern);
// use RegexOptions.IgnoreCase for case-insensitve search
Regex rx = new Regex(pattern);
// Notes:
// - Skip(1): used to ignore first groupname of 0 (entire match)
// - The idea is to use the groupname and its corresponding match. The Where
// clause matches the pair up correctly based on the current match value
// and returns the appropriate groupname
string result = rx.Replace(text, m => String.Format(@"<span class=""{0}"">{1}</span>",
rx.GetGroupNames()
.Skip(1)
.Where(g => m.Value == m.Groups[rx.GroupNumberFromName(g)].Value)
.Single(),
m.Value));
Console.WriteLine("Original Text: {0}\n", text);
Console.WriteLine("Result: {0}", result);
Output:
Dynamic pattern: (?<c0>quick)|(?<c1>fox)|(?<c2>lazy)
Original Text: The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog.
Result: The <span class="c0">quick</span> brown <span class="c1">fox</span> jumps over the <span class="c2">lazy</span> dog. The <span class="c0">quick</span> brown <span class="c1">fox</span> jumps over the <span class="c2">lazy</span> dog.