Как бы вы реорганизовали этот код LINQ?

У меня много уродливого кода, который выглядит так:

if (!string.IsNullOrEmpty(ddlFileName.SelectedItem.Text))
    results = results.Where(x => x.FileName.Contains(ddlFileName.SelectedValue));
if (chkFileName.Checked)
    results = results.Where(x => x.FileName == null);

if (!string.IsNullOrEmpty(ddlIPAddress.SelectedItem.Text))
    results = results.Where(x => x.IpAddress.Contains(ddlIPAddress.SelectedValue));
if (chkIPAddress.Checked)
    results = results.Where(x => x.IpAddress == null);

...etc.

results - это IQueryable<MyObject>.
Идея состоит в том, что для каждого из этих бесчисленных раскрывающихся списков и флажков, если в раскрывающемся списке что-то выбрано, пользователь хочет сопоставить этот элемент. Если флажок установлен, пользователю нужны именно те записи, в которых это поле имеет значение NULL или пустую строку. (Пользовательский интерфейс не позволяет выбирать оба одновременно.) Все это добавляет к выражению LINQ, которое выполняется в конце, после того как мы добавили все условия.

Это кажется, как будто должен быть какой-то способ вытащить Expression<Func<MyObject, bool>> или два, чтобы я мог поместить повторяющиеся части в метод и просто передать, какие изменения. Я делал это в других местах, но этот набор кода поставил меня в тупик. (Кроме того, я бы хотел избежать "Dynamic LINQ", потому что я хочу, чтобы вещи были безопасными по типу, если это возможно.) Есть идеи?

Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
2
0
840
10

Ответы 10

results = results.Where(x => 
    (string.IsNullOrEmpty(ddlFileName.SelectedItem.Text) || x.FileName.Contains(ddlFileName.SelectedValue))
    && (!chkFileName.Checked || string.IsNullOrEmpty(x.FileName))
    && ...);

Я бы преобразовал его в один оператор Linq:

var results =
    //get your inital results
    from x in GetInitialResults()
    //either we don't need to check, or the check passes
    where string.IsNullOrEmpty(ddlFileName.SelectedItem.Text) ||
       x.FileName.Contains(ddlFileName.SelectedValue)
    where !chkFileName.Checked ||
       string.IsNullOrEmpty(x.FileName)
    where string.IsNullOrEmpty(ddlIPAddress.SelectedItem.Text) ||
       x.FileName.Contains(ddlIPAddress.SelectedValue)
    where !chkIPAddress.Checked ||
       string.IsNullOrEmpty(x. IpAddress)
    select x;

Не короче, но мне эта логика понятнее.

Ни один из этих ответов пока не совсем то, что я ищу. Чтобы дать пример того, к чему я стремлюсь (я тоже не считаю это полным ответом), я взял приведенный выше код и создал несколько методов расширения:

static public IQueryable<Activity> AddCondition(
    this IQueryable<Activity> results,
    DropDownList ddl, 
    Expression<Func<Activity, bool>> containsCondition)
{
    if (!string.IsNullOrEmpty(ddl.SelectedItem.Text))
        results = results.Where(containsCondition);
    return results;
}
static public IQueryable<Activity> AddCondition(
    this IQueryable<Activity> results,
    CheckBox chk, 
    Expression<Func<Activity, bool>> emptyCondition)
{
    if (chk.Checked)
        results = results.Where(emptyCondition);
    return results;
}

Это позволило мне преобразовать приведенный выше код в следующее:

results = results.AddCondition(ddlFileName, x => x.FileName.Contains(ddlFileName.SelectedValue));
results = results.AddCondition(chkFileName, x => x.FileName == null || x.FileName.Equals(string.Empty));

results = results.AddCondition(ddlIPAddress, x => x.IpAddress.Contains(ddlIPAddress.SelectedValue));
results = results.AddCondition(chkIPAddress, x => x.IpAddress == null || x.IpAddress.Equals(string.Empty));

Это не такой уродливый довольно, но он все же длиннее, чем я бы предпочел. Пары лямбда-выражений в каждом наборе, очевидно, очень похожи, но я не могу придумать способ их дальнейшего уплотнения ... по крайней мере, не прибегая к динамическому LINQ, который заставляет меня жертвовать безопасностью типов.

Есть другие идеи?

@Kyralessa,

Вы можете создать метод расширения AddCondition для предикатов, который принимает параметр типа Control плюс лямбда-выражение и возвращает комбинированное выражение. Затем вы можете комбинировать условия с помощью свободного интерфейса и повторно использовать свои предикаты. Чтобы увидеть пример того, как это можно реализовать, см. Мой ответ на этот вопрос:

