Skip to content

ProjectTo error with aggregate functions on IEnumerable<T> #2593

@KSid

Description

@KSid

ProjectTo fails with to an unhandled exception when trying to project to a destination containing an aggregate function (e.g. Count) where the matching source is an IEnumerable<T>. This works as expected when the aggregate function is Count and the source is an ICollection<T> (e.g. List<T>) because the name matches a property on the destination object. Other aggregate functions cause the same error with ICollection<T> (but not Sum which is dicussed below).

The documentation suggests this should work.

This error is due to the correct extension method being located but then treated as a field/property accessor in MemberGetterExpressionResultConverter.cs:

private static ExpressionResolutionResult ExpressionResolutionResult(
    ExpressionResolutionResult expressionResolutionResult, MemberInfo getter)
{
    var member = Expression.MakeMemberAccess(expressionResolutionResult.ResolutionExpression, getter);
    return new ExpressionResolutionResult(member, member.Type);
}

I replaced the method above with the code below while investigating and it passes existing tests. This also allows other aggregate functions (e.g. Sum, Max, Min, etc.) to work with IEnumerable<T> but not all aggregate functions work with ICollection<T> (e.g. Sum) and I haven't found the cause. This second behaviour can be seen by changing the Points property to an ICollection<T> and then back to an IEnumerable<T>.

private static ExpressionResolutionResult ExpressionResolutionResult(
    ExpressionResolutionResult expressionResolutionResult, MemberInfo getter)
{
    var member = (getter is MethodInfo method && method.IsStatic)
        ? Expression.Call(method, expressionResolutionResult.ResolutionExpression) as Expression
        : Expression.MakeMemberAccess(expressionResolutionResult.ResolutionExpression, getter);
    return new ExpressionResolutionResult(member, member.Type);
}

I have created a gist with a full example.

I discovered this while trying to use AutoMapper with Linq2DB/EFCore and immutable models where the collections and associations are exposed as IEnumerable<T>. Although EF6 requires the many side of a one-to-many association to be an ICollection<T>, this problem may still apply due to the lack of fields/properties that match the name of aggregate functions (e.g. not Count).

Source/destination types

class TeamHistory
{
    public int TeamId { get; set; }
    public IEnumerable<int> Points { get; set; }
}

class ViewModel
{
    public int TeamId { get; private set; }
    public int PointsCount { get; private set; }
    public int PointsSum { get; private set; }
    public int PointsMin { get; private set; }
    public int PointsMax { get; private set; }
}

Mapping configuration

var config = new MapperConfiguration(_ => { });

Version: 6.2.2

Expected behavior

Output:

Team: 4
- 3 total game(s)
- 10 total point(s)
- 1 min point(s)
- 5 max point(s)

Actual behavior

Receive an unhandled exception:

System.ArgumentException: 'Member 'Int32 Count[Int32](System.Collections.Generic.IEnumerable`1[System.Int32])' not field or property'

Steps to reproduce

var teams = new List<TeamHistory>()
{
    new TeamHistory()
    {
        TeamId = 4,
        Points = new List<int>() { 1, 4, 5 }
    }
}.AsQueryable();

var config = new MapperConfiguration(_ => { });
var model = teams.ProjectTo<ViewModel>(config).Single();

Console.WriteLine($@"Team: {model.TeamId}
- {model.PointsCount} total game(s)
- {model.PointsSum} total point(s)
- {model.PointsMin} min point(s)
- {model.PointsMax} max point(s)");
Console.Write("Press enter to exit...");
Console.ReadLine();

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions