多组连接 lambda 到表达式树

Posted

技术标签:

【中文标题】多组连接 lambda 到表达式树【英文标题】:Multiple group join lambda to expression tree 【发布时间】:2014-09-10 17:53:48 【问题描述】:

在我们的数据库中,我们有许多表,它们具有相应的翻译表,语言和地区 ID(映射到其他表),语言 1 是英语,语言 1 的默认地区是英国。所有具有转换表的表都具有以下默认列(尽管实体框架类上没有定义接口):

<EntityTableName>
EntityTableNameID INT PK
Reference NVARCHAR NULL
[Any other columns]

<EntityTableNameTranslation>
EntityTableNameID INT NOT NULL
LanguageID INT NOT NULL
RegionID INT NULL
Title NVARCHAR NOT NULL
Description NVARCHAR NULL

整个数据库的命名是一致的,所以如果需要我们可以添加接口,但现在我一直在尝试这样做而不是为了节省工作量。

确定返回哪个翻译标题和描述的逻辑是: 1)如果语言和地区都完全匹配,则返回 2) 如果语言匹配,但地区不匹配,则返回该语言的“默认”(RegionID 为空,并且每种语言总是有一个) 3) 如果语言不匹配,则返回系统默认值(LanguageID = 1,RegionID IS NULL)。

我知道这听起来可能很奇怪,每个人都有更好的方法来做这件事,但这是我必须处理的简短内容。所以这是我创建的 lambda 组连接函数,它使用数据库中名为“OrgGroup”的实体:

public static IEnumerable<TransViewModel> GetUserAreaOrgGroups(TransTestEntities context, int companyID, int languageID, int? regionID)

    var transFull = context.OrgGroupTranslations.Where(tr => tr.LanguageID == languageID && tr.RegionID == regionID);
    var transLang = context.OrgGroupTranslations.Where(tr => tr.LanguageID == languageID && !tr.RegionID.HasValue);
    var transDefault = context.OrgGroupTranslations.Where(tr => tr.LanguageID == 1 && !tr.RegionID.HasValue);

    var results = context.OrgGroups.Where(en => en.CompanyID == companyID)
            .GroupJoin(transFull, en => en.OrgGroupID, tr => tr.OrgGroupID,
                        (en, tr) => new TransJoin<OrgGroup, OrgGroupTranslation>  Entity = en, TransFull = tr.DefaultIfEmpty().FirstOrDefault(), TransLang = null, TransDefault = null)
            .GroupJoin(transLang, en => en.Entity.OrgGroupID, tr => tr.OrgGroupID,
                        (en, tr) => new TransJoin<OrgGroup, OrgGroupTranslation>  Entity = en.Entity, TransFull = en.TransFull, TransLang = tr.DefaultIfEmpty().FirstOrDefault(), TransDefault = null )
            .GroupJoin(transDefault, en => en.Entity.OrgGroupID, tr => tr.OrgGroupID,
                        (en, tr) => new TransJoin<OrgGroup, OrgGroupTranslation>  Entity = en.Entity, TransFull = en.TransFull, TransLang = en.TransLang, TransDefault = tr.DefaultIfEmpty().FirstOrDefault() )
            .Select(vm => new TransViewModel
                
                    EntityID = vm.Entity.OrgGroupID,
                    Title = (vm.TransFull ?? vm.TransLang ?? vm.TransDefault).Title,
                    Description = (vm.TransFull ?? vm.TransLang ?? vm.TransDefault).Description
                );
    return results;

这似乎按预期工作,现在我试图将其转换为一个函数,该函数将接受两种表类型并使用表达式树来创建、执行和返回等效查询。据我所知:

