我们将如何重构此LINQ代码?

时间:2020-03-05 18:50:58  来源:igfitidea点击:

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

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.

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

似乎应该有某种方法可以提取出一两个" Expression <Func <MyObject,bool >>",以便我可以将重复的部分放在一个方法中,然后直接传入更改。我在其他地方也这样做,但是这组代码使我受阻。 (此外,我想避免使用" Dynamic LINQ",因为我想尽可能保持类型安全。)有什么想法吗?

解决方案

回答

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

这虽然不那么丑陋,但仍然比我想要的更长。每组中的成对的lambda表达式显然非常相似,但是我无法找到进一步压缩它们的方法……至少在没有不使用动态LINQ的情况下,这使我牺牲了类型安全性。

还有其他想法吗?

回答

@Kyralessa,

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

如何撰写现有的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听起来像我们所追求的(尽管我们可能想阅读TomasP.NET上LINQ查询中的Post Calling函数,以获取更多信息)。

回答

我会警惕以下形式的解决方案:

// 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中,评估不是立即进行的,而是在每次进行迭代时进行。代表可以捕获变量,并在所需范围之外使用它们。

感觉我们查询的位置离UI太近了。查询是向下一层,而linq并不是UI向下通信的方式。

我们最好执行以下操作。将搜索逻辑与表示分离开来,它是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 );

是的,它的输入更多,但是我们读的代码比编写的多十倍。UI更加清晰,搜索参数类会自行处理,并确保相互排斥的选项不会发生冲突,并且搜索代码是从任何UI中抽象出来的,甚至根本不在乎是否使用Linq。

回答

由于我们想使用无数过滤器来重复减少原始结果查询,因此可以使用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));

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

回答

我们可能会考虑的一件事是通过取消选中复选框并在下拉列表中使用"&lt;empty>"或者"&lt;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( );

然后,我们可以通过简单地枚举控件并执行关联的过滤器来概括结果过滤器(这要感谢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 );

回答

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