我们有一个使用 ASP.NET MVC 和 EF6 的现有应用程序。我们迁移到 ASP.NET Core 和 EF Core 7。正如标题所示,我们在使用
DayOfWeek
的现有查询中遇到了问题。 DayOfWeek
无法通过 EF Core 转换为等效的 SQL 查询。我无法修改代码/查询,因为 LINQ 查询是由 DevExtreme Dashboards 生成的,他们表示目前无法修复其代码。
在仪表板中,我们使用日期字段并将分组间隔分配给那些可以是 DayOfWeek、Date-Hour 等的字段。这些函数的 LINQ 转换与 EF Core 不兼容。由于我们无权访问他们的代码,因此我们不可能修改它生成这些查询的方式。
经过进一步调查,我最终阅读了 EF Core 7 中的
QueryExpressionInterceptor
。现在我的问题是,如何将 DayOfWeek
的所有用法转换为 EF Core 可以转换为 SQL 查询的内容 (DateDiff
)。
感谢
@Svyatoslav Danyliv
我能够解决多个问题。
我已经实现了通用扩展,它通过适当的模式替换任何成员或方法。如果您需要支持其他成员的翻译,它会很有用。
使用
UseMemberReplacement
和 HasDbFunction
按以下方式配置数据库上下文。示例有两个替换项:DateTime.DayOfWeek
和 DateTime.AddHours
。
DateTime.AddHours
替换解决了 SQL Server 的日期转换问题。
public class MyDbContext: DbContext
{
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
base.OnConfiguring(optionsBuilder);
// We add AddHours Method replacement to call function DayOfWeekImpl.
optionsBuilder.UseMemberReplacement((DateTime d) => d.DayOfWeek,
d => (DayOfWeek)(DayOfWeekImpl(d) - 1));
// We add DayOfWeek method replacement to call function DateAddHoursImpl.
optionsBuilder.UseMemberReplacement((DateTime d, double hours) => d.AddHours(hours),
(d, hours) => DateAddHoursImpl(d, hours));
}
static int DayOfWeekImpl(DateTime date) => (int)date.DayOfWeek;
static DateTime DateAddHoursImpl(DateTime date, double hours) => date.AddHours(hours);
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
... // other model configuring code
// register 'DayOfWeekImpl' translation code
modelBuilder.HasDbFunction(() => DayOfWeekImpl(default))
.HasTranslation(args =>
new SqlFunctionExpression("DATEPART",
new[] { new SqlFragmentExpression("weekday"), args[0] },
false,
new[] { false, false, false },
typeof(int),
null
));
// register 'DateAddHoursImpl' translation code
modelBuilder.HasDbFunction(() => DateAddHoursImpl(default, default))
.HasTranslation(args =>
new SqlFunctionExpression("DATEADD",
new[]
{
new SqlFragmentExpression("hour"), args[1], new SqlFunctionExpression("CONVERT",
new[] { new SqlFragmentExpression("datetime"), args[0] }, false,
new[] { true, false }, typeof(DateTime), RelationalTypeMapping.NullMapping)
},
false,
new[] { false, false, false },
typeof(DateTime),
null
));
}
}
测试代码:
var query = ctx.AnyTable
.Select(x => new
{
Date = DateTime.Today,
DayOfWeek = DateTime.Today.DayOfWeek,
HourPlus = DateTime.Today.AddHours(1),
});
var result = query.First();
if (result.Date.DayOfWeek != result.DayOfWeek)
throw new InvalidOperationException("Something wrong with 'DATEFIRST', check current setting in database 'SELECT @@DATEFIRST'");
它应该生成以下 SQL:
SELECT TOP(1)
CONVERT(date, GETDATE()) AS [Date],
CAST((DATEPART(weekday, CONVERT(date, GETDATE())) - 1) AS int) AS [DayOfWeek],
DATEADD(hour, 1.0E0, CONVERT(datetime, CONVERT(date, GETDATE()))) AS [HourPlus]
FROM [AnyTable] AS [e]
以及扩展实现本身:
public static class EFCoreLinqExtensions
{
public static DbContextOptionsBuilder UseMemberReplacement<TValue>(this DbContextOptionsBuilder optionsBuilder, Expression<Func<TValue>> whatToReplace, Expression<Func<TValue>> replacement)
{
AddMemberReplacement(optionsBuilder, whatToReplace, replacement);
return optionsBuilder;
}
public static DbContextOptionsBuilder UseMemberReplacement<TObject, TValue>(this DbContextOptionsBuilder optionsBuilder, Expression<Func<TObject, TValue>> whatToReplace, Expression<Func<TObject, TValue>> replacement)
{
AddMemberReplacement(optionsBuilder, whatToReplace, replacement);
return optionsBuilder;
}
public static DbContextOptionsBuilder UseMemberReplacement<TParam1, TParam2, TResult>(this DbContextOptionsBuilder optionsBuilder,
Expression<Func<TParam1, TParam2, TResult>> whatToReplace,
Expression<Func<TParam1, TParam2, TResult>> replacement)
{
AddMemberReplacement(optionsBuilder, whatToReplace, replacement);
return optionsBuilder;
}
public static DbContextOptionsBuilder UseMemberReplacement<TParam1, TParam2, TParam3, TResult>(this DbContextOptionsBuilder optionsBuilder,
Expression<Func<TParam1, TParam2, TParam3, TResult>> whatToReplace,
Expression<Func<TParam1, TParam2, TParam3, TResult>> replacement)
{
AddMemberReplacement(optionsBuilder, whatToReplace, replacement);
return optionsBuilder;
}
static void AddMemberReplacement(DbContextOptionsBuilder optionsBuilder, LambdaExpression whatToReplace, LambdaExpression replacement)
{
var coreExtension = optionsBuilder.Options.GetExtension<CoreOptionsExtension>();
QueryExpressionReplacementInterceptor? currentInterceptor = null;
if (coreExtension.Interceptors != null)
{
currentInterceptor = coreExtension.Interceptors.OfType<QueryExpressionReplacementInterceptor>()
.FirstOrDefault();
}
if (currentInterceptor == null)
{
currentInterceptor = new QueryExpressionReplacementInterceptor();
optionsBuilder.AddInterceptors(currentInterceptor);
}
MemberInfo member;
if (whatToReplace.Body is MemberExpression memberExpression)
{
member = memberExpression.Member;
}
else
if (whatToReplace.Body is MethodCallExpression methodCallExpression)
{
member = methodCallExpression.Method;
}
else
throw new InvalidOperationException($"Expression '{whatToReplace.Body}' is not MemberExpression or MethodCallExpression");
if (whatToReplace.Parameters.Count != replacement.Parameters.Count)
throw new InvalidOperationException($"Replacement Lambda should have exact count of parameters as input expression '{whatToReplace.Parameters.Count}' but found {replacement.Parameters.Count}");
currentInterceptor.AddMemberReplacement(member, replacement);
}
class MemberReplacementVisitor : ExpressionVisitor
{
private readonly List<(MemberInfo member, LambdaExpression replacenment)> _replacements;
public MemberReplacementVisitor(List<(MemberInfo what, LambdaExpression replacenmet)> replacements)
{
_replacements = replacements;
}
protected override Expression VisitMember(MemberExpression node)
{
foreach (var (what, replacement) in _replacements)
{
if (node.Member == what)
{
var args = new List<Expression>();
if (node.Expression != null)
args.Add(node.Expression);
var visitor = new ReplacingExpressionVisitor(replacement.Parameters, args);
var newNode = visitor.Visit(replacement.Body);
return Visit(newNode);
}
}
return base.VisitMember(node);
}
protected override Expression VisitMethodCall(MethodCallExpression node)
{
foreach (var (what, replacement) in _replacements)
{
if (node.Method == what)
{
var args = new List<Expression>(node.Arguments);
if (node.Object != null)
args.Insert(0, node.Object);
var visitor = new ReplacingExpressionVisitor(replacement.Parameters, args);
var newNode = visitor.Visit(replacement.Body);
return Visit(newNode);
}
}
return base.VisitMethodCall(node);
}
}
sealed class QueryExpressionReplacementInterceptor : IQueryExpressionInterceptor
{
readonly List<(MemberInfo member, LambdaExpression replacenment)> _memberReplacements = new();
public Expression QueryCompilationStarting(Expression queryExpression, QueryExpressionEventData eventData)
{
if (_memberReplacements.Count == 0)
return queryExpression;
var visitor = new MemberReplacementVisitor(_memberReplacements);
var result = visitor.Visit(queryExpression);
return result;
}
public void AddMemberReplacement(MemberInfo member, LambdaExpression replacement)
{
_memberReplacements.Add((member, replacement));
}
}
}