Repository: riok/mapperly (2026-06-25T15:35:47Z)

Issue:
Queryable projection inlines Use= helper with supertype parameter using an unparenthesized cast (CS0030)
### 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

```csharp
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

```csharp
// 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:

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

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

```csharp
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.



Discussion:

Patch:
diff --git a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedInlinedExpressionMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedInlinedExpressionMapping.cs
index 7cc2311408..7722a32da1 100644
--- a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedInlinedExpressionMapping.cs
+++ b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedInlinedExpressionMapping.cs
@@ -116,7 +116,7 @@ public override SyntaxNode VisitParenthesizedLambdaExpression(ParenthesizedLambd
             // replace source parameter
             if (node.Identifier.Text.Equals(sourceParameter.Identifier.Text, StringComparison.Ordinal))
             {
-                return ctx.Source.WithTriviaFrom(node);
+                return ParenthesizeIfNeeded(ctx.Source).WithTriviaFrom(node);
             }
 
             // replace additional parameters with their corresponding expressions from the context
@@ -128,13 +128,18 @@ public override SyntaxNode VisitParenthesizedLambdaExpression(ParenthesizedLambd
                 foreach (var (key, value) in additionalParams)
                 {
                     if (string.Equals(key, node.Identifier.Text, StringComparison.Ordinal))
-                        return value.WithTriviaFrom(node);
+                        return ParenthesizeIfNeeded(value).WithTriviaFrom(node);
                 }
             }
 
             return base.VisitIdentifierName(node);
         }
 
+        // A cast that replaces a parameter must be parenthesized to keep the parameter's precedence wherever
+        // it was used: a bare `(T)x.Member` would otherwise bind as `(T)(x.Member)`.
+        private static ExpressionSyntax ParenthesizeIfNeeded(ExpressionSyntax replacement) =>
+            replacement is CastExpressionSyntax ? ParenthesizedExpression(replacement) : replacement;
+
         public override SyntaxNode? VisitInvocationExpression(InvocationExpressionSyntax node)
         {
             var annotation = node.GetAnnotations(InlineExpressionRewriter.SyntaxAnnotationKindMapperInvocation).FirstOrDefault();


Test patch:
diff --git a/test/Riok.Mapperly.IntegrationTests/Dto/SupertypeProjectionDto.cs b/test/Riok.Mapperly.IntegrationTests/Dto/SupertypeProjectionDto.cs
new file mode 100644
index 0000000000..85212dd23a
--- /dev/null
+++ b/test/Riok.Mapperly.IntegrationTests/Dto/SupertypeProjectionDto.cs
@@ -0,0 +1,11 @@
+namespace Riok.Mapperly.IntegrationTests.Dto
+{
+    public class SupertypeProjectionDto
+    {
+        public int Id { get; set; }
+
+        public int? MappedValue { get; set; }
+
+        public string MappedName { get; set; } = string.Empty;
+    }
+}
diff --git a/test/Riok.Mapperly.IntegrationTests/Mapper/SupertypeProjectionMapper.cs b/test/Riok.Mapperly.IntegrationTests/Mapper/SupertypeProjectionMapper.cs
new file mode 100644
index 0000000000..ca5bac1ece
--- /dev/null
+++ b/test/Riok.Mapperly.IntegrationTests/Mapper/SupertypeProjectionMapper.cs
@@ -0,0 +1,23 @@
+using System.Linq;
+using Riok.Mapperly.Abstractions;
+using Riok.Mapperly.IntegrationTests.Dto;
+using Riok.Mapperly.IntegrationTests.Models;
+
+namespace Riok.Mapperly.IntegrationTests.Mapper
+{
+    [Mapper(AutoUserMappings = false)]
+    public static partial class SupertypeProjectionMapper
+    {
+        public static partial IQueryable<SupertypeProjectionDto> ProjectToDto(this IQueryable<SupertypeProjectionSource> source);
+
+        [MapPropertyFromSource(nameof(SupertypeProjectionDto.MappedValue), Use = nameof(MapValue))]
+        [MapPropertyFromSource(nameof(SupertypeProjectionDto.MappedName), Use = nameof(MapName))]
+        private static partial SupertypeProjectionDto Map(SupertypeProjectionSource source);
+
+        // The parameter types are supertypes (interface / base class) of the projection's source element
+        // type, so inlining these helpers inserts upcasts that must stay parenthesized: `((I)x).Value`.
+        private static int? MapValue(ISupertypeProjectionValue source) => source.Value;
+
+        private static string MapName(SupertypeProjectionBase source) => source.Name;
+    }
+}
diff --git a/test/Riok.Mapperly.IntegrationTests/Models/SupertypeProjectionSource.cs b/test/Riok.Mapperly.IntegrationTests/Models/SupertypeProjectionSource.cs
new file mode 100644
index 0000000000..cf4ebbe185
--- /dev/null
+++ b/test/Riok.Mapperly.IntegrationTests/Models/SupertypeProjectionSource.cs
@@ -0,0 +1,19 @@
+namespace Riok.Mapperly.IntegrationTests.Models
+{
+    public interface ISupertypeProjectionValue
+    {
+        int? Value { get; }
+    }
+
+    public abstract class SupertypeProjectionBase
+    {
+        public string Name { get; set; } = string.Empty;
+    }
+
+    public class SupertypeProjectionSource : SupertypeProjectionBase, ISupertypeProjectionValue
+    {
+        public int Id { get; set; }
+
+        public int? Value { get; set; }
+    }
+}
diff --git a/test/Riok.Mapperly.IntegrationTests/SupertypeProjectionMapperTest.cs b/test/Riok.Mapperly.IntegrationTests/SupertypeProjectionMapperTest.cs
new file mode 100644
index 0000000000..21d46c57d0
--- /dev/null
+++ b/test/Riok.Mapperly.IntegrationTests/SupertypeProjectionMapperTest.cs
@@ -0,0 +1,69 @@
+#if NET7_0_OR_GREATER
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.Data.Sqlite;
+using Microsoft.EntityFrameworkCore;
+using Riok.Mapperly.IntegrationTests.Mapper;
+using Riok.Mapperly.IntegrationTests.Models;
+using Shouldly;
+using Xunit;
+
+namespace Riok.Mapperly.IntegrationTests
+{
+    // Regression test for https://github.com/riok/mapperly/issues/2340: a queryable projection that
+    // inlines a Use= helper whose parameter is a supertype (interface or base class) of the source
+    // element type must emit a parenthesized upcast (`((I)x).Value`). Failing to compile or to
+    // translate both reproduce the bug.
+    public class SupertypeProjectionMapperTest
+    {
+        [Fact]
+        public async Task ProjectionWithSupertypeUseMappingTranslatesToRenamedColumns()
+        {
+            await using var connection = new SqliteConnection("Data Source=:memory:");
+            await connection.OpenAsync();
+
+            var options = new DbContextOptionsBuilder().UseSqlite(connection).Options;
+            await using var ctx = new SupertypeProjectionDbContext(options);
+            await ctx.Database.EnsureCreatedAsync();
+            ctx.Sources.Add(
+                new SupertypeProjectionSource
+                {
+                    Id = 1,
+                    Value = 42,
+                    Name = "foo",
+                }
+            );
+            await ctx.SaveChangesAsync();
+
+            var query = ctx.Sources.ProjectToDto();
+
+            // The inserted upcast must resolve each member to its renamed backing column, proving the cast
+            // is bound to the mapped property and not to a coincidentally matching name.
+            var sql = query.ToQueryString();
+            sql.ShouldContain("raw_value");
+            sql.ShouldContain("entity_name");
+
+            var dto = (await query.ToListAsync()).ShouldHaveSingleItem();
+            dto.Id.ShouldBe(1);
+            dto.MappedValue.ShouldBe(42);
+            dto.MappedName.ShouldBe("foo");
+        }
+
+        sealed class SupertypeProjectionDbContext : DbContext
+        {
+            public SupertypeProjectionDbContext(DbContextOptions options)
+                : base(options) { }
+
+            public DbSet<SupertypeProjectionSource> Sources { get; set; } = null!;
+
+            protected override void OnModelCreating(ModelBuilder modelBuilder)
+            {
+                var entity = modelBuilder.Entity<SupertypeProjectionSource>();
+                entity.HasKey(p => p.Id);
+                entity.Property(p => p.Value).HasColumnName("raw_value");
+                entity.Property(p => p.Name).HasColumnName("entity_name");
+            }
+        }
+    }
+}
+#endif
diff --git a/test/Riok.Mapperly.Tests/Mapping/QueryableProjectionUseNamedMappingTest.cs b/test/Riok.Mapperly.Tests/Mapping/QueryableProjectionUseNamedMappingTest.cs
index 6c3e8be1b0..d64b0a622c 100644
--- a/test/Riok.Mapperly.Tests/Mapping/QueryableProjectionUseNamedMappingTest.cs
+++ b/test/Riok.Mapperly.Tests/Mapping/QueryableProjectionUseNamedMappingTest.cs
@@ -163,6 +163,48 @@ class B
         return TestHelper.VerifyGenerator(source);
     }
 
+    [Fact]
+    public Task ShouldUseReferencedMappingWithSupertypeParameter()
+    {
+        var source = TestSourceBuilder.MapperWithBodyAndTypes(
+            """
+            public partial System.Linq.IQueryable<Target> Map(System.Linq.IQueryable<Source> source);
+
+            [MapPropertyFromSource(nameof(Target.Result), Use = nameof(MapResult))]
+            private partial Target Map(Source source);
+
+            private int? MapResult(IBase source) => source.Value;
+            """,
+            TestSourceBuilderOptions.WithDisabledAutoUserMappings,
+            "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; } }"
+        );
+        return TestHelper.VerifyGenerator(source);
+    }
+
+    [Fact]
+    public Task ShouldUseReferencedMappingWithExplicitlyImplementedSupertypeParameter()
+    {
+        var source = TestSourceBuilder.MapperWithBodyAndTypes(
+            """
+            public partial System.Linq.IQueryable<Target> Map(System.Linq.IQueryable<Source> source);
+
+            [MapPropertyFromSource(nameof(Target.Result), Use = nameof(MapResult))]
+            private partial Target Map(Source source);
+
+            private int? MapResult(IBase source) => source.Value;
+            """,
+            TestSourceBuilderOptions.WithDisabledAutoUserMappings,
+            "public interface IBase { int? Value { get; } }",
+            // Value is implemented explicitly, so it can only be read through the interface:
+            // the inlined upcast must stay parenthesized or the projection won't compile.
+            "public class Source : IBase { public int Id { get; set; } int? IBase.Value => Id; }",
+            "public class Target { public int Id { get; set; } public int? Result { get; set; } }"
+        );
+        return TestHelper.VerifyGenerator(source);
+    }
+
     [Fact]
     public Task TwoQueryableMappingsWithNamedUsedMappingsAndAmbiguousName()
     {
diff --git a/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionUseNamedMappingTest.ShouldUseReferencedMappingWithExplicitlyImplementedSupertypeParameter#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionUseNamedMappingTest.ShouldUseReferencedMappingWithExplicitlyImplementedSupertypeParameter#Mapper.g.verified.cs
new file mode 100644
index 0000000000..39dbd2395a
--- /dev/null
+++ b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionUseNamedMappingTest.ShouldUseReferencedMappingWithExplicitlyImplementedSupertypeParameter#Mapper.g.verified.cs
@@ -0,0 +1,29 @@
+﻿//HintName: Mapper.g.cs
+// <auto-generated />
+#nullable enable
+public partial class Mapper
+{
+    [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")]
+    public partial global::System.Linq.IQueryable<global::Target> Map(global::System.Linq.IQueryable<global::Source> source)
+    {
+#nullable disable
+        return global::System.Linq.Queryable.Select(
+            source,
+            x => new global::Target()
+            {
+                Id = x.Id,
+                Result = ((global::IBase)x).Value,
+            }
+        );
+#nullable enable
+    }
+
+    [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")]
+    private partial global::Target Map(global::Source source)
+    {
+        var target = new global::Target();
+        target.Id = source.Id;
+        target.Result = MapResult((global::IBase)source);
+        return target;
+    }
+}
\ No newline at end of file
diff --git a/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionUseNamedMappingTest.ShouldUseReferencedMappingWithSupertypeParameter#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionUseNamedMappingTest.ShouldUseReferencedMappingWithSupertypeParameter#Mapper.g.verified.cs
new file mode 100644
index 0000000000..39dbd2395a
--- /dev/null
+++ b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionUseNamedMappingTest.ShouldUseReferencedMappingWithSupertypeParameter#Mapper.g.verified.cs
@@ -0,0 +1,29 @@
+﻿//HintName: Mapper.g.cs
+// <auto-generated />
+#nullable enable
+public partial class Mapper
+{
+    [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")]
+    public partial global::System.Linq.IQueryable<global::Target> Map(global::System.Linq.IQueryable<global::Source> source)
+    {
+#nullable disable
+        return global::System.Linq.Queryable.Select(
+            source,
+            x => new global::Target()
+            {
+                Id = x.Id,
+                Result = ((global::IBase)x).Value,
+            }
+        );
+#nullable enable
+    }
+
+    [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")]
+    private partial global::Target Map(global::Source source)
+    {
+        var target = new global::Target();
+        target.Id = source.Id;
+        target.Result = MapResult((global::IBase)source);
+        return target;
+    }
+}
\ No newline at end of file