使用自定义表达式扩展EF Core'where'子句

时间:2018-12-18 15:10:07

标签: c# linq entity-framework-core

我有一堆实体,它们的有效期已定义为“ 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,以便为任何实体类型添加这种“ where”条件? 我不能使用过滤器,因为有时我需要查询原始数据或只进行一次定期检查(不能同时查询),而且某些实体对这些字段的命名也不同。

2 个答案:

答案 0 :(得分:3)

您需要将Func<T, DateTime>参数更改为Expression<Func<T, DateTime>>,并将其合并到所需的表达式中。

不幸的是,C#编译器和BCL都不对后面的任务(其他表达式的表达式组成)有所帮助。有一些第三方软件包,例如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);
}

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

可以通过添加更多辅助方法(如此处Entity Framework + DayOfWeek)来简化代码,但同样,如果您打算大量使用此方法,则更好的选择是使用一些现成的库,因为最后您会开始重新发明这些库的功能。

答案 1 :(得分:-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;
}