2008-09-10 226 views
2

我已经得到了很多丑陋的代码看起来像这样:你会如何重构这个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. 

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

似乎喜欢,就必须有某种方式拉出一个Expression<Func<MyObject, bool>>或两个,这样我可以把重复部分的方法,只是在传递什么样的变化。我在其他地方做过,但是这组代码让我感到困惑。 (另外,我想避免使用“动态LINQ”,因为如果可能,我想保持类型安全。)任何想法?

回答

0
results = results.Where(x => 
    (string.IsNullOrEmpty(ddlFileName.SelectedItem.Text) || x.FileName.Contains(ddlFileName.SelectedValue)) 
    && (!chkFileName.Checked || string.IsNullOrEmpty(x.FileName)) 
    && ...); 
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; 

这并不短,但我觉得这个逻辑清晰。

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,这使我牺牲了类型安全性。

还有其他想法吗?

0

@Kyralessa,

可以创建为谓词扩展方法AddCondition接受型控制加lambda表达式并返回联合表达的参数。然后,您可以使用流畅的界面组合条件并重用谓词。要查看示例的它如何可以实现看到关于这个问题我的回答:

How do I compose existing Linq Expressions

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的属性,你需要检查,并且你会hav e使用反射。

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不是用于UI通信的方式。

你可能最好做以下事情。从演示中分离搜索逻辑 - 它更灵活,可重用 - 面向对象的基础。

// 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

由于您想重复减少含有无数过滤器的原始结果查询,因此可以使用Aggregate()(对应于函数式语言中的reduce())。

过滤器是可预测的形式,根据我从您的帖子收集的信息,为每个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

您可能会考虑的一件事是通过取消复选框并在下拉列表中使用“<empty>”或“<null>”项目来简化您的UI。这将减少占用窗口空间的控件数量,消除复杂的“仅在未检查到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为总结想法):

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); 
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; 
} 
+0

请记住,这是LINQ to SQL(我没有在问题中说过,但它是其中一个标签)。我想过滤发生在数据库端,而不是客户端。 – 2008-09-30 01:42:39