> For the complete documentation index, see [llms.txt](https://docs.shouldly.org/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.shouldly.org/documentation/4to5.md).

# Upgrade 4 to 5

This is a work in progress. Please send a PR with any amendments.

## `ShouldBeOneOf` and friends take a collection, not `params`

`ShouldBeOneOf`, `ShouldNotBeOneOf`, and `ShouldBeOfTypes` were variadic in v4. In v5 they take their candidates as a single collection, so call sites passing individual values fail with **`CS1503`**:

```csharp
// v4 — compiles
result.ShouldBeOneOf(Status.Active, Status.Pending, Status.Closed);

// v5 — error CS1503: Argument 2: cannot convert from 'Status' to 'Status[]'
result.ShouldBeOneOf([Status.Active, Status.Pending, Status.Closed]);
```

Wrap the values in a collection expression, or in `new[] { … }` below C# 12. Call sites that already pass an array or a collection expression are unaffected.

This is the most widely hit compile break in the upgrade — `ShouldBeOneOf(a, b, c)` was the canonical v4 spelling — but it is a purely mechanical fix and the compiler points at every occurrence.

The `params` array was dropped because a `params` parameter must come last, which leaves no room for the `[CallerArgumentExpression]` parameter these assertions now carry. That parameter is why the failure message reports your source text:

```
result
    should be one of
[Status.Active, Status.Pending, Status.Closed]
    but was
Status.Draft
```

## String assertions default to case-sensitive comparison

`ShouldContain`, `ShouldNotContain`, `ShouldStartWith`, `ShouldEndWith`, `ShouldNotStartWith`, and `ShouldNotEndWith` defaulted to `Case.Insensitive` in v4, while `ShouldBe` compared case-sensitively. In v5 all string assertions are case-sensitive by default, aligning with `ShouldBe`, C# string semantics, and other assertion libraries. `Case.Insensitive` remains available as an opt-in.

## String failure messages echo less of the value

In v4, `ShouldBe` on strings echoed the actual and expected values up to a hardcoded 5000 characters, silently, and computed the `difference` section from those already-truncated values. Two long strings differing past character 5000 therefore produced a 10 KB message showing two identical-looking values and no difference at all.

In v5 the difference is always computed from the full values, and only the echo is clipped — to [`ShouldlyConfiguration.MaxStringLengthInMessages`](/documentation/configuration.md#maxstringlengthinmessages), which now defaults to **1000**. Truncation is announced rather than silent:

```
"Lorem ipsum dolor sit amet" (truncated to 1000 of 42317 characters, see ShouldlyConfiguration.MaxStringLengthInMessages)
```

Messages for long strings get substantially shorter, and the `difference` section carries the information the raw echo used to bury. If you were relying on the old volume of output, set `ShouldlyConfiguration.MaxStringLengthInMessages = 5000;`.

Line-mode diffs also now cap at 20 changed lines per side, with an `... and N more line(s)` marker.

## Dictionary value assertions compare values structurally

`ShouldContainKeyAndValue` and `ShouldNotContainValueForKey` compared the value with `object.Equals` in v4, while the rest of the library — including `ShouldBe` on the very same value — goes through Shouldly's structural comparer. A collection-valued dictionary therefore behaved differently depending on which assertion you reached for:

```csharp
var dictionary = new Dictionary<string, int[]> { ["key"] = [1, 2, 3] };

dictionary["key"].ShouldBe([1, 2, 3]);                    // v4 and v5: passes
dictionary.ShouldContainKeyAndValue("key", [1, 2, 3]);    // v4: fails.   v5: passes
dictionary.ShouldNotContainValueForKey("key", [1, 2, 3]); // v4: passes.  v5: fails
```

In v5 both assertions compare the value the way `ShouldBe` does: collections element-wise, numerics across types and honouring `ShouldlyConfiguration.DefaultFloatingPointTolerance`, then `IEquatable<T>`/`IComparable<T>` ahead of `object.Equals`. `string` and the types registered in `ShouldlyConfiguration.CompareAsObjectTypes` keep comparing with their own `Equals`, as they already did under `ShouldBe`.

This is *not* member-wise comparison. A value that is neither a collection nor implements `IEquatable<T>`/`IComparable<T>`/`IComparable` still falls through to `object.Equals`, so a reference type that doesn't override it compares by reference. (Member-wise remains [`ShouldBeEquivalentTo`](#shouldbeequivalentto-has-been-rewritten).)

The break runs in both directions, wherever `Equals` and structural equality disagree. The common direction is the one above — two structurally-equal but distinct instances, which makes `ShouldContainKeyAndValue` start passing and `ShouldNotContainValueForKey` start failing. The reverse happens when a value's `Equals` reports equality the structural walk doesn't: a collection type that overrides `Equals` to compare only an id, say, is now walked element-wise, so `ShouldContainKeyAndValue` can go from passing to failing. If a dictionary value assertion changes result on upgrade, check it against `dictionary[key].ShouldBe(value)` — that is now the definition.

Key lookup is unchanged — `ShouldContainKey` and friends still defer to the dictionary's own key comparer.

## The `Shouldly.Configuration` namespace has been removed

The approval-test configuration types — `ShouldMatchConfiguration`, `ShouldMatchConfigurationBuilder`, and friends — lived in the `Shouldly.Configuration` namespace in v4. In v5 they have been collapsed into the root `Shouldly` namespace, and `Shouldly.Configuration` no longer exists. (`FirstNonShouldlyMethodFinder`, `ITestMethodFinder`, and `FindMethodUsingAttribute<T>` are gone entirely — see [`ShouldMatchApproved` no longer walks the stack](#shouldmatchapproved-no-longer-walks-the-stack).)

Any `using Shouldly.Configuration;` now fails to compile with **`CS0234`**:

```csharp
using Shouldly.Configuration;
// error CS0234: The type or namespace name 'Configuration' does not exist in the namespace 'Shouldly'
```

Delete the `using Shouldly.Configuration;` line — the types resolve via `using Shouldly;`.

## `ShouldMatchApprovedDefaults` has moved

The static `ShouldMatchApprovedDefaults` builder used to configure approval tests has moved off `ShouldlyConfiguration` and onto a new `ShouldMatchConfiguration` class. Code referencing the old accessor fails to compile with **`CS0117`**:

```csharp
// v4 — compiles
ShouldlyConfiguration.ShouldMatchApprovedDefaults.DoNotIgnoreLineEndings();

// v5 — error CS0117: 'ShouldlyConfiguration' does not contain a definition for 'ShouldMatchApprovedDefaults'
ShouldMatchConfiguration.ShouldMatchApprovedDefaults.DoNotIgnoreLineEndings();
```

Change the class name only. Both types live in the root `Shouldly` namespace, so no `using` change is needed beyond `using Shouldly;`.

## `ShouldMatchApproved` no longer walks the stack

In v4, `ShouldMatchApproved` walked the runtime stack trace to find the test method, and used its reflected declaring type, method name, and PDB source information to name and place the approval files. In v5 the test method is captured at compile time instead, via `[CallerMemberName]` and `[CallerFilePath]` parameters on `ShouldMatchApproved` itself. This removes the reflection dependency (`ShouldMatchApproved` no longer carries `[RequiresUnreferencedCode]`, so it works under trimming and Native AOT), and no longer requires compiling with full debug information.

Two things to check when upgrading:

### Approval files are now prefixed with the source file name

The default approval file name is now `{SourceFileName}.{MethodName}.approved.{extension}` where v4 used the declaring type's name. Under the usual one-class-per-file convention these are identical, so most suites are unaffected. If a test class lives in a file with a different name — including nested classes and multiple classes per file — the expected file name changes, and the test will fail reporting the new missing `.approved` file. Either rename the source file to match the class, rename the approval file to the reported name, or restore the old name with `WithFilenameGenerator`. Custom `FilenameGenerator` delegates keep compiling: `TestMethodInfo.DeclaringTypeName` still exists but is `[Obsolete]` and now returns the source file name; prefer the new `TestMethodInfo.SourceFileName`.

### `UseCallerLocation` and `LocateTestMethodUsingAttribute` are removed

Both existed to point the stack walk at the right frame when `ShouldMatchApproved` was wrapped in a helper method. With compile-time capture there is no stack walk to redirect, so both now fail with **`CS0619`**. The replacement is for the helper to capture the caller info itself and pass it through:

```csharp
// v4
public static void ShouldMatchMySnapshot(this string actual) =>
    actual.ShouldMatchApproved(c => c.UseCallerLocation());

// v5
public static void ShouldMatchMySnapshot(this string actual,
    [CallerMemberName] string testMethodName = "",
    [CallerFilePath] string sourceFilePath = "") =>
    actual.ShouldMatchApproved(testMethodName: testMethodName, sourceFilePath: sourceFilePath);
```

This is the same forwarding idiom used for `[CallerArgumentExpression]`, and unlike the v4 stack walk it is immune to inlining, async state machines, and obfuscation. Helpers nested more than one level deep forward the same two parameters at each level.

## Custom assertion extensions no longer need `[ShouldlyMethods]`

In v4, custom assertion classes had to be annotated with `[ShouldlyMethods]` so the stack-walking machinery could tell Shouldly frames from test frames when reconstructing the asserted expression. In v5 the expression is captured at compile time with `[CallerArgumentExpression]`, so the attribute is no longer required — a custom assertion is just an extension method. See [Writing custom assertions](/documentation/extending.md) for the v5 pattern, including how to verify your `[CallerArgumentExpression]` wiring with `ShouldlyConfiguration.AssertCallerArgumentExpressionIsUsed()`.

The attribute still exists and is still honored by the legacy stack-walking fallback that only runs for `netstandard2.0` consumers whose compiler does not supply caller argument expressions; keep it if your custom assertion library supports those targets.

## `ShouldBeEquivalentTo` has been rewritten

The comparison engine behind `ShouldBeEquivalentTo` was rewritten for v5 (see [the roadmap](https://github.com/shouldly/shouldly/issues/1265) for the full rationale). The public API is unchanged, plus a new optional `EquivalencyOptions` parameter.

### The new model

A comparison strategy is selected per node in the object graph:

| Node type                                                                                                                                                                   | Strategy                                                                                                                                            |
| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| Leaf values: primitives, enums, `string` (ordinal), `Guid`, `DateTime`/`DateTimeOffset`/`TimeSpan`, `Uri`, `Type`, delegates, and `System.*` value-semantic types generally | Compared with `Equals`, with lossless cross-numeric equality (`int 1` ≡ `long 1`)                                                                   |
| Dictionaries (`IDictionary`, `IDictionary<,>`, `IReadOnlyDictionary<,>`, immutable variants)                                                                                | Matched **by key**, recursing into values                                                                                                           |
| Sets (`ISet<>`, `IReadOnlySet<>`, immutable variants)                                                                                                                       | Order-insensitive                                                                                                                                   |
| Other collections                                                                                                                                                           | Order-sensitive, element-by-element, recursing; container type ignored (array ≡ `List<T>`); multidimensional arrays compared by rank and dimensions |
| Everything else — classes *and* structs                                                                                                                                     | Member-wise and recursive over public properties and fields; indexers skipped                                                                       |

Member selection uses **the expectation's members, with declared types and subset semantics**: every member on the expectation must exist and match on the actual object; extra members on the actual object are ignored; runtime type identity is no longer required. This enables the most-requested scenario:

```csharp
person.ShouldBeEquivalentTo(new { Name = "John" });
```

Failures now collect **every difference** and report them all with their paths, instead of stopping at the first.

### What newly passes

Most changes turn v4 failures (or crashes) into passes: cross-type and anonymous-type expectations, dictionaries compared insertion-order-insensitively with complex values compared structurally, sets compared order-insensitively, array vs `List<T>` with equal elements, structs and tuples holding reference-type members, types with indexers (previously `NotSupportedException`), `Type`- and `Uri`-valued members, and numeric members of different types holding the same value.

### What newly fails

* **`Equals` overrides on complex types are ignored** — members are compared. Values that were `Equals`-equal but structurally different passed in v4 and now fail. (Equivalency is for comparing structure; `ShouldBe` honors `Equals`.)
* **Structs are compared by public members**, not `ValueType.Equals`, so structs whose equality depended on private state compare only their public surface — and a struct with *no* public members now trips the vacuous-comparison guard below.
* **Multidimensional arrays check shape**: a 2×3 array no longer equals a 3×2 array with the same flat content.
* **Vacuous comparisons fail.** If a node selects zero comparable members (e.g. `new object()`, an empty marker interface as the declared type, or ignoring every member via options), the assertion fails with guidance instead of silently passing.

### Audit note: derived members in base-typed slots

v4 walked **runtime** types, so asserting on derived instances held in base-typed members also compared derived-only members. v5 selects members from the **declared** type, so those members are no longer compared — if your tests rely on this, cast the expectation to the concrete type. The vacuous-comparison guard catches the zero-member extreme, but a partial narrowing will not fail loudly.

### Deliberate differences from FluentAssertions

Lists and arrays remain **order-sensitive** by default (opt out with `EquivalencyOptions.IgnoreOrder`), different enum types with equal underlying values are **not** equivalent, cyclic graphs are compared gracefully without configuration, and recursion depth is unbounded. The executable comparison suite at `src/EquivalencyComparisonTests` documents every remaining divergence.

### `EquivalencyOptions`

```csharp
actual.ShouldBeEquivalentTo(expected, new EquivalencyOptions
{
    IgnoreOrder = true,                    // order-insensitive collection comparison
    MembersToIgnore = { "Id", "Created" }, // skip these members wherever they appear
});
```

### Trimming and AOT

`ShouldBeEquivalentTo` no longer carries `[RequiresUnreferencedCode]`. The reflection-based comparison sits behind the `Shouldly.Equivalency.IsReflectionEnabledByDefault` feature switch (the same pattern as `System.Text.Json`); a companion source-generation package that registers AOT-safe type shapes is planned, at which point trimmed publishes can disable the switch and drop the reflection path entirely.