public static IEnumerable<TransViewModel> GetUserAreaTranslations<TEntity, TTrans>(TransTestEntities context, int companyID, int languageID, int? regionID)

    // Get types
    Type entityType = typeof(TEntity);
    Type transType = typeof(TTrans);

    string entityName = entityType.Name;
    string transName = transType.Name;

    // Parameters
    var entityParam = Expression.Parameter(entityType, "en");
    var transParam = Expression.Parameter(transType, "tr");
    var combinedParam = new ParameterExpression[]  entityParam, transParam ;

    // Properties
    var CompanyIDProp = Expression.Property(entityParam, "CompanyID");
    var entityIDProp = Expression.Property(entityParam, entityName + "ID");
    var transIDProp = Expression.Property(transParam, entityName + "ID");
    var transLanProp = Expression.Property(transParam, "LanguageID");
    var transRegProp = Expression.Property(transParam, "RegionID");
    var transTitleProp = Expression.Property(transParam, "Title");
    var transDescProp = Expression.Property(transParam, "Description");

    // Tables
    //TODO: Better way of finding pluralised table names
    var entityTable = Expression.PropertyOrField(Expression.Constant(context), entityName + "s");
    var transTable = Expression.PropertyOrField(Expression.Constant(context), transName + "s");

    // Build translation subqueries
    //e.g. context.OrgGroupTranslations.Where(tr => tr.LanguageID == languageID && tr.RegionID == regionID);

    MethodCallExpression fullTranWhereLambda = Expression.Call(typeof(Queryable),
                                    "Where",
                                    new Type[]  transType ,
                                    new Expression[]
                                    
                                        transTable,
                                        Expression.Quote
                                            (
                                                Expression.Lambda
                                                    (
                                                        Expression.AndAlso
                                                            (
                                                                Expression.Equal(transLanProp, Expression.Constant(languageID)),
                                                                Expression.Equal(transRegProp, Expression.Convert(Expression.Constant(languageID), transRegProp.Type))
                                                            ), transParam
                                                    )
                                            )
                                    );

    MethodCallExpression lanTranWhereLambda = Expression.Call(typeof(Queryable),
                                    "Where",
                                    new Type[]  transType ,
                                    new Expression[]
                                    
                                        transTable,
                                        Expression.Quote
                                            (
                                                Expression.Lambda
                                                    (
                                                        Expression.AndAlso
                                                            (
                                                                Expression.Equal(transLanProp, Expression.Constant(languageID)),
                                                                Expression.IsFalse(MemberExpression.Property(transRegProp, "HasValue"))
                                                            ), transParam
                                                    )
                                            )
                                    );

    MethodCallExpression defaultTranWhereLambda = Expression.Call(typeof(Queryable),
                                    "Where",
                                    new Type[]  transType ,
                                    new Expression[]
                                    
                                        transTable,
                                        Expression.Quote
                                            (
                                                Expression.Lambda
                                                    (
                                                        Expression.AndAlso
                                                            (
                                                                Expression.Equal(transLanProp, Expression.Constant(1)),
                                                                Expression.IsFalse(MemberExpression.Property(transRegProp, "HasValue"))
                                                            ), transParam
                                                    )
                                            )
                                    );

    MethodCallExpression entityWhereLambda = Expression.Call(typeof(Queryable),
                                                "Where",
                                                new Type[]  entityType ,
                                                new Expression[]
                                                
                                                    entityTable,
                                                    Expression.Quote(
                                                        Expression.Lambda
                                                        (
                                                            Expression.Equal(CompanyIDProp, Expression.Convert(Expression.Constant(companyID), CompanyIDProp.Type))
                                                            , entityParam
                                                        )
                                                    )
                                                );

    // Create the "left join" call:
    // tr.DefaultIfEmpty().FirstOrDefault()
    var joinType = typeof(TransJoin<TEntity, TTrans>);
    var joinParam = Expression.Parameter(joinType, "tr");
    var leftJoinMethods =
        Expression.Call(
            typeof(Enumerable),
            "FirstOrDefault",
            new Type[]  transType ,
            Expression.Call(
                typeof(Enumerable),
                "DefaultIfEmpty",
                new Type[]  transType ,
                Expression.Parameter(typeof(IEnumerable<TTrans>), "tr"))
        );

    // Create the return bindings
    var emptyTrans = Expression.Constant(null, typeof(TTrans));
    //var emptyTrans = Expression.Constant(null);
    var fullBindings = new List<MemberBinding>();
    fullBindings.Add(Expression.Bind(joinType.GetProperty("Entity"), entityParam));
    fullBindings.Add(Expression.Bind(joinType.GetProperty("TransFull"), leftJoinMethods));
    fullBindings.Add(Expression.Bind(joinType.GetProperty("TransLang"), emptyTrans));
    fullBindings.Add(Expression.Bind(joinType.GetProperty("TransDefault"), emptyTrans));
    // Create an object initialiser which also sets the properties
    Expression fullInitialiser = Expression.MemberInit(Expression.New(joinType), fullBindings);
    // Create the lambda expression, which represents the complete delegate
    Expression<Func<TEntity, TTrans, TransJoin<TEntity, TTrans>>> fullResultSelector =
        Expression.Lambda <Func<TEntity, TTrans, TransJoin<TEntity, TTrans>>>(fullInitialiser, combinedParam);

    // Create first group join
    var fullJoin = Expression.Call(
        typeof(Queryable),
        "GroupJoin",
        new Type[]
        
            typeof (TEntity),       // TOuter,
            typeof (TTrans),        // TInner,
            typeof (int),           // TKey,
            typeof (TransJoin<TEntity, TTrans>) // TResult
        ,
        new Expression[]
        
            entityWhereLambda,
            fullTranWhereLambda,
            Expression.Lambda<Func<TEntity, int>>(entityIDProp, entityParam),
            Expression.Lambda<Func<TTrans, int>>(transIDProp, transParam),
            fullResultSelector
        
    );

