C# List<string> to string with delimiter
Master System Design with Codemia
Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.
The fastest way to convert a List<string> to a single delimited string in C# is string.Join(). It accepts a separator and an enumerable, and it returns the concatenated result in one call. This article covers string.Join() in depth, compares it to alternatives like StringBuilder and LINQ aggregation, and walks through edge cases you will encounter in production code.
Using string.Join()
string.Join() is the idiomatic C# approach for this task. It concatenates every element in a collection, inserting the separator between each pair.
The method has several overloads, but the two most relevant ones for list conversion are:
The first overload is what you use with List<string>. The second overload is useful when the list contains non-string types and you want automatic ToString() conversion.
Handling Null, Empty, and Mixed Lists
Production code must account for lists that are null, empty, or contain null elements. Here is a utility method that covers all three cases:
An important detail: string.Join() treats null elements as empty strings. It does not throw. So string.Join(", ", new[] { "A", null, "C" }) produces "A", , C". If that behavior is undesirable, filter nulls out before joining, as JoinNonEmpty does above.
Alternatives to string.Join()
StringBuilder in a Loop
When the concatenation logic involves conditional formatting, StringBuilder gives you full control:
This approach is useful when each element needs transformation beyond what a simple Select() projection would handle cleanly.
LINQ Aggregate
Aggregate can also build a delimited string, but it is slower and less readable for this use case:
Aggregate creates intermediate string allocations on every iteration because it uses plain concatenation. For a list of 10,000 elements, this becomes meaningfully slower than string.Join().
Comparison Table
| Method | Readability | Performance | Conditional Logic | Handles Empty List |
string.Join() | Excellent | Excellent (uses StringBuilder internally) | Limited | Yes (returns "") |
StringBuilder loop | Good | Excellent | Full control | Must handle manually |
LINQ Aggregate | Moderate | Poor (O(n) allocations) | Moderate | Throws on empty list |
string.Concat() | Good | Good | None (no separator) | Yes (returns "") |
Real-World Examples
Building a CSV Line
Creating a SQL IN Clause
Logging Multiple Values
Performance Considerations
string.Join() internally allocates a single StringBuilder, calculates the total required length upfront, and fills it in one pass. This makes it significantly faster than manual concatenation with + or Aggregate.
For very large lists (100,000+ elements), the primary bottleneck shifts from CPU to memory. Each call produces a single contiguous string in memory. If you are writing to a stream or file, consider writing elements individually with a StreamWriter instead of building one massive string in memory first.
Common Pitfalls
- Using
+concatenation in a loop instead ofstring.Join()orStringBuilder. Every+allocates a new string, turning an O(n) operation into O(n^2) memory usage. - Calling
Aggregateon an empty list without a seed value. It throwsInvalidOperationException. Always preferstring.Join(), which safely returns an empty string. - Forgetting that
string.Join()treats null elements as empty strings. If your list is{ "A", null, "B" }, the output is"A, , B"with a visible gap. - Not escaping elements that contain the delimiter character. If you are building CSV output and a field contains a comma, the result is silently malformed.
- Building a massive string in memory when the output destination is a file or stream. Write directly to the output instead.
Summary
string.Join() is the correct default for converting a List<string> to a delimited string in C#. It is concise, performant, and handles edge cases like empty lists gracefully. Use StringBuilder when you need conditional formatting per element, and avoid Aggregate for this use case due to its poor allocation behavior. Always filter or escape list elements before joining when the data is not fully controlled.

