尝试实现LeftJoin扩展方法以与EF Core 2.0一起使用 [英] Trying to implement a LeftJoin extension method to work with EF Core 2.0

查看:743
本文介绍了尝试实现LeftJoin扩展方法以与EF Core 2.0一起使用的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在尝试一种扩展方法来实现左外部联接,该联接返回针对EF Core 2.0数据上下文运行的IQueryable.

I am trying to have an extension method to implement left outer join which returns IQueryable that run against EF Core 2.0 data context.

我在这里阅读了堆栈溢出线程以寻求帮助:扩展LINQ的IQueryable左外部联接的方法. Jan Van der Haegen在此处的后续答案中解决了已接受的答案的某些问题.我试图将此处描述的LeftJoin扩展方法与EF Core 2.0数据上下文一起使用,但遇到了一个似乎无法解决的异常.

I read the Stack Overflow thread here for help: Extension method for IQueryable left outer join using LINQ. The accepted answer had some problems that were addressed in a later answer here by Jan Van der Haegen. I attempted to use the LeftJoin extension method described there with an EF Core 2.0 data context and ran into an exception which I cannot seem to resolve.

由于我发现LeftJoin扩展方法的实现没有任何问题,因此我尝试使用EF6对数据上下文运行相同的方法,并且该方法按预期工作.我已经分享了我的实验,包括用于在此处生成测试数据的SQL脚本:

As I cannot find any fault with the implementation of the LeftJoin extension method, I attempted to run the same method against a data context using EF6 and it worked as expected. I have shared my experiment including the SQL scripts to generate the test data here:

具有LeftJoin扩展功能的实验可在EF6和EF Core 2.0上运行

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

It contains two projects one running LeftJoin against EF6 and the other running it against EF Core 2.0. The extension method is shared via a .Net Standard 2.0 class library.

LeftJoin的扩展方法如下:

The extension method for LeftJoin is as follows:

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在数据上下文中运行以上代码时,在运行时会引发以下异常:

When I run the above with a data context by EF Core 2.0 I get the following exception thrown at runtime:

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 Standard类库中起作用的情况下,是否有更好的方法来生成将GroupJoin和SelectMany方法组合在一起以执行LeftJoin的扩展方法?

谢谢.

推荐答案

根据建议此处,仅可行的解决方案正在使用 LinqKit.Core 库.尽管付出了所有努力,但我仍无法在.Net Core或.Net Standard库项目下使用基于Expression的解决方案与EF.Core一起使用. LinqKit.Core 库是使扩展具有可移植性并在.Net Standard 2.0类库项目中使用的关键.

As it is suggested here, Only workable solution was using the LinqKit.Core library. Despite all effort I was not able to get the Expression based solution to work with EF.Core under .Net Core or .Net Standard library projects. LinqKit.Core library was the key to have the extension be portable and used within a .Net Standard 2.0 class library project.

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

As it was suggested, with LinkqKit.Core the solution is very simple:

    /// <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中的实验已更新,以在此处说明有效的解决方案:说明了.Net标准库中实现的LeftJoin扩展的GitHub解决方案

The Experiment in Github updated to illustrate working solution here: GitHub solution illustrating LeftJoin extension implemented in a .Net Standard Library

这篇关于尝试实现LeftJoin扩展方法以与EF Core 2.0一起使用的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