Repository navigation
Fix ExceptionResult<Exception> conversion ambiguity and noisy NUnit Result output #4
Description
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 serializeContext,ExprNode, or nestedEvaluationResultrecords. - 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
yevhen commentedon Oct 9, 2026
Fixed and released in v1.6.1.
Fix commit: f0428c4. Release commit: e12b943.
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.