StringBuilder Approach
Here's an Aggregate with a StringBuilder. There's some position determinations that are made to clean up the string and insert the "and" but it's all done at the StringBuilder level.
var people = new[]
{
new { Id = 1, Name = "John" },
new { Id = 2, Name = "Mark" },
new { Id = 3, Name = "George" }
};
var sb = people.Aggregate(new StringBuilder(),
(s, p) => s.AppendFormat("{0}:{1}, ", p.Id, p.Name));
sb.Remove(sb.Length - 2, 2); // remove the trailing comma and space
var last = people.Last();
// index to last comma (-2 accounts for ":" and space prior to last name)
int indexComma = sb.Length - last.Id.ToString().Length - last.Name.Length - 2;
sb.Remove(indexComma - 1, 1); // remove last comma between last 2 names
sb.Insert(indexComma, "and ");
// 1:John, 2:Mark and 3:George
Console.WriteLine(sb.ToString());
A String.Join approach could have been used instead but the "and" insertion and comma removal would generate ~2 new strings.
Regex Approach
Here's another approach using regex that is quite understandable (nothing too cryptic).
var people = new[]
{
new { Id = 1, Name = "John" },
new { Id = 2, Name = "Mark" },
new { Id = 3, Name = "George" }
};
var joined = String.Join(", ", people.Select(p => p.Id + ":" + p.Name).ToArray());
Regex rx = new Regex(", ", RegexOptions.RightToLeft);
string result = rx.Replace(joined, " and ", 1); // make 1 replacement only
Console.WriteLine(result);
The pattern is simply ", ". The magic lies in the RegexOptions.RightToLeft which makes the match occur from the right and thereby makes the replacement occur at the last comma occurrence. There is no static Regex method that accepts the number of replacements with the RegexOptions, hence the instance usage.