找到与在目标的字典中存储的属性匹配的 SQL 元素,使用 Entity Framework Core。

huangapple go评论73阅读模式
英文:

Find SQL element that matches properties stored in a dictionary of a target using Entity Framework Core

问题

使用`.Net 6`和`EF Core 7`,

我有一个`Candidates`的`DbSet`。每个候选人都有一个`Id`和一个存储`LanguageId`和`LanguageLevel`的`Language`对象列表。
要搜索一个`Candidate`,我有一个存储`LanguageIds`作为键和`LanguageLevels`作为值的字典的`TargetProfile`。

候选人需要匹配语言和级别要求。

我尝试了类似以下的方式,检查是否在删除所有匹配后仍然存在任何所需的语言
```cs
var matches = from candidate in DbContext.Candidates
where
!targetProfile.Languages.Keys.Except(candidate.Languages
   .Where(l => targetProfile.Languages.Keys.Contains(l.LanguageId) &&
    targetProfile.Languages[l.LanguageId] <= l.Level).Select(l => l.LanguageId))
   .Any()
select candidate;

但当我尝试通过ToList()执行时,我收到了一个实体框架错误,说我的查询太复杂。

'LINQ 表达式无法被转译。要么以可转译的形式重写查询,要么通过插入对 'AsEnumerable'、'AsAsyncEnumerable'、'ToList' 或 'ToListAsync' 的调用来明确切换到客户端评估。有关更多信息,请参见 https://go.microsoft.com/fwlink/?linkid=2101038。'

然后我尝试使用LINQ,但我还没有找到一种动态访问字典的方法。

是否有一种好的方式可以像这样实现?

var result = DbContext.Candidates.Where(
   candidate => {
   foreach (language in candidate.Languages) => {
   targetProfile[language.LanguageId] == language.LanguageLevel;
   }}).ToList();

我知道如何在普通的C#中做到这一点,但不知道如何在EF Core查询中实现。


<details>
<summary>英文:</summary>

Using `.Net 6` and `EF Core 7`, 

I have a DbSet of `Candidates`. Each candidate has an `Id` and a list of `Language` objects that stores the `LanguageId` and the `LanguageLevel`.
To search for a `Candidate`, I got a `TargetProfile` with a dictionary, storing `LanguageIds` as key and `LanguageLevels` as values.

~~~cs
public class CandidateEntity {
   public List&lt;LanguageEntity&gt; Languages;
}

public class LanguageEntity {
  public int LanguageId;
  public int LanguageLevel;
}

public class TargetProfileDto {
  public Dictionary&lt;int,int&gt; Languages;
}
~~~

The candidates need to match both the language and level requirements.

I tried something like this where I checked, if any required language would remain after I remove all that match
~~~cs
var matches = from candidate in DbContext.Candidates
where
!targetProfile.Languages.Keys.Except(candidate.Languages
   .Where(l =&gt; targetProfile.Languages.Keys.Contains(l.LanguageId) &amp;&amp;
    targetProfile.Languages[l.LanguageId] &lt;= l.Level).Select(l =&gt; l.LanguageId))
   .Any()
select candidate;
~~~
but when I try to execute via ToList(), I get an entity framework error, that my query is too complex.

&gt; &#39;The LINQ expression could not be translated. Either rewrite the query in a form that can be translated, or switch to client evaluation explicitly by inserting a call to &#39;AsEnumerable&#39;, &#39;AsAsyncEnumerable&#39;, &#39;ToList&#39;, or &#39;ToListAsync&#39;. See https://go.microsoft.com/fwlink/?linkid=2101038 for more information.&#39;


I then tried using `LINQ`, but I have not found a way to access the dictionary dynamically.

Is there a good way to have something like this?
~~~cs
var result = DbContext.candidates.Where(
   candidate =&gt; {
   foreach(language in candidate.Language) =&gt; {
   targetProfile[language.LanguageId) == language.LanguageLevel;
   }}).ToList();
~~~
I know how to do this in plain C#, but not as a EF Core query.

</details>


# 答案1
**得分**: 1

以下是您要求的中文翻译部分:

这种查询需要动态表达式构建。思路是构建类似于这样的表达式:
```cs
var query = DbContext.Candidates
    .Where(c => c.Languages.Count(l => l.LanguageId == 1 && l.LanguageLevel == 2 || l.LanguageId == 2 && l.LanguageLevel == 3 || ...) 
       == targetProfile.Languages.Count);

对于这种方法的一个要求是:匹配的项应该是一组唯一的项,这由Dictionary容器来保证。

使用方法很简单:

var query = DbContext.Candidates
    .MatchByCriteria(targetProfile.Languages, c => c.Languages, (l, t) => l.LanguageId == t.LanguageId && l.LanguageLevel == t.LanguageLevel)

以及实现:

public static class MatchExtensions
{
    public static IQueryable<TEntity> MatchByCriteria<TEntity, TProp, TItem>(
        this IQueryable<TEntity> query, 
        IEnumerable<TItem> items, 
        Expression<Func<TEntity, IEnumerable<TProp>>> prop, 
        Expression<Func<TProp, TItem, bool>> filterPattern)
    {
        var itemsList = items.ToList();
        var matchPredicate = GetItemsPredicate(itemsList, filterPattern);

        var propBody = prop.Body;

        // e.SomeCollection.Count(matchPredicate)
        var countExpr = Expression.Call(typeof(Enumerable), nameof(Enumerable.Count), new[] { typeof(TProp) },
            propBody, matchPredicate);

        // e.SomeCollection.Count(matchPredicate) == items.Count
        var equalsExpr = Expression.Equal(countExpr, Expression.Constant(itemsList.Count));

        // e => e.SomeCollection.Count(matchPredicate) == items.Count
        var matchLambda = Expression.Lambda<Func<TEntity, bool>>(equalsExpr, prop.Parameters[0]);

        // query.Where(e => e.SomeCollection.Count(matchPredicate) == items.Count)
        return query.Where(matchLambda);
    }


    public static Expression<Func<TEntity, bool>> GetItemsPredicate<TEntity, TItem>(
        IEnumerable<TItem> items, 
        Expression<Func<TEntity, TItem, bool>> filterPattern, bool isOr = true, bool emptyValue = false)
    {
        Expression? predicate = null;
        foreach (var item in items)
        {
            var itemExpr = Expression.Constant(item);
            var itemCondition = ReplacingExpressionVisitor.Replace(filterPattern.Parameters[1], itemExpr, filterPattern.Body);
            if (predicate == null)
                predicate = itemCondition;
            else
            {
                predicate = Expression.MakeBinary(isOr ? ExpressionType.OrElse : ExpressionType.AndAlso, predicate,
                    itemCondition);
            }
        }

        predicate ??= Expression.Constant(emptyValue);
        var filterLambda = Expression.Lambda<Func<TEntity, bool>>(predicate, filterPattern.Parameters[0]);
        return filterLambda;
    }
}

请注意,这只是代码的翻译部分,不包括问题的回答。如果您需要进一步的帮助,请随时提问。

英文:

Such query needs dynamic expression building. Idea is to build expression like this:

var query = DbContext.Candidates
    .Where(c =&gt; c.Languages.Count(l =&gt; l.LanguageId == 1 &amp;&amp; l.LanguageLevel == 2 || l.LanguageId == 2 &amp;&amp; l.LanguageLevel == 3 || ...) 
       == targetProfile.Languages.Count);

One requirement for such approcach: match items should be list of unique items, which is guaranteed by Dictionary container.

Usage is simple:

var query = DbContext.Candidates
    .MatchByCriteria(targetProfile.Languages, c =&gt; c.Languages, (l, t) =&gt; l.LanguageId == t.LanguageId &amp;&amp; l.LanguageLevel == t.LanguageLevel)

And realuisation:

public static class MatchExtensions
{
    public static IQueryable&lt;TEntity&gt; MatchByCriteria&lt;TEntity, TProp, TItem&gt;(
        this IQueryable&lt;TEntity&gt; query, 
        IEnumerable&lt;TItem&gt; items, 
        Expression&lt;Func&lt;TEntity, IEnumerable&lt;TProp&gt;&gt;&gt; prop, 
        Expression&lt;Func&lt;TProp, TItem, bool&gt;&gt; filterPattern)
    {
        var itemsList = items.ToList();
        var matchPredicate = GetItemsPredicate(itemsList, filterPattern);

        var propBody = prop.Body;

        // e.SomeCollection.Count(matchPredicate)
        var countExpr = Expression.Call(typeof(Enumerable), nameof(Enumerable.Count), new[] { typeof(TProp) },
            propBody, matchPredicate);

        // e.SomeCollection.Count(matchPredicate) == items.Count
        var equalsExpr = Expression.Equal(countExpr, Expression.Constant(itemsList.Count));

        // e =&gt; e.SomeCollection.Count(matchPredicate) == items.Count
        var matchLambda = Expression.Lambda&lt;Func&lt;TEntity, bool&gt;&gt;(equalsExpr, prop.Parameters[0]);

        // query.Where(e =&gt; e.SomeCollection.Count(matchPredicate) == items.Count)
        return query.Where(matchLambda);
    }


    public static Expression&lt;Func&lt;TEntity, bool&gt;&gt; GetItemsPredicate&lt;TEntity, TItem&gt;(
        IEnumerable&lt;TItem&gt; items, 
        Expression&lt;Func&lt;TEntity, TItem, bool&gt;&gt; filterPattern, bool isOr = true, bool emptyValue = false)
    {
        Expression? predicate = null;
        foreach (var item in items)
        {
            var itemExpr = Expression.Constant(item);
            var itemCondition = ReplacingExpressionVisitor.Replace(filterPattern.Parameters[1], itemExpr, filterPattern.Body);
            if (predicate == null)
                predicate = itemCondition;
            else
            {
                predicate = Expression.MakeBinary(isOr ? ExpressionType.OrElse : ExpressionType.AndAlso, predicate,
                    itemCondition);
            }
        }

        predicate ??= Expression.Constant(emptyValue);
        var filterLambda = Expression.Lambda&lt;Func&lt;TEntity, bool&gt;&gt;(predicate, filterPattern.Parameters[0]);
        return filterLambda;
    }
}

huangapple
  • 本文由 发表于 2023年6月19日 16:23:47
  • 转载请务必保留本文链接:https://go.coder-hub.com/76504874.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定