【问题标题】:Multiple group join lambda to expression tree多组连接 lambda 到表达式树
【发布时间】: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,我似乎无法绑定它,并且我无法将其更改为标准连接,因为我将无法使用在投影中合并,因为不会返回任何结果。

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

【问题讨论】:

    标签: c# lambda expression-trees


    【解决方案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,而不是单个实例。
    猜你喜欢
    • 2020-12-28
    • 2016-06-26
    • 2012-09-14
    • 1970-01-01
    • 1970-01-01
    • 2010-11-21
    • 1970-01-01
    • 1970-01-01
    • 2015-11-24
    相关资源
    最近更新 更多