riok / mapperly

A .NET source generator for generating object mappings. No runtime reflection.
https://mapperly.riok.app
Apache License 2.0
2.53k stars 138 forks source link

parent child IQueryable mapping: recursion depth support #785

Open Azaferany opened 11 months ago

Azaferany commented 11 months ago

Describe the bug when i want map parent class with prop of list of child and child class have prop of parent class (simple parent child relationship) and use IQueryable mapping mappings not generated and "Stack overflow" error occurre on build

To Reproduce

public class CarDto
{
    public string Name { get; set; }
    public List<SeatDto> Seats { get; set; }
}
public class SeatDto { 
    public int Position { get; set; }
    public CarDto Car { get; set; }
}

public class Car
{
    public string Name { get; set; }
    public List<Seat> Seats { get; set; }
}
public class Seat
{
    public int Position { get; set; }
    public Car Car { get; set; }
}
[Mapper]
public static partial class CarMapper
{
    public static partial IQueryable<CarDto> ProjectToDto(this IQueryable<Car> q);
}

Expected behavior i thing its stuck in loop of mapping but i think would be great if ignoring Carproperty on Seat class mapping or atlest it could be ignord by MapperIgnoreSourceAttribute (MapperIgnoreSourceAttribute & MapperIgnoreTargetAttribute not working on IQueryable mappings )

Environment (please complete the following information):

Additional context MapperIgnoreSourceAttribute & MapperIgnoreTargetAttribute not working on IQueryable mappings

latonz commented 11 months ago

Does it work if you define your mapper like follows:

[Mapper]
public static partial class CarMapper
{
    public static partial IQueryable<CarDto> ProjectToDto(this IQueryable<Car> q);

    [MapperIgnoreSource(nameof(Seat.Car))]
    private static partial SeatDto MapSeat(this Seat source);
}

(IQueryable projection mappings pick up configurations in the same mapper for types which need to be mapped by the IQueryable projection mapping).

tvardero commented 11 months ago

I also have got an stack overflow. With those models:

public class Node
{
    public int Id { get; init; }

    public int? ParentNodeId { get; init; }
    public Node? ParentNode { get; init; }

    public Node[] ChildNodes { get; init; }
}

public class NodeDto
{
    public int Id { get; init; }

    public int? ParentNodeId { get; init; }

    public NodeDto[] ChildNodes { get; init; }
}

public static partial IQueryable<NodeDto> ProjectToDto(this IQueryable<Node> query);

I assume that mapper does not know how deep he needs to project child nodes:

// assumption:
return query.Select(n1 => new NodeDto
        {
            Id = n1.Id,
            ParentNodeId = n1.ParentNodeId,
            ChildNodes = n1.ChildNodes.Select(n2 => new NodeDto
            {
                Id = n2.Id,
                ParentNodeId = n2.ParentNodeId,
                ChildNodes = n2.ChildNodes.Select(n3 => new NodeDto
                {
                    Id = n3.Id,
                    ParentNodeId = n3.ParentNodeId,
                    ChildNodes = n3.ChildNodes.Select(n4 => new ) // and so on
                })
            })
        })

For now I don't see any solution to map it using only mapper. Probably children entities need to be mapped outside of query.

TimothyMakkison commented 11 months ago

(IQueryable projection mappings pick up configurations in the same mapper for types which need to be mapped by the IQueryable projection mapping).

Does Mapperly do this? I know IQueryable can reuse user implemented methods.

I had the following generate with a user implemented method. Not sure what should go in MapSeat.

[Mapper]
public static partial class Mapper
{
    public static partial IQueryable<CarDto> ProjectToDto(this IQueryable<Car> q);

    private static SeatDto MapSeat(this Seat source) => throw new NotImplementedException();
}

// generates
public static partial class Mapper
{
    public static partial global::System.Linq.IQueryable<global::Riok.Mapperly.Sample.CarDto> ProjectToDto(this global::System.Linq.IQueryable<global::Riok.Mapperly.Sample.Car> q)
    {
#nullable disable
        return System.Linq.Queryable.Select(q, x => new global::Riok.Mapperly.Sample.CarDto()
        {
            Name = x.Name,
            Seats = global::System.Linq.Enumerable.ToList(global::System.Linq.Enumerable.Select(x.Seats, x1 => MapSeat(x1))),
        });
#nullable enable
    }
}

Does EF handle recursive queries (uses a CTE?). Perhaps this would be best done manually with raw SQL. How do Automapper/mapster handle this?

We probably need a separate issue for enhanced loop detection. Perhaps a MaxDepth property could be added? See here