Skip to content

[Bug]: IsEqualTo overload became ambiguous in 1.40.0 for same-type expected values (CS0121) #5765

@brad-jones

Description

@brad-jones

Description

After upgrading from 1.34.5 → 1.40.0, every IsEqualTo(...) call site where the expected value has the same type as the asserted value now fails to compile with CS0121 — "The call is ambiguous".

The regression was introduced by #5751 — feat(assertions): IsEqualTo with implicitly-convertible wrappers, which added a second overload alongside the existing one:

public static InvokableValueAssertionBuilder<TValue> IsEqualTo<TValue, TOther>(
    this IAssertionSource<TValue> source,
    TOther? expected,
    string? because = null);

public static InvokableValueAssertionBuilder<TValue> IsEqualTo<TValue>(
    this IAssertionSource<TValue> source,
    TValue? expected,
    string? because = null);

When expected has the same type as TValue, both overloads are equally applicable and C# overload resolution can't pick a winner — TOther = TValue satisfies the two-generic version exactly as well as the single-generic one binds.

This is a source-breaking change affecting effectively every existing test suite that asserts on enums, primitives, value types, records, etc. The 1.40.0 release notes don't flag it as breaking.

IsEqualTo ambiguity has been a recurring class of regression — prior history worth referencing:

Expected Behavior

IsEqualTo(expected) should compile and bind unambiguously when expected is the same type as the value under assertion (the overwhelmingly common case), as it did on 1.39.0 and all earlier versions.

Actual Behavior

The compiler reports CS0121 and the build fails:

error CS0121: The call is ambiguous between the following methods or properties:
  'TUnit.Assertions.Extensions.EqualsAssertionExtensions.IsEqualTo<TValue, TOther>(IAssertionSource<TValue>, TOther?, string?)'
  and
  'TUnit.Assertions.Extensions.EqualsAssertionExtensions.IsEqualTo<TValue>(IAssertionSource<TValue>, TValue?, string?)'

Steps to Reproduce

  1. Create a new TUnit test project targeting any current .NET TFM (8/9/10).

  2. Reference TUnit 1.40.0.

  3. Add a test that asserts equality where the expected value has the same exact type as the asserted value:

    using System.Net;
    using TUnit.Assertions;
    using TUnit.Assertions.Extensions;
    
    public class Repro
    {
      [Test]
      public async Task IsEqualTo_Same_Type_Is_Ambiguous()
      {
        using var http = new HttpClient();
        var response = await http.GetAsync("https://example.com/");
    
        // CS0121 on 1.40.0 — compiled cleanly on 1.39.0 and earlier.
        await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.OK);
      }
    }
  4. Run dotnet build — observe the CS0121 error.

Workaround: specify the generic argument explicitly:

await Assert.That(response.StatusCode).IsEqualTo<HttpStatusCode>(HttpStatusCode.OK);

TUnit Version

1.40.0

.NET Version

.NET 8.0 / .NET 9.0 / .NET 10.0 (reproduces on all three TFMs)

Operating System

Linux

IDE / Test Runner

dotnet CLI (dotnet test / dotnet run)

Error Output / Stack Trace

error CS0121: The call is ambiguous between the following methods or properties:
  'TUnit.Assertions.Extensions.EqualsAssertionExtensions.IsEqualTo<TValue, TOther>(TUnit.Assertions.Core.IAssertionSource<TValue>, TOther?, string?)'
  and
  'TUnit.Assertions.Extensions.EqualsAssertionExtensions.IsEqualTo<TValue>(TUnit.Assertions.Core.IAssertionSource<TValue>, TValue?, string?)'

Additional Context

  • Not using AOT or trimming.
  • No custom configuration; stock TUnit setup.
  • Reproduces deterministically on every same-type IsEqualTo call (independent of whether the type is an enum, primitive, value type, or record).

Suggested fix — disambiguate at the API level so callers don't have to:

  1. Constrain the new overload so it only kicks in when TOther is not TValue — e.g. by using a distinct method name (IsEqualToConverted, IsEquivalentValue, ...) or a different parameter shape.
  2. Merge the two overloads — keep only IsEqualTo<TValue>(TValue?) and rely on the language's existing implicit-conversion behaviour, which already binds anything implicitly convertible to TValue to that overload.
  3. Disambiguate by receiver type — make the two-generic overload an extension on a more specialised receiver so overload resolution is decided by the receiver, not by the argument.

Option 2 is the cleanest — the existing TValue overload already accepts implicitly-convertible values; the new overload only adds value for cases the language won't implicitly convert, which would be more clearly served by a distinct method name.

IDE-Specific Issue?

  • I've confirmed this issue occurs when running via dotnet test or dotnet run, not just in my IDE

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions