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.
Summary
Two public API details make normal SharpAssert usage fail or produce unusably large NUnit diagnostics.
1.
ThrowsAsync<Exception>cannot be assigned toExceptionReproduction
Compiler output:
Root cause
ExceptionResult<T>declares both:When
Tis exactlyException, 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-
Exceptionoperator preserves assignments for both:If the standard reference conversion after
operator Tis insufficient forgeneral, expose the caught exception through the existing.Exceptionproperty 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:
For a compound assertion this can be hundreds of lines.
Root cause
SharpAssertionExceptionhas a public property namedResult. NUnit specially appends any exceptionResultproperty to its error output. A minimal custom exception confirms this behavior:NUnit prints:
Because
AssertionEvaluationResultis a record, its generatedToString()recursively renders the entire structured assertion graph.Suggested direction
Preserve the structured
SharpAssertionException.ResultAPI, but overrideAssertionEvaluationResult.ToString()with a bounded representation such asPassed/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.ExceptionResult<DerivedException>is either supported or has an explicit documented replacement.Result:line is bounded to one short line and does not serializeContext,ExprNode, or nestedEvaluationResultrecords.