【问题标题】:Spliting up long linq queries for improved maintainability拆分长 linq 查询以提高可维护性
【发布时间】:2012-09-03 08:16:16
【问题描述】:

我有一堆这些任务都基于 LINQ 查询。我正在寻找重构它们并使它们更易于阅读并允许我根据语言/地区等更改查询的好方法。

var mailTaskOne = CreateTask(() => myService.Mail.Where(p => p.ProjectName == "Delta"
    && (p.MailLang== (int)MailLanguage.EU || p.MailLang == (int)MailLanguage.RU)
    && (p.DateEntered >= startDate && p.DateEntered <= endDate)
    && p.MailPriority == (int)MailPriority.High).Count());

我认为方便的一种方法是将查询拆分为类似的内容。

var results = myService.Mail.Where(x => x.ProjectName == "Delta");
results = results.Where(p => p.MailLang== (int)MailLanguage.EU);
results = results.Where(p => p.DateModified >= startDate && p.DateModified <= endDate);

这样我就可以做到这一点,而不必为每个区域重复整个查询。

if (MailLanguage == "English")
    results = results.Where(p => p.MailLang== (int)MailLanguage.EU);
else
    results = results.Where(p => p.MailLang== (int)MailLanguage.RU);

有没有人知道更好的解决方案?我最终拥有了巨大的功能,因为我需要根据要求执行大约 20 个这样的查询;例如地区、项目名称等。


编辑:

由于我不知道后端(网络服务/api)的一些限制,很遗憾我不能使用这个问题中提到的一些很棒的答案。

例如,这没有得到正确翻译,但绝不是因为答案不正确,根本无法与我正在使用的 API 一起工作——可能是因为它实现得很差。

public bool IsValid(Type x)
{
    return (x.a == b) && (x.c ==d) && (x.d == e);
}

无论如何,任何寻找类似解决方案的人都是有效的答案,但最后我最终选择了与提供的解决方案类似的东西。

【问题讨论】:

  • 为什么不用语言作为参数/变量?
  • 欢迎进一步解释——我现在脑子里一片空白。请记住,可能有多种语言,例如英语可能是 US + EU。 :]
  • 你查询的是数据库还是内存?
  • 它是一个数据库。所以这些都是IQueryable

标签: c# linq refactoring


【解决方案1】:

我会按照您的建议将查询拆分到不同的行,这意味着您可以每行放置 cmets 来描述它在做什么。您仍然只访问数据库 1 次,因此您不会在性能方面损失任何东西,而是获得更好的可读性。

【讨论】:

  • 谢谢。我自己也是这么想的,但想确保我没有遗漏任何明显的东西。
【解决方案2】:

为什么不简单地有一个方法呢?

public static IQueryable<Mail> Count(this IQueryable<Mail> mails, 
                  string projectName, 
                  MailLanguage mailLanguage,
                  DateTime startDate,
                  DateTime endDate) {
    return mails.Count(p=>
           p.ProjectName == projectName
           && p.MailLang == mailLanguage
           && p.DateEntered >= startDate 
           && p.DateEntered <= endDate
           && p.MailPriority == (int)MailPriority.High);
}

那么你可以像这样简单地使用它

CreateTask(() => myService.Mail.Count("Delta",MailLanguage.EU,startDate,endDate));

