AutoMapper/AutoMapper.Extensions.ExpressionMapping

EF.Property not mapped correctly

Xriuk opened this issue · 5 comments

Xriuk commented
public class TestProduct {
  // Empty, has shadow key named BrandId
}

public class TestProductDTO {
  public int Brand { get; set; }
}

void Test{
  var config = new MapperConfiguration(c =>
  {
    c.CreateMap<TestProduct, TestProductDTO>()
      .ForMember(p => p.Brand, c => c.MapFrom(p => EF.Property<int>(p, "BrandId")));
  });
  
  config.AssertConfigurationIsValid();
  var mapper = config.CreateMapper();
  
  var products = new List<TestProduct>() {
    new TestProduct { }
  }.AsQueryable();
  
  Expression<Func<TestProductDTO, bool>> expr = x => x.Brand == 2;
  var mappedExpression = mapper.MapExpression<Expression<Func<TestProduct, bool>>>(expr);
  // The expression above returns: x => EF.Property(p, "BrandId") == 2
}

I don't know if this happens for other "functions", apparently the parameter "p" does not get replaced by some visitor during the conversion

Xriuk commented

Side note: this time I'm pretty sure that this didn't happen when using UseAsDataSource, is there any reason why there are two different behaviours for the same goal?

You are correct. Add the following to PrependParentNameVisitor and your test should work.

        protected override Expression VisitParameter(ParameterExpression node)
        {
            if (object.ReferenceEquals(CurrentParameter, node))
                return NewParameter;

            return base.VisitParameter(node);
        }

On your second question you can contribute by reconciling the two i.e. get SourceInjectedQueryProvider expression mapper to pass the MapExpression tests or get XpressionMapperVisitor to integrate with SourceInjectedQueryProvider.

Xriuk commented

I tried and it works... partially.
If I have a complex model with inclusion, the property is still mapped to the top level:

public class TestCategory {
  // Has FK BrandId
}
public class TestProduct {
  public TestCategory? Category { get; set; }
}

public class TestProductDTO {
  public int Brand { get; set; }
}

void Test(){
  var config = new MapperConfiguration(c =>
  {
    c.CreateMap<TestCategory, TestProductDTO>()
      .ForMember(p => p.Brand, c => c.MapFrom(p => EF.Property<int>(p, "BrandId"))); ;
    
    c.CreateMap<TestProduct, TestProductDTO>()
      .IncludeMembers(p => p.Category);
  });
  
  config.AssertConfigurationIsValid();
  var mapper = config.CreateMapper();
  
  var products = new List<TestProduct>() {
    new TestProduct {
      Category = new TestCategory{ }
    }
  }.AsQueryable();
  
  Expression<Func<TestProductDTO, bool>> expr = x => x.Brand == 2;
  var mappedExpression = mapper.MapExpression<Expression<Func<TestProduct, bool>>>(expr);
  // Expected: x => EF.Property<int>(x.Category, "BrandId") == 2
  // Actual:   x => EF.Property<int>(x, "BrandId") == 2
}

I took a look at the code, and I'm a bit lost... I was looking here

Expression GetMappedMemberExpression(Expression parentExpression, List<PropertyMapInfo> propertyMapInfoList)
{
Expression mappedParentExpression = this.Visit(parentExpression);
FindDestinationFullName(parentExpression.Type, mappedParentExpression.Type, node.GetPropertyFullName(), propertyMapInfoList);
if (propertyMapInfoList.Any(x => x.CustomExpression != null))
{
var fromCustomExpression = GetMemberExpressionFromCustomExpression
(
propertyMapInfoList,
propertyMapInfoList.Last(x => x.CustomExpression != null),
mappedParentExpression
);
if (node.Type.IsLiteralType())
fromCustomExpression = fromCustomExpression.ConvertTypeIfNecessary(node.Type);
this.TypeMappings.AddTypeMapping(ConfigurationProvider, node.Type, fromCustomExpression.Type);
return fromCustomExpression;
}
Expression memberExpression = GetMemberExpressionFromMemberMaps(BuildFullName(propertyMapInfoList), mappedParentExpression);
if (node.Type.IsLiteralType())
memberExpression = memberExpression.ConvertTypeIfNecessary(node.Type);
this.TypeMappings.AddTypeMapping(ConfigurationProvider, node.Type, memberExpression.Type);
return memberExpression;
}

it calls FindDestinationFullName(), which seems to populate propertyMapInfoList which is a list of mapping expressions for the passed node(?), so to convert the node to the mapped one you should apply all these expressions in order, right?
But it seems that when it calls GetMemberExpressionFromCustomExpression() it only uses the last one.
var fromCustomExpression = GetMemberExpressionFromCustomExpression
(
propertyMapInfoList,
propertyMapInfoList.Last(x => x.CustomExpression != null),
mappedParentExpression
);

Am I on the right track or not?

Right track - but that's what PrependParentNameVisitor does. Sorry my bad advice. The code should be as follows similar to VisitTypeBinary, VisitMember and VisitMethodCall in the same class.

        protected override Expression VisitParameter(ParameterExpression node)
        {
            if (object.ReferenceEquals(CurrentParameter, node))
            {
                return string.IsNullOrEmpty(ParentFullName)
                        ? NewParameter
                        : ExpressionHelpers.MemberAccesses(ParentFullName, NewParameter);
            }

            return base.VisitParameter(node);
        }

I'll update the code unless you're interested in doing the PR.

Xriuk commented

Yep, this works! You can update the code, I've never created a PR for open source projects (yet).
When I get some time I'll take a look at the SourceInjectedQueryProvider matter, wanted to make it async too.