In this article, we are going to cover six different ways to concatenate lists in C#. After learning about every approach, we are going to evaluate some benchmarks to find out which is the most efficient approach. 

To download the source code for this article, you can visit our GitHub repository.

Let’s dive in.

Concatenate Lists in C# Using Add

Let’s create a UsingAdd method to concatenate two lists:

public List<string> UsingAdd(List<string> firstList, List<string> secondList)
{
    var result = new List<string>(firstList.Count() + secondList.Count());

    foreach (var item in firstList)
    {
        result.Add(item);
    }

    foreach (var item in secondList)
    {
        result.Add(item);
    }

    return result;
}

This method receives two lists as input parameters that we want to concatenate.

List works as a reference type, so if we modify it within our method, we will modify every reference. For this reason, changing one of the input lists is not a good practice. You can read more about lists in our article List Collection in C#.

That said, we instantiate a new string list to represent the result of our method. Also, we set up the list’s capacity to the sum of the two lists’ lengths we want to combine.

In the second step, we iterate through the firstList and add each element to the result list. Then, we repeat the process for the secondList.

Once we have iterated through the two lists, we return the result list containing the elements of both lists.

Concatenate Using Enumerable.Concat

We can use two extension methods in the Enumerable static class to concatenate two list elements. The first one is Enumerable.Concat:

public List<string> UsingEnumerableConcat(List<string> firstList, List<string> secondList)
{
    return Enumerable.Concat(firstList, secondList).ToList();
}

We simply call the Concat method from the Enumerable static class. This method requires two IEnumerable input parameters and returns a new IEnumerable containing every element of the input lists.

After that, we call the ToList() method and return the result.

Concatenate Lists in C# Using Enumerable.Union

The second method that we can use from the Enumerable static class is the Union method:

public List<string> UsingEnumerableUnion(List<string> firstList, List<string> secondList)
{
    return Enumerable.Union(firstList, secondList).ToList();
}

The Union method receives two parameters (firstList and secondList). After calling this method, it is necessary to call the ToList() method to create a new list from the IEnumerable and return it.

It is important to mention that Union method removes duplicated elements from its result

This method also provides an overloaded method with a third parameter representing an IEqualityComparer to compare the elements within the two lists. 

Concatenate Using AddRange 

Let’s implement a UsingAddRange method to concatenate two lists elements:

public List<string> UsingAddRange(List<string> firstList, List<string> secondlist)
{
    var result = new List<string>(firstList.Count() + secondList.Count());

    result.AddRange(firstList);
    result.AddRange(secondlist);

    return result;
}

The UsingAddRange method receives two lists as input parameters and returns a new list with the combined elements from the two lists.

First, we instantiate an object (result) that is going to be our method’s return.

Then, we call the AddRange method to add every element from the firstList inside the result object and repeat the process with the secondList

Finally, we return the result list with every element from both lists.

Similarly to the UsingAdd method, it is not a good practice to call the AddRange method against one of the input lists because we don’t want to change it.

Concatenate Using List.CopyTo

Let’s create a UsingCopyTo method to concatenate two lists using an array:

public List<string> UsingCopyTo(List<string> firstList, List<string> secondList)
{
    var combinedArray = new string[firstList.Count() + secondList.Count()];
    
    firstList.CopyTo(combinedArray, 0);
    secondList.CopyTo(combinedArray, firstList.Count());

    return combinedArray.ToList();
}

First, we instantiate a new array (combinedArray) and set its length to the sum of both input lists’ lengths. 

Then, we copy the first list of elements into the combinedArray starting at index 0.

After that, we repeat the process with the secondList, but this time, the start index needs to be the firstList.Count() (after the last element from the firstList). 

Finally, we call ToList() to return a new list of strings based on the combinedArray.

Concatenate Lists in C# Using SelectMany

Let’s concatenate two lists using SelectMany method:

public List<string> UsingSelectMany(List<string> firstList, List<string> secondList)
{
    var combinedArray = new[] { firstList, secondList}.SelectMany(x => x);

    return combinedArray.ToList();
}

