Skip to content

Cannot CompileProjection for a mapping derived types between abstract classes with .Include() #801

Closed

Description

@Tommsy64

Description

When trying to create a mapping between abstract base classes and including their derived concrete implementations, Mapster throws an exception during CompileProjection(). There appears to be an issue in the expression tree generation.

Environment

  • Mapster version: 7.4.0
  • Mapster.EFCore 5.1.1
  • .NET version: net9.0
  • EF Core version: 9.0.4

I have tried Mapster 7.4.2-pre02 with net9.0 and net8.0 as well.

Steps to Reproduce

  1. Define the following class hierarchy:
public abstract class PocoA
{
    public int Id { get; set; }
}

public class PocoDerived : PocoA
{
    public int DerivedVal { get; set; }
}

public abstract class DtoA
{
    public int Id { get; set; }
}

public class DtoDerived : DtoA
{
    public int DerivedVal { get; set; }
}
  1. Configure the mapping:
config.NewConfig<PocoA, DtoA>()
    .Include<PocoDerived, DtoDerived>();
  
config.CompileProjection();

Alternatively, edit the WhenIncludeDerivedClasses test in Mapster.Tests so that the Compile() lines are CompileProjection();

Expected Behavior

The mapping should compile successfully, allowing for projection between these types.

Actual Behavior

An exception is thrown during CompileProjection():

Unhandled exception. Mapster.CompileException: Error while compiling
source=Backend.Services.Models.PocoA
destination=Backend.Services.Models.DtoA
type=Projection
 ---> System.InvalidCastException: Unable to cast object of type 'System.Linq.Expressions.UnaryExpression' to type 'System.Linq.Expressions.NewExpression'.
   at Mapster.Adapters.ClassAdapter.CreateInlineExpression(Expression source, CompileArgument arg)
   at Mapster.Adapters.BaseAdapter.CreateInlineExpressionBody(Expression source, CompileArgument arg)
   at Mapster.Adapters.BaseAdapter.CreateExpressionBody(Expression source, Expression destination, CompileArgument arg)
   at Mapster.Adapters.BaseAdapter.CreateAdaptFunc(CompileArgument arg)
   at Mapster.TypeAdapterConfig.CreateMapExpression(CompileArgument arg)
   --- End of inner exception stack trace ---
   at Mapster.TypeAdapterConfig.CreateMapExpression(CompileArgument arg)
   at Mapster.TypeAdapterConfig.CreateMapExpression(TypeTuple tuple, MapType mapType)
   at Mapster.TypeAdapterConfig.CreateProjectionCallExpression(TypeTuple tuple)
   at Mapster.TypeAdapterConfig.CompileProjection()