【讨论】:

    【解决方案3】:

    您可以将项目名称、修改的数据、邮件语言和任何其他标准转换为变量,并根据任何条件为它们赋予您想要的值。那么您的查询将使用变量而不是文字值。

    var projectName="Delta";
    var mailLanguage=(int)MailLanguage.RU;
    
    var results=myService.Mail.Where(x => x.ProjectName == projectName)
                && (p.MailLang== mailLanguage);
    

    这样,您可以将大部分复杂性放在为变量赋值时,并且 linq 查询会更容易阅读和维护。

    【讨论】:

      【解决方案4】:

      您可以创建一个参数类,如:

      public class MailParameters
      {
          public DateTime EndTime { get; private set; }
          public IEnumerable<int> Languages { get; private set; }
          public int Priority { get; private set; }
          public string ProjectName { get; private set; }
          public DateTime StartTime { get; private set; }
      
          public MailParameters(string projectName, DateTime startTime, DateTime endTime, MailLang language, Priority priority)
              : this(projectName, startTime, endTime, new[] { language }, priority)
      
          public MailParameters(string projectName, DateTime startTime, DateTime endTime, IEnumerable<MailLang> languages, Priority priority)
          {
              ProjectName = projectName;
              StartTime = startTime;
              EndTime = endTime;
              Languages = languages.Cast<int>();
              Priority = (int)priority;
          }
      }
      

      然后添加这些扩展方法:

      public static int Count(this IQueryable<Mail> mails, MailCountParameter p)
      {
          return mails.Count(m =>
              m.ProjectName == p.ProjectName &&
              p.Languages.Contains(m.MailLang) &&
              m.EnteredBetween(p.StartTime, p.EndTime) &&
              m.Priority == p.Priority);
      }
      
      public static bool EnteredBetween(this Mail mail, DateTime startTime, DateTime endTime)
      {
          return mail.DateEntered >= startTime && mail.DateEntered <= endTime;
      }
      

      那么用法是:

      var mailParametersOne = new MailParameters("Delta", startDate, endDate, new[] { MailLang.EU, MailLang.RU }, MailPriority.High);
      var mailTaskOne = CreateTask(() => myService.Mail.Count(mailParametersOne));
      

      【讨论】:

        【解决方案5】:

        考虑将复杂的比较转移到一个函数中。例如,而不是

        Results.Where(x => (x.a == b) && (x.c == d) && (x.d == e))
        

        考虑

        Results.Where(x => IsValid(x))
        
        ...
        
        public bool IsValid(Type x)
        {
            return (x.a == b) && (x.c ==d) && (x.d == e);
        }
        

        代码变得更具可读性,并且 IsValid 易于使用自动化测试框架进行测试。

        【讨论】:

        • 更易读的版本是:Results.Where(IsValid)
        • 谢谢!有这么多好的答案,你们并不能很容易地选择正确的答案! :D
        • 如果您要在查询中使用自己的函数/方法,请注意仅在本地对象查询上运行它。在面向数据库的查询中插入,这可能会强制其余查询在本地完成,并将一半的数据库拖到内存中以促进这一点,或者它可能会在运行时爆炸,因为 LINQ 提供程序无法计算出如何翻译这个。 (当然,只需使用一种方法来构造一个带有附加过滤的 IQueryable 就可以了,而且你所追求的也是如此。)
        【解决方案6】:

        我的最终解决方案基于 ScottGu 的一篇文章。 http://weblogs.asp.net/scottgu/archive/2008/01/07/dynamic-linq-part-1-using-the-linq-dynamic-query-library.aspx

        我像这样构建 LINQ 查询。

            var linqStatements = new List<String>();
        
            linqStatements.Add(parser.StringToLinqQuery<Project>("ProjectId", report.Project));
            linqStatements.Add(parser.StringToLinqQuery<Region>("RegionId", report.Region));
            linqStatements.Add(parser.StringToLinqQuery<Status>("Status", report.Status));
            linqStatements.Add(parser.StringToLinqQuery<Priority>("Priority", report.Priority));
            linqStatements.Add(parser.StringToLinqQuery<Category>("CategoryId", report.Category));
            linqStatements.Add(AccountIdsToLinqQuery(report.PrimaryAssignment));
        
            string baseQuery = String.Join(" AND ", linqStatements.Where(s => !String.IsNullOrWhiteSpace(s)));
            var linqQuery = service.Mail.Where(baseQuery).Cast<Mail>();
        

        StringToLinqQuery 看起来像这样(简化版)。

        public string StringToLinqQuery<TEnum>(string field, string value) where TEnum : struct
        {
            if (String.IsNullOrWhiteSpace(value))
                return String.Empty;
        
            var valueArray = value.Split('|');
            var query = new StringBuilder();
        
            for (int i = 0; i < valueArray.Count(); i++)
            {
                TEnum result;
                if (Enum.TryParse<TEnum>(valueArray[i].ToLower(), true, out result))
                {
                    if (i > 0)
                        query.Append(" OR ");
                    query.AppendFormat("{0} == {1}", field, Convert.ToInt32(result));
                }
                else
                {
                    throw new DynoException("Item '" + valueArray[i] + "' not found. (" + type of (TEnum) + ")",
                                            query.ToString());
                }
            }
        
            // Wrap field == value with parentheses ()
            query.Insert(0, "(");
            query.Insert(query.Length, ")");
        
            return query.ToString();
        }
        

        最终结果会是这样的。

        service.Mail.Where("(ProjectId == 5) AND (RegionId == 6 OR RegionId == 7) AND (Status == 5) and (Priority == 5)")
        

        在我的项目中,我将值存储在一个 XML 文件中,然后将它们输入到上述 LINQ 查询中。如果一个字段为空,它将被忽略。它还支持使用 | 符号的多个值,例如EU|US 将转换为 (Region == 5 OR Region == 6)

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2011-03-08
          • 2012-07-09
          • 1970-01-01
          • 1970-01-01
          • 2015-07-19
          • 1970-01-01
          相关资源
          最近更新 更多