【问题标题】:Query and map complex objects查询和映射复杂对象
【发布时间】:2017-07-25 15:21:57
【问题描述】:

我的目标是以尽可能少的开销查询和映射复杂对象。我正在使用一个包含大量相关表的大型数据库。我正在尝试使用 LINQ 选择和投影来仅选择制作对象所需的必要信息。

这是我最初的查询,速度很快,效果很好。

List<ClientDTO> clientList = dbClients.Select(client =>
new ClientDTO
{
    ID = client.ClientID,
    FirstName = client.FirstName,
    LastName = client.LastName,
    //etc....
    Products = client.Products
        .Select(prod => new ProductDTO
        {
            ID = prod.ID,
            DateOfTransaction = prod.Date,
            //etc...
        }).ToList(),
    Items = client.Items
        .Select(item => new ItemDTO
        {
            ID = item.ID,
            Date = item.Date,
            //etc...
        }
});

请记住,Client 表有 50 多个相关表,因此这个查询非常有效,因为它只选择了我需要创建对象的字段。

现在我需要为这些对象制作映射器,并尝试构建相同的查询语句,但这次使用映射器。这就是我最终得到的结果。

List<ClientDTO> clients = dbClients.ProjectToClientDTO();

使用这些映射器

public static List<ClientDTO> ProjectToClientDTO(this IQueryable<Clients> query)
{
    var clientList = query.Select(client => new
    {
        ID = client.ClientID,
        FirstName = client.FirstName,
        LastName = client.LastName,
        //etc...
        Products = client.Products.AsQueryable().ProjectToProductDTO().ToList(),
        Items = client.Items.AsQueryable().ProjectToItemDTO().ToList()
    }

    List<ClientDTO> dtoClientList = new List<ClientDTO>();
    foreach (var client in clientList)
    {
        ClientDTO clientDTO = new ClientDTO();

        clientDTO.EncryptedID = EncryptID(client.ID, client.FirstName, client.LastName);
        //etc...
        clientDTO.Products = client.Products;
        clientDTO.Items = client.Items;
    }
    return dtoClientList;
}

public static IQueryable<ProductDTO> ProjectToProductDTO(this IQueryable<Products> query)
{
    return query.Select(prod => new ProductDTO
    {
        ID = prod.ID,
        DateOfTransaction = prod.Date,
        //etc...
    });
}

public static IQueryable<ItemDTO> ProjectToItemDTO(this IQueryable<Items> query)
{
    return query.Select(item => new ItemDTO
    {
        ID = item.ID,
        Date = item.Date,
        //etc...
    });
}

尝试运行此程序后,我收到以下错误。

LINQ to Entities 无法识别方法“ProjectToProductDTO(IQueryable[Products])”,并且该方法无法转换为存储表达式。"}

我可以让 LINQ 调用这些方法来构建查询吗? 或者有没有更好的方法来查询和映射这些对象,而无需为数百个客户端抓取 50 多个不必要的数据表?

更新

用户 Tuco 提到我可以尝试查看表达式树。在阅读了一段时间后,我想出了这个。

public static Expression<Func<Product, ProductDTO>> test = prod =>
        new ProductDTO()
        {
            ID= prod.ID,
            Date= prod.Date,
            //etc...
        };

就这样使用它。

Products = client.Products.Select(prod => test.Compile()(prod)),

但是运行这个我收到这个错误。

LINQ to Entities 不支持 LINQ 表达式节点类型“Invoke”

【问题讨论】:

  • EF 默认不返回所有图表,您必须为每个导航属性使用 Include
  • 这不是叫懒加载吗,不是默认开启的吗?
  • 是的,但在您请求之前它不会提取数据
  • 你要做的事情需要表达式树
  • 你为什么“需要”使用映射器?一般来说,您不能将用户定义的函数放入其他查询中,但您可以使用 LINQKit 之类的东西对用户定义的函数进行 lambda 扩展。

标签: c# .net linq linq-to-entities expression-trees


【解决方案1】:

您的第二种方法非常接近!

假设您像以前一样定义产品实体到 DTO(您称之为映射器)的投影:

Expression<Func<Product, ProductDTO>> productProjection = prod => new ProductDTO
{
    ID = prod.ID,
    DateOfTransaction = prod.Date
    // ...
};

以及将客户端实体投影到它的 DTO 是这样的(稍微简单,但在逻辑上等同于您所做的):

Expression<Func<Client, ClientDTO>> clientProjection = client => new ClientDTO
{
    ID = client.ClientID,
    FirstName = client.FirstName,
    // ...
    Products = client.Products.Select(productProjection.Compile()).ToList(),
    // ...
};

编译器让你这样做,但可查询的不会理解这一点。但是,您所取得的是 productProjection 以某种方式包含在表达式树中。您所要做的就是进行一些表达式操作。

如果您查看编译器为.Select 的参数构建的子树,您会发现MethodCallExpression - 对.Compile() 的调用。它是 .Object 表达式 - 要编译的东西 - 是一个 MemberExpression 访问 ConstantExpression 上名为 productProjection(!) 的字段,其中包含一个奇怪命名的编译器生成的闭包类的实例。

所以:找到.Compile() 调用并将它们替换为将要编译的,最终得到原始版本中的表达式树。

我正在维护一个名为 Express 的表达式帮助类。 (有关类似情况,请参阅另一个处理 .Compile().Invoke(...)answer

clientProjection = Express.Uncompile(clientProjection);
var clientList = dbClients.Select(clientProjection).ToList();

这是Express 类的相关片段。

public static class Express
{
    /// <summary>
    /// Replace .Compile() calls to lambdas with the lambdas themselves.
    /// </summary>
    public static Expression<TDelegate> Uncompile<TDelegate>(Expression<TDelegate> lambda)
    => (Expression<TDelegate>)UncompileVisitor.Singleton.Visit(lambda);

    /// <summary>
    /// Evaluate an expression to a value.
    /// </summary>
    private static object GetValue(Expression x)
    {
        switch (x.NodeType)
        {
            case ExpressionType.Constant:
                return ((ConstantExpression)x).Value;
            case ExpressionType.MemberAccess:
                var xMember = (MemberExpression)x;
                var instance = xMember.Expression == null ? null : GetValue(xMember.Expression);
                switch (xMember.Member.MemberType)
                {
                    case MemberTypes.Field:
                        return ((FieldInfo)xMember.Member).GetValue(instance);
                    case MemberTypes.Property:
                        return ((PropertyInfo)xMember.Member).GetValue(instance);
                    default:
                        throw new Exception(xMember.Member.MemberType + "???");
                }
            default:
                // NOTE: it would be easy to compile and invoke the expression, but it's intentionally not done. Callers can always pre-evaluate and pass a member of a closure.
                throw new NotSupportedException("Only constant, field or property supported.");
        }
    }

    private sealed class UncompileVisitor : ExpressionVisitor
    {
        public static UncompileVisitor Singleton { get; } = new UncompileVisitor();
        private UncompileVisitor() { }

        protected override Expression VisitMethodCall(MethodCallExpression node)
        {
            if (node.Method.Name != "Compile" || node.Arguments.Count != 0 || node.Object == null || !typeof(LambdaExpression).IsAssignableFrom(node.Object.Type))
                return base.VisitMethodCall(node);
            var lambda = (LambdaExpression)GetValue(node.Object);
            return lambda;

            // alternatively recurse on the lambda if it possibly could contain .Compile()s
            // return Visit(lambda); // recurse on the lambda
        }
    }
}

【讨论】:

    【解决方案2】:

    使用 LINQKit 将用户定义的 lambda 函数扩展为查询所需的 lambda:

    https://github.com/scottksmith95/LINQKit

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-10-21
      • 1970-01-01
      • 2019-02-13
      • 2012-10-16
      • 1970-01-01
      相关资源
      最近更新 更多