Расширение предложения EF Core 'where' с помощью пользовательского выражения - PullRequest
0 голосов
/ 18 декабря 2018

У меня есть группа сущностей, у которых активный период определен как поля '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 для добавления такого рода условия «где» для любого типа объекта?Я не могу использовать фильтры, так как иногда мне нужно запрашивать необработанные данные или только с одной проверкой периода (не обе), а также у некоторых объектов эти поля имеют разные имена.

Ответы [ 2 ]

0 голосов
/ 19 декабря 2018

Вам необходимо изменить аргументы Func<T, DateTime> на Expression<Func<T, DateTime>> и включить их в желаемое выражение.

К сожалению, ни компилятор C #, ни BCL не помогают с более поздней задачей (составление выражения из других выражений).Существуют сторонние пакеты, такие как LinqKit , NeinLinq и т. Д., Которые решают эту проблему, поэтому, если вы планируете интенсивно использовать композицию выражений, вы можете рассмотреть возможность использования одной из этих библиотек.

Но принцип один и тот же.В какой-то момент пользовательский ExpressionVisitor используется для замены частей исходного выражения другими выражениями.Например, для таких простых сценариев я использую лямбда-выражение времени компиляции с дополнительными параметрами, используемыми в качестве заполнителей, которые затем заменяются фактическими выражениями почти так же, как string.Replace.

Для этого я использую следующий вспомогательный метод для замены параметра лямбда-выражения другим выражением:

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 , но, опять же, если вы планируете использовать это часто, лучшим выбором будет использование некоторой готовой библиотеки, потому что в конце вы начнете изобретать то, что делают эти библиотеки.

0 голосов
/ 18 декабря 2018

Когда мне нужно выполнить сложный поиск и сортировку, я использую SQL-сервер следующим образом

  • Создание таблиц для хранения данных и связывание их с помощью ключей Foriegn
  • Создатьпредставления из одной или нескольких таблиц вместо использования EF и C # для их связи.Я нахожу это естественным и более быстрым

  • Наконец, создайте хранимые процедуры, которые возвращают представление SQL Server на основе любой фильтрации и сортировки по мере необходимости.

MVC Core EF еще не поддерживает SP, поэтому я создаю частичный класс с тем же именем, что и модель.Вот пример того, который я использую для поиска некоторых данных журнала IIS в SQL Server, используя хранимую процедуру в MVC Core 2.2.Он позволяет выполнять поиск, разбивать на страницы и другие фильтры, такие как диапазоны дат.

Модель использует таблицу данных в качестве общей валюты, и у меня есть частичное представление, которое может отображать таблицу данных с использованием

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

Context helper

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;
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...