---
title: Join or reduce sequences
summary: Fill LINQ gaps: iteration, joining, string-joining, and keyed distinct.
status: draft
owner: saif-docgen
last_reviewed: 2026-09-15
library: SAIF.Framework.Common
reference_type: package
operation_id: OP-035
trigger: Caller performs a collection operation LINQ does not provide natively
---

# Join or reduce sequences

Fill LINQ gaps: iteration, joining, string-joining, and keyed distinct, triggered when caller performs a collection operation LINQ does not provide natively.

| Field | Value |
| --- | --- |
| **Operation ID** | OP-035 |
| **Trigger** | Caller performs a collection operation LINQ does not provide natively |
| **Inputs** | One or two sequences plus key selectors/projections |
| **Outputs** | `void` / `string` / `bool` / `IEnumerable<T>` |
| **Primary components** | ``IEnumerableExtensions.ForEach`, `ToStringAll` (2 overloads), `IsIn`, `TakeFromEnd`, `Distinct<TSource,TKey>`, `None`, `ToLowerAll`, `FullOuterGroupJoin`, `FullOuterJoin`, `LeftOuterJoin`` |
| **Entry point** | `Common/SAIF.Common/Extensions/IEnumerableExtensions.cs` |

## Steps

1. `ForEach` guards the `action` argument with `Enforce.ArgumentNotNull` and then invokes it for each element eagerly. ([BR-039](../business-rules.md#br-039-a-guarded-argument-must-not-be-null))
2. `ToStringAll(separator)` guards the separator, then delegates to the projection overload using `x.ToString()`; the projection overload guards both `separator` and `function` and joins with `string.Join`. ([BR-039](../business-rules.md#br-039-a-guarded-argument-must-not-be-null), [BR-085](../business-rules.md#br-085-joining-a-sequence-to-a-string-requires-a-non-null-separator))
3. `TakeFromEnd(n)` reverses the sequence, takes `n`, and reverses again — preserving the original relative order of the trailing items. ([BR-086](../business-rules.md#br-086-taking-n-items-from-the-end-of-a-sequence-preserves-their-original-relative-order))
4. `Distinct(keySelector)` groups by an anonymous wrapper over the selected key and returns the first element of each group — a "distinct by" that keeps the first occurrence. ([BR-087](../business-rules.md#br-087-key-based-distinct-retains-the-first-occurrence-of-each-key))
5. `None(predicate)` is the negation of `Any(predicate)`.
6. The outer-join helpers build `ILookup`s for both sides using an optional `IEqualityComparer<TKey>` (defaulting to `EqualityComparer<TKey>.Default`), union the key sets — left keys only for `LeftOuterJoin` — and project each key's matches, substituting `defaultLeft`/`defaultRight` via `DefaultIfEmpty` where a side has no match. ([BR-088](../business-rules.md#br-088-outer-joins-compare-keys-with-the-caller's-iequalitycomparer%3Ctkey%3E-when-supplied%2C-otherwise-equalitycomparer%3Ctkey%3E.default%2C-and-substitute-caller-supplied-defaults-for-unmatched-sides))

## Process flow

The following diagram visualizes this operation's control flow.

```mermaid
flowchart TD
	Start(["Caller invokes an<br>IEnumerableExtensions helper"]) --> S1["ForEach: guard action,<br>invoke for each element"]
	S1 --> S2["ToStringAll: guard separator,<br>join via string.Join"]
	S2 --> S3["TakeFromEnd: reverse,<br>take n, reverse again"]
	S3 --> S4["Distinct: group by key,<br>return first of each group"]
	S4 --> S5["None: negate Any(predicate)"]
	S5 --> S6["Outer-join helpers: build ILookups,<br>union keys, project matches"]
	S6 --> End(["Result returned to caller"])
```

## Related documentation
- [Operations and processes](../operations-processes.md)
- [Business rules](../business-rules.md)

## Sources
- `Common/SAIF.Common/Extensions/IEnumerableExtensions.cs`
