AutoMapper/AutoMapper.Extensions.ExpressionMapping

Flattened properties via IncludeMembers don't get mapped correctly in expressions

Opened this issue · 5 comments

Xriuk commented

This time I think this issue is related to this library.
I have a model and dtos like this:

public class Category{
  [...]
  
  public string? Name {get; set;}
}

public class Product{
  [...]
  
  public Category? Category {get; set;}
}


public class ProductDTO{
  [...]
  
  public string? CategoryName {get; set;}
}

If I create the maps "manually", everything works:

CreateMap<Product, ProductDTO>()
  [...]
  .ForMember(p => p.CategoryName, c => c.MapFrom(p => p.Category!.Name));

I can then query it regularly:

Db.Products!.UseAsDataSource(_mapper.ConfigurationProvider).For<ProductDTO>()
  .FirstOrDefault(p => p.CategoryName == "MyCategory");

But if I try to include the Category entity:

CreateMap<Category, ProductDTO>()
  [...]
  .ForMember(p => p.CategoryName, c => c.MapFrom(c => c.Name));

CreateMap<Product, ProductDTO>()
  [...]
  .IncludeMembers(p => p.Category);

Then I get an exception in the query:

'Property 'String CategoryName' is not defined for type '[...].Product' Arg_ParamName_Name'

