Linq 匹配可变数量的 AND 条件

Linq matching variable number of AND conditions

提问人:mrAtari 提问时间:12/13/2022 最后编辑:mrAtari 更新时间:12/29/2022 访问量:74

问:

我有一个数据库,其中包含一组规则,应查询这些规则以查找匹配的规则。

简化表包含 5 列,其中包含 4 列和模式字符串(我们称它们为 And1..And4) 和一个用于操作 (ToDo):

A  B  C  D  Todo1
A  -  -  -  Todo2
A  C  -  -  Todo3
C  B  -  -  Todo4

现在我有一个最多有 4 个条件的输入模式,这些条件应该是 AND 条件。

预期结果应为 s.th。喜欢:

(A) -> Todo2
(A,C) -> Todo3, Todo2    // Both rules match the input
(A,B,C) -> Todo3, Todo2
(B,C) -> Todo4 //order of the input should be irrelevant    
(A,B,C,D) -> Todo1, Todo2 

2022年12月29日更新:

这里有一些示例代码,它说明了旧方法(第一个代码片段不完整且不起作用),这个示例代码确实如此:

public List<CombinationRule> GetCombinationRules(string firstElement, List<string> andConditions)
{
    using (var context = _dbContextProvider.GetDbContext())
    {
        var result = new List<CombinationRule>();
        var query = context.CombinationTable
            .Where(x => Match(firstElement, x.ColumnA))

        result.AddRange(SelectItems(query, new List<string> { andConditions[0] }));
        if (andConditions.Count > 1)
        {
            result.AddRange(SelectItems(query, new List<string> { andConditions[1] }));
            result.AddRange(SelectItems(query, new List<string> { andConditions[0], andConditions[1] }));
            result.AddRange(SelectItems(query, new List<string> { andConditions[1], andConditions[0] }));
        }
        if (andConditions.Count > 2)
        {
            result.AddRange(SelectItems(query, new List<string> { andConditions[2] }));
            result.AddRange(SelectItems(query, new List<string> { andConditions[0], andConditions[2] }));
            result.AddRange(SelectItems(query, new List<string> { andConditions[1], andConditions[2] }));
            result.AddRange(SelectItems(query, new List<string> { andConditions[2], andConditions[0] }));
            result.AddRange(SelectItems(query, new List<string> { andConditions[1], andConditions[0] }));
            result.AddRange(SelectItems(query, new List<string> { andConditions[0], andConditions[1], andConditions[2] }));
            result.AddRange(SelectItems(query, new List<string> { andConditions[0], andConditions[2], andConditions[1] }));
            result.AddRange(SelectItems(query, new List<string> { andConditions[1], andConditions[0], andConditions[2] }));
            result.AddRange(SelectItems(query, new List<string> { andConditions[1], andConditions[2], andConditions[0] }));
            result.AddRange(SelectItems(query, new List<string> { andConditions[2], andConditions[0], andConditions[1] }));
            result.AddRange(SelectItems(query, new List<string> { andConditions[2], andConditions[1], andConditions[0] }));
        }
        if (andConditions.Count > 3)
        {
            result.AddRange(SelectItems(query, new List<string> { andConditions[0], andConditions[1], andConditions[3] }));
            result.AddRange(SelectItems(query, new List<string> { andConditions[0], andConditions[3], andConditions[1] }));
            result.AddRange(SelectItems(query, new List<string> { andConditions[0], andConditions[2], andConditions[3] }));
            result.AddRange(SelectItems(query, new List<string> { andConditions[0], andConditions[3], andConditions[2] }));
            result.AddRange(SelectItems(query, new List<string> { andConditions[1], andConditions[0], andConditions[3] }));
            result.AddRange(SelectItems(query, new List<string> { andConditions[1], andConditions[3], andConditions[0] }));
            result.AddRange(SelectItems(query, new List<string> { andConditions[1], andConditions[2], andConditions[3] }));
            result.AddRange(SelectItems(query, new List<string> { andConditions[1], andConditions[3], andConditions[2] }));
            result.AddRange(SelectItems(query, new List<string> { andConditions[2], andConditions[0], andConditions[3] }));
            result.AddRange(SelectItems(query, new List<string> { andConditions[2], andConditions[3], andConditions[0] }));
            result.AddRange(SelectItems(query, new List<string> { andConditions[2], andConditions[1], andConditions[3] }));
            result.AddRange(SelectItems(query, new List<string> { andConditions[2], andConditions[3], andConditions[1] }));
            result.AddRange(SelectItems(query, new List<string> { andConditions[3], andConditions[0], andConditions[1] }));
            result.AddRange(SelectItems(query, new List<string> { andConditions[3], andConditions[1], andConditions[0] }));
            result.AddRange(SelectItems(query, new List<string> { andConditions[3], andConditions[0], andConditions[2] }));
            result.AddRange(SelectItems(query, new List<string> { andConditions[3], andConditions[2], andConditions[0] }));
            result.AddRange(SelectItems(query, new List<string> { andConditions[3], andConditions[1], andConditions[2] }));
            result.AddRange(SelectItems(query, new List<string> { andConditions[3], andConditions[2], andConditions[1] }));
        }

        return result.Distinct().ToList();
    }
}
                 