问题是 groupjoin 期望返回一个 TTrans 的 IEnumerable,我似乎无法绑定它,并且我无法将其更改为标准连接,因为我将无法使用在投影中合并,因为不会返回任何结果。

我确定我在做一些非常愚蠢的事情,所以有人可以帮我让我的小组加入工作吗?

【问题讨论】:

【参考方案1】:

您要查找的表达式节点是 MemberInitExpression,它是编译包含 new 语句的 lambda 的结果。

假设我们有一个像这样的简单键值类:

public class KV

    public int Key;
    public string Value;

我可以为此构建一个new 表达式来加载一些常量,如下所示:

Type tKV = typeof(KV);
MemberInfo miKey = tKV.GetMember("Key")[0];
MemberInfo miValue = tKV.GetMember("Value")[0];

Expression meminit = 
    Expression.MemberInit(
        Expression.New(tKV),
        Expression.Bind(miKey, Expression.Constant(1)),
        Expression.Bind(miValue, Expression.Constant("Some Value"))
    );

或者对于更完整的版本,构造一个完全初始化变量的 lambda 表达式:

public Expression<Func<int, string, KV>> InitKV()

    var pK = Expression.Parameter(typeof(int), "k");
    var pV = Expression.Parameter(typeof(string), "v");

    Type tKV = typeof(KV);
    MemberInfo miKey = tKV.GetMember("Key")[0];
    MemberInfo miValue = tKV.GetMember("Value")[0];

    Expression meminit = 
        Expression.MemberInit(
            Expression.New(tKV),
            Expression.Bind(miKey, pK),
            Expression.Bind(miValue, pV)
        );

    return (Expression<Func<int, string, KV>>)Expression.Lambda(meminit, pK, pV);       

在你的情况下,那里会有更多的Bind 表达式。

【讨论】:

我尝试改用 MemberInit 但得到了相同的结果:System.Core.dll 中发生了“System.InvalidOperationException”类型的未处理异常附加信息:“System”类型上没有通用方法“GroupJoin” .Linq.Queryable' 与提供的类型参数和参数兼容。如果方法是非泛型的,则不应提供类型参数。 对不起,我显然没有很好地阅读这个问题。我的错,我再看看它。【参考方案2】:

答案#2...这次是更实际的答案:P

问题似乎是您传递给 GroupJoin 方法的 lambda 类型等是错误的。

具体来说:

// Create the lambda expression, which represents the complete delegate
Expression<Func<TEntity, TTrans, TransJoin<TEntity, TTrans>>> fullResultSelector =
    Expression.Lambda<Func<TEntity, TTrans, TransJoin<TEntity, TTrans>>>(fullInitialiser, combinedParam);

...虽然其他一些人看起来也有点可疑,但那可能就是我。

GroupJoin 期望的选择器表达式是 Expression&lt;Func&lt;TEntity, IEnumerable&lt;TTrans&gt;, TransJoin&lt;TEntity, TTrans&gt;&gt;&gt; 类型。它会传入一个TEntity 和一组TTrans(如IEnumerable&lt;TTrans&gt;),即使该组中只有一个实例。您的表达式树需要正确处理 IEnumerable&lt;TTrans&gt;,但目前还没有。

你确定你想要GroupJoin而不是Join吗?

我在 LINQPad 中编写了一些代码来测试这个概念。如果您想查看它,请访问>PasteBin<。


顺便说一句,在表达式上使用 LINQPad 的 Dump 扩展将使您全面了解表达式的构造方式。将 lambda 分配给适当的 Expression&lt;Func&lt;....&gt;&gt; 类型的变量,然后调用 Dump 以查看它是如何构造的。帮助找出用法并展示构建它需要做什么。

【讨论】:

> 您的表达式树需要正确处理该 IEnumerable ,它目前不完全正确,但我不明白为什么标准查询(在我的测试 GetUserAreaOrgGroups 方法中)可以使用 .DefaultIfEmpty().FirstOrDefault() 但在使用表达式树时它必须是一个集合。我该如何解决这个问题?我尝试将 TransJoin 模型上的类型更改为 IEnumerable 最初似乎可行,但随后在尝试编译查询时出现错误(尽管公平地说我不知道​​编译表达式应该是什么。我的大脑是被这一切炸了) 如果你想要FirstOrDefault,那么你必须将它从IEnumerable&lt;TTrans&gt; 构建到表达式树中。它不会对你暗示。标准用法也是如此 - 您将得到一个必须处理的 IEnumerable,而不是单个实例。

以上是关于多组连接 lambda 到表达式树的主要内容,如果未能解决你的问题,请参考以下文章

Lambda 表达式树解析

Lambda表达式树构建(上)

带有语句体的 lambda 表达式无法转换为 nopCommerce 中的表达式树 [重复]

Lambda表达式树解析(下)

C# 表达式树 创建生成使用lambda转成表达式树~表达式树的知识详解

用lambda表达式树优化反射