DayOfWeek 的 EF Core QueryExpressionInterceptor [已关闭]

问题描述 投票:0回答:1

我们有一个使用 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
我能够解决多个问题。

c# entity-framework .net-core entity-framework-core ef-core-7.0
1个回答
1
投票

我已经实现了通用扩展,它通过适当的模式替换任何成员或方法。如果您需要支持其他成员的翻译,它会很有用。

使用

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));
        }
    }
}
© www.soinside.com 2019 - 2024. All rights reserved.