简体   繁体   中英

Entity Framework Core Invoke an Expression on Navigation property

Tried to search a lot for this but couldn't come up with an answer that works. Here's what I am trying to do:

I have Entity ObjectA that has a Navigation property ObjectB (not a Collection type, it's a virtual property that gets loaded via lazy loading). When I do a Where query for A, I want to also expand property B in the expression using another Expression that maps B.

Here's some code to demonstrate, question is in the ToObjectADto() function

public static Expression<Func<ObjectB, ObjectBDto>> ToObjectBDto()
{
  return b => new ObjectBDto
  {
    Prop1 = b.Prop1,
    Prop2 = b.Prop2;
  };
}

public static Expression<Func<ObjectA, ObjectADto>> ToObjectADto()
{
  return a => new ObjectADto
  {
    Name = a.Name,
    SomeProperty = a.SomeProperty,
    ObjectB = /* How can I call the ToObjectBDto Expression here without re-writing it? */
  };
}

var aDto = _dbContext.ObjectAs.Where(q => q.SomeProperty > 0).Select(ToObjectADto());

I tried to create a compiled Expression:

private static _toBDtoCompiled = ToObjectBDto().Compile();

and then call it in ToObjectADto() like below but I get the API Error There is already an open DataReader associated error because it's doing it client side.

public static Expression<Func<ObjectA, ObjectADto>> ToObjectADto()
{
  return a => new ObjectADto
  {
    Name = a.Name,
    SomeProperty = a.SomeProperty,
    ObjectB = _toBDto().Invoke(a.ObjectB)
  };
}

My recommendation would be to save yourself the work and leverage AutoMapper. The benefit here is that Automapper can feed off EF's IQueryable implementation via ProjectTo to build queries and populate DTO graphs.

var config = new MapperConfiguration(cfg =>
{
   cfg.CreateMap<ObjectA, ObjectADto>();
   cfg.CreateMap<ObjectB, ObjectBDto>();
});

var aDto = _dbContext.ObjectAs.Where(q => q.SomeProperty > 0).ProjectTo<ObjectADto>(config);

Any particular mapping that cannot be inferred between Object and DTO can be set up in the mapping. The benefit of ProjectTo vs. custom mapping is that it will build a related query without tripping out lazy load hits or risking triggering code that EF cannot translate into SQL. (One query to populate all relevant DTOs)

Automapper can assist with copying values from a DTO back into either a new entity or update an existing entity:

var config = new MapperConfiguration(cfg =>
{
   cfg.CreateMap<NewObjectADto, ObjectA>(); 
   cfg.CreateMap<UpdateObjectADto, ObjectA>(); 
});

var mapper = config.CreateMapper();

New..

var objectA = mapper.Map<ObjectA>(dto);
_dbContext.ObjectAs.Add(objectA);

... or Update existing.

var objectA = _dbContext.ObjectAs.Single(x => x.ObjectAId == dto.ObjectAId);
mapper.Map(objectA, dto);

Where the DTO reflects either the data needed to create a new object, or the data that the client is allowed to update for updating the existing object. The goal being to keep update/add/delete operations as atomic as possible vs. passing large complex objects /w relatives to be updated all at once. Ie actions like "AddObjectBToA" "RemoveObjectBFromA" etc. rather than resolving all operations via a single "UpdateObjectA".

It's a pity that C# doesn't handle compiling a lambda into an expression, where one expression calls another. Particularly since an expression tree can represent this case. But then EF Core 3 or higher won't look through invoke expressions anyway.

Automapper is probably easier. But if you don't want to use 3rd party code, you'll have to inline the expression yourself. Including replacing any ParameterExpression with the argument to the method.

public static R Invoke<T, R>(this Expression<Func<T, R>> expression, T argument) => throw new NotImplementedException();
// etc for expressions with more parameters

public class InlineVisitor : ExpressionVisitor {
    protected override Expression VisitMethodCall(MethodCallExpression node)
    {
        if (node.Method.Name == "Invoke"
            && node.Object == null
            && node.Arguments.Count >= 1
            && node.Arguments[0] is LambdaExpression expr)
            return Visit(
                new ReplacingExpressionVisitor(
                    expr.Parameters.ToArray(),
                    node.Arguments.Skip(1).ToArray())
                .Visit(expr.Body)
            );
        return base.VisitMethodCall(node);
    }
}

// usage;
public static Expression<Func<ObjectA, ObjectADto>> ToObjectADto()
{
    var ToBorNotToB = ToObjectBDto();
    Expression<Func<ObjectA, ObjectADto>> expr = a => new ObjectADto
    {
        Name = a.Name,
        SomeProperty = a.SomeProperty,
        ObjectB = ToBorNotToB.Invoke(a.ObjectB)
    };
    return new InlineVisitor().VisitAndConvert(expr), "");
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM