Skip to content

Query projection with subquery can generate non-compiling expression #3563

@Erythnul

Description

@Erythnul

Hey guys,

I've run into an issue with query projection when subqueries are included.
I've spent some time debugging and seeing if I could get it fixed up, I've got a pull request coming up with a fix that definitely works for my scenario and most probably other ones as well.
The code isn't the most elegant, but I'm not exactly at home in your source so I tried to keep my additions small and easily readable. If you've got a better way of fixing this problem, I'll gladly change the source (or feel free to change it yourself and fix up the pull request before merging).

More details in the reproduction repo:

https://github.com/Erythnul/AutomapperIssue

Source/destination types

//Source
public class Order
{
    public Guid Id { get; set; }
    public Guid CustomerId { get; set; }

    public int? MostImportantOrderLine { get; set; }

    public ICollection<OrderLine> OrderLines { get; set; } = null!;
}
public class OrderLine
{
    public Guid Id { get; set; }
    public Guid OrderId { get; set; }

    public int OrderLineNumber { get; set; }
    public string Description { get; set; } = null!;

    public Order Order { get; set; } = null!;
}

//Destination
public class OrderModel
{
    public OrderSubModel? OrderSubModel { get; set; }
    public OrderLineModel? MostImportantOrderLine { get; set; }
}

public class OrderSubModel
{
    public Guid? OrderId { get; set; }
}

public class OrderLineModel
{
    public Guid OrderLineId { get; set; }
    public string Description { get; set; } = string.Empty;
}

Mapping configuration

new MapperConfiguration(cfg =>
{
    cfg.CreateMap<Entities.Order, Dtos.OrderModel>()
        .ForMember(dst => dst.OrderSubModel, opt => opt.MapFrom(src => src.MostImportantOrderLine != null ? src : null))
        .ForMember(dst => dst.MostImportantOrderLine, opt =>
        {
            opt.MapFrom(src => src.OrderLines.FirstOrDefault(x => x.OrderLineNumber == src.MostImportantOrderLine));
        });

    cfg.CreateMap<Entities.Order, Dtos.OrderSubModel>()
        .ForMember(dst => dst.OrderId, opt => opt.MapFrom(src => src.Id));

    cfg.CreateMap<Entities.OrderLine, Dtos.OrderLineModel>()
        .ForMember(dst => dst.OrderLineId, opt => opt.MapFrom(src => src.Id))
        .ForMember(dst => dst.Description, opt => opt.MapFrom(src => src.Description));
});

Version: 10.1.1

Expected behavior

//projectedQuery.Expression
queryable.Select(
    dtoOrder => new Object_1548421275___MostImportantOrderLine_MostImportantOrderLine_MostImportantOrderLine
    {
        __MostImportantOrderLine = dtoOrder.OrderLines.FirstOrDefault(x => ((int?)x.OrderLineNumber) == dtoOrder.MostImportantOrderLine),
        MostImportantOrderLine = dtoOrder.MostImportantOrderLine,
        Id = dtoOrder.Id
    }).Select(
    dtoLet => new Dtos.OrderModel
    {
        MostImportantOrderLine = (dtoLet.__MostImportantOrderLine == null)
            ? null
            : new Dtos.OrderLineModel
            {
                Description = dtoLet.__MostImportantOrderLine.Description,
                OrderLineId = dtoLet.__MostImportantOrderLine.Id
            },
        OrderSubModel = (((dtoLet.MostImportantOrderLine != null) ? dtoLet : null) == null)
            ? null
            : new Dtos.OrderSubModel
            {
                OrderId = (Guid?)((dtoLet.MostImportantOrderLine != null) ? dtoLet : null).Id
            }
    });

Actual behavior

//projectedQuery.Expression
queryable.Select(
    dtoOrder => new Object_1548421275___MostImportantOrderLine_MostImportantOrderLine_MostImportantOrderLine
    {
        __MostImportantOrderLine = dtoOrder.OrderLines.FirstOrDefault(x => ((int?)x.OrderLineNumber) == dtoOrder.MostImportantOrderLine),
        MostImportantOrderLine = dtoOrder.MostImportantOrderLine
        //, Id = dtoOrder.Id
        // Doesn't collect OrderId into dtoLet, is also not added as possible field to Proxy Object above
    }).Select(
    dtoLet => new Dtos.OrderModel
    {
        MostImportantOrderLine = (dtoLet.__MostImportantOrderLine == null)
            ? null
            : new Dtos.OrderLineModel
            {
                Description = dtoLet.__MostImportantOrderLine.Description,
                OrderLineId = dtoLet.__MostImportantOrderLine.Id
            },
        OrderSubModel = (((dtoLet.MostImportantOrderLine != null) ? dtoOrder : null) == null) //Doesn't compile, should be dtoLet
            ? null
            : new Dtos.OrderSubModel
            {
                OrderId = (Guid?)((dtoLet.MostImportantOrderLine != null) ? dtoOrder : null).Id //Doesn't compile, should be dtoLet
            }
    });

Steps to reproduce

https://github.com/Erythnul/AutomapperIssue

var orders = new[]
{
    new Order
    {
        Id = Guid.Parse("00000000-0000-0000-0000-000000000001"),
        MostImportantOrderLine = 1,
        OrderLines = new List<OrderLine>
        {
            new OrderLine
            {
                Id = Guid.Parse("00000000-0000-0000-0000-000000000101"),
                OrderId = Guid.Parse("00000000-0000-0000-0000-000000000001"),
                OrderLineNumber = 1,
                Description = "ChessSet"
            }
        }
    }
}.AsQueryable();

var projection = orders.ProjectTo<OrderModel>(Configuration);
var orderModel = projection.First(); //throws

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions