Updated on
string.Join() builds a comma-separated string from a list in one call: string.Join(",", fruitList) returns apple,orange,pineapple,grape,coconut.
The first argument is the separator and the second is the collection. The separator goes between the members and nowhere else, so five fruits produce four commas and there is no trailing one to strip afterwards.
How Does string.Join() Turn a List Into a String?
string.Join() is a static method on string. It walks a collection once and returns a single string with the separator placed between each pair of members.
The overload at work here is Join(String, IEnumerable<String>). The separator is a full string rather than a single character, so ", " with a trailing space is as valid as ",", and a separator of several characters is fine too.
The separator lands between members and nowhere else. A five-item list produces four commas, a one-item list produces none, and an empty list returns an empty string, so none of the trailing-comma cleanup that hand-rolled loops need applies here.
Because the second parameter is an IEnumerable<string>, anything that yields strings fits: a List<string>, a string[], a HashSet<string>, or the result of a LINQ query. That is why filtering costs nothing extra. fruitList.Where(fruit => fruit.Contains("apple")) is still an IEnumerable<string>, so it goes straight into the same call with no ToList() in between.
The String.Join documentation on Microsoft Learn puts the separator rule in one line: “separator is included in the returned string only if values has more than one element.”
Here is the call on the list this article works with:
var fruitList = new List<string>
{
"apple",
"orange",
"pineapple",
"grape",
"coconut"
};
var fruits = string.Join(",", fruitList);
Console.WriteLine($"Fruits: {fruits}");
This is the two-parameter overload of string.Join():
Join(String, IEnumerable<String>)
The string parameter represents the separator between each member of the list, which in this case is a comma. While the second parameter is the list we are concatenating with the specified separator.
The second parameter is an IEnumerable<string>, so a LINQ query can go in directly:
var filterFruit = string.Join(",", fruitList.Where(fruit => fruit.Contains("apple")));
Console.WriteLine($"Filtered Fruit: {filterFruit}");
That returns apple,pineapple: only the members whose text contains “apple”.
That IEnumerable<string> parameter is worth understanding on its own, because it is what makes the same call work on lists, arrays and query results alike.
How Do We Join Only Part of a List?
Two overloads take a start index and a count: Join(String, String[], Int32, Int32) and its char twin. Both want an array, which is why the list is converted with ToArray() first.
string.Join(",", fruitList.ToArray(), 2, 3) starts at index 2 and takes three elements, so it returns pineapple,grape,coconut.
The bounds are checked rather than clamped. A negative startIndex or count throws ArgumentOutOfRangeException, and so does a startIndex greater than the array length minus count. A count of zero returns an empty string, and a null array throws ArgumentNullException.
LINQ reaches the same result without the array: string.Join(",", fruitList.Skip(2).Take(3)) works on any IEnumerable<string> and reads closer to the intent. The indexed overload is the older shape and the LINQ one is easier to scan, so pick on readability rather than on a guess about which is faster.
The sample uses the indexed overload directly:
var trimmedFruits = string.Join(",", fruitList.ToArray(), 2, 3);
Console.WriteLine($"Trimmed Fruits: {trimmedFruits}");
In this case, we are using an overload of string.Join() that accepts four parameters:
Join(String, String[], Int32, Int32)
The string as usual, represents the separator. The second parameter is the array of strings we wish to concatenate. This is the reason we are converting fruitList to an array. The third parameter is the index position we want to commence our concatenation from. Lastly, the fourth parameter represents the number of elements to return from the list.
So, since we specify that we want to start from the second index position and return 3 elements, we get:
pineapple,grape,coconut
Which string.Join() Overload Should We Use?
The choice comes down to two questions: what the collection holds, and what the separator is.
If the collection holds strings, Join(String, IEnumerable<String>) takes it as it is. If it holds anything else, the generic Join<T>(String, IEnumerable<T>) takes it and calls ToString() on every member, so string.Join(",", new List<int> { 1, 2, 3 }) returns 1,2,3 with no Select(n => n.ToString()) in front of it. Microsoft Learn calls this a convenience method for exactly that reason.
A one-character separator has its own overloads that take a char. string.Join(',', fruitList) is the same call with the quotes gone.
Nulls are absorbed, not thrown. A null separator is treated as an empty string, and any null member becomes an empty string, so a list with a gap in it produces two commas in a row rather than an exception. That is convenient right up to the moment the output is meant to be a data format, where an invisible empty field is worse than a crash.
A List<int> goes into the same call with nothing in front of it:
var numbers = new List<int> { 1, 2, 3 };
var numberList = string.Join(",", numbers);
That gives 1,2,3. And a single-character separator can be a char rather than a string:
var fruitsWithCharSeparator = string.Join(',', fruitList);
That gives the same apple,orange,pineapple,grape,coconut as the "," form.
| What we have | What to call | What happens |
|---|---|---|
a List<string> or any IEnumerable<string> | string.Join(",", fruitList) | Members are concatenated with the separator between them |
a collection of anything else, such as List<int> | string.Join(",", numbers) | The generic overload calls ToString() on every member first |
a string[] | string.Join(",", fruitArray) | Same result as the IEnumerable<string> overload |
part of a string[], by position | string.Join(",", fruitArray, 2, 3) | Three elements starting at index 2 |
| a single-character separator | string.Join(',', fruitList) | A char separator, no quotes to escape |
a list containing null entries | string.Join(",", listWithNulls) | Each null becomes an empty string, so two commas appear in a row |
The same method is the answer when the source is an array rather than a list, which is the subject of converting a string array to a single string.
Building a string in a loop instead is the case where StringBuilder belongs. string.Join() already does one pass over the collection, so wrapping it in a loop gives back the thing it was avoiding.
Joining two lists into one list is a different job from joining one list into a string, and it has its own article: concatenating lists in C#.
Going the other way, from a comma-separated string back to a list, is String.Split() rather than string.Join(), and it has its own article with every overload.
Joining with commas is not the same as producing CSV. A member that itself contains a comma or a quote needs escaping, which string.Join() does not do, so a real CSV file wants the proper writing routine.
Conclusion
In this article, we have learned how to return a comma-separated string from a list of strings in C#. We’ve also seen how we can filter our results or return only a small portion of our collection as a comma-separated string.
Tested with .NET 10.0.10.
