0

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<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)
            )
        );
}       

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<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),
    };

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 => p.IsActive);
}   

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

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 'AsEnumerable', 'AsAsyncEnumerable', 'ToList', or 'ToListAsync'

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?

Chi
  • 1,320
  • 1
  • 14
  • 48
  • Add ToList() : staff = staff.Where(p => p.IsActive).ToList(); – jdweng Jul 03 '23 at 08:35
  • 2
    Try converting `Func` into -> `Expression>` and then use it `IsActive = staffIsActiveFunction(staffInstitute)` – Eldar Jul 03 '23 at 08:40
  • I converted it to an expression but when I try to call IsActive = staffIsActiveFunction(staffInstitute) it doesn't work, tells me "method name expected – Chi Jul 03 '23 at 08:56
  • 1
    After you change your `StaffIsActive` method to return an `Expression`, then here's how you use it in your `Select` to fill the `IsActive` property: https://dotnetfiddle.net/8c9bNV Hope this example is clear. Note how the method is called on line 15. – Orion Jul 03 '23 at 09:08

2 Answers2

2

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<Func<T, bool>> to handle situations like that.

        public static readonly Expression<Func<TEntity, bool>> IsSomethingExpression =
            e => !e.DeleteFlag && ... ; // boolean conditions

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

            IQueryable<TEntity> basicQuery = DbContext.TEntity
                .Where(IsSomethingExpression);

IQueryable<T> has a .Where(Expression<Func<T, bool>>) overload.

So I guess you can:

  1. Refactor StaffIsActive to return Expression<Func<StaffInstitute, bool>>.
  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.

Xiang Wei Huang
  • 336
  • 1
  • 9
  • Thank you! I just dont get, how do I use that expression for getting IsActive? – Chi Jul 03 '23 at 08:52
  • 1
    Something like `IsSomethingExpression.Compile().Invoke(staffInstitute)`. `Compile()` makes the `Expression` into `Func` which is your original return type. But, notice that `Compile()` takes performance too, so you can keep the compiled `Func` as a static variable along with the `Expression`, if performance is critical. – Xiang Wei Huang Jul 03 '23 at 08:57
  • 1
    It will not work inside projection. – Svyatoslav Danyliv Jul 03 '23 at 09:03
1

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<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)
            )
        );
}

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

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

For EF6 use AsExpandable()

IQueryable<StaffListDTO> 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),
    };
Svyatoslav Danyliv
  • 21,911
  • 3
  • 16
  • 32