你会如何重构这个LINQ代码?

时间:2008-09-10 18:34:58

标签: c# .net linq linq-to-sql

我有很多看起来像这样丑陋的代码:

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.

resultsIQueryable<MyObject> 我们的想法是,对于这些无数下拉列表和复选框中的每一个,如果下拉列表中选择了某些内容,则用户希望匹配该项目。如果选中该复选框,则用户特别需要该字段为空或空字符串的那些记录。 (UI不允许同时选择它们。)这一切都增加了LINQ表达式,在我们添加了所有条件之后,它将在最后执行。

似乎就像应该有一些方法来提取Expression<Func<MyObject, bool>>或两个,以便我可以将重复的部分放在一个方法中,然后传递一些变化。我在其他地方做过这个,但这套代码让我受阻。 (另外,我想避免使用“动态LINQ”,因为如果可能,我希望保持类型安全。)任何想法?

10 个答案:

答案 0 :(得分:5)

我将其转换为单个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 :(得分:5)

在那种情况下:

//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的属性,为此你必须使用反射。

答案 2 :(得分:1)

你见过LINQKit吗? AsExpandable听起来就像你所追求的那样(尽管你可能想要阅读更多深度的TomasP.NET帖子Calling functions in LINQ queries。)

答案 3 :(得分:1)

如果影响可读性,请不要使用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;
}

答案 4 :(得分:0)

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

答案 5 :(得分:0)

到目前为止,这些答案都不是我想要的。举一个我瞄准的例子(我不认为这是一个完整的答案),我采用了上面的代码并创建了几个扩展方法:

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

这不是相当丑陋,但它仍然比我更喜欢。每组中的lambda表达式显然非常相似,但是我无法想出一种方法来进一步压缩它们......至少不是没有采用动态LINQ,这使我牺牲了类型的安全性。

还有其他想法吗?

答案 6 :(得分:0)

@Kyralessa,

您可以为接受类型为Control plus lambda expression的参数的谓词创建扩展方法AddCondition,并返回组合表达式。然后,您可以使用流畅的界面组合条件并重用谓词。要查看如何实现它的示例,请参阅我对此问题的回答:

How do I compose existing Linq Expressions

答案 7 :(得分:0)

我对表格的解决方案持谨慎态度:

// 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不是用户界面进行通信的方式。

您可能最好不要执行以下操作。将搜索逻辑与演示文稿分离 - 它更灵活,可重用--OO的基础知识。

// 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倍左右。您的UI更清晰,搜索参数类自行处理并确保互斥选项不会发生冲突,搜索代码将从任何UI中抽象出来,甚至根本不关心您是否使用Linq。

答案 8 :(得分:0)

由于您希望使用无数过滤器反复减少原始结果查询,因此可以使用 Aggregate(),(对应于函数式语言中的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));

我在一个带有模拟测试值的控制台应用程序中运行它,它按预期工作。我认为这至少证明了这一原则。

答案 9 :(得分:0)

您可以考虑的一件事是通过取消复选框并在下拉列表中使用“<empty>”或“<null>”项来简化您的用户界面。这将减少占用窗口空间的控件数量,无需复杂的“仅在未检查Y时启用X”逻辑,并且将启用一个不错的单控制每个查询字段。


转到结果查询逻辑,我首先要创建一个简单的对象来表示域对象的过滤器:

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

您可以将过滤器的相应实例与每个UI控件相关联,然后在用户启动查询时检索该实例:

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

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

然后,您可以通过简单地枚举控件并执行关联的过滤器来推广结果过滤(感谢hurst对于Aggregate的想法):

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