Skip to content

Fix ExceptionResult<Exception> conversion ambiguity and noisy NUnit Result output #4

Description

@yevhen

Summary

Two public API details make normal SharpAssert usage fail or produce unusably large NUnit diagnostics.

1. ThrowsAsync<Exception> cannot be assigned to Exception

Reproduction

Exception failure = await ThrowsAsync<Exception>(() => operation());

Compiler output:

CS0457: Ambiguous user defined conversions
'Sharp.ExceptionResult<Exception>.implicit operator Exception(ExceptionResult<Exception>)'
and
'Sharp.ExceptionResult<Exception>.implicit operator Exception(ExceptionResult<Exception>)'

Root cause

ExceptionResult<T> declares both:

public static implicit operator T(ExceptionResult<T> result)
public static implicit operator Exception(ExceptionResult<T> result)

When T is exactly Exception, both operators have the same effective source and target types. Overload resolution cannot choose one.

Suggested direction

Keep one non-overlapping conversion path. Verify that removing the explicit base-Exception operator preserves assignments for both:

ArgumentException specific = Throws<ArgumentException>(...);
Exception general = Throws<ArgumentException>(...);
Exception exact = Throws<Exception>(...);

If the standard reference conversion after operator T is insufficient for general, expose the caught exception through the existing .Exception property rather than overlapping generic operators.

2. NUnit prints the complete recursive evaluation graph

Reproduction

A failed SharpAssert assertion under NUnit 4.6.1 + NUnit3TestAdapter 6.3.0 produces useful formatted diagnostics, followed by output similar to:

Result: AssertionEvaluationResult {
  ExpressionText = ...,
  Context = AssertionContext { ... ExprNode = ... },
  Result = LogicalEvaluationResult { ... },
  Passed = False
}

For a compound assertion this can be hundreds of lines.

Root cause

SharpAssertionException has a public property named Result. NUnit specially appends any exception Result property to its error output. A minimal custom exception confirms this behavior:

public record Payload(string Value);
public sealed class CustomException : Exception
{
    public Payload Result { get; } = new("huge graph");
}

NUnit prints:

CustomException : useful message
Result: Payload { Value = huge graph }

Because AssertionEvaluationResult is a record, its generated ToString() recursively renders the entire structured assertion graph.

Suggested direction

Preserve the structured SharpAssertionException.Result API, but override AssertionEvaluationResult.ToString() with a bounded representation such as Passed/Failed (or another short one-line value). The exception message already contains the intended rich formatted diagnostic.

Acceptance criteria

  • Exception failure = await ThrowsAsync<Exception>(...) compiles.
  • Specific exception assignment remains supported.
  • Base exception assignment from ExceptionResult<DerivedException> is either supported or has an explicit documented replacement.
  • NUnit failure output keeps SharpAssert's formatted expression diagnostics.
  • NUnit's appended Result: line is bounded to one short line and does not serialize Context, ExprNode, or nested EvaluationResult records.
  • Add compile probes for all three conversion examples.
  • Add an integration test using current NUnit/test adapter versions that verifies bounded console failure output.

Activity

  1. yevhen commented on Oct 9, 2026

    @yevhen
    OwnerAuthor

    Fixed and released in v1.6.1.

    Fix commit: f0428c4. Release commit: e12b943.

    • Removed the overlapping base-exception conversion. Specific, derived-to-base, and exact Exception assignments compile; awaited ThrowsAsync works.
    • AssertionEvaluationResult.ToString() now returns Passed or Failed. Structured Result and primary formatted diagnostics remain available.

    Validation: 586 main tests, 11 isolated package tests, current NUnit 4.6.1/adapter 6.3.0 console-output integration, and four public-consumer regressions restored directly from NuGet.org. Release builds, package creation, and restore passed with warnings as errors. Both GitHub workflows passed.

    Publication verified: both SharpAssert 1.6.1 and SharpAssert.Runtime 1.6.1 are in the public NuGet version indexes and their nupkg files were downloaded. Both published nuspecs identify release commit e12b943.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions