AutoMapper/AutoMapper.Extensions.ExpressionMapping

Wrong Parameter Expression used in mapped expression

marcOcram opened this issue · 2 comments

Hello,

I've got an issue with the expression mapping not using the parameter of a lambda. The configuration maps an expression to the boolean property IsLatest of the DTO: cfg.CreateMap<Check, CheckDTO>().ForMember(dest => dest.IsLatest, c => c.MapFrom(src => src.Finished == null && !src.Part.History.Any(ch => ch.Started > src.Started)));

This creates the wrong expression c => ((c.Finished == null) AndAlso Not(c.Part.History.Any(ch => (c.Started > c.Started)))) where the parameter ch is not used. c is used instead for both c.Started > c.Started.

.Lambda #Lambda1<System.Func`2[AutomapperExpression.Check,System.Boolean]>(AutomapperExpression.Check $c) {
    $c.Finished == null && !.Call System.Linq.Enumerable.Any(
        ($c.Part).History,
        .Lambda #Lambda2<System.Func`2[AutomapperExpression.Check,System.Boolean]>)
}

.Lambda #Lambda2<System.Func`2[AutomapperExpression.Check,System.Boolean]>(AutomapperExpression.Check $ch) {
    $c.Started > $c.Started
}

If I map the checks via mapper.Map<List<CheckDTO>>(part.History); the property IsLatest is set correctly to only one check object.

Version used:
.NET 5
AutoMapper 10.1.1
AutoMapper.Extensions.ExpressionMapping 4.1.1

Reproduction:

using AutoMapper;
using AutoMapper.Extensions.ExpressionMapping;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;

namespace AutomapperExpression
{
    public class Check
    {
        public DateTime? Finished { get; set; }
        public Part Part { get; set; }
        public DateTime Started { get; set; }
    }

    public class CheckDTO
    {
        public bool IsLatest { get; set; }
        public Guid Part { get; set; }
    }

    public class Part
    {
        public List<Check> History { get; } = new List<Check>();
        public Guid ID { get; } = Guid.NewGuid();
    }

    public static class Program
    {
        private static void Main( string[] args )
        {
            // source
            Part part = new Part();
            part.History.Add( new Check() { Started = DateTime.Now.AddMinutes( -2 ), Part = part } );
            part.History.Add( new Check() { Started = DateTime.Now, Part = part } );

            // mapping configuration
            MapperConfiguration mapperConfiguration = new MapperConfiguration( cfg =>
            {
                cfg.AddExpressionMapping();
                cfg.CreateMap<Check, CheckDTO>()
                    .ForMember( dest => dest.Part, c => c.MapFrom( src => src.Part.ID ) )
                    // check is latest if history does not contain any check which has a greater started timestamp
                    .ForMember( dest => dest.IsLatest, c => c.MapFrom( src => src.Finished == null && !src.Part.History.Any( ch => ch.Started > src.Started ) ) );
            } );

            IMapper mapper = mapperConfiguration.CreateMapper();

            // get check DTO which is the latest via DTO expression and mapped expression
            Expression<Func<CheckDTO, bool>> dtoExpression = c => c.IsLatest;
            // this creates an expression where the parameter is not used
            //.Lambda #Lambda2<System.Func`2[AutomapperExpression.Check,System.Boolean]>(AutomapperExpression.Check $ch) {
            //  $c.Started > $c.Started
            //}
            Expression<Func<Check, bool>> expression = mapper.MapExpression<Expression<Func<Check, bool>>>( dtoExpression );

            Console.WriteLine( "Result:" );

            foreach ( var check in part.History.Where( expression.Compile() ) )
            {
                Console.WriteLine( $"Check: {check.Started}" );
            }

            Console.WriteLine( "Expectation:" );

            Console.WriteLine( $"Check: {part.History.Single( c => c.Finished == null && !c.Part.History.Any( ch => ch.Started > c.Started ) ).Started}" );

            Console.ReadLine();
        }
    }
}

You are correct. We need a more specific check here.

It should work now with the myget build.