【问题标题】:Trying to implement a LeftJoin extension method to work with EF Core 2.0尝试实现 LeftJoin 扩展方法以使用 EF Core 2.0
【发布时间】:2017-10-03 04:20:57
【问题描述】:

我正在尝试使用扩展方法来实现左外连接,它返回针对 EF Core 2.0 数据上下文运行的 IQueryable

我在这里阅读了堆栈溢出线程以寻求帮助:Extension method for IQueryable left outer join using LINQ。接受的答案存在一些问题,这些问题在 Jan Van der Haegen 稍后的答案here 中得到解决。我尝试将此处描述的 LeftJoin 扩展方法与 EF Core 2.0 数据上下文一起使用,但遇到了我似乎无法解决的异常。

由于我在 LeftJoin 扩展方法的实现中找不到任何错误,因此我尝试使用 EF6 对数据上下文运行相同的方法,并且它按预期工作。我在这里分享了我的实验,包括用于生成测试数据的 SQL 脚本:

Experiment with LeftJoin extension run against EF6 and EF Core 2.0

它包含两个项目,一个针对 EF6 运行 LeftJoin,另一个针对 EF Core 2.0 运行它。扩展方法通过 .Net Standard 2.0 类库共享。

LeftJoin的扩展方法如下:

namespace QueryableExtensions
{
    // Much of the code copied from following URL:
    // https://stackoverflow.com/questions/21615693/extension-method-for-iqueryable-left-outer-join-using-linq
    internal class KeyValuePairHolder<T1, T2>
    {
        public T1 Item1 { get; set; }
        public T2 Item2 { get; set; }
    }

    internal class ResultSelectorRewriter<TOuter, TInner, TResult> : ExpressionVisitor
    {
        private Expression<Func<TOuter, TInner, TResult>> resultSelector;
        public Expression<Func<KeyValuePairHolder<TOuter, IEnumerable<TInner>>, TInner, TResult>> CombinedExpression { get; private set; }

        private ParameterExpression OldTOuterParamExpression;
        private ParameterExpression OldTInnerParamExpression;
        private ParameterExpression NewTOuterParamExpression;
        private ParameterExpression NewTInnerParamExpression;

        public ResultSelectorRewriter(Expression<Func<TOuter, TInner, TResult>> resultSelector)
        {
            this.resultSelector = resultSelector;
            this.OldTOuterParamExpression = resultSelector.Parameters[0];
            this.OldTInnerParamExpression = resultSelector.Parameters[1];

            this.NewTOuterParamExpression = Expression.Parameter(typeof(KeyValuePairHolder<TOuter, IEnumerable<TInner>>));
            this.NewTInnerParamExpression = Expression.Parameter(typeof(TInner));

            var newBody = this.Visit(this.resultSelector.Body);
            var combinedExpression = Expression.Lambda(newBody, new ParameterExpression[] { this.NewTOuterParamExpression, this.NewTInnerParamExpression });
            this.CombinedExpression = (Expression<Func<KeyValuePairHolder<TOuter, IEnumerable<TInner>>, TInner, TResult>>)combinedExpression;
        }

        protected override Expression VisitParameter(ParameterExpression node)
        {
            if (node == this.OldTInnerParamExpression)
                return this.NewTInnerParamExpression;
            else if (node == this.OldTOuterParamExpression)
                return Expression.PropertyOrField(this.NewTOuterParamExpression, "Item1");
            else
                throw new InvalidOperationException("What is this sorcery?", new InvalidOperationException("Did not expect a parameter: " + node));
        }
    }

    public static class JoinExtensions
    { 
        internal static readonly System.Reflection.MethodInfo
            Enumerable_DefaultIfEmpty = typeof(Enumerable).GetMethods()
                .First(x => x.Name == "DefaultIfEmpty" && x.GetParameters().Length == 1);
        internal static readonly System.Reflection.MethodInfo
            Queryable_SelectMany = typeof(Queryable).GetMethods()
                .Where(x => x.Name == "SelectMany" && x.GetParameters().Length == 3)
                .OrderBy(x => x.ToString().Length).First();
        internal static readonly System.Reflection.MethodInfo
            Queryable_Where = typeof(Queryable).GetMethods()
                .First(x => x.Name == "Where" && x.GetParameters().Length == 2);
        internal static readonly System.Reflection.MethodInfo
            Queryable_GroupJoin = typeof(Queryable).GetMethods()
                .First(x => x.Name == "GroupJoin" && x.GetParameters().Length == 5);

