C#
String Manipulation
List Conversion
Delimiter
Programming

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.

csharp
1using System;
2using System.Collections.Generic;
3
4class Program
5{
6    static void Main()
7    {
8        List<string> fruits = new List<string> { "Apple", "Banana", "Cherry" };
9        string result = string.Join(", ", fruits);
10        Console.WriteLine(result);  // Output: Apple, Banana, Cherry
11    }
12}

The method has several overloads, but the two most relevant ones for list conversion are:

csharp
1// Accepts IEnumerable<string>
2public static string Join(string separator, IEnumerable<string> values);
3
4// Accepts an object array (calls ToString() on each element)
5public static string Join(string separator, params object[] values);

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.

csharp
List<int> ids = new List<int> { 101, 202, 303 };
string csv = string.Join(",", ids);
Console.WriteLine(csv);  // Output: 101,202,303

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:

csharp
1using System;
2using System.Collections.Generic;
3using System.Linq;
4
5public static class StringHelper
6{
7    public static string JoinSafe(IEnumerable<string> values, string delimiter)
8    {
9        if (values == null || !values.Any())
10            return string.Empty;
11
12        return string.Join(delimiter, values);
13    }
14
15    public static string JoinNonEmpty(IEnumerable<string> values, string delimiter)
16    {
17        if (values == null)
18            return string.Empty;
19
20        return string.Join(delimiter, values.Where(v => !string.IsNullOrEmpty(v)));
21    }
22}

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:

csharp
1using System;
2using System.Collections.Generic;
3using System.Text;
4
5class Program
6{
7    static void Main()
8    {
9        List<string> tags = new List<string> { "csharp", "dotnet", "linq" };
10        var sb = new StringBuilder();
11
12        for (int i = 0; i < tags.Count; i++)
13        {
14            if (i > 0)
15                sb.Append(" | ");
16            sb.Append('#').Append(tags[i]);
17        }
18
19        Console.WriteLine(sb.ToString());
20        // Output: #csharp | #dotnet | #linq
21    }
22}

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:

csharp
List<string> words = new List<string> { "Hello", "World" };
string result = words.Aggregate((current, next) => current + ", " + next);
Console.WriteLine(result);  // Output: Hello, World

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

MethodReadabilityPerformanceConditional LogicHandles Empty List
string.Join()ExcellentExcellent (uses StringBuilder internally)LimitedYes (returns "")
StringBuilder loopGoodExcellentFull controlMust handle manually
LINQ AggregateModeratePoor (O(n) allocations)ModerateThrows on empty list
string.Concat()GoodGoodNone (no separator)Yes (returns "")

Real-World Examples

Building a CSV Line

csharp
List<string> fields = new List<string> { "John", "Doe", "42", "Engineering" };
string csvLine = string.Join(",", fields.Select(f => f.Contains(",") ? $"\"{f}\"" : f));
Console.WriteLine(csvLine);  // Output: John,Doe,42,Engineering

Creating a SQL IN Clause

csharp
1List<string> userIds = new List<string> { "u001", "u002", "u003" };
2string inClause = string.Join(", ", userIds.Select(id => $"'{id}'"));
3Console.WriteLine($"SELECT * FROM users WHERE id IN ({inClause})");
4// Output: SELECT * FROM users WHERE id IN ('u001', 'u002', 'u003')

Logging Multiple Values

csharp
1List<string> errors = new List<string>
2{
3    "Timeout on service A",
4    "Null reference in handler B",
5    "Auth token expired"
6};
7
8string logEntry = $"[{DateTime.UtcNow:O}] Errors: {string.Join(" | ", errors)}";
9Console.WriteLine(logEntry);

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.

csharp
1using (var writer = new StreamWriter("output.txt"))
2{
3    for (int i = 0; i < largeList.Count; i++)
4    {
5        if (i > 0)
6            writer.Write(",");
7        writer.Write(largeList[i]);
8    }
9}

Common Pitfalls

  • Using + concatenation in a loop instead of string.Join() or StringBuilder. Every + allocates a new string, turning an O(n) operation into O(n^2) memory usage.
  • Calling Aggregate on an empty list without a seed value. It throws InvalidOperationException. Always prefer string.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.


Course illustration
Course illustration

All Rights Reserved.