Skip to content

Queryable projection inlines Use= helper with supertype parameter using an unparenthesized cast (CS0030) #2340

Description

@Demivan

Describe the bug

In a queryable projection (ProjectToXxx(this IQueryable<T>)), when Mapperly inlines a user-mapping helper referenced via Use = nameof(Helper) (through [MapPropertyFromSource] or [MapProperty]) whose parameter type is a supertype (base class or interface) of the projection's source element type, the generated expression upcasts the substituted source parameter with the wrong operator precedence.

It emits (global::IBase)x.Value instead of ((global::IBase)x).Value, so the cast binds to the member-access result rather than to x. The generated projection then fails to compile:

error CS0030: Cannot convert type 'int?' to 'IBase'

The instance Map path for the same mapping is generated correctly — it casts the whole source (MapResult((global::IBase)source)). Only the queryable-projection inlining mis-places the cast.

To Reproduce

using System.Linq;
using Riok.Mapperly.Abstractions;

public interface IBase { int? Value { get; } }
public class Source : IBase { public int Id { get; set; } public int? Value { get; set; } }
public class Target { public int Id { get; set; } public int? Result { get; set; } }

[Mapper(AutoUserMappings = false)]
public static partial class MyMapper
{
    public static partial IQueryable<Target> ProjectToTarget(this IQueryable<Source> q);

    [MapPropertyFromSource(nameof(Target.Result), Use = nameof(MapResult))]
    private static partial Target Map(Source source);

    // Parameter type IBase is a supertype of the projection's source element type (Source).
    private static int? MapResult(IBase source) => source.Value;
}

Generated code

// projection — invalid, the (IBase) cast binds to the member access:
Result = (global::IBase)x.Value,

// instance Map path — correct:
target.Result = MapResult((global::IBase)source);

Expected behavior

The projection should parenthesize the cast operand:

Result = ((global::IBase)x).Value,

or, since Source : IBase, omit the (unnecessary) upcast entirely:

Result = x.Value,

Mapperly version

5.0.0-next.8 (also reproduces on 5.0.0-next.7).

Additional context

The same defect surfaces when sharing configuration between mappings of different concrete source types via [IncludeMappingConfiguration] on a base mapping whose source is an interface — any Use= helper taking the interface-typed whole record then hits this in the projection path.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions