LINQ查询中的函数:无法翻译

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

Function in LINQ Query: could not be translated

问题

我有一个处理人员(学生、员工等)的数据库结构,他们可以被分配到不同的机构,以便在不同的日期工作。现在有一个查询,可以获取所有员工的信息,包括单个人员当前是否“活跃”(意味着他们在特定日期是机构的一部分,没有休产假)。由于这个“isActive”事情相当复杂,我不仅需要在这个查询中使用它,还需要在其他地方使用,所以我创建了这个方法:

public static Func<StaffInstitute, bool> StaffIsActive(DateTime referenceDate)
{
    return pe => (pe.EntryDate == null || pe.EntryDate <= referenceDate.Date) &&
        (pe.ExitDate == null || pe.ExitDate >= referenceDate.Date) &&
        (
            !pe.Staff.MaternityLeave ||
            (
                pe.Staff.MaternityLeave &&
                pe.Staff.MaternityLeaveFrom.HasValue &&
                pe.Staff.MaternityLeaveUntil.HasValue &&
                (pe.Staff.MaternityLeaveFrom.Value.Date > referenceDate.Date || pe.Staff.MaternityLeaveUntil.Value.Date < referenceDate.Date)
            ) ||
            (
                pe.Staff.MaternityLeave &&
                (pe.Staff.MaternityLeaveFrom.HasValue || pe.Staff.MaternityLeaveUntil.HasValue) &&
                (!pe.Staff.MaternityLeaveFrom.HasValue || pe.Staff.MaternityLeaveFrom.Value.Date >= referenceDate.Date) &&
                (!pe.Staff.MaternityLeaveUntil.HasValue || pe.Staff.MaternityLeaveUntil.Value.Date < referenceDate.Date)
            )
        );
}

我的想法是轻松地重用它,以便将来在任何查询中都能找到IsActive值,而无需复制这17行。所以我获取所有员工列表的查询看起来像这样:

Func<StaffInstitute, bool> staffIsActiveFunction = StaffIsActive(referenceDate);
IQueryable<StaffListDTO> staff =
    from staffInstitute
    in _context.StaffInstitute.Include(s => s.Staff)
    select new StaffListDTO()
    {
        StaffId = staffInstitute.StaffId,
        Name = staffInstitute.Staff.Name,
        IsActive = staffIsActiveFunction.Invoke(staffInstitute),
    };

这实际上运行得很好,这让我很惊喜。由于这个列表需要分页,因为它可能会变得相当长,所以最后会添加以下查询:

var res = staff.Skip(0).Take(30).ToList();

不幸的是,员工列表中有一个IsActive值的过滤器。有时,我只想看到当前活跃的员工,所以我在分页之前添加了这个:

if (istAktiv.HasValue)
{
    staff = staff.Where(p => p.IsActive);
}

这是它停止工作并出现以下错误的地方:

无法翻译 LINQ 表达式 '...'. 请以可以转换的形式重写查询,或通过插入对 'AsEnumerable'、'AsAsyncEnumerable'、'ToList' 或 'ToListAsync' 的调用来明确地切换到客户端评估

我已经束手无策了。此时我不能使用 .ToList,因为这将从数据库中获取所有条目,而不仅仅是30条。我该如何做才能让它起作用呢?如果我直接将 StaffIsActive 的内容写入我的查询中,它突然就起作用了,我只是不明白为什么!为什么会这样呢?

有没有办法让这个工作?

英文:

I have a Database Structure to handle people (students, staff,...) that can be assigned to different institutes for different dates. Now there is a query to get all staff with the info if single people are currently "active" or not (meaning they are part of the institute on a specific day and are not away on maternityleave). Because this "isActive" thing is rather complicated and I will need it not only for this query but in other places as well, I created this method:

public static Func&lt;StaffInstitute, bool&gt; StaffIsActive(DateTime referenceDate)
{
	return pe =&gt; (pe.EntryDate == null || pe.EntryDate &lt;= referenceDate.Date) &amp;&amp;
		(pe.ExitDate == null || pe.ExitDate &gt;= referenceDate.Date) &amp;&amp;
		(
			!pe.Staff.MaternityLeave ||
			(
				pe.Staff.MaternityLeave &amp;&amp;
				pe.Staff.MaternityLeaveFrom.HasValue &amp;&amp;
				pe.Staff.MaternityLeaveUntil.HasValue &amp;&amp;
				(pe.Staff.MaternityLeaveFrom.Value.Date &gt; referenceDate.Date || pe.Staff.MaternityLeaveUntil.Value.Date &lt; referenceDate.Date)
			) ||
			(
				pe.Staff.MaternityLeave &amp;&amp;
				(pe.Staff.MaternityLeaveFrom.HasValue || pe.Staff.MaternityLeaveUntil.HasValue) &amp;&amp;
				(!pe.Staff.MaternityLeaveFrom.HasValue || pe.Staff.MaternityLeaveFrom.Value.Date &gt;= referenceDate.Date) &amp;&amp;
				(!pe.Staff.MaternityLeaveUntil.HasValue || pe.Staff.MaternityLeaveUntil.Value.Date &lt; referenceDate.Date)
			)
		);
}		

