2

I added the function UserHasFilter function so I can filter and see if a user has a filter following the logic as you can see, but when I run it it gives the following error:

enter image description here

I dont know if I'm using the right method to filter or is there a better way ? Also I don't know how the error happens.

Here is my code:

 public async Task<IEnumerable<ConditionDataModel>> GetUserFilters(string pageName)
        {
            var user = await _configurationService.GetCurrentUser();
            if (user == null)
            {
                return null;
            }
            var conditions = _context.FilterUserGroups
                .Include(f => f.CompanyDataRight).ThenInclude(d => d.Page)
                .Include(f => f.FilterUsers).ThenInclude(d => d.User)
                .Include(f => f.FilterGroups).ThenInclude(d => d.Group).ThenInclude(g => g.UserGroups).ThenInclude(ug => ug.User)
                .Where(f => f.CompanyDataRight.Page.ClassName == pageName && UserHasFilter(user.Id, f))
                .Include(f => f.Conditions)
                .SelectMany(f => f.Conditions)
                .Distinct()
                .AsEnumerable();
            return conditions;
        }

        public virtual bool UserHasFilter(Guid userId, FilterUserGroupDataModel filterUserGroup)
        {
            if(filterUserGroup == null)
            {
                return false;
            }
            if (filterUserGroup.FilterUsers?.Any(u => u.User.Id == userId) == true)
            {
                return true;
            }

            return false;
        }

EDIT:

Thanks to @MindSwipe I did a change on the query:

var conditions = _context.FilterUserGroups
                .Include(f => f.CompanyDataRight).ThenInclude(d => d.Page)
                .Include(f => f.FilterUsers).ThenInclude(d => d.User)
                .Include(f => f.FilterGroups).ThenInclude(d => d.Group).ThenInclude(g => g.UserGroups).ThenInclude(ug => ug.User)
                .Where(f => f.CompanyDataRight.Page.ClassName == pageName
                    && (f.FilterUsers != null && f.FilterUsers.Any(u => u.User.Id == user.Id) // checks if a filter user contains the current user
                            || (f.FilterGroups != null && f.FilterGroups.Any(g => g.Group != null && g.Group.UserGroups.Any(ug => ug.UserId == user.Id))))) // checks if user group has the current user
                .Include(f => f.Conditions)
                .SelectMany(f => f.Conditions)
                .Distinct()
                .AsEnumerable();

Because I need the query to execute on the database, this query shouldn't take lot of a time (in memory).

Updates:

It's working for EF 5, they have added this particular query.

Hasagiii
  • 345
  • 1
  • 3
  • 21
  • 1
    Could you add the error details as text to the question please, instead of as a screenshot? – phuzi Aug 12 '21 at 10:03
  • 4
    because `UserHasFilter` is a C# function which can not be translated to SQL that's why you are getting this error. – Chetan Aug 12 '21 at 10:05
  • I couldn't add the text error it gives an error in stackoverflow as I'm putting a non-formatted code – Hasagiii Aug 12 '21 at 10:06
  • As the error states, `UserHasFilter` cannot be converted to SQL. Try moving the `Where` to after `.Include(f => f.Conditions)` failing that, you'll have to rewrite the logic of your query. – phuzi Aug 12 '21 at 10:07
  • @Chetan how can I add a function that does the same thing ? – Hasagiii Aug 12 '21 at 10:07
  • You are checking if `filterUserGroup.FilterUsers` has the user.id in it.. if `filterUserGroup.FilterUsers` is not very large you can create a list of `userid` from it and then do `.Where(f => f.CompanyDataRight.Page.ClassName == pageName && list.Contains(user.Id)` – Chetan Aug 12 '21 at 10:09
  • Error : `InvalidOperationException: The LINQ expression 'DbSet .LeftJoin( outer: DbSet, inner: f => EF.Property>(f, "CompanyDataRightId"), outerKeySelector: c => EF.Property>(c, "Id"), innerKeySelector: (o, i) => new TransparentIdentifier( Outer = o, Inner = i ))` – Hasagiii Aug 12 '21 at 10:09

2 Answers2

4

It is important to understand that the Entity Framework provider can only translate LINQ expression trees into SQL statements. When it comes to common IEnumerable function like IEnumerable<T>.Contains or CLR functions like String.ToUpper() the provider maps these common C# function calls to known SQL implementations.

This is why your standard custom functions cannot be translated to SQL, the provider simply does not have a corresponding known implementation. Think about it this way, if I use reflection to inspect a method, I only get back the prototype, so the name, inputs and return types, there is no way to access the internal workings of that method. It is also true that not all ALL CLR functions have been mapped, so you will see this same error when you try to use a CLR function that is not recognised by the provider.

Even though the error message suggests these options:

If this method can be mapped to your custom function, 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'

Many answers on SO just tell the OP to switch to client evaluation when in fact that is a hack and there are usually better solutions, especially for our custom functions that encapsulate common business expressions for re-use. We write these specifically because we expect them to be defined once and re-used, now lets look at how to write them correctly!

Client Evaluation of Filter is an Anti-Pattern!
Yes the exception message states client evaluation as a potential option, but don't be fooled. If you materialise the entire dataset into memory and then apply a filter, then you may have wasting network bandwidth, CPU ticks and execution time. If the results is a large set and the filter results in zero records then you have wasted a LOT of resources! In LINQ-to-SQL scenarios we really want to avoid client-side filtering at ALL costs!

Don't be lazy, do it properly! The irony of this is that if you constructed your LINQ expressions correctly you would not have likely thought about client evaluation in the first place which most likely to result in further degrading query performance and can easily spawn off Stack Overflow exceptions or violate other Memory Constraints.

Expression<Func<>>

You can define your custom C# methods specifically for LINQ by making them return a lambda expression in the form of expression tree, that is Expression<Func<>>.

You need Expression when the code needs to be analyzed, serialized, or optimized before it is run. Expression is for thinking about code, Func/Action is for running it.

That is precisely what we want to achieve here, we want to enable the LINQ provider to analyze the code to translate it into SQL!

public virtual Expression<Func<FilterUserGroupDataModel, bool>> UserHasFilter(Guid userId)
{
    return x => x == null ? false :
                x.FilterUsers == null ? false :
                x.FilterUsers.Any(u => u.User.Id == userId);
}

This should actually translate into a SQL CASE expression, what you may notice here is that the instance of the FilterUserGroupDataModel is never passed into this method at all! That is the point of this, to be executed in the server, so in SQL, we need to use SQL parameters and references to express our logic, we don't want the SQL engine to wait for each execution instance to call back to the client to resolve the state of FilterUserGroupDataModel and if it has any FilterUsers that match our current userId.

That is in essence what the error message is describing, you have told it to call back to the C# function in the middle of what it is trying to compile into SQL, more on this later...

The implementation of this is only slightly different, notice again that we are not passing through a reference to the current

public async Task<IEnumerable<ConditionDataModel>> GetUserFilters(string pageName)
{
    var user = await _configurationService.GetCurrentUser();
    if (user == null)
    {
        return null;
    }
    var conditions = _context.FilterUserGroups
        .Include(f => f.CompanyDataRight).ThenInclude(d => d.Page)
        .Include(f => f.FilterUsers).ThenInclude(d => d.User)
        .Include(f => f.FilterGroups).ThenInclude(d => d.Group).ThenInclude(g => g.UserGroups).ThenInclude(ug => ug.User)
        .Where(f => f.CompanyDataRight.Page.ClassName == pageName)
        .Where(UserHasFilter(user.Id)) // <-- this is the custom function call
        .Include(f => f.Conditions)
        .SelectMany(f => f.Conditions)
        .Distinct()
        .AsEnumerable();
    return conditions;
}