        public static IQueryable<TResult> LeftJoin<TOuter, TInner, TKey, TResult>(
                   this IQueryable<TOuter> outer,
                   IQueryable<TInner> inner,
                   Expression<Func<TOuter, TKey>> outerKeySelector,
                   Expression<Func<TInner, TKey>> innerKeySelector,
                   Expression<Func<TOuter, TInner, TResult>> resultSelector)
        {

            var keyValuePairHolderWithGroup = typeof(KeyValuePairHolder<,>)
                .MakeGenericType(
                    typeof(TOuter),
                    typeof(IEnumerable<>).MakeGenericType(typeof(TInner))
                );
            var paramOuter = Expression.Parameter(typeof(TOuter));
            var paramInner = Expression.Parameter(typeof(IEnumerable<TInner>));

            var resultSel = Expression
                .Lambda(
                    Expression.MemberInit(
                        Expression.New(keyValuePairHolderWithGroup),
                        Expression.Bind(
                            keyValuePairHolderWithGroup.GetMember("Item1").Single(),
                            paramOuter
                            ),
                        Expression.Bind(
                            keyValuePairHolderWithGroup.GetMember("Item2").Single(),
                            paramInner
                            )
                        ),
                    paramOuter,
                    paramInner
                );
            var groupJoin = Queryable_GroupJoin
                .MakeGenericMethod(
                    typeof(TOuter),
                    typeof(TInner),
                    typeof(TKey),
                    keyValuePairHolderWithGroup
                )
                .Invoke(
                    "ThisArgumentIsIgnoredForStaticMethods",
                    new object[]{
                        outer,
                        inner,
                        outerKeySelector,
                        innerKeySelector,
                        resultSel
                    }
                );


            var paramGroup = Expression.Parameter(keyValuePairHolderWithGroup);
            Expression collectionSelector = Expression.Lambda(
                            Expression.Call(
                                    null,
                                    Enumerable_DefaultIfEmpty.MakeGenericMethod(typeof(TInner)),
                                    Expression.MakeMemberAccess(paramGroup, keyValuePairHolderWithGroup.GetProperty("Item2")))
                            ,
                            paramGroup
                        );

            Expression newResultSelector =
                new ResultSelectorRewriter<TOuter, TInner, TResult>(resultSelector)
                    .CombinedExpression;


            var selectMany1Result = Queryable_SelectMany
                .MakeGenericMethod(
                    keyValuePairHolderWithGroup,
                    typeof(TInner),
                    typeof(TResult)
                )
                .Invoke(
                    "ThisArgumentIsIgnoredForStaticMethods",
                    new object[]
                    {
                        groupJoin,
                        collectionSelector,
                        newResultSelector
                    }
                );
            return (IQueryable<TResult>)selectMany1Result;
        }
    }
}

当我使用 EF Core 2.0 的数据上下文运行上述内容时,我在运行时抛出以下异常:

