Sto cercando di avere un metodo di estensione per implementare il join esterno sinistro che restituisce IQueryable
che viene eseguito nel contesto di dati EF Core 2.0.
Ho letto il thread Stack Overflow qui per un aiuto: Metodo di estensione per il join esterno sinistro IQueryable usando LINQ . La risposta accettata presentava alcuni problemi che sono stati affrontati in una risposta successiva qui da Jan Van der Haegen. Ho tentato di utilizzare il metodo di estensione LeftJoin descritto lì con un contesto di dati EF Core 2.0 e ho riscontrato un'eccezione che non riesco a risolvere.
Poiché non riesco a trovare alcun difetto nell'implementazione del metodo di estensione LeftJoin, ho tentato di eseguire lo stesso metodo su un contesto di dati utilizzando EF6 e ha funzionato come previsto. Ho condiviso il mio esperimento, inclusi gli script SQL, per generare qui i dati di test:
Sperimenta l'estensione di LeftJoin con EF6 ed EF Core 2.0
Contiene due progetti uno con LeftJoin contro EF6 e l'altro con EF Core 2.0. Il metodo di estensione è condiviso tramite una libreria di classi .Net Standard 2.0.
Il metodo di estensione per LeftJoin è il seguente:
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;
}
}
}
Quando eseguo quanto sopra con un contesto di dati di EF Core 2.0 ottengo la seguente eccezione generata durante l'esecuzione:
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
Le mie domande:
Grazie.
Come suggerito qui , solo la soluzione praticabile utilizzava la libreria LinqKit.Core . Nonostante tutti gli sforzi, non sono riuscito a far funzionare la soluzione basata su Expression con EF.Core in progetti di libreria .Net Core o .Net Standard. La libreria LinqKit.Core è stata la chiave affinché l'estensione fosse portatile e utilizzata all'interno di un progetto di libreria di classe .Net Standard 2.0.
Come è stato suggerito, con LinkqKit.Core la soluzione è molto semplice:
/// <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));
}
L'esperimento in Github aggiornato per illustrare la soluzione di lavoro qui: soluzione GitHub che illustra l'estensione LeftJoin implementata in una libreria standard .Net