【问题标题】:Extending EF Core 'where' clause with custom expression使用自定义表达式扩展 EF Core 'where' 子句
【发布时间】:2018-12-18 15:10:07
【问题描述】:

我有一堆实体,它们具有定义为“StartDate”和“EndDate”字段的活动期。 大多数时候我需要查询他们检查他们的活动期是否与一些自定义值。 代码几乎是这样的:

public static Expression<Func<T, bool>> IsPeriodActive<T>(DateTime checkPeriodStart, DateTime checkPeriodEnd, Func<T, DateTime> entityPeriodStart, Func<T, DateTime> entityPeriodEnd) =>
    entity =>
        (checkPeriodEnd >= entityPeriodStart(entity) && checkPeriodEnd <= entityPeriodEnd(entity))
        || (checkPeriodStart >= entityPeriodStart(entity) && checkPeriodEnd <= entityPeriodEnd(entity))
        || (entityPeriodStart(entity) >= checkPeriodStart && entityPeriodStart(entity) <= checkPeriodEnd)
        || (entityPeriodEnd(entity) >= checkPeriodStart && entityPeriodEnd(entity) <= checkPeriodEnd)
        || (entityPeriodStart(entity) >= checkPeriodStart && entityPeriodStart(entity) <= checkPeriodEnd);

问题是 Func.Invoke() 不能翻译成 SQL,这是显而易见的。 如何扩展 EF Core 以为任何实体类型添加这种“位置”条件? 我不能使用过滤器,因为有时我需要查询原始数据或只使用一个周期检查(不是两者),而且有些实体的这些字段名称不同。

