我可以将 SqlFunctions.DateDiff() in Entity Framework 与动态 datePartArg 一起使用吗?
Can I use SqlFunctions.DateDiff() in Entity Framework with a dynamic datePartArg?
由于注释行,以下代码抛出 EntityCommandCompilationException
:
var datePartArg = "dd";
var minutesInStatePerSegment = await db.History_WorkPlaceStates
.Where(x => selector.StartTimeUtc <= x.Started && x.Ended < selector.EndTimeUtc)
.Select(x => new {
start = x.Started,
minutes = x.Minutes,
state = x.State,
})
.GroupBy(x => new {
//This causes an exception:
segment = SqlFunctions.DateDiff(datePartArg, selector.StartTimeUtc, x.start),
state = x.state,
})
.Select(x => new {
state = x.Key.state,
segment = x.Key.segment,
minutes = x.Sum(y => y.minutes),
}).ToListAsync();
发生这种情况是因为 DateDiff
在 SQL 服务器中只能使用文字字符串作为其第一个参数,而不能使用变量。 Entity Framework 在 SQL 中生成了一个变量,所以我们得到了异常。
有办法解决这个问题吗?
您可以创建以下扩展方法来解决这个问题。使用 DateDiff 时,只需将 GroupBy 函数替换为此扩展方法即可:
public static IQueryable<IGrouping<TKey, TSource>> GroupByDateDiff<TSource, TKey>(this IQueryable<TSource> source, Expression<Func<TSource, TKey>> keySelector) {
var body = (NewExpression)keySelector.Body;
var transformedBodyArguments = body.Arguments.Select(arg => arg switch {
MethodCallExpression callNode
when callNode.Method.Name == "DateDiff" && callNode.Arguments[0].NodeType != ExpressionType.Constant
=> getTransformedDateDiffCall(callNode),
_ => arg,
}).ToArray();
var updatedExpr = keySelector.Update(body.Update(transformedBodyArguments), keySelector.Parameters);
return source.GroupBy(updatedExpr);
MethodCallExpression getTransformedDateDiffCall(MethodCallExpression dateDiffCallNode) {
var dateDiffFirstArg = dateDiffCallNode.Arguments[0];
if (dateDiffFirstArg.NodeType != ExpressionType.MemberAccess) {
throw new ArgumentException($"{nameof(GroupByDateDiff)} was unable to parse the datePartArg argument to the DateDiff function.");
}
var replacementExpression = Expression.Constant((string)GetMemberValue((MemberExpression)dateDiffFirstArg));
var alternativeArgs = dateDiffCallNode.Arguments.Skip(1).Prepend(replacementExpression).ToArray();
return dateDiffCallNode.Update(dateDiffCallNode.Object, alternativeArgs);
};
}
private static object GetMemberValue(MemberExpression member) {
var objectMember = Expression.Convert(member, typeof(object));
var getterLambda = Expression.Lambda<Func<object>>(objectMember);
var getter = getterLambda.Compile();
return getter();
}
由于注释行,以下代码抛出 EntityCommandCompilationException
:
var datePartArg = "dd";
var minutesInStatePerSegment = await db.History_WorkPlaceStates
.Where(x => selector.StartTimeUtc <= x.Started && x.Ended < selector.EndTimeUtc)
.Select(x => new {
start = x.Started,
minutes = x.Minutes,
state = x.State,
})
.GroupBy(x => new {
//This causes an exception:
segment = SqlFunctions.DateDiff(datePartArg, selector.StartTimeUtc, x.start),
state = x.state,
})
.Select(x => new {
state = x.Key.state,
segment = x.Key.segment,
minutes = x.Sum(y => y.minutes),
}).ToListAsync();
发生这种情况是因为 DateDiff
在 SQL 服务器中只能使用文字字符串作为其第一个参数,而不能使用变量。 Entity Framework 在 SQL 中生成了一个变量,所以我们得到了异常。
有办法解决这个问题吗?
您可以创建以下扩展方法来解决这个问题。使用 DateDiff 时,只需将 GroupBy 函数替换为此扩展方法即可:
public static IQueryable<IGrouping<TKey, TSource>> GroupByDateDiff<TSource, TKey>(this IQueryable<TSource> source, Expression<Func<TSource, TKey>> keySelector) {
var body = (NewExpression)keySelector.Body;
var transformedBodyArguments = body.Arguments.Select(arg => arg switch {
MethodCallExpression callNode
when callNode.Method.Name == "DateDiff" && callNode.Arguments[0].NodeType != ExpressionType.Constant
=> getTransformedDateDiffCall(callNode),
_ => arg,
}).ToArray();
var updatedExpr = keySelector.Update(body.Update(transformedBodyArguments), keySelector.Parameters);
return source.GroupBy(updatedExpr);
MethodCallExpression getTransformedDateDiffCall(MethodCallExpression dateDiffCallNode) {
var dateDiffFirstArg = dateDiffCallNode.Arguments[0];
if (dateDiffFirstArg.NodeType != ExpressionType.MemberAccess) {
throw new ArgumentException($"{nameof(GroupByDateDiff)} was unable to parse the datePartArg argument to the DateDiff function.");
}
var replacementExpression = Expression.Constant((string)GetMemberValue((MemberExpression)dateDiffFirstArg));
var alternativeArgs = dateDiffCallNode.Arguments.Skip(1).Prepend(replacementExpression).ToArray();
return dateDiffCallNode.Update(dateDiffCallNode.Object, alternativeArgs);
};
}
private static object GetMemberValue(MemberExpression member) {
var objectMember = Expression.Convert(member, typeof(object));
var getterLambda = Expression.Lambda<Func<object>>(objectMember);
var getter = getterLambda.Compile();
return getter();
}