System.ArgumentNullException occurred
  HResult = 0x80004003
  Message = Value cannot be null.
    Source =< Cannot evaluate the exception source>
      StackTrace:
   at Remotion.Utilities.ArgumentUtility.CheckNotNull[T](String argumentName, T actualValue)
   at Remotion.Utilities.ArgumentUtility.CheckNotNullOrEmpty(String argumentName, String actualValue)
   at Remotion.Linq.Clauses.GroupJoinClause..ctor(String itemName, Type itemType, JoinClause joinClause)
   at Remotion.Linq.Parsing.Structure.IntermediateModel.GroupJoinExpressionNode.ApplyNodeSpecificSemantics(QueryModel queryModel, ClauseGenerationContext clauseGenerationContext)
   at Remotion.Linq.Parsing.Structure.IntermediateModel.MethodCallExpressionNodeBase.Apply(QueryModel queryModel, ClauseGenerationContext clauseGenerationContext)
   at Remotion.Linq.Parsing.Structure.QueryParser.ApplyAllNodes(IExpressionNode node, ClauseGenerationContext clauseGenerationContext)
   at Remotion.Linq.Parsing.Structure.QueryParser.ApplyAllNodes(IExpressionNode node, ClauseGenerationContext clauseGenerationContext)
   at Remotion.Linq.Parsing.Structure.QueryParser.GetParsedQuery(Expression expressionTreeRoot)
   at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.CompileQueryCore[TResult](Expression query, INodeTypeProvider nodeTypeProvider, IDatabase database, IDiagnosticsLogger`1 logger, Type contextType)
   at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.<> c__DisplayClass15_0`1.< Execute > b__0()
   at Microsoft.EntityFrameworkCore.Query.Internal.CompiledQueryCache.GetOrAddQueryCore[TFunc](Object cacheKey, Func`1 compiler)
   at Microsoft.EntityFrameworkCore.Query.Internal.CompiledQueryCache.GetOrAddQuery[TResult](Object cacheKey, Func`1 compiler)
   at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.Execute[TResult](Expression query)
   at Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryProvider.Execute[TResult](Expression expression)
   at Remotion.Linq.QueryableBase`1.GetEnumerator()
   at System.Collections.Generic.List`1.AddEnumerable(IEnumerable`1 enumerable)
   at System.Linq.Enumerable.ToList[TSource](IEnumerable`1 source)
   at TestWithEFCore2.Program.Main() in C: \Users\hewasud\Git\TestLeftJoinExtensionWithEF6\TestWithEF6\TestWithEFCore2\Program.cs:line 27

我的问题:

  1. 这是 EF Core 2.0 中的错误还是我在这里做错了什么?
  2. 在使用 EF Core 2.0 时,假设此代码必须在允许可移植性的 .Net 标准类库中正常运行,是否有更好的方法来生成组合 GroupJoin 和 SelectMany 方法来执行 LeftJoin 的扩展方法?

谢谢。

【问题讨论】:

  • 查看生成的表达式树,编写C#代码进行匹配,看看会发生什么。如果它可以工作,请找出两者之间的区别并努力在扩展方法中得到它。如果它仍然不起作用,至少您已将问题简化为更简单的问题。
  • 因为有三个表达式作为参数传递给 LeftJoin 扩展和由表达式访问者修改的修改后的 resultSelector 表达式。我假设您希望我用 C# 代码替换修改后的 resultSelector 表达式以进行测试。我不确定我是否可以有效地使用直接 C# 来代替修改后的 resultSel 表达式,以使该方法适用于任何情况。我可以通过用 C# 替换它来测试特定情况来获得一些见解。我会试试这个。
  • 对我无法让这个 LeftJoin 与表达式一起使用的所有努力提出异议,以便与 EF Core 以及 .Net Core 和 .Net Standard 项目一起使用。 LinqKit 库确实提供了一个可行的解决方案,可移植到 .Net Core 项目并在 .Net 标准库中使用。

标签: c# linq-to-sql entity-framework-core


【解决方案1】:

正如 here 所建议的那样,唯一可行的解​​决方案是使用 LinqKit.Core 库。尽管付出了所有努力,我还是无法让基于表达式的解决方案在 .Net Core 或 .Net Standard 库项目下与 EF.Core 一起使用。 LinqKit.Core 库是使扩展可移植并在 .Net Standard 2.0 类库项目中使用的关键。

正如建议的那样,使用 LinkqKit.Core 解决方案非常简单:

    /// <summary>
    /// Implement Left Outer join implemented by calling GroupJoin and
    /// SelectMany within this extension method
    /// </summary>
    /// <typeparam name="TOuter">Outer Type</typeparam>
    /// <typeparam name="TInner">Inner Type</typeparam>
    /// <typeparam name="TKey">Key Type</typeparam>
    /// <typeparam name="TResult">Result Type</typeparam>
    /// <param name="outer">Outer set</param>
    /// <param name="inner">Inner set</param>
    /// <param name="outerKeySelector">Outer Key Selector</param>
    /// <param name="innerKeySelector">Inner Key Selector</param>
    /// <param name="resultSelector">Result Selector</param>
    /// <returns>IQueryable Result set</returns>
    public static IQueryable<TResult> LeftJoin<TOuter, TInner, TKey, TResult>(
       this IQueryable<TOuter> outer,
       IQueryable<TInner> inner,
       Expression<Func<TOuter, TKey>> outerKeySelector,
       Expression<Func<TInner, TKey>> innerKeySelector,
       Expression<Func<TOuter, TInner, TResult>> resultSelector)
    {
        //LinqKit allows easy runtime evaluation of inline invoked expressions
        // without manually writing expression trees.
        return outer
            .AsExpandable()// Tell LinqKit to convert everything into an expression tree.
            .GroupJoin(
                inner,
                outerKeySelector,
                innerKeySelector,
                (outerItem, innerItems) => new { outerItem, innerItems })
            .SelectMany(
                joinResult => joinResult.innerItems.DefaultIfEmpty(),
                (joinResult, innerItem) =>
                    resultSelector.Invoke(joinResult.outerItem, innerItem));
    }

Github 中的实验已更新以在此处说明可行的解决方案:GitHub solution illustrating LeftJoin extension implemented in a .Net Standard Library

【讨论】:

    猜你喜欢
    • 2023-03-06
    • 2020-07-09
    • 1970-01-01
    • 1970-01-01
    • 2015-03-12
    • 1970-01-01
    • 2020-07-01
    • 2021-03-09
    • 2018-01-24
    相关资源
    最近更新 更多