Back to Blog
C#

C# LINQ Union: Merging Sequences Without Duplicates

c# linq union: Learn how C# LINQ Union merges two sequences while removing duplicates, how it differs from Concat, and how to control equality for custom types.

LINQC#.NETSet OperationsIEnumerable
Diagram showing two overlapping sets merging into a single distinct set, illustrating the C# LINQ Union operation.

c# linq union requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.

In C#, the LINQ Union method combines two sequences and returns only distinct elements. If the same element appears in both input sequences, it appears exactly once in the result. This makes Union the standard tool for merging collections when duplicates should be eliminated, and it is the direct LINQ counterpart to the mathematical set union operation.

int[] first = { 1, 2, 3, 4 }; int[] second = { 3, 4, 5, 6 }; var union = first.Union(second); // Result: 1, 2, 3, 4, 5, 6 var concat = first.Concat(second); // Result: 1, 2, 3, 4, 3, 4, 5, 6

The method uses the default equality comparer for the element type unless you supply a custom one. For value types like int, equality is based on the value. For reference types, equality is based on reference identity unless the type overrides Equals and GetHashCode.

What LINQ Union Does

Union performs a set union over two sequences. It enumerates the first sequence, yields each element, and tracks every element it has already produced in an internal hash set. It then enumerates the second sequence and yields only those elements that were not already present. The result is an IEnumerable<T> that contains every distinct element from both inputs.

The signature has two overloads. The first takes a single IEnumerable<T> argument and uses the default equality comparer. The second takes an additional IEqualityComparer<T> parameter, which lets you control how elements are compared for equality.

public static IEnumerable<TSource> Union<TSource>( this IEnumerable<TSource> first, IEnumerable<TSource> second); public static IEnumerable<TSource> Union<TSource>( this IEnumerable<TSource> first, IEnumerable<TSource> second, IEqualityComparer<TSource>? comparer);

Both overloads throw ArgumentNullException if either source sequence is null. The comparer parameter itself may be null, in which case the default equality comparer is used.

How Union Differs from Concat

The practical difference between Union and Concat is whether duplicates matter in your result. If you are merging two lists of unique IDs, Union gives you the combined set without needing a separate Distinct call. Concat preserves every element, which is useful when order or multiplicity carries meaning.

OperationDuplicates removedOrder preservedTypical use
UnionYesYes (first occurrence)Merging distinct sets
ConcatNoYesAppending sequences

Order is preserved in the sense that elements from the first sequence appear before elements from the second, and the first occurrence of a duplicate is kept. This behavior matches the .NET implementation, though you should not rely on it as a formal guarantee across all LINQ providers.

Working with Custom Types

When your sequences contain custom classes, Union uses reference equality by default. Two objects with identical property values are treated as different elements because they are different references.

public record Product(int Id, string Name); Product[] warehouse = { new(1, "Laptop"), new(2, "Mouse") }; Product[] shipment = { new(1, "Laptop"), new(3, "Keyboard") }; var combined = warehouse.Union(shipment); // Result: 4 elements, because the two Product(1, "Laptop") instances // are distinct references

To make Union treat equal-valued objects as duplicates, you have two options: implement IEquatable<T> on the type, or pass a custom IEqualityComparer<T> to the Union overload.

public class ProductComparer : IEqualityComparer<Product> { public bool Equals(Product? x, Product? y) => x?.Id == y?.Id; public int GetHashCode(Product obj) => obj.Id.GetHashCode(); } var combined = warehouse.Union(shipment, new ProductComparer()); // Result: 3 elements: Laptop, Mouse, Keyboard

The comparer must implement both Equals and GetHashCode consistently. If two objects return true from Equals, they must return the same hash code; otherwise the internal hash-based deduplication will not work correctly.

Union with Anonymous Types

Anonymous types are a convenient case because the compiler generates value-based Equals and GetHashCode implementations automatically. Two anonymous objects with the same property names and values compare as equal, so Union deduplicates them without any custom comparer.

var local = new[] { new { Id = 1, Name = "Laptop" } }; var remote = new[] { new { Id = 1, Name = "Laptop" }, new { Id = 2, Name = "Mouse" } }; var merged = local.Union(remote); // Result: 2 elements, because the Laptop entries are equal

This works only when both anonymous types have the same property names, types, and order. If the property sets differ, the compiler treats them as different types and the sequences cannot be combined directly.

Performance and Deferred Execution

Union uses deferred execution. The method returns an IEnumerable<T> immediately, and the actual enumeration happens when you iterate the result. The deduplication is performed incrementally as elements are pulled from the source sequences.

Internally, Union maintains a hash set of elements it has already yielded. Each element from the first sequence is checked against this set; if it is new, it is yielded and added to the set. Then the same process runs for the second sequence. This means memory usage grows with the number of distinct elements, not with the total number of elements.

For large sequences, the hash set allocation is the dominant cost. If you are merging two already-distinct sequences and do not need deduplication, Concat is cheaper because it performs no equality checks and allocates no hash set. If you need distinct results, Union is usually preferable to calling Concat followed by Distinct, because it avoids materializing the intermediate concatenated sequence.

Common Mistakes and Edge Cases

One frequent mistake is expecting Union to work with custom classes without providing an equality comparer. As shown earlier, reference types default to reference equality, so the result contains more elements than expected.

Another edge case is null handling. Union handles null elements without throwing. A null element is treated like any other element for deduplication purposes, and the default comparer handles it correctly. If you write a custom comparer, you must decide how to handle null values in Equals and GetHashCode.

Ordering is also worth noting. Union does not sort the result. It preserves the order of first occurrence, which means elements from the first sequence appear first, followed by elements from the second sequence that were not already present. If you need a sorted result, apply OrderBy after Union.

For LINQ to Entities or other database-backed providers, Union translates to a SQL UNION operation, which also removes duplicates at the database level. This can be more efficient than pulling both sequences into memory and deduplicating client-side, but the exact translation depends on the provider and the query shape.

c# linq union: Practical Usage and Code Examples | RYUSLOG DEV