The idea was to easily reuse this so that in any query in the future I will be able to find the IsActive value without having to copy these 17 lines. So my query to get a list of all the staff looks like this:

Func&lt;StaffInstitute, bool&gt; staffIsActiveFunction = StaffIsActive(referenceDate);
IQueryable&lt;StaffListDTO&gt; staff =
	from staffInstitute
	in _context.StaffInstitute.Include(s =&gt; s.Staff)
	select new StaffListDTO()
	{
		StaffId = staffInstitute.StaffId,
		Name = staffInstitute.Staff.Name,
		IsActive = staffIsActiveFunction.Invoke(staffInstitute),
	};

This actually works fine, which was a pleasant surprise. This list has a pagination, since it get get quite long, so at the end this will be added to the query:

var res = staff.Skip(0).Take(30).ToList();

Unfortunately, the list of staff has a filter for the IsActive value. On occasion, I want to see only employees that are currently active, so I added this before the pagination:

if (istAktiv.HasValue)
{
	staff = staff.Where(p =&gt; p.IsActive);
}	

And this is where it stops working and I get the following error:

The LINQ expression &#39;...&#39; 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;

And I am at my wits end. I cannot do .ToList at this point, because then the query will fetch ALL the entries from the database instead of only 30. What can I do to make this work? If I write the contents of StaffIsActive directly into my query, it suddenly works and I just don't understand. WHY?!

Any ideas on how to make this work?

答案1

得分: 2

我对你的EF版本不太确定,但根据我最近在EF6中的经验,即使是 staffIsActiveFunction.Invoke(staffInstitute) 也不应该起作用。我猜他们在更新版本中修复了这个问题?

不管怎样,在EF6中,我使用 Expression&lt;Func&lt;T, bool&gt;&gt; 来处理这种情况。

        public static readonly Expression&lt;Func&lt;TEntity, bool&gt;&gt; IsSomethingExpression =
            e =&gt; !e.DeleteFlag &amp;&amp; ... ; // 布尔条件

然后,我可以在不同的地方应用相同的条件,就像这样:

            IQueryable&lt;TEntity&gt; basicQuery = DbContext.TEntity
                .Where(IsSomethingExpression);

IQueryable&lt;T&gt; 有一个 .Where(Expression&lt;Func&lt;T, bool&gt;&gt;) 重载。

所以我猜你可以:

  1. 重构 StaffIsActive 以返回 Expression&lt;Func&lt;StaffInstitute, bool&gt;&gt;
  2. 用它来处理 IsActive.Where()

当我们编写LINQ时,EF需要将语句转换为SQL查询。如果语句过于复杂或者EF不支持,它会抛出异常。因此,在调用EF查询中的自定义函数时,应始终小心。解决这个问题的一般方法是使用上面提到的 Expression

英文:

I am not sure about your EF version, but from my recent experience in EF6, even that staffIsActiveFunction.Invoke(staffInstitute) shouldn't work. I guess they fixed that in the newer version?

Regardless, in EF6 I use Expression&lt;Func&lt;T, bool&gt;&gt; to handle situations like that.

        public static readonly Expression&lt;Func&lt;TEntity, bool&gt;&gt; IsSomethingExpression =
            e =&gt; !e.DeleteFlag &amp;&amp; ... ; // boolean conditions

Then, I can apply the same conditions in different places like this:

            IQueryable&lt;TEntity&gt; basicQuery = DbContext.TEntity
                .Where(IsSomethingExpression);

IQueryable&lt;T&gt; has a .Where(Expression&lt;Func&lt;T, bool&gt;&gt;) overload.

So I guess you can:

  1. Refactor StaffIsActive to return Expression&lt;Func&lt;StaffInstitute, bool&gt;&gt;.
  2. Use it for both IsActive and .Where().

When we write LINQ, EF needs to translates the statements into SQL query. If the statement is too complicated or simply not supported by EF, it will throw. Therefore, one should always be careful when calling custom functions in EF queries. One general solution to that is to use Expressions like mentioned above.

答案2

得分: 1

您的查询需要进行表达式扩展。可以通过不同的库来实现,几乎可以在这里找到完整的列表。我选择了LINQKit:

