有没有一种方法可以简化 ef core 中多个字段的许多连接?
Is there a way to simplify many joins over multiple fields in ef core?
我有很多非常相似的函数,可以像这样连接多个字段:
public IQueryable<TableJoinResult<Ta,Tb>> JoinFittingRows(IQueryable<Ta> theQueryable, IQueryable<Tb> theOtherQueryable)
{
return theQueryable
.Join(theOtherQueryable, t => new
{
f1 = t.f1,
f3 = t.f3,
f4 = t.f4,
f6 = t.f6,
f7 = t.f7,
f8 = t.f8,
f9 = t.f9,
f10 = t.f10,
}, p => new
{
f1 = p.f1,
f3 = p.f3,
f4 = p.f4,
f6 = p.f6,
f7 = p.f7,
f8 = p.f8,
f9 = p.f9,
f10 = p.f10,
}, (t, p) => new TableJoinResult<Ta,Tb> {ta= t, tb= p});
}
它们看起来都很相似,但必须相同的字段会发生变化。例如,我不需要 f2 相等。现在我有这个代码片段的几十个版本,其中的字段每次都不同。有没有办法简化或概括这个?例如取一个参数来获取必须相等的字段?
public IQueryable<TableJoinResult<Ta,Tb>> JoinFittingRows(IQueryable<Ta> theQueryable, IQueryable<Tb> theOtherQueryable, SomeType fieldsThatShouldBeEqual)
这是一个棘手的部分,因为这种语法需要某种通用的可变元组。所以我选择了最简单的方法来创建连接,它基于 SelectMany
函数。
用法简单
var joined = query.JoinFittingRows(otherQuery, t => t.f1, t => t.f2, t => t.f5...);
实现
public static class QueryableExtensions
{
public class JoinResult<TOuter, TInner>
{
public TOuter Outer { get; set; } = default!;
public TInner Inner { get; set; } = default!;
}
public static IQueryable<JoinResult<TOuter, TInner>> JoinFittingRows<TOuter, TInner>(this IQueryable<TOuter> outer, IQueryable<TInner> inner, params Expression<Func<TOuter, object>>[] properties)
{
Expression<Func<TOuter, TInner, JoinResult<TOuter, TInner>>> resultPattern = (o, i) => new JoinResult<TOuter, TInner> {Outer = o, Inner = i};
var outerParam = resultPattern.Parameters[0];
var innerParam = resultPattern.Parameters[1];
Expression predicate = null;
foreach (var property in properties)
{
var outerPropExpr = (MemberExpression)ExpressionReplacer.GetBody(property, outerParam).Unwrap();
var innerProp = typeof(TInner).GetProperty(outerPropExpr.Member.Name);
if (innerProp == null)
throw new InvalidOperationException(
$"Property '{outerPropExpr.Member.Name}' not found in class '{typeof(TInner).Name}'");
var innerPropExpr = (Expression)Expression.MakeMemberAccess(innerParam, innerProp);
if (innerPropExpr.Type != outerPropExpr.Type)
innerPropExpr = Expression.Convert(innerPropExpr, outerPropExpr.Type);
var equality = Expression.Equal(outerPropExpr, innerPropExpr);
if (predicate == null)
predicate = equality;
else
predicate = Expression.AndAlso(predicate, equality);
}
var predicateLambda = Expression.Lambda(predicate, innerParam);
var detailBody = Expression.Convert(
Expression.Call(typeof(Queryable), "Where", new[] {typeof(TInner)},
inner.Expression,
predicateLambda),
typeof(IEnumerable<TInner>));
var joinCall = Expression.Call(typeof(Queryable), "SelectMany", new []{typeof(TOuter), typeof(TInner), typeof(JoinResult<TOuter, TInner>) },
outer.Expression,
Expression.Lambda(detailBody, outerParam),
resultPattern);
return outer.Provider.CreateQuery<JoinResult<TOuter, TInner>>(joinCall);
}
public static Expression? Unwrap(this Expression? ex)
{
if (ex == null)
return null;
switch (ex.NodeType)
{
case ExpressionType.Quote:
case ExpressionType.ConvertChecked:
case ExpressionType.Convert:
return ((UnaryExpression)ex).Operand.Unwrap();
}
return ex;
}
class ExpressionReplacer : ExpressionVisitor
{
readonly IDictionary<Expression, Expression> _replaceMap;
public ExpressionReplacer(IDictionary<Expression, Expression> replaceMap)
{
_replaceMap = replaceMap ?? throw new ArgumentNullException(nameof(replaceMap));
}
public override Expression Visit(Expression exp)
{
if (exp != null && _replaceMap.TryGetValue(exp, out var replacement))
return replacement;
return base.Visit(exp);
}
public static Expression Replace(Expression expr, Expression toReplace, Expression toExpr)
{
return new ExpressionReplacer(new Dictionary<Expression, Expression> { { toReplace, toExpr } }).Visit(expr);
}
public static Expression Replace(Expression expr, IDictionary<Expression, Expression> replaceMap)
{
return new ExpressionReplacer(replaceMap).Visit(expr);
}
public static Expression GetBody(LambdaExpression lambda, params Expression[] toReplace)
{
if (lambda.Parameters.Count != toReplace.Length)
throw new InvalidOperationException();
return new ExpressionReplacer(Enumerable.Zip(lambda.Parameters, toReplace, (f, s) => Tuple.Create(f, s))
.ToDictionary(e => (Expression)e.Item1, e => e.Item2)).Visit(lambda.Body);
}
}
}
通过添加 DefaultIfEmpty
对 Where
的调用,可以轻松地将此实现扩展到 LEFT JOIN
。
我有很多非常相似的函数,可以像这样连接多个字段:
public IQueryable<TableJoinResult<Ta,Tb>> JoinFittingRows(IQueryable<Ta> theQueryable, IQueryable<Tb> theOtherQueryable)
{
return theQueryable
.Join(theOtherQueryable, t => new
{
f1 = t.f1,
f3 = t.f3,
f4 = t.f4,
f6 = t.f6,
f7 = t.f7,
f8 = t.f8,
f9 = t.f9,
f10 = t.f10,
}, p => new
{
f1 = p.f1,
f3 = p.f3,
f4 = p.f4,
f6 = p.f6,
f7 = p.f7,
f8 = p.f8,
f9 = p.f9,
f10 = p.f10,
}, (t, p) => new TableJoinResult<Ta,Tb> {ta= t, tb= p});
}
它们看起来都很相似,但必须相同的字段会发生变化。例如,我不需要 f2 相等。现在我有这个代码片段的几十个版本,其中的字段每次都不同。有没有办法简化或概括这个?例如取一个参数来获取必须相等的字段?
public IQueryable<TableJoinResult<Ta,Tb>> JoinFittingRows(IQueryable<Ta> theQueryable, IQueryable<Tb> theOtherQueryable, SomeType fieldsThatShouldBeEqual)
这是一个棘手的部分,因为这种语法需要某种通用的可变元组。所以我选择了最简单的方法来创建连接,它基于 SelectMany
函数。
用法简单
var joined = query.JoinFittingRows(otherQuery, t => t.f1, t => t.f2, t => t.f5...);
实现
public static class QueryableExtensions
{
public class JoinResult<TOuter, TInner>
{
public TOuter Outer { get; set; } = default!;
public TInner Inner { get; set; } = default!;
}
public static IQueryable<JoinResult<TOuter, TInner>> JoinFittingRows<TOuter, TInner>(this IQueryable<TOuter> outer, IQueryable<TInner> inner, params Expression<Func<TOuter, object>>[] properties)
{
Expression<Func<TOuter, TInner, JoinResult<TOuter, TInner>>> resultPattern = (o, i) => new JoinResult<TOuter, TInner> {Outer = o, Inner = i};
var outerParam = resultPattern.Parameters[0];
var innerParam = resultPattern.Parameters[1];
Expression predicate = null;
foreach (var property in properties)
{
var outerPropExpr = (MemberExpression)ExpressionReplacer.GetBody(property, outerParam).Unwrap();
var innerProp = typeof(TInner).GetProperty(outerPropExpr.Member.Name);
if (innerProp == null)
throw new InvalidOperationException(
$"Property '{outerPropExpr.Member.Name}' not found in class '{typeof(TInner).Name}'");
var innerPropExpr = (Expression)Expression.MakeMemberAccess(innerParam, innerProp);
if (innerPropExpr.Type != outerPropExpr.Type)
innerPropExpr = Expression.Convert(innerPropExpr, outerPropExpr.Type);
var equality = Expression.Equal(outerPropExpr, innerPropExpr);
if (predicate == null)
predicate = equality;
else
predicate = Expression.AndAlso(predicate, equality);
}
var predicateLambda = Expression.Lambda(predicate, innerParam);
var detailBody = Expression.Convert(
Expression.Call(typeof(Queryable), "Where", new[] {typeof(TInner)},
inner.Expression,
predicateLambda),
typeof(IEnumerable<TInner>));
var joinCall = Expression.Call(typeof(Queryable), "SelectMany", new []{typeof(TOuter), typeof(TInner), typeof(JoinResult<TOuter, TInner>) },
outer.Expression,
Expression.Lambda(detailBody, outerParam),
resultPattern);
return outer.Provider.CreateQuery<JoinResult<TOuter, TInner>>(joinCall);
}
public static Expression? Unwrap(this Expression? ex)
{
if (ex == null)
return null;
switch (ex.NodeType)
{
case ExpressionType.Quote:
case ExpressionType.ConvertChecked:
case ExpressionType.Convert:
return ((UnaryExpression)ex).Operand.Unwrap();
}
return ex;
}
class ExpressionReplacer : ExpressionVisitor
{
readonly IDictionary<Expression, Expression> _replaceMap;
public ExpressionReplacer(IDictionary<Expression, Expression> replaceMap)
{
_replaceMap = replaceMap ?? throw new ArgumentNullException(nameof(replaceMap));
}
public override Expression Visit(Expression exp)
{
if (exp != null && _replaceMap.TryGetValue(exp, out var replacement))
return replacement;
return base.Visit(exp);
}
public static Expression Replace(Expression expr, Expression toReplace, Expression toExpr)
{
return new ExpressionReplacer(new Dictionary<Expression, Expression> { { toReplace, toExpr } }).Visit(expr);
}
public static Expression Replace(Expression expr, IDictionary<Expression, Expression> replaceMap)
{
return new ExpressionReplacer(replaceMap).Visit(expr);
}
public static Expression GetBody(LambdaExpression lambda, params Expression[] toReplace)
{
if (lambda.Parameters.Count != toReplace.Length)
throw new InvalidOperationException();
return new ExpressionReplacer(Enumerable.Zip(lambda.Parameters, toReplace, (f, s) => Tuple.Create(f, s))
.ToDictionary(e => (Expression)e.Item1, e => e.Item2)).Visit(lambda.Body);
}
}
}
通过添加 DefaultIfEmpty
对 Where
的调用,可以轻松地将此实现扩展到 LEFT JOIN
。