riok / mapperly

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

OrderBy for properties in IQueryable Projection #1127

Open johnwnowlin opened 8 months ago

johnwnowlin commented 8 months ago

Is your feature request related to a problem? Please describe.

I would love to OrderBy a child list in a IQueryable projections map. The example I have is a list of counties for a particular state.

Describe the solution you'd like

Ability to add [OrderBy(nameof(Counties.Name))] maybe similar to current Constructor Mappings.

[Mapper]
public partial class TerritoryMapper : IMapper<TerritoryRequest, Territory>
{
    [OrderBy(nameof(Counties.Name))]
    public partial IQueryable<TerritoryResponse> ToResponse(IQueryable<Territory> territory);
}

// Expected generated code
public partial class TerritoryMapper
{
    public partial global::System.Linq.IQueryable<global::RepData.Entities.Models.TerritoryResponse> ToResponse(global::System.Linq.IQueryable<global::RepData.Entities.Models.Territory> territory)
    {
#nullable disable
        return System.Linq.Queryable.Select(territory, x => new global::RepData.Entities.Models.TerritoryResponse()
        { 
               ...
        },
       Counties = global::System.Linq.Enumerable.ToList(global::System.Linq.Enumerable.Select(x.Counties, x1 => new global::RepData.Entities.Models.CountyResponse()
        {
            Id = x1.Id,
            Name = x1.Name,
        })
        .OrderBy(x2 => x2.Name)
 ),

Describe alternatives you've considered

After mapping I apply order by to the items. This requires another loop. Also does not let efcore know to apply order by to sql statements.

var mapper = new TerritoryMapper();
var response = await mapper.ToResponse(query)
    .ToArrayAsync();

foreach (var item in response)
{
    item.Counties = item.Counties
        .OrderBy(a => a.Name)
        .ToList();
}
latonz commented 8 months ago

Thank you for opening this issue. Couldn't this be achieved by a combination of https://github.com/riok/mapperly/issues/783 and https://github.com/riok/mapperly/issues/863 by implementing a user-implemented mapping from IEnumerable<TSource> to IEnumerable<TTarget> which handles the ordering?

johnwnowlin commented 8 months ago

Upgraded to Mapperley 3.4 and tried this

    [MapProperty(nameof(CountyResponse), nameof(TerritoryResponse.Counties))]
    public List<CountyResponse> OrderCounties(ICollection<County> counties) => counties.OrderBy(a => a.Name).ToList();

But that generates

}
Counties = OrderCounties(x.Counties),

Which is close, but requires that I add the new CountyResponse() { ... } into the MapProperty function.

[MapProperty(nameof(CountyResponse), nameof(TerritoryResponse.Counties))]
public List<CountyResponse> OrderCounties(ICollection<County> x) =>
    Enumerable.ToList(Enumerable.Select(x, x1 => new CountyResponse()
    {
        Id = x1.Id,
        Name = x1.Name,
    }))
    .OrderBy(a => a.Name)
    .ToList();

I tried several variations with less success.

If you see how to use the MapProperty function more accurately please guide me.

latonz commented 8 months ago

Oh https://github.com/riok/mapperly/issues/783 is not yet released and #863 is not yet implemented.