Here's the full stack trace:

   in System.Linq.Expressions.Expression.Property(Expression expression, PropertyInfo property) in /_/src/libraries/System.Linq.Expressions/src/System/Linq/Expressions/MemberExpression.cs: riga 284
   in System.Linq.Expressions.Expression.MakeMemberAccess(Expression expression, MemberInfo member) in /_/src/libraries/System.Linq.Expressions/src/System/Linq/Expressions/MemberExpression.cs: riga 398
   in System.Linq.Enumerable.Aggregate[TSource,TAccumulate](IEnumerable`1 source, TAccumulate seed, Func`3 func) in /_/src/libraries/System.Linq/src/System/Linq/Aggregate.cs: riga 54
   in System.Linq.Expressions.MemberExpression.Accept(ExpressionVisitor visitor) in /_/src/libraries/System.Linq.Expressions/src/System/Linq/Expressions/MemberExpression.cs: riga 68
   in System.Linq.Expressions.ExpressionVisitor.Visit(Expression node) in /_/src/libraries/System.Linq.Expressions/src/System/Linq/Expressions/ExpressionVisitor.cs: riga 35
   in AutoMapper.Mappers.ExpressionMapper.MappingVisitor.VisitBinary(BinaryExpression node) in /_/src/AutoMapper.Extensions.ExpressionMapping/ExpressionMapper.cs: riga 106
   in System.Linq.Expressions.BinaryExpression.Accept(ExpressionVisitor visitor) in /_/src/libraries/System.Linq.Expressions/src/System/Linq/Expressions/BinaryExpression.cs: riga 310
   in System.Linq.Expressions.ExpressionVisitor.Visit(Expression node) in /_/src/libraries/System.Linq.Expressions/src/System/Linq/Expressions/ExpressionVisitor.cs: riga 35
   in AutoMapper.Mappers.ExpressionMapper.MappingVisitor.VisitLambdaExpression[T](Expression`1 expression) in /_/src/AutoMapper.Extensions.ExpressionMapping/ExpressionMapper.cs: riga 170
   in System.Linq.Expressions.Expression`1.Accept(ExpressionVisitor visitor) in /_/src/libraries/System.Linq.Expressions/src/System/Linq/Expressions/LambdaExpression.cs: riga 290
   in System.Linq.Expressions.ExpressionVisitor.Visit(Expression node) in /_/src/libraries/System.Linq.Expressions/src/System/Linq/Expressions/ExpressionVisitor.cs: riga 35
   in System.Linq.Enumerable.Aggregate[TSource,TAccumulate](IEnumerable`1 source, TAccumulate seed, Func`3 func) in /_/src/libraries/System.Linq/src/System/Linq/Aggregate.cs: riga 54
   in System.Linq.Expressions.Expression`1.Accept(ExpressionVisitor visitor) in /_/src/libraries/System.Linq.Expressions/src/System/Linq/Expressions/LambdaExpression.cs: riga 290
   in System.Linq.Expressions.ExpressionVisitor.Visit(Expression node) in /_/src/libraries/System.Linq.Expressions/src/System/Linq/Expressions/ExpressionVisitor.cs: riga 35
   in System.Linq.Expressions.ExpressionVisitor.VisitUnary(UnaryExpression node) in /_/src/libraries/System.Linq.Expressions/src/System/Linq/Expressions/ExpressionVisitor.cs: riga 540
   in System.Linq.Expressions.UnaryExpression.Accept(ExpressionVisitor visitor) in /_/src/libraries/System.Linq.Expressions/src/System/Linq/Expressions/UnaryExpression.cs: riga 84
   in System.Linq.Expressions.ExpressionVisitor.Visit(Expression node) in /_/src/libraries/System.Linq.Expressions/src/System/Linq/Expressions/ExpressionVisitor.cs: riga 35
   in System.Linq.Expressions.ExpressionVisitor.Visit(ReadOnlyCollection`1 nodes) in /_/src/libraries/System.Linq.Expressions/src/System/Linq/Expressions/ExpressionVisitor.cs: riga 69
   in AutoMapper.Mappers.ExpressionMapper.MappingVisitor.GetConvertedMethodCall(MethodCallExpression node) in /_/src/AutoMapper.Extensions.ExpressionMapping/ExpressionMapper.cs: riga 76
   in AutoMapper.Mappers.ExpressionMapper.MappingVisitor.VisitMethodCall(MethodCallExpression node) in /_/src/AutoMapper.Extensions.ExpressionMapping/ExpressionMapper.cs: riga 65
   in System.Linq.Expressions.MethodCallExpression.Accept(ExpressionVisitor visitor) in /_/src/libraries/System.Linq.Expressions/src/System/Linq/Expressions/MethodCallExpression.cs: riga 108
   in System.Linq.Expressions.ExpressionVisitor.Visit(Expression node) in /_/src/libraries/System.Linq.Expressions/src/System/Linq/Expressions/ExpressionVisitor.cs: riga 35
   in System.Linq.Expressions.ExpressionVisitor.Visit(ReadOnlyCollection`1 nodes) in /_/src/libraries/System.Linq.Expressions/src/System/Linq/Expressions/ExpressionVisitor.cs: riga 69
   in AutoMapper.Mappers.ExpressionMapper.MappingVisitor.GetConvertedMethodCall(MethodCallExpression node) in /_/src/AutoMapper.Extensions.ExpressionMapping/ExpressionMapper.cs: riga 76
   in AutoMapper.Mappers.ExpressionMapper.MappingVisitor.VisitMethodCall(MethodCallExpression node) in /_/src/AutoMapper.Extensions.ExpressionMapping/ExpressionMapper.cs: riga 65
   in System.Linq.Expressions.MethodCallExpression.Accept(ExpressionVisitor visitor) in /_/src/libraries/System.Linq.Expressions/src/System/Linq/Expressions/MethodCallExpression.cs: riga 108
   in AutoMapper.Extensions.ExpressionMapping.Impl.SourceInjectedQueryProvider`2.ConvertDestinationExpressionToSourceExpression(Expression expression) in /_/src/AutoMapper.Extensions.ExpressionMapping/Impl/SourceInjectedQueryProvider.cs: riga 320
   in AutoMapper.Extensions.ExpressionMapping.Impl.SourceInjectedQueryProvider`2.Execute[TResult](Expression expression) in /_/src/AutoMapper.Extensions.ExpressionMapping/Impl/SourceInjectedQueryProvider.cs: riga 82
   in System.Linq.Queryable.FirstOrDefault[TSource](IQueryable`1 source) in /_/src/libraries/System.Linq.Queryable/src/System/Linq/Queryable.cs: riga 1064

What you've posted runs for me without an exception. This [...] means removed for brevity correct?

        [Fact]
        public void Issuew()
        {
            var config = new MapperConfiguration(c =>
            {
                c.CreateMap<Category, ProductDTO>()
                    .ForMember(p => p.CategoryName, c => c.MapFrom(c => c.Name));

                c.CreateMap<Product, ProductDTO>()
                    .IncludeMembers(p => p.Category);
            });

            config.AssertConfigurationIsValid();
            var mapper = config.CreateMapper();

            Expression<Func<ProductDTO, bool>> expr = x => x.CategoryName == "MyCategory";

            //var mappedExpression = mapper.MapExpression<Expression<Func<Product, bool>>>(expr);
            IQueryable<Product> products = new List<Product>() { new Product { Category = new Category { Name = "MyCategory" } } }.AsQueryable();
            var dto = products.UseAsDataSource(mapper.ConfigurationProvider).For<ProductDTO>().FirstOrDefault(p => p.CategoryName == "MyCategory");
        }

        public class Category
        {
            public string? Name { get; set; }
        }

        public class Product
        {
            public Category? Category { get; set; }
        }

        public class ProductDTO
        {
            public string? CategoryName { get; set; }
        }