使用ExpandableAttribute定义存根函数:

[Expandable(nameof(StaffIsActiveImpl))]
public static bool StaffIsActive(StaffInstitute pe, DateTime referenceDate)
{
    throw new NotImplementedException();
} 

定义返回表达式的私有实现。结果表达式的参数应该模仿原始函数:

private static Expression<Func<StaffInstitute, DateTime, bool>> StaffIsActiveImpl()
{
    return (pe, referenceDate) => (pe.EntryDate == null || pe.EntryDate <= referenceDate.Date) &&
        (pe.ExitDate == null || pe.ExitDate >= referenceDate.Date) &&
        (
            !pe.Staff.MaternityLeave ||
            (
                pe.Staff.MaternityLeave &&
                pe.Staff.MaternityLeaveFrom.HasValue &&
                pe.Staff.MaternityLeaveUntil.HasValue &&
                (pe.Staff.MaternityLeaveFrom.Value.Date > referenceDate.Date || pe.Staff.MaternityLeaveUntil.Value.Date < referenceDate.Date)
            ) ||
            (
                pe.Staff.MaternityLeave &&
                (pe.Staff.MaternityLeaveFrom.HasValue || pe.Staff.MaternityLeaveUntil.HasValue) &&
                (!pe.Staff.MaternityLeaveFrom.HasValue || pe.Staff.MaternityLeaveFrom.Value.Date >= referenceDate.Date) &&
                (!pe.Staff.MaternityLeaveUntil.HasValue || pe.Staff.MaternityLeaveUntil.Value.Date < referenceDate.Date)
            )
        );
}

如果您使用EF Core,在构建DbContextOptions期间激活LINQKit:

builder
    .UseSqlServer(connectionString)
    .WithExpressionExpanding(); // 启用LINQKit扩展

对于EF6,请使用AsExpandable()

IQueryable<StaffListDTO> staff =
    from staffInstitute in _context.StaffInstitute.AsExpandable() // 对于EF Core不需要
    select new StaffListDTO()
    {
        StaffId = staffInstitute.StaffId,
        Name = staffInstitute.Staff.Name,
        IsActive = StaffIsActive(staffInstitute, referenceDate),
    };
英文:

Your query needs expression expanding. It can be done by different libraries, almost full list you can find here. I choose LINQKit:

Define stub function with ExpandableAttribute:

[Expandable(nameof(StaffIsActiveImpl))]
public static bool StaffIsActive(StaffInstitute pe, DateTime referenceDate)
{
    throw new NotImplementedException();
} 

Define private realisation which returns Expression. Result Expression's parameters should mimic orginal function:

private static Expression&lt;Func&lt;StaffInstitute, DateTime, bool&gt;&gt; StaffIsActiveImpl()
{
    return (pe, referenceDate) =&gt; (pe.EntryDate == null || pe.EntryDate &lt;= referenceDate.Date) &amp;&amp;
        (pe.ExitDate == null || pe.ExitDate &gt;= referenceDate.Date) &amp;&amp;
        (
            !pe.Staff.MaternityLeave ||
            (
                pe.Staff.MaternityLeave &amp;&amp;
                pe.Staff.MaternityLeaveFrom.HasValue &amp;&amp;
                pe.Staff.MaternityLeaveUntil.HasValue &amp;&amp;
                (pe.Staff.MaternityLeaveFrom.Value.Date &gt; referenceDate.Date || pe.Staff.MaternityLeaveUntil.Value.Date &lt; referenceDate.Date)
            ) ||
            (
                pe.Staff.MaternityLeave &amp;&amp;
                (pe.Staff.MaternityLeaveFrom.HasValue || pe.Staff.MaternityLeaveUntil.HasValue) &amp;&amp;
                (!pe.Staff.MaternityLeaveFrom.HasValue || pe.Staff.MaternityLeaveFrom.Value.Date &gt;= referenceDate.Date) &amp;&amp;
                (!pe.Staff.MaternityLeaveUntil.HasValue || pe.Staff.MaternityLeaveUntil.Value.Date &lt; referenceDate.Date)
            )
        );
}

If you are using EF Core, activate LINQKit during DbContextOptions building:

builder
    .UseSqlServer(connectionString)
    .WithExpressionExpanding(); // enabling LINQKit extension

For EF6 use AsExpandable()

IQueryable&lt;StaffListDTO&gt; staff =
    from staffInstitute in _context.StaffInstitute.AsExpandable() // not needed for EF Core
    select new StaffListDTO()
    {
        StaffId = staffInstitute.StaffId,
        Name = staffInstitute.Staff.Name,
        IsActive = StaffIsActive(staffInstitute, referenceDate),
    };

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

发表评论

匿名网友

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

确定