【问题标题】:LINQ - select these if they exist, otherwise fall back to theseLINQ - 如果存在则选择这些,否则回退到这些
【发布时间】:2011-08-16 02:52:12
【问题描述】:

考虑本地化文本,存储在此表中:

表格文本

  • 文本标识
  • 语言
  • 价值

现在我想为 TextId 1 选择一个文本。如果在“丹麦语”中没有此 TextId 的文本,我想回退到“英语”。

我可以这样做:

var texts = MyDb.Texts.Where(x=>x.TextId == 1 & x.Language == "Danish");

if (!texts.Any()){
    texts = MyDb.Texts.Where(x=>x.TextId == 1 & x.Language == "English");
}

...但是我必须重复 Where 子句的其余部分,这意味着我在重复自己(在这个例子中并没有那么糟糕,但可能还有更多子句)。

有没有更简单的方法来做到这一点?

【问题讨论】:

  • 它不漂亮,但您可以创建一个函数来决定是否必须按 x.Language 对结果进行升序或降序排序,因此当您执行 FirstOrDefault() 时,如果存在所需的语言,您将获得所需的语言或者是英文(查询将是 Where(x.Language == "Danish" || x.Language == "English"))。
  • 订购是个坏主意 - 您依赖的巧合是丹麦语按字母顺序排在英语之前...如果您想提供丹麦语、英语和法语怎么办!
  • 这就是为什么我说“一个决定是否必须对结果进行升序或降序排序的函数”。您实际上可以使用 str.CompareTo

标签: c# linq


【解决方案1】:

一种解决方案是检索所需 id 的所有文本,然后加入语言偏好映射:

var languages = new[]
                    {
                        new {Language = "Danish", Priority = 1},
                        new {Language = "English", Priority = 2}
                    };
var id = 1;
var text = (from t in db.Texts.Where(t => t.TextId == id).AsEnumerable()
            join l in languages on t.Language equals l.Language
            orderby l.Priority
            select t).FirstOrDefault();

如果您只有两种语言,那么这可以更简单地完成并避免返回任何不必要的行:

var id = 1;
var text = (from t in db.Texts
            let priority = t.Language == "Danish" ? 1 : 2
            where t.TextId == id
            orderby priority
            select t).FirstOrDefault();

如果您想支持动态数量的语言,您可以动态构建优先级表达式(使用 System.Linq.Expressions)。这将导致一个数据库调用,该调用将只返回您想要的一条记录:

var id = 1;
var text = db.Texts.Where(t => t.TextId == id).OrderBy(CreatePriorityExpression()).FirstOrDefault();

private static Expression<Func<Text, int>> CreatePriorityExpression()
{
    var languages = new[]
                        {
                            new {Language = "Danish", Priority = 1},
                            new {Language = "English", Priority = 2}
                        };

    // Creates an expression of nested if-else statements & translates to a SQL CASE 
    var param = Expression.Parameter(typeof(Text));
    var lang = Expression.PropertyOrField(param, "Language");
    Expression ex = Expression.Constant(languages.Last().Priority);
    foreach (var l in languages.Reverse().Skip(1))
        ex = Expression.Condition(Expression.Equal(lang, Expression.Constant(l.Language)), Expression.Constant(l.Priority), ex);
    return Expression.Lambda<Func<Text, int>>(ex, param);
}

【讨论】:

  • +1 - 多个后备的不错的解决方案; -1 - 在添加越来越多的 where 子句时,对降低复杂性的原始问题没有帮助。
  • @RichardW1001 - 我的新方法通过为任意数量的语言动态生成嵌套条件来优雅地处理多种语言。
【解决方案2】:

我建议使用过滤器模式,您可以在其中针对 IEnumerable 编写扩展方法。这样一来,您的大部分逻辑就被封装到您可以反复使用的方法中:

public static class TextExtensions
    {
        [System.Runtime.CompilerServices.Extension]
        public static IEnumerable<Text> ByTextId(this IEnumerable<Text> qry, int textId)
        {
            return qry.Where(t => t.TextId == textId);
        }

        [System.Runtime.CompilerServices.Extension]            
        public static IEnumerable<Text> ByLanguage(this IEnumerable<Text> qry, string language)
        {
            return qry.Where(t => t.Language == language);
        }
    }

然后你的代码变成:

var texts = MyDB.Texts.ByTextId(1).ByLanguage("Danish");

然后重复就不再是问题了。我还建议让自己成为一个静态类来保存各种语言值以避免硬编码:

public static class LanguageValues
{
    public static string English
    {
        get
        {
            return "English";
        }
    }

    public static string Danish
    {
        get
        {
            return "Danish";
        }
    }
}

然后你的代码变成:

var texts = MyDB.Texts.ByTextId(1).ByLanguage(LanguageValues.Danish);

您可以将其与 DefaultIfEmpty 方法结合使用,该方法为您提供:

var texts = MyDB.Texts.DefaultIfEmpty(MyDB.Texts.ByTextId(1).ByLanguage(LanguageValues.English).Single()).ByTextId(1).ByLanguage(LanguageValues.Danish);

然后将其放入一个单独的扩展方法中以供重复使用:

[System.Runtime.CompilerServices.Extension]
public static IEnumerable<Text> GetValueOrDefault(this IEnumerable<Text> qry, int textId, string language)
{
    return qry.DefaultIfEmpty(qry.ByTextId(textId).ByLanguage(LanguageValues.English).Single()).ByTextId(textId).ByLanguage(language);
}

您现在可以简单地调用:

var text = MyDB.Texts.GetValueOrDefault(1, LanguageValues.Danish);

请注意,这可以用作任何查询的最后一步,所以这样的事情也可以工作:

var text = MyDB.Texts.Where(<some funky clause>).Where(<some other funky clause>).GetValueOrDefault(1,LanguageValues.Danish);

正如人们所指出的那样,如果您使用多种备份语言,则有比在原始问题中一次检查一种语言更好的方法,但是这种过滤器模式可以很好地工作,只需定义正确的过滤您的用例和策略。

【讨论】:

  • 这种方法的潜在问题是大量往返数据库以检索单个记录。想象一下,如果您有 100 种语言,并且必须为每种语言进行数据库查询以找到第一个可用的后备。
  • 这不会进行大量的往返,丹麦语最多2-1,英语1,只有在丹麦语没有结果的情况下。如果你想返回多种语言的结果,你可以用不同的方式构造 where 子句,但这不是这个问题要问的。这个问题只提到了一个后备。对我来说,问题在于避免重复和代码清晰。
  • 我喜欢这个(也赞成)。我相信 C# 会自己发出 [System.Runtime.CompilerServices.Extension] 。干杯
  • 啊——它会的——从VB翻译过来的!
【解决方案3】:

就像我在上面的评论中所说的那样,它并不完全漂亮,但它应该可以工作:)

public string GetText(string lang, string fallback)
{
    // lang precedes fallback
    if (lang.CompareTo(fallback) < 1)
    {
        return MyDb.Texts.Where(x => x.Language == lang || x.Language == fallback).OrderBy(x => x.Language).FirstOrDefault();
    }
    else
    {
        return MyDb.Texts.Where(x => x.Language == lang || x.Language == fallback).OrderByDescending(x => x.Language).FirstOrDefault();
    }
}

【讨论】:

    【解决方案4】:

    如果您主要关心的是避免重复您的 where 子句,那么最简单的方法是将您的 where 子句拆分,如下所示:

    var alltexts = MyDb.Texts.Where(x => x.TextId == 1);
    var text = alltexts.Any(x => x.Language == "Danish")
             ? alltexts.Where(x => x.Language == "Danish")
             : alltexts.Where(x => x.Language == "English")
    

    如果你能保证你最多只有一个匹配的条目(对于本地化表来说,这可能是真的)你可以进一步简化它(并将整个东西包装在一个函数中以便于重用):

    public Text GetLocalizedText(Func<Text, bool> predicate, string language )
    {
        var temp = MyDb.Texts.Where(predicate);
        return temp.SingleOrDefault(x => x.Language == language)
            ?? temp.Single(x => x.Language == "English");
    }
    
    var caption = GetLocalizedText(x => x.TextId == 1, "Danish")
    

    【讨论】:

    • 不认为 ?运算符将在这里完成工作,您将始终从查询中返回一个对象,它永远不会为空,但它并不总是有结果 - 因此 if(! texts.Any()) 检查是否存在是任何结果。
    • 为什么要限制第二种方法?只需使用 First 而不是 Single(和 FirstOrDefault 而不是 SingleOrDefault)。
    猜你喜欢
    • 2018-01-11
    • 2020-01-01
    • 1970-01-01
    • 2013-06-01
    • 1970-01-01
    • 2018-07-28
    • 1970-01-01
    • 2014-11-24
    • 2012-07-03
    相关资源
    最近更新 更多