Quick Refactor Hack
Getting the method signature correct for these types of expressions is important and hard to get right the first couple of times, one hack is to write your predicate as a complete .Where() clause using Fluent notation. Then highlight the content inside the .Where(), right click and select the Quick Actions and Refactorings... context menu option, then Extract Method. This will create a new method with the correct prototype for a predicate at that point in the query.

Mapped User Defined Function

There is of course another way, and that is we can define a custom function in C# and map it to a SQL Function. Mapping means the implementation will not be interpreted at all, the Function in the database will represent the SQL implementation to use.

  • You can map to System Functions if you really need them, but we generally only do this for custom UDFs.

This technique is helpful for legacy applications or in large scale organisations where the DBAs really want to manage the related logic, applications using the technique would often include a lot of mapped Stored Procedures as well.

In a practical sense, this technique would only be used to implement existing CLR functions or complicated SQL logic.

Map CLR Method to a SQL Function using .NET EF Core walks through the mechanics, but basically we create a C# and an SQL definition of the function, then we can Map the function to the DbContext.

  • When the IQueryable LINQ-to-SQL provider translates the expression it will ignore the C# implementation altogether, but we still make a C# implementation in case the function is used by non LINQ-to-SQL contexts or if the query is evaluated on the client.

I will not post an implementation of this method here because the logic needs to lookup data in external tables. This is NOT appropriate to be implemented as a User Defined Function. UDFs should be self contained or static calculations based on the inputs and internal constants where possible and not affect or select from external resources, Views and Stored Procedures are better mechanisms to encapsulate that type of logic.

Chris Schaller
  • 13,704
  • 3
  • 43
  • 81
1

Not all C# functions, and especially no "custom" C# functions can be translated into SQL by the Entity Framework provider, and starting with EF Core 3.x Entity Framework will throw an exception when it tries to silently switch from server side evaluation to client side evaluation. To solve your problem there are 2 solutions.

  1. Switch to client side evaluation manually by calling AsEnumerable() earlier.
  2. Rewrite your LINQ query so that EF Core can translate it to SQL.

Here's how to do #2:

var conditions = _context.FilterUserGroups
    .Include(f => f.CompanyDataRight).ThenInclude(d => d.Page)
    .Include(f => f.FilterUsers).ThenInclude(d => d.User)
    .Include(f => f.FilterGroups).ThenInclude(d => d.Group).ThenInclude(g => g.UserGroups).ThenInclude(ug => ug.User)
    .Where(f => f.CompanyDataRight.Page.ClassName == pageName && (f.FilterUsers != null && f.FilterUsers.Any(u => u.User.Id == user.Id)))
    .Include(f => f.Conditions)
    .SelectMany(f => f.Conditions)
    .Distinct()
    .AsEnumerable();

This should work (I currently have no way of testing this). What I did was re-write your method call inline as a statement, EF Core should be able to translate this into SQL. If not (and you can't fix it yourself) there is always option #1: Switching to client side evaluation, this is how you would "optimally" do that:

var conditions = _context.FilterUserGroups
    .Include(f => f.CompanyDataRight).ThenInclude(d => d.Page)
    .Include(f => f.FilterUsers).ThenInclude(d => d.User)
    .Include(f => f.FilterGroups).ThenInclude(d => d.Group).ThenInclude(g => g.UserGroups).ThenInclude(ug => ug.User)
    .Include(f => f.Conditions)
    .SelectMany(f => f.Conditions)
    .Distinct()
    .AsEnumerable()
    .Where(f => f.CompanyDataRight.Page.ClassName == pageName && UserHasFilter(user.Id, f));

See how I moved the Where after the AsEnumerable, EF loads the object into memory when you call AsEnumerable meaning you can then do everything you want with them. This is suboptimal at best, because right now it's loading more objects into memory than it really should, but sometimes the only way to do more complex queries is to perform them in memory*. This solution however does have one benefit: A class that derives from this class can override the UserHasFilter method, altering the query logic without having to re-create said query.


* Not that you couldn't achieve this with just SQL, just that EF can't translate every single LINQ query into SQL

MindSwipe
  • 7,193
  • 24
  • 47