This approach consists of instantiating a new array (combinedArray) and filling it with the two input parameter lists (firstList and secondList).

Then we call the SelectMany method to flatten the result into a single IEnumerable. The SelectMany method receives a Func delegate to apply to each element of the combinedArray.

Finally, we return a new list calling the ToList() method on the combinedArray

Benchmark

In this article, we are going to evaluate benchmark results against each approach with lists of 50 thousand elements and lists of 1 million elements.

Let’s start by running the benchmark with two lists of 50 thousand elements:

private readonly List<string> _firstList = Enumerable.Repeat("Code", 50_000).ToList();
private readonly List<string> _secondList = Enumerable.Repeat("Maze", 50_000).ToList();
|                Method |       Mean |    Error |    StdDev | Rank |    Gen 0 |    Gen 1 |    Gen 2 |   Allocated |
|---------------------- |-----------:|---------:|----------:|-----:|---------:|---------:|---------:|------------:|
|         UsingAddRange |   589.3 us |  4.19 us |  51.47 us |    1 |  19.5313 |  19.5313 |  19.5313 |   800,196 B |
| UsingEnumerableConcat |   596.2 us | 11.58 us |  15.85 us |    2 | 249.0234 | 249.0234 | 249.0234 |   800,140 B |
|       UsingSelectMany |   698.2 us | 16.16 us |  47.39 us |    3 |   8.7891 |   8.7891 |   8.7891 | 1,200,233 B |
|           UsingCopyTo | 1,097.7 us | 27.67 us |  78.48 us |    4 | 498.0469 | 498.0469 | 498.0469 | 1,600,248 B |
|              UsingAdd | 1,109.6 us | 13.40 us | 12.54 us  |    5 | 248.0469 | 248.0469 | 248.0469 |   800,140 B |
|  UsingEnumerableUnion | 2,704.1 us | 28.86 us |  24.10 us |    6 |        - |        - |        - |       402 B |

We can see that the UsingAddRange method is the fastest approach, more than 4,5 times faster than the slowest approach (UsingEnumerableUnion).

Let’s increase the number of elements to 1 million and run the benchmark:

|                Method |     Mean |    Error |   StdDev | Rank |    Gen 0 |    Gen 1 |    Gen 2 | Allocated |
|---------------------- |---------:|---------:|---------:|-----:|---------:|---------:|---------:|----------:|
|         UsingAddRange | 10.74 ms | 0.207 ms | 0.212 ms |    1 | 125.0000 | 125.0000 | 125.0000 | 15,626 KB |
| UsingEnumerableConcat | 10.89 ms | 0.195 ms | 0.314 ms |    2 | 125.0000 | 125.0000 | 125.0000 | 15,626 KB |
|       UsingSelectMany | 14.92 ms | 0.298 ms | 0.673 ms |    3 | 125.0000 | 125.0000 | 125.0000 | 23,438 KB |
|           UsingCopyTo | 18.93 ms | 0.433 ms | 0.844 ms |    4 | 187.5000 | 187.5000 | 187.5000 | 31,251 KB |
|              UsingAdd | 19.58 ms | 0.370 ms | 1.005 ms |    5 | 281.2500 | 281.2500 | 281.2500 | 15,625 KB |
|  UsingEnumerableUnion | 54.89 ms | 0.442 ms | 0.392 ms |    6 |        - |        - |        - |      1 KB |

Now that we are running a much heavier benchmark, we can see that our result isn’t in us (microsecond 0,000001 sec) anymore but in ms (0,001 sec).

The order of the approaches remains the same and the fastest approach is more than 5 times faster than the slowest approach.

Considering both benchmarks’ results (50 thousand elements and 1 million elements), the difference between the fastest and the second-fastest approach is irrelevant (0,7 us and 0,15 ms, respectively).

List Capacity Performance Impact