You'll want to post something anyone can copy, paste, run and see the exception.

Xriuk commented

I'll check tomorrow better, does it work for Ordering too for you? Like OrderBy(p => p.CategoryName)

It's going to be up to you to reproduce exceptions :). Plenty of tests here including OrderBy.

Xriuk commented

I tried again, with the following code:

public class TestCategory {
  public string? Name { get; set; }
}

public class TestProduct {
  public TestCategory? Category { get; set; }
}

public class TestProductDTO {
  public string? Name { get; set; }
}

void Test(){
  var config = new MapperConfiguration(c =>
  {
    c.CreateMap<TestCategory, TestProductDTO>();
    c.CreateMap<TestProduct, TestProductDTO>()
      .IncludeMembers(p => p.Category);
  });
  
  config.AssertConfigurationIsValid();
  var mapper = config.CreateMapper();
  
  var products = new List<TestProduct>() {
    new TestProduct {
      Category = new TestCategory { Name = "MyCategory" }
    }
  }.AsQueryable();
  
  Expression<Func<TestProductDTO, bool>> expr = x => x.Name == "MyCategory";
  var mappedExpression = mapper.MapExpression<Expression<Func<TestProduct, bool>>>(expr);
  
  var aaa = products.UseAsDataSource(mapper.ConfigurationProvider).For<TestProductDTO>()
    .FirstOrDefault(x => x.Name == "MyCategory");
}

And I get weird results... mappedExpression is getting mapped correctly to x => x.Category.Name == "MyCategory" while when querying below I get the same exception:

'Property 'System.String Name' is not defined for type '[...].TestProduct' Arg_ParamName_Name'