该方法使用 2 个帮助程序:MatchSelectItems。Match 是一种特殊的字符串匹配,基于特殊的业务逻辑规则。SelectItems 为 1、2 或 3 个条件选择规则。SelectItems 的实现方式如下:

private List<CombinationRule> SelectItems(IQueryable<CombinationTable> query,
                                                             List<string> andOrderNumbers)
    {
        switch(andOrderNumbers.Count)
        {
            case 1:
                query = query.Where(x => string.IsNullOrEmpty(x.And2OrderNumber) &&
                                     string.IsNullOrEmpty(x.And3OrderNumber))
                         .Where(x => Match(andOrderNumbers[0], x.And1OrderNumber));
                break;
            case 2:
                query = query.Where(x => string.IsNullOrEmpty(x.And3OrderNumber))
                                             .Where(x => Match(andOrderNumbers[0], x.And1OrderNumber) &&
                                                         Match(andOrderNumbers[1], x.And2OrderNumber));
                break;
            case 3:
                query = query.Where(x => Match(andOrderNumbers[0], x.And1OrderNumber) &&
                                       Match(andOrderNumbers[1], x.And2OrderNumber) &&
                                       Match(andOrderNumbers[2], x.And3OrderNumber));
                break;
            default:
                throw new NotImplementedException();
        }
        var result = query.Select(x => new CombinationRule(x.PrimaryOrderNumber,                                                    
                                                new List<string> { x.And1OrderNumber, x.And2OrderNumber, x.And3OrderNumber },
                                                x.ToDo))
                .Distinct()
                .ToList();

        return result;
    }

该方法依赖于手动输入 4 个条件元素的所有可能变体。它可以归结为构建所有可能的变化。我在 CodeProject 上找到了一篇很棒的文章,它解释了变体、排列和组合(Variations、Combinations、Permutations)

{A B C} 的变体选择 2,预期结果:

{A B}, {A C}, {B A}, {B C}, {C A}, {C B}

在进入问题所在之后,如何摆脱丑陋的代码这个问题就可以轻松回答了。通过方法创建变体,然后将它们扔到 SelectItems 方法中是一件容易的事情。

C# LINQ 匹配

评论

0赞 jdweng 12/13/2022
通常,您将每个条件设置为 2 的幂,例如 A = 1、B = 2、C = 3、D = 4。然后,您可以测试 Todo1 = 15(或 1 或 2、4 或 8)。
1赞 Metro Smurf 12/13/2022
实际问题是什么?如何设计数据库?如何查询数据库?如何将查询具体化到 CLR 对象?什么?
1赞 WhatsThePoint 12/13/2022
你看过 Dynamic LINQ 吗?
1赞 Mocas 12/13/2022
我认为你的问题还不够清楚。我没有得到你想要实现的目标
0赞 mrAtari 12/13/2022
@MetroSmurf:数据库已经设计好了。我认为这很好。我的问题是代码,看起来丑陋且效率低下。

答:

0赞 mrAtari 12/29/2022 #1

所以这里是函数,它可以构建选择 n 个元素的 k 个元素的 VARIATIONS:

static void GenerateVariationsNoRepetitions(int[] kArray, int[] nArray, int index, List<List<int>> result)
{
    if (index >= kArray.Length)
    {
        result.Add(kArray.ToList());
        return;
    }
    for (int i = index; i < nArray.Length; i++)
    {
        kArray[index] = nArray[i];
        Swap(ref nArray[i], ref nArray[index]);
        GenerateVariationsNoRepetitions(kArray, nArray, index + 1, result);
        Swap(ref nArray[i], ref nArray[index]);
    }
}
private static void Swap<T>(ref T v1, ref T v2)
{
    T old = v1;
    v1 = v2;
    v2 = old;
}

考虑到这一点,GetCombinationRules 方法可以重写得更小、更短:

public List<CombinationRule> GetCombinationRules(string firstElement, List<string> andConditions)
{
    using (var context = _dbContextProvider.GetDbContext())
    {
        var result = new List<CombinationRule>();
        var query = context.CombinationTable.AsNoTracking()
            .Where(x => Match(firstElement, x.ColumnA));

        for (int i=0; i < 3; i++)
        {
            int[] kArr = new int[i+1];
            int[] nArr = Enumerable.Range(0, andConditions.Count).ToArray();
            var combinations = new List<List<int>>();

            GenerateVariationsNoRepetitions(kArr, nArr, 0, combinations);
            foreach (var combi in combinations)
            {
                var list = new List<string>();
                foreach (int c in combi)
                {
                    list.Add(andConditions[c]);
                }
                result.AddRange(SelectItems(query, list));
            }
        }

        return result.Distinct().ToList();
    }
}

我敢肯定,这不是最短和最优雅的实现,但它具有良好的可读性并且易于扩展。

顺便说一句,即使在第一个实现中,我也进行了小的重新排列(包括对数据格式的轻微重新定义),以提高性能。您还记得,我需要 4 个元素中的变体,但实现包含一个主要元素和 3 个元素中的变体。这大大提高了性能,因为对主要元素(必须始终为类型 A)的第一个查询将初始结果从大约 10k 个元素减少到 10 个以下,并且还减少了其余变体的数量。这样,性能大致可以接受。