In C#, the List generic class has three constructors. In one of them, it is possible to specify the list’s capacity, as we did in the UsingAdd and UsingAddRange methods.

For this benchmark, let’s remove the list’s capacity from the constructors and run the benchmark with the lists of 50 thousand elements:

|                Method |       Mean |    Error |   StdDev | Rank |    Gen 0 |    Gen 1 |    Gen 2 |   Allocated |
|---------------------- |-----------:|---------:|----------:|-----:|---------:|---------:|---------:|------------:|
| UsingEnumerableConcat |   607.3 us | 11.96 us |  23.05 us |    1 | 249.0234 | 249.0234 | 249.0234 |   800,196 B |
|         UsingAddRange |   705.5 us | 17.58 us |  51.55 us |    2 |  15.6250 |  15.6250 |  15.6250 |  1,00,145 B |
|       UsingSelectMany |   797.5 us | 16.10 us |  47.22 us |    3 |  14.6484 |  14.6484 |  14.6484 | 1,200,256 B |
|           UsingCopyTo | 1,258.5 us | 24.89 us |  65.58 us |    4 | 498.0469 | 498.0469 | 498.0469 | 1,600,248 B |
|              UsingAdd | 1,693.1 us | 41.68 us | 120.91 us |    5 | 167.9688 | 105.4688 | 105.4688 | 2,745,462 B |
|  UsingEnumerableUnion | 2,675.8 us | 37.52 us |  31.33 us |    6 |        - |        - |        - |       402 B |

Note that when we don’t set the list’s capacity, the UsingAddRange loses the fastest position and becomes 14% slower than the UsingEnumerableConcat approach. Also, it becomes 16% slower than when we set the list’s capacity. 

Let’s evaluate the benchmark result with two lists of 1 million elements: 

|                Method |     Mean |    Error |   StdDev | Rank |    Gen 0 |    Gen 1 |    Gen 2 |    Allocated |
|---------------------- |---------:|---------:|---------:|-----:|---------:|---------:|---------:|-------------:|
| UsingEnumerableConcat | 11.06 ms | 0.218 ms | 0.478 ms |    1 | 125.0000 | 125.0000 | 125.0000 | 16,000,649 B |
|         UsingAddRange | 15.36 ms | 0.320 ms | 0.929 ms |    2 | 125.0000 | 125.0000 | 125.0000 | 24,000,632 B |
|       UsingSelectMany | 15.37 ms | 0.301 ms | 0.565 ms |    3 | 125.0000 | 125.0000 | 125.0000 | 24,000,645 B |
|           UsingCopyTo | 22.14 ms | 0.364 ms | 0.357 ms |    4 | 187.5000 | 187.5000 | 187.5000 | 32,000,915 B |
|              UsingAdd | 31.40 ms | 0.693 ms | 2.022 ms |    5 | 437.5000 | 375.0000 | 375.0000 | 33,555,977 B |
|  UsingEnumerableUnion | 54.36 ms | 0.872 ms | 0.816 ms |    6 |        - |        - |        - |        527 B |

This time, the difference between UsingAddRange and UsingEnumerableConcat is even more significant since UsingEnumerableConcat is 38% faster. 

When using the predefined list’s capacity, the UsingAddRange approach is 43% faster than when we are using the non-predefined list’s capacity.

The same applies to the UsingAdd approach, which achieves more than a 60% increase in performance when we set up the list’s capacity.

The memory allocation increases from 15 MB to 22 MB in the UsingAddRange approach and from 15 MB to 32 MB  in the UsingAdd approach when we compare the predefined and non-predefined list’s capacity benchmarks.

Conclusion

In this article, we have shown six different ways to concatenate lists in C#.

Also, we investigated the performance difference by running some benchmarks that showed us the fastest (UsingAddRange) and the slowest (UsingEnumerableUnion) approach. However, we can’t discard the Unionmethod, because, it can be useful in scenarios where we need to remove duplicates

Finally, we have seen that sometimes, using the list’s constructor with capacity is very useful to make our code more efficient.