簡體   English   中英

使用自定義表達式擴展EF Core'where'子句

[英]Extending EF Core 'where' clause with custom expression

我有一堆實體,它們的活動周期定義為'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”條件? 我不能使用過濾器,因為有時我需要查詢原始數據或僅使用一個周期檢查(不是兩者),並且某些實體也會以不同的方式命名這些字段。

您需要將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);
}

請注意,您需要將傳遞的Expression<Func<T, DateTime>>表達式的主體重新綁定(使用相同的ReplaceParameter輔助方法)到要在結果表達式中使用的公共參數。

通過添加更多輔助方法(例如Entity Framework + DayOfWeek)可以簡化代碼,但是,如果您計划大量使用它,更好的選擇是使用一些現成的庫,因為最后您將開始重新發明這些圖書館。

當我有復雜的搜索和排序來執行我以這種方式使用SQL服務器

  • 創建表以存儲數據並使用Foriegn Keys鏈接它們
  • 從一個或多個表創建視圖,而不是使用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;
}

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM