Introduction: Why Joining Strings Is Not Just "Concatenation"
\n\nImagine: you are writing a web service in C# that receives a list of tags from a user and needs to send them to the database as a single comma-separated string. Or you are generating a CSV report for export. Or you are logging a set of identifiers. Sound familiar? The question "Creating a comma separated list from IList
It seems simple enough, right? Take a list of strings, loop through it, add commas. But in practice, this task hides several pitfalls: from trailing commas to catastrophic performance degradation on large data. In this article, we will not just show ready-made solutions but look under the hood of each one. You will learn how string.Join, StringBuilder, and even LINQ Aggregate work, and learn to choose the right tool for your task.
Let's go!
\n\nThe Problem: A Simple Request, Non-Obvious Complexities
\n\nSuppose we have a collection of strings:
\n\nIEnumerable<string> fruits = new List<string> { "Apple", "Banana", "Cherry", "Date" };\n\nWe want to get: "Apple,Banana,Cherry,Date". The first thing that comes to a beginner's mind is to write a loop:
string result = "";\nbool first = true;\nforeach (var fruit in fruits)\n{\n if (!first)\n result += ",";\n result += fruit;\n first = false;\n}\n\nThis code works, but it does so terribly. Why? Because strings in C# are immutable. Each += operation creates a new string in memory, copying the contents of the previous one. With 4 elements, this is unnoticeable, but with 10,000 strings, you get quadratic complexity O(n²) and massive memory consumption. Under the hood — thousands of allocations and garbage collections.
Clearly, a smarter approach is needed. And here come the main protagonists.
\n\nMethod 1: string.Join — The King of Performance
\n\nThe most correct and performant way is to use the static method string.Join:
string csv = string.Join(",", fruits);\nConsole.WriteLine(csv); // Apple,Banana,Cherry,Date\n\nOne line of code — and it's done. Let's look under the hood of string.Join (in .NET Core / .NET 5+).
What happens inside?
\n- \n
- Length calculation: The method first iterates through the entire collection (if it's an
IEnumerable<T>) to find the total number of characters. If the collection is an array orIList, it immediately gets the length viaCount. \n - Buffer allocation: One large character array (
char[]) of the required size is allocated: the sum of all string lengths + (number of strings - 1) * separator length. \n - Copying: All strings and separators are copied into this buffer without a single extra allocation. \n
- String creation: One final string is created from the buffer. \n
Result: one memory allocation (for the final string) + allocation of a temporary buffer. No intermediate strings. Complexity — strictly O(n).
\n\nFor IList<string>, the method works even faster because it can immediately get Count and the length of each element via an indexer, without calling GetEnumerator.
Method 2: StringBuilder — An Old Friend
\n\nBefore the optimized string.Join appeared (or if you are stuck on an older version of .NET Framework), the standard solution was StringBuilder:
var sb = new StringBuilder();\nbool first = true;\nforeach (var fruit in fruits)\n{\n if (!first)\n sb.Append(',');\n sb