The stack trace is a little bit different:

   in System.Linq.Expressions.Expression.Property(Expression expression, PropertyInfo property) in /_/src/libraries/System.Linq.Expressions/src/System/Linq/Expressions/MemberExpression.cs: riga 284
   in System.Linq.Expressions.Expression.MakeMemberAccess(Expression expression, MemberInfo member) in /_/src/libraries/System.Linq.Expressions/src/System/Linq/Expressions/MemberExpression.cs: riga 398
   in System.Linq.Enumerable.Aggregate[TSource,TAccumulate](IEnumerable`1 source, TAccumulate seed, Func`3 func) in /_/src/libraries/System.Linq/src/System/Linq/Aggregate.cs: riga 54
   in System.Linq.Expressions.MemberExpression.Accept(ExpressionVisitor visitor) in /_/src/libraries/System.Linq.Expressions/src/System/Linq/Expressions/MemberExpression.cs: riga 68
   in System.Linq.Expressions.ExpressionVisitor.Visit(Expression node) in /_/src/libraries/System.Linq.Expressions/src/System/Linq/Expressions/ExpressionVisitor.cs: riga 35
   in AutoMapper.Mappers.ExpressionMapper.MappingVisitor.VisitBinary(BinaryExpression node) in /_/src/AutoMapper.Extensions.ExpressionMapping/ExpressionMapper.cs: riga 106
   in System.Linq.Expressions.BinaryExpression.Accept(ExpressionVisitor visitor) in /_/src/libraries/System.Linq.Expressions/src/System/Linq/Expressions/BinaryExpression.cs: riga 310
   in System.Linq.Expressions.ExpressionVisitor.Visit(Expression node) in /_/src/libraries/System.Linq.Expressions/src/System/Linq/Expressions/ExpressionVisitor.cs: riga 35
   in AutoMapper.Mappers.ExpressionMapper.MappingVisitor.VisitLambdaExpression[T](Expression`1 expression) in /_/src/AutoMapper.Extensions.ExpressionMapping/ExpressionMapper.cs: riga 170
   in System.Linq.Expressions.Expression`1.Accept(ExpressionVisitor visitor) in /_/src/libraries/System.Linq.Expressions/src/System/Linq/Expressions/LambdaExpression.cs: riga 290
   in System.Linq.Expressions.ExpressionVisitor.Visit(Expression node) in /_/src/libraries/System.Linq.Expressions/src/System/Linq/Expressions/ExpressionVisitor.cs: riga 35
   in System.Linq.Enumerable.Aggregate[TSource,TAccumulate](IEnumerable`1 source, TAccumulate seed, Func`3 func) in /_/src/libraries/System.Linq/src/System/Linq/Aggregate.cs: riga 54
   in System.Linq.Expressions.Expression`1.Accept(ExpressionVisitor visitor) in /_/src/libraries/System.Linq.Expressions/src/System/Linq/Expressions/LambdaExpression.cs: riga 290
   in System.Linq.Expressions.ExpressionVisitor.Visit(Expression node) in /_/src/libraries/System.Linq.Expressions/src/System/Linq/Expressions/ExpressionVisitor.cs: riga 35
   in System.Linq.Expressions.ExpressionVisitor.VisitUnary(UnaryExpression node) in /_/src/libraries/System.Linq.Expressions/src/System/Linq/Expressions/ExpressionVisitor.cs: riga 540
   in System.Linq.Expressions.UnaryExpression.Accept(ExpressionVisitor visitor) in /_/src/libraries/System.Linq.Expressions/src/System/Linq/Expressions/UnaryExpression.cs: riga 84
   in System.Linq.Expressions.ExpressionVisitor.Visit(Expression node) in /_/src/libraries/System.Linq.Expressions/src/System/Linq/Expressions/ExpressionVisitor.cs: riga 35
   in System.Linq.Expressions.ExpressionVisitor.Visit(ReadOnlyCollection`1 nodes) in /_/src/libraries/System.Linq.Expressions/src/System/Linq/Expressions/ExpressionVisitor.cs: riga 69
   in AutoMapper.Mappers.ExpressionMapper.MappingVisitor.GetConvertedMethodCall(MethodCallExpression node) in /_/src/AutoMapper.Extensions.ExpressionMapping/ExpressionMapper.cs: riga 76
   in AutoMapper.Mappers.ExpressionMapper.MappingVisitor.VisitMethodCall(MethodCallExpression node) in /_/src/AutoMapper.Extensions.ExpressionMapping/ExpressionMapper.cs: riga 65
   in System.Linq.Expressions.MethodCallExpression.Accept(ExpressionVisitor visitor) in /_/src/libraries/System.Linq.Expressions/src/System/Linq/Expressions/MethodCallExpression.cs: riga 108
   in AutoMapper.Extensions.ExpressionMapping.Impl.SourceInjectedQueryProvider`2.ConvertDestinationExpressionToSourceExpression(Expression expression) in /_/src/AutoMapper.Extensions.ExpressionMapping/Impl/SourceInjectedQueryProvider.cs: riga 320
   in AutoMapper.Extensions.ExpressionMapping.Impl.SourceInjectedQueryProvider`2.Execute[TResult](Expression expression) in /_/src/AutoMapper.Extensions.ExpressionMapping/Impl/SourceInjectedQueryProvider.cs: riga 82
   in System.Linq.Queryable.FirstOrDefault[TSource](IQueryable`1 source, Expression`1 predicate) in /_/src/libraries/System.Linq.Queryable/src/System/Linq/Queryable.cs: riga 1095

The recommended approach is the following:

        [Fact]
        public void Issue()
        {
            var config = new MapperConfiguration(c =>
            {
                c.CreateMap<TestCategory, TestProductDTO>();
                c.CreateMap<TestProduct, TestProductDTO>()
                  .IncludeMembers(p => p.Category);
            });

            config.AssertConfigurationIsValid();
            var mapper = config.CreateMapper();

            var products = new List<TestProduct>() {
                new TestProduct {
                  Category = new TestCategory { Name = "MyCategory" }
                }
              }.AsQueryable();

            Expression<Func<TestProductDTO, bool>> expr = x => x.Name == "MyCategory";
            var mappedExpression = mapper.MapExpression<Expression<Func<TestProduct, bool>>>(expr);

            products = products.Where(mappedExpression);
            var result = mapper.ProjectTo<TestProductDTO>(products).FirstOrDefault();

            //var aaa = products.UseAsDataSource(mapper.ConfigurationProvider).For<TestProductDTO>()
              //.FirstOrDefault(x => x.Name == "MyCategory");
        }

It is less "black boxed" for one thing. Also filtering before projection is recommended - see this issue from a month ago. The ReadMe has a couple of examples of extension methods - using Map not ProjectTo but that's the idea.

Note that the expression mapping code in UseAsDataSource is not the same code used by MapExpression and is less frequently maintained. Ok to submit a PR if you're interested.