Please do the checklist before filing an issue:
Describe the bug
When mapping a nullable reference-type member whose type defines user-defined equality operators against another type (the common "strongly-typed value object" pattern: ==/!=(Vo, Vo) and ==/!=(Vo, string)), Mapperly emits a bare null check (source.Member != null) in the generated mapping. That expression is ambiguous for the compiler, both operator overloads are applicable to the null literal, so the generated code does not compile.
This affects every value-object library that generates primitive comparison operators (e.g. Vogen, where primitiveEqualityGeneration is enabled by default), as well as hand-written value objects following the same pattern.
Repro
Self-contained declaration code below (no third-party dependency). I can push a repro repository on request.
Declaration code
public class Code
{
public string Value { get; init; } = "";
public static explicit operator string(Code v) => v.Value;
public static bool operator ==(Code? l, Code? r) => Equals(l, r);
public static bool operator !=(Code? l, Code? r) => !Equals(l, r);
public static bool operator ==(Code? l, string? r) => l?.Value == r;
public static bool operator !=(Code? l, string? r) => !(l == r);
public override bool Equals(object? o) => o is Code c && c.Value == Value;
public override int GetHashCode() => Value.GetHashCode();
}
public class Order { public Code? PromoCode { get; init; } }
public record OrderDto { public string? PromoCode { get; init; } }
[Mapper]
public static partial class OrderMapper
{
public static partial OrderDto ToDto(this Order source);
}
Actual relevant generated code
var target = new OrderDto()
{
PromoCode = source.PromoCode != null ? (string)source.PromoCode : default,
// ^^^^^^^
// error CS9342: Operator resolution is ambiguous between the following members:
// 'Code.operator !=(Code?, Code?)' and 'Code.operator !=(Code?, string?)'
};
Expected relevant generated code
var target = new OrderDto()
{
PromoCode = source.PromoCode != (Code?)null ? (string)source.PromoCode : default,
};
Casting the null literal to the source type disambiguates overload resolution without changing anything else, and seems the most general fix:
- it is valid both in object (in-memory) mappings and inside queryable projection expression trees, so a single codegen shape fixes every context;
- it is behavior-preserving: where the bare
!= null compiles today (types with a single applicable == overload), Mapperly already dispatches to the user-defined operator — the typed cast only disambiguates, it does not change which operator runs;
- in projections the operator is never executed anyway, the ORM translates the comparison to
IS NULL / IS NOT NULL.
An alternative for object mappings only would be pattern matching (source.PromoCode is not null), but it is not valid inside expression trees and it silently changes semantics from operator dispatch to reference equality, so the typed-null cast seems the safer and more consistent fix.
Reported relevant diagnostics
- None from Mapperly, the failure is the C# compiler error
CS9342 reported on the generated file (obj/.../Riok.Mapperly/...Mapper.g.cs).
Environment (please complete the following information):
- Mapperly Version: 4.3.1 (also reproduced on 5.0.0-next.8)
- Nullable reference types: enabled
- .NET Version: SDK 10.0.300
- Target Framework: net10.0
- Compiler Version: 5.6.0-2.26230.102 (caa81fa4971f74880cdab61990cb1b11420939ec)
- C# Language Version: latest (14.0)
- IDE: Rider 2026.1
- OS: Windows 11 Pro (10.0.26200)
Additional context
Workaround I use today: a user-implemented mapping with an explicitly typed null comparison, repeated for every nullable value-object type in every mapper:
private static string? MapPromoCode(Code? code) => code == (Code?)null ? null : (string)code;
I am happy to contribute a PR for this if you agree with the direction.
Please do the checklist before filing an issue:
Describe the bug
When mapping a nullable reference-type member whose type defines user-defined equality operators against another type (the common "strongly-typed value object" pattern:
==/!=(Vo, Vo)and==/!=(Vo, string)), Mapperly emits a bare null check (source.Member != null) in the generated mapping. That expression is ambiguous for the compiler, both operator overloads are applicable to thenullliteral, so the generated code does not compile.This affects every value-object library that generates primitive comparison operators (e.g. Vogen, where
primitiveEqualityGenerationis enabled by default), as well as hand-written value objects following the same pattern.Repro
Self-contained declaration code below (no third-party dependency). I can push a repro repository on request.
Declaration code
Actual relevant generated code
Expected relevant generated code
Casting the null literal to the source type disambiguates overload resolution without changing anything else, and seems the most general fix:
!= nullcompiles today (types with a single applicable==overload), Mapperly already dispatches to the user-defined operator — the typed cast only disambiguates, it does not change which operator runs;IS NULL/IS NOT NULL.An alternative for object mappings only would be pattern matching (
source.PromoCode is not null), but it is not valid inside expression trees and it silently changes semantics from operator dispatch to reference equality, so the typed-null cast seems the safer and more consistent fix.Reported relevant diagnostics
CS9342reported on the generated file (obj/.../Riok.Mapperly/...Mapper.g.cs).Environment (please complete the following information):
Additional context
Workaround I use today: a user-implemented mapping with an explicitly typed null comparison, repeated for every nullable value-object type in every mapper:
I am happy to contribute a PR for this if you agree with the direction.