查询和映射复杂对象

时间:2017-07-25 15:21:58

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

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

这是我原来的查询,它快速且效果很好。

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();

使用这些Mappers

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无法识别方法&#39; ProjectToProductDTO(IQueryable [Products])&#39;,此方法无法转换为商店表达式。&#34;}

我可以让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表达式节点类型&#39;调用&#39; LINQ to Entities

不支持

2 个答案:

答案 0 :(得分:1)

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

https://github.com/scottksmith95/LINQKit

答案 1 :(得分: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访问productProjection上名为ConstantExpression(!)的字段,其中包含一个实例一个奇怪命名的编译器生成的闭包类。

所以:查找.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
        }
    }
}