【问题讨论】:

    标签: c# linq entity-framework-core


    【解决方案1】:

    您需要将 Func&lt;T, DateTime&gt; 参数更改为 Expression&lt;Func&lt;T, DateTime&gt;&gt; 并将它们合并到所需的表达式中。

    不幸的是,C# 编译器和 BCL 都不能帮助完成后面的任务(来自其他表达式的表达式组合)。有一些 3rd 方包,如 LinqKitNeinLinq 等可以解决这个问题,所以如果您打算大量使用表达式组合,您可以考虑使用这些库之一。

    但原理是一样的。在某些时候,自定义ExpressionVisitor 用于将原始表达式的一部分替换为另一个表达式。例如,我在这种简单的场景中使用的是创建编译时 lambda 表达式,其中包含用作占位符的附加参数,然后用与 string.Replace 几乎相同的方式替换为实际表达式。

    为此,我使用以下辅助方法将 lambda 表达式参数替换为另一个表达式:

    public static partial class ExpressionUtils
    {
        public static Expression ReplaceParameter(this Expression expression, ParameterExpression source, Expression target)
        {
            return new ParameterReplacer { Source = source, Target = target }.Visit(expression);
        }
    
        class ParameterReplacer : ExpressionVisitor
        {
            public ParameterExpression Source;
            public Expression Target;
            protected override Expression VisitParameter(ParameterExpression node)
                => node == Source ? Target : base.VisitParameter(node);
        }
    }
    

    而有问题的方法可能是这样的:

    public static Expression<Func<T, bool>> IsPeriodActive<T>(
        DateTime checkPeriodStart,
        DateTime checkPeriodEnd,
        Expression<Func<T, DateTime>> entityPeriodStart,
        Expression<Func<T, DateTime>> entityPeriodEnd)
    {
        var entityParam = Expression.Parameter(typeof(T), "entity");
        var periodStartValue = entityPeriodStart.Body
            .ReplaceParameter(entityPeriodStart.Parameters[0], entityParam);
        var periodEndValue = entityPeriodEnd.Body
            .ReplaceParameter(entityPeriodEnd.Parameters[0], entityParam);
    
        Expression<Func<DateTime, DateTime, bool>> baseExpr = (periodStart, periodEnd) =>
            (checkPeriodEnd >= periodStart && checkPeriodEnd <= periodEnd)
            || (checkPeriodStart >= periodStart && checkPeriodEnd <= periodEnd)
            || (periodStart >= checkPeriodStart && periodStart <= checkPeriodEnd)
            || (periodEnd >= checkPeriodStart && periodEnd <= checkPeriodEnd)
            || (periodStart >= checkPeriodStart && periodStart <= checkPeriodEnd);
    
        var periodStartParam = baseExpr.Parameters[0];
        var periodEndParam = baseExpr.Parameters[1];
    
        var expr = baseExpr.Body
            .ReplaceParameter(periodStartParam, periodStartValue)
            .ReplaceParameter(periodEndParam, periodEndValue);
    
        return Expression.Lambda<Func<T, bool>>(expr, entityParam);
    }
    

    请注意,您需要将传递的Expression&lt;Func&lt;T, DateTime&gt;&gt; 表达式的主体重新绑定(使用相同的ReplaceParameter 辅助方法)到要在结果表达式中使用的公共参数。

    可以通过添加更多帮助方法来简化代码,例如Entity Framework + DayOfWeek,但同样,如果您打算大量使用它,更好的选择是使用一些现成的库,因为最后您将开始重新发明这些库的作用。

    【讨论】:

    • 感谢您的详细解释。试用了 LinqKit 和 NeinLinq,并选择了后者,因为它的子实体问题更少,用法更清晰。
    • @IvanStoev 试图解决这个问题。您能否也发布一个使用示例,即如何在 LINQ 查询中使用 IsPeriodActive 的示例(以及如何设置它的最后两个参数)。
    • @hhtech1 它仅适用于顶级查询。想象一下,您有一个实体类Foo,有两个DateTime 类型属性,分别称为StartDateEndDate。此外,您还有两个 DateTime 变量,分别称为 checkPeriodStartcheckPeriodEnd。您可以按如下方式使用上述方法:dbContext.Foos.Where(SomeStaticClass.IsPeriodActive&lt;Foo&gt;(checkPeriodStart, checkPeriodEnd, x =&gt; x.StartDate, x.EndDate))。最后两个参数是 lambda 表达式,类似于 .OrderBy(x =&gt; x.StartDate).Select(x =&gt; new { x.StartDate, x.EndDate }) 等。
    【解决方案2】:

    当我执行复杂的搜索和排序时,我会以这种方式使用 SQL Server

    • 创建表以存储数据并使用外键链接它们
    • 从一个或多个表创建视图,而不是使用 EF 和 C# 链接它们。我觉得这很自然而且更快

    • 最后,创建返回基于 SQL Server 视图的存储过程 根据需要进行任何过滤和排序。

    MVC Core EF 还不支持 SP,所以我创建了一个与模型同名的部分类。这是我使用 MVC Core 2.2 中的存储过程在 SQL Server 中搜索一些 IIS 日志数据的示例。它允许搜索和分页以及其他过滤器,例如日期范围。

    模型使用数据表作为通用货币,我有一个局部视图,可以使用

    @await Html.PartialAsync("_DataTableView", Model.Data)
    

    上下文帮助器

    public async Task<ViewDataResult> IIS_File_Log_DataView_Get(int siteId, DateTime? dateTimeFrom, DateTime? dateTimeTo,
        string searchText,
        int httpStatus,
        string csHost,
        string csUserName,
        string sortColumn, Helpers.TableSortDirection sortDirection,
        int rowsPerPage, int pageNumber)
    {
        // get site SP name
        var site = await this.FtpSites.FindAsync(siteId);
    
        // set an empty return list at a minimum
        var t = new DataTable();
        var result = new ViewDataResult();
    
        // set the skip value from the current page number and rows per page
        int skip = ((pageNumber - 1) * rowsPerPage) - 1;
    
        //  if -ve, set to zero
        if (skip < 0)
        {
            skip = 0;
        }
    
        var sp = this.StoredProcedure_Get(site.LogDataViewStoredProcedure)
            .WithSqlParam("@DateTimeFrom", dateTimeFrom)
            .WithSqlParam("@DateTimeTo", dateTimeTo)
            .WithSqlParam("@SearchText", searchText ?? "")
            .WithSqlParam("@HttpStatus", httpStatus)
            .WithSqlParam("@CsHost", csHost)
            .WithSqlParam("@CsUserName", csUserName)
            .WithSqlParam("@SortColumn", sortColumn ?? "")
            .WithSqlParam("@SortDirection", sortDirection.ToString())
            .WithSqlParam("@Skip", skip)
            .WithSqlParam("@Take", rowsPerPage)
            // output param
            .WithSqlParam("@RowCount", 0, true);
    
        // open connection if not already open
        if (sp.Connection.State != ConnectionState.Open)
        {
            sp.Connection.Open();
        }
    
        // seconds
        sp.CommandTimeout = 120;
    
        // execute the SP
        using (var r = await sp.ExecuteReaderAsync())
        {
            if (r.HasRows)
            {
                // add columns
                for (int index = 0; index < r.FieldCount; index += 1)
                {
                    t.Columns.Add(r.GetName(index), r.GetFieldType(index));
                }
    
                while (await r.ReadAsync())
                {
                    var row = t.NewRow();
    
                    for (int index = 0; index < r.FieldCount; index += 1)
                    {
                        row[index] = r[index];
                    }
    
                    t.Rows.Add(row);
                }
            }
        }
    
        // get row count. By design, Microsoft implementation means this can't be read until reader is finished with
        if (sp.Parameters["@RowCount"].Value != null)
        {
            // set row count
            result.RowCount = (int)sp.Parameters["@RowCount"].Value;
        }
    
        // set data
        result.Data = t;
    
        result.CurrentPage = pageNumber;
        result.PageCount = pageNumber;
        result.PageCount = (result.RowCount / rowsPerPage) + (result.RowCount % rowsPerPage == 0 ? 0 : 1);
        result.RowsPerPage = rowsPerPage;
    
        // return
        return result;
    }
    

    【讨论】:

    • 谢谢,但不幸的是,这不是我想要的。
    猜你喜欢
    • 2021-10-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-06-22
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多