【发布时间】: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
我的问题:
- 这是 EF Core 2.0 中的错误还是我在这里做错了什么?
- 在使用 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