Как составить существующие выражения Linq

В таком случае:

//list of predicate functions to check
var conditions = new List<Predicate<MyClass>> 
{
    x => string.IsNullOrEmpty(ddlFileName.SelectedItem.Text) ||
         x.FileName.Contains(ddlFileName.SelectedValue),
    x => !chkFileName.Checked ||
         string.IsNullOrEmpty(x.FileName),
    x => string.IsNullOrEmpty(ddlIPAddress.SelectedItem.Text) ||
         x.IpAddress.Contains(ddlIPAddress.SelectedValue),
    x => !chkIPAddress.Checked ||
         string.IsNullOrEmpty(x.IpAddress)
}

//now get results
var results =
    from x in GetInitialResults()
    //all the condition functions need checking against x
    where conditions.All( cond => cond(x) )
    select x;

Я только что явно объявил список предикатов, но они могут быть сгенерированы, например:

ListBoxControl lbc;
CheckBoxControl cbc;
foreach( Control c in this.Controls)
    if ( (lbc = c as ListBoxControl ) != null )
         conditions.Add( ... );
    else if ( (cbc = c as CheckBoxControl ) != null )
         conditions.Add( ... );

Вам понадобится какой-то способ проверить свойство MyClass, которое вам нужно проверить, и для этого вам нужно будет использовать отражение.

Вы видели LINQKit? AsExpandable звучит как то, что вам нужно (хотя вы можете прочитать сообщение Вызов функций в запросах LINQ на TomasP.NET для большей глубины).

Я бы с осторожностью относился к решениям вида:

// from Keith
from x in GetInitialResults()
    //either we don't need to check, or the check passes
    where string.IsNullOrEmpty(ddlFileName.SelectedItem.Text) ||
       x.FileName.Contains(ddlFileName.SelectedValue)

Мое рассуждение - захват переменных. Если вы сразу выполните только один раз, вы, вероятно, не заметите разницы. Однако в linq оценка выполняется не сразу, а при каждой итерации. Делегаты могут захватывать переменные и использовать их за пределами запланированной вами области.

Такое ощущение, что вы запрашиваете слишком близко к пользовательскому интерфейсу. Запросы - это уровень вниз, а linq - это не способ взаимодействия пользовательского интерфейса.

Возможно, вам будет лучше сделать следующее. Отделите логику поиска от презентации - она ​​более гибкая и многоразовая - основы объектно-ориентированного программирования.

// my search parameters encapsulate all valid ways of searching.
public class MySearchParameter
{
    public string FileName { get; private set; }
    public bool FindNullFileNames { get; private set; }
    public void ConditionallySearchFileName(bool getNullFileNames, string fileName)
    {
        FindNullFileNames = getNullFileNames;
        FileName = null;

        // enforce either/or and disallow empty string
        if (!getNullFileNames && !string.IsNullOrEmpty(fileName) )
        {
            FileName = fileName;
        }
    }
    // ...
}

// search method in a business logic layer.
public IQueryable<MyClass> Search(MySearchParameter searchParameter)
{
    IQueryable<MyClass> result = ...; // something to get the initial list.

    // search on Filename.
    if (searchParameter.FindNullFileNames)
    {
        result = result.Where(o => o.FileName == null);
    }
    else if ( searchParameter.FileName != null )
    {   // intermixing a different style, just to show an alternative.
        result = from o in result
                 where o.FileName.Contains(searchParameter.FileName)
                 select o;
    }
    // search on other stuff...

    return result;
}

// code in the UI ... 
MySearchParameter searchParameter = new MySearchParameter();
searchParameter.ConditionallySearchFileName(chkFileNames.Checked, drpFileNames.SelectedItem.Text);
searchParameter.ConditionallySearchIPAddress(chkIPAddress.Checked, drpIPAddress.SelectedItem.Text);

IQueryable<MyClass> result = Search(searchParameter);

// inform control to display results.
searchResults.Display( result );

Да, набирать больше, но вы читаете код примерно в 10 раз больше, чем пишете. Ваш пользовательский интерфейс более понятен, класс параметров поиска позаботится о себе и гарантирует, что взаимоисключающие параметры не конфликтуют, а код поиска абстрагируется от любого пользовательского интерфейса и даже не заботится о том, используете ли вы Linq вообще.

Поскольку вы хотите многократно сокращать исходный запрос результатов с помощью бесчисленных фильтров, вы можете использовать Агрегат () (что соответствует reduce () в функциональных языках).

Фильтры имеют предсказуемую форму, состоящую из двух значений для каждого члена MyObject - в соответствии с информацией, которую я почерпнул из вашего сообщения. Если каждый сравниваемый член является строкой, которая может иметь значение NULL, я рекомендую использовать метод расширения, который позволяет связывать пустые ссылки с методом расширения предполагаемого типа.

public static class MyObjectExtensions
{
    public static bool IsMatchFor(this string property, string ddlText, bool chkValue)
    {
        if (ddlText!=null && ddlText! = "")
        {
            return property!=null && property.Contains(ddlText);
        }
        else if (chkValue==true)
        {
            return property==null || property= = "";
        }
        // no filtering selected
        return true;
    }
}

Теперь нам нужно расположить фильтры свойств в коллекции, чтобы можно было выполнять итерацию по многим. Они представлены как выражения для совместимости с IQueryable.

var filters = new List<Expression<Func<MyObject,bool>>>
{
    x=>x.Filename.IsMatchFor(ddlFileName.SelectedItem.Text,chkFileName.Checked),
    x=>x.IPAddress.IsMatchFor(ddlIPAddress.SelectedItem.Text,chkIPAddress.Checked),
    x=>x.Other.IsMatchFor(ddlOther.SelectedItem.Text,chkOther.Checked),
    // ... innumerable associations
};

Теперь мы объединяем бесчисленные фильтры в запрос с исходными результатами:

var filteredResults = filters.Aggregate(results, (r,f) => r.Where(f));

Я запустил это в консольном приложении с смоделированными тестовыми значениями, и он работал, как ожидалось. Думаю, это как минимум демонстрирует принцип.

Одна вещь, которую вы могли бы рассмотреть, - это упростить свой пользовательский интерфейс, убрав флажки и вместо этого используя элемент «<empty>» или «<null>» в раскрывающемся списке. Это уменьшит количество элементов управления, занимающих место в вашем окне, устранит необходимость в сложной логике «включить X, только если Y не отмечен» и позволит создать красивое поле с одним элементом управления для каждого запроса.


Переходя к логике запроса результатов, я бы начал с создания простого объекта, представляющего фильтр для вашего объекта домена:

interface IDomainObjectFilter {
  bool ShouldInclude( DomainObject o, string target );
}

Вы можете связать соответствующий экземпляр фильтра с каждым из элементов управления пользовательского интерфейса, а затем получить его, когда пользователь инициирует запрос:

sealed class FileNameFilter : IDomainObjectFilter {
  public bool ShouldInclude( DomainObject o, string target ) {
    return string.IsNullOrEmpty( target )
        || o.FileName.Contains( target );
  }
}

...
ddlFileName.Tag = new FileNameFilter( );

Затем вы можете обобщить фильтрацию результатов, просто перечислив элементы управления и выполнив связанный фильтр (спасибо спешить за идею агрегирования):

var finalResults = ddlControls.Aggregate( initialResults, ( c, r ) => {
  var filter = c.Tag as IDomainObjectFilter;
  var target = c.SelectedValue;
  return r.Where( o => filter.ShouldInclude( o, target ) );
} );


Поскольку ваши запросы настолько регулярны, вы можете еще больше упростить реализацию, используя один класс фильтра, принимающий селектор членов:

sealed class DomainObjectFilter {
  private readonly Func<DomainObject,string> memberSelector_;
  public DomainObjectFilter( Func<DomainObject,string> memberSelector ) {
    this.memberSelector_ = memberSelector;
  }

  public bool ShouldInclude( DomainObject o, string target ) {
    string member = this.memberSelector_( o );
    return string.IsNullOrEmpty( target )
        || member.Contains( target );
  }
}

...
ddlFileName.Tag = new DomainObjectFilter( o => o.FileName );

Не используйте LINQ, если это влияет на удобочитаемость. Разложите отдельные тесты на логические методы, которые можно использовать в качестве выражения where.

IQueryable<MyObject> results = ...;

results = results
    .Where(TestFileNameText)
    .Where(TestFileNameChecked)
    .Where(TestIPAddressText)
    .Where(TestIPAddressChecked);

Таким образом, отдельные тесты - это простые методы класса. Их можно даже индивидуально тестировать.

bool TestFileNameText(MyObject x)
{
    return string.IsNullOrEmpty(ddlFileName.SelectedItem.Text) ||
           x.FileName.Contains(ddlFileName.SelectedValue);
}

bool TestIPAddressChecked(MyObject x)
{
    return !chkIPAddress.Checked ||
        x.IpAddress == null;
}

Имейте в виду, что это LINQ to SQL (что я не сказал в вопросе, но это один из тегов). Я хочу, чтобы фильтрация происходила на стороне базы данных, а не на стороне клиента.

Ryan Lundy 30.09.2008 05:42

Ах! -1 на таком подходе то.

loudej 01.10.2008 09:33

Другие вопросы по теме