查询和映射复杂对象

Query and map complex objects

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

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

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...
        }
});

请记住,客户端 table 有 50 多个相关的 table,因此此查询效果很好,因为它仅 selected 了我创建对象所需的字段。

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

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 does not recognize the method 'ProjectToProductDTO(IQueryable[Products])', and this method cannot be translated into a store expression."}

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

更新

用户 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)),

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

The LINQ expression node type 'Invoke' is not supported in LINQ to Entities

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

https://github.com/scottksmith95/LINQKit

您非常接近第二种方法!

假设您像以前一样定义产品实体到 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 上生成闭包 class.

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

我正在维护一个名为 Express 的用于表达内容的助手 class。 (参见另一个处理 .Compile().Invoke(...) 的类似情况的 )。

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

这是 Express class 的相关片段。

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
        }
    }
}