Как бы вы реорганизовали этот код LINQ? - PullRequest
2 голосов
/ 10 сентября 2008

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

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

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

Ответы [ 10 ]

5 голосов
/ 11 сентября 2008

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

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

5 голосов
/ 10 сентября 2008

Я бы преобразовал это в один оператор 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;

Это не короче, но я считаю эту логику более понятной.

1 голос
/ 27 сентября 2008

Не используйте 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;
}
1 голос
/ 11 сентября 2008

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

0 голосов
/ 27 сентября 2008

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

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

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 );
0 голосов
/ 25 сентября 2008

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

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

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));

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

0 голосов
/ 25 сентября 2008

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

// 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 вообще.

0 голосов
/ 11 сентября 2008

@ Kyralessa

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

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

0 голосов
/ 11 сентября 2008

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

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, что заставляет меня жертвовать безопасностью типов.

Есть еще идеи?

0 голосов
/ 10 сентября 2008
results = results.Where(x => 
    (string.IsNullOrEmpty(ddlFileName.SelectedItem.Text) || x.FileName.Contains(ddlFileName.SelectedValue))
    && (!chkFileName.Checked || string.IsNullOrEmpty(x.FileName))
    && ...);
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...