Activity

  1. DocSvartz commented on May 8, 2025

    @DocSvartz
    Contributor

    @Tommsy64 .Include to querytable not supported.

    Projection is intended to create a query to the database.
    And at the moment of creating a query, we cannot know what derived type will be obtained from the database (if this is even possible).

    Even if this can be done, the scope of application will be limited to Linq to Object cases (after loading from the database into memory).

  2. zsr2531 commented on Jul 11, 2025

    @zsr2531

    It is possible to know the type from within the queryable, this works:

    class Address
    {
      public int Id { get; set; }
      public string AddressLine { get; set; }
    }
    
    class DetailedAddress : Address
    {
      public string HouseNumber { get; set; }
    }
    
    class AddressDto
    {
      public int Id { get; set; }
      public string AddressLine { get; set; }
    }
    
    class DetailedAddressDto : AddressDto
    {
      public string HouseNumber { get; set; }
    }
    
    // ....
    
    var mappedDtos = await source.Select(x =>
        x.GetType() == typeof(DetailedAddress)
            ? new DetailedAddressDto { Id = x.Id, AddressLine = x.AddressLine, HouseNumber = ((DetailedAddress)x).HouseNumber }
            : new Address{ Id = x.Id, AddressLine = x.AddressLine }
    ).ToListAsync();

    I don't know since when, but EFCore supports translating the .GetType() call.

  3. zsr2531 commented on Jul 11, 2025

    @zsr2531

    I was able to get this working like so:

    private static readonly LambdaExpression SimpleAddressProjection = TypeAdapterConfig
        .CreateMapExpression(new TypeTuple(typeof(Address), typeof(AddressDto)), MapType.Projection);
    private static readonly LambdaExpression DetailedAddressProjection = TypeAdapterConfig
        .CreateMapExpression(new TypeTuple(typeof(DetailedAddress), typeof(DetailedAddressDto)), MapType.Projection);
    
    public IQueryable<AddressDto> ProjectTo(IQueryable<Address> source)
    {
        var param = Expression.Parameter(typeof(Address));
        var lambda = Expression.Lambda(Expression.Condition(
            Expression.TypeIs(param, typeof(DetailedAddress)),
            Expression.TypeAs(Expression.Invoke(DetailedAddressProjection, Expression.TypeAs(param, typeof(DetailedAddress))), typeof(AddressDto)),
            Expression.Invoke(SimpleAddressProjection, param)
        ), param);
    
        var selectExpr = CreateProjectionCallExpression<Address, AddressDto>(lambda);
        var finalExpr = Expression.Call(selectExpr.Method, source.Expression, selectExpr.Arguments[1]);
    
        return source.Provider.CreateQuery<AddressDto>(finalExpr);
    }
    
    // From Mapster
    private MethodCallExpression CreateProjectionCallExpression<TSource, TDestination>(LambdaExpression lambda)
    {
        var source = Expression.Parameter(typeof(IQueryable<>).MakeGenericType(typeof(TSource)));
        var methodInfo = (from method in typeof(Queryable).GetMethods()
                          where method.Name == nameof(Queryable.Select)
                          let p = method.GetParameters()[1]
                          where p.ParameterType.GetGenericArguments()[0].GetGenericTypeDefinition() == typeof(Func<,>)
                          select method).First().MakeGenericMethod(typeof(TSource), typeof(TDestination));
        return Expression.Call(methodInfo, source, Expression.Quote(lambda));
    }
  4. DocSvartz commented on Jul 11, 2025

    @DocSvartz
    Contributor

    @zsr2531 If x.GetType() == typeof(DetailedAddress) it really is converted into a SQL query to the database. In this case adding is really possible.

    Can you show the resulting SQL query?

  5. zsr2531 commented on Jul 11, 2025

    @zsr2531

    Sure:

    select
    	a."Discriminator" = 'DetailedAddress',
    	a."AddressLine",
            a."HouseNumber"
    from
    	"Addresses" as a

    It seems like they lower x.GetType() == ... to a discriminator column check.

  6. zsr2531 commented on Jul 11, 2025

    @zsr2531

    Forgot to mention, but I am also on EFCore version 9.

  7. DocSvartz commented on Jul 11, 2025

    @DocSvartz
    Contributor

    It seems like they lower x.GetType() == ... to a discriminator column check.

    Yes, if it can work, then only like this :)

  8. DocSvartz commented on Jul 11, 2025

    @DocSvartz
    Contributor

    @zsr2531 It's a kind of filter.

    Expression.TypeIs(param, typeof(DetailedAddress)),
    Expression.TypeAs(Expression.Invoke(DetailedAddressProjection, Expression.TypeAs(param, typeof(DetailedAddress))), typeof(AddressDto)),

    But .Include works differently.
    Using the source type It create instance of destination type defined in the config.

  9. DocSvartz commented on Jul 11, 2025

    @DocSvartz
    Contributor

    It seems like an idea of ​​how this can be done has appeared :)
    But it will be done, not quickly.

  10. added 2 commits that reference this issue on Jun 15, 2026
  11. Tommsy64 commented on Aug 20, 2026

    @Tommsy64
    Author

    Fixed as of Mapster 10.0.9. Closing this issue.

    I did notice that a derived source type with no matching Include<TDerivedSource, TDerivedDestination>() registration projects to default(TDestination), i.e. a null row rather than an error. So adding a derived type and forgetting to register it with Include<>() in the Mapster config will silently yield nulls.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions