Skip to content

feat: handle null/empty async Because reasons#974

Merged
vbreuss merged 1 commit into
mainfrom
topic/update-because-to-handle-null
May 31, 2026
Merged

feat: handle null/empty async Because reasons#974
vbreuss merged 1 commit into
mainfrom
topic/update-because-to-handle-null

Conversation

@vbreuss

@vbreuss vbreuss commented May 31, 2026

Copy link
Copy Markdown
Member

The synchronous Because(string?) overloads were already updated (#973) to ignore null or empty reasons. This applies the same behavior to the asynchronous Because(Task<string>) overloads.

The async overloads now accept Task<string?>, mirroring the nullable string? of the synchronous variants: the task reference is still required, but the value it resolves to may be null or empty. When the resolved value is null or empty, the reason is ignored (no because text is appended) instead of throwing a NullReferenceException (from reason.Trim()) or producing a dangling ", because " fragment.

Because the resolved string is only available after awaiting, the null/empty guard lives in AsyncBecauseReason.ApplyTo, where the result is left unchanged when the reason resolves to null or empty.

Changes:

  • ExpectationResult.Because and ExpectationResult<TType, TSelf>.Because now take Task<string?>.
  • ExpectationBuilder.AddReason and AsyncBecauseReason updated to Task<string?>; the resolved value is checked for null/empty.
  • Regenerated the public-API approval files for all target frameworks.
  • Added tests covering null and empty resolved reasons for both the non-generic and generic async overloads.

The synchronous `Because(string?)` overloads were already updated (#973)
to ignore null or empty reasons. This applies the same behavior to the
asynchronous `Because(Task<string>)` overloads.

The async overloads now accept `Task<string?>`, mirroring the nullable
`string?` of the synchronous variants: the task reference is still
required, but the value it resolves to may be null or empty. When the
resolved value is null or empty, the reason is ignored (no `because`
text is appended) instead of throwing a `NullReferenceException` (from
`reason.Trim()`) or producing a dangling ", because " fragment.

Because the resolved string is only available after awaiting, the
null/empty guard lives in `AsyncBecauseReason.ApplyTo`, where the result
is left unchanged when the reason resolves to null or empty.

Changes:
- `ExpectationResult.Because` and `ExpectationResult<TType, TSelf>.Because`
  now take `Task<string?>`.
- `ExpectationBuilder.AddReason` and `AsyncBecauseReason` updated to
  `Task<string?>`; the resolved value is checked for null/empty.
- Regenerated the public-API approval files for all target frameworks.
- Added tests covering null and empty resolved reasons for both the
  non-generic and generic async overloads.
@vbreuss vbreuss enabled auto-merge (squash) May 31, 2026 06:42
@vbreuss vbreuss disabled auto-merge May 31, 2026 06:46
@vbreuss vbreuss merged commit 4641506 into main May 31, 2026
9 checks passed
@vbreuss vbreuss deleted the topic/update-because-to-handle-null branch May 31, 2026 06:46
@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
2 New issues

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

github-actions Bot added a commit that referenced this pull request May 31, 2026
github-actions Bot added a commit that referenced this pull request May 31, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🚀 Benchmark Results

Details

BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
AMD EPYC 9V74 2.60GHz, 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.300
[Host] : .NET 8.0.27 (8.0.27, 8.0.2726.22922), X64 RyuJIT x86-64-v3

Job=InProcess Toolchain=InProcessEmitToolchain IterationCount=15
LaunchCount=1 WarmupCount=10

Method Mean Error StdDev Gen0 Gen1 Allocated
Bool_aweXpect 259.6 ns 0.79 ns 0.70 ns 0.0415 - 696 B
Bool_FluentAssertions 272.8 ns 0.35 ns 0.29 ns 0.0567 - 952 B
Equivalency_aweXpect 342,267.3 ns 1,964.80 ns 1,837.88 ns 20.0195 0.4883 335444 B
Equivalency_FluentAssertions 2,442,608.2 ns 27,908.18 ns 26,105.33 ns 289.0625 46.8750 4841651 B
Int_GreaterThan_aweXpect 266.2 ns 0.86 ns 0.81 ns 0.0515 - 864 B
Int_GreaterThan_FluentAssertions 264.4 ns 0.59 ns 0.49 ns 0.0730 - 1224 B
ItemsCount_AtLeast_aweXpect 489.5 ns 0.58 ns 0.45 ns 0.0811 - 1360 B
ItemsCount_AtLeast_FluentAssertions 548.2 ns 1.94 ns 1.82 ns 0.1192 - 2008 B
String_aweXpect 484.0 ns 1.71 ns 1.51 ns 0.0668 - 1128 B
String_FluentAssertions 1,198.5 ns 2.17 ns 1.81 ns 0.2346 - 3944 B
StringArray_aweXpect 1,910.4 ns 2.53 ns 1.97 ns 0.1564 - 2624 B
StringArray_FluentAssertions 1,419.0 ns 3.83 ns 3.59 ns 0.2480 - 4152 B
StringArrayInAnyOrder_aweXpect 2,486.5 ns 2.65 ns 2.48 ns 0.1678 - 2816 B
StringArrayInAnyOrder_FluentAssertions 20,249.2 ns 100.17 ns 88.80 ns 1.9836 0.0610 33471 B

@github-actions

Copy link
Copy Markdown
Contributor

👽 Mutation Results

Mutation testing badge

aweXpect

Details
File Score Killed Survived Timeout No Coverage Ignored Compile Errors Total Detected Total Undetected Total Mutants

The final mutation score is NaN%

Coverage Thresholds: high:80 low:60 break:0

aweXpect.Core

Details
File Score Killed Survived Timeout No Coverage Ignored Compile Errors Total Detected Total Undetected Total Mutants
Core/ExpectationBuilder.cs 88.30% 83 9 0 2 41 19 83 11 154
Core/Helpers/AsyncBecauseReason.cs 53.85% 7 5 0 1 3 1 7 6 17
Results/ExpectationResult.cs 97.67% 42 0 0 1 17 10 42 1 70

The final mutation score is 88.00%

Coverage Thresholds: high:80 low:60 break:0

@github-actions

github-actions Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

This is addressed in release v2.35.0.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

state: released The issue is released

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant