Link to home
Start Free TrialLog in
Avatar of Paul Williams
Paul WilliamsFlag for United States of America

asked on

How to specify an LINQ query using an enum in the DefaultIfEmpty cause?

I'm trying to write a LINQ expression in .Net 6 C# where one field is an enum and I need to specify a default value.

In the expression, UserFunctionality is of type Functionality which is an enum. The value of Functionality.NONE is zero. 

The query as shown converts the DefaultIfEmpty to ".DefaultIfEmpty(NONE)" and I get an error that it could not be translated.


IQueryable <DTO> DTOquery = mContext. ... .Select(bp => new PrinterAllDTO

{

   UserFunctionality   = mContext.Permissions

                  .Where(perm => perm.UserID.Equals(userID))

                  .Select(perm => perm.FunctionalityID)

                  .DefaultIfEmpty(Functionality.NONE)

                  .Max(),


If I specify ".DefaultIfEmpty((int)0)", then I get an error:

IQueryable<Functionality> does not contain a definition for 'DefaultIfEmpty'

Avatar of gr8gonzo
gr8gonzo
Flag of United States of America image

> If I specify ".DefaultIfEmpty((int)0)" 


Yes, because you're working with a queryable list of enum values, and if you specify (int)0 as the default value, you're trying to tell .NET "You're trying to assign the right Functionality value, so if the resulting list is empty, assign the integer 0" and .NET is telling you, "Umm... I can't do that because integer 0 isn't a Functionality value."


The way you have it with   .DefaultIfEmpty(Functionality.NONE) should work fine, so I'm not sure why you'd be assigning an integer? However, if you want to look up the Functionality value corresponding to value 0, then just swap out "int" for "Functionality":


.DefaultIfEmpty((Functionality)0)

By the way, I threw together a simple reproducible code example that hopefully replicated what you have, and it works for me:


    internal class EEQuestion_29248353
    {
        public EEQuestion_29248353()
        {
            var mContext = new MContext();
            var userID = 0;
            var UserFunctionality = mContext.Permissions
                             .Where(perm => perm.UserID.Equals(userID))
                             .Select(perm => perm.FunctionalityID)
                             .DefaultIfEmpty(Functionality.NONE)
                             .Max();
        }
    }


    internal class MContext
    {
        public List<Permission> Permissions = new List<Permission>() { };
    }


    internal class Permission
    {
        public enum Functionality { NONE, SOME, ALL }
        public int UserID = 0;
        public Functionality FunctionalityID;
    }

Open in new window


Avatar of Paul Williams

ASKER

When I use ".DefaultIfEmpty(Functionality.NONE)" I get the following error during execution:


An exception of type 'System.InvalidOperationException' occurred in Microsoft.EntityFrameworkCore.dll but was not handled in user code

The LINQ expression 'DbSet<Permission>()

    .Where(perm => perm.UserID.Equals(__userID_0))

    .Select(perm => perm.FunctionalityID)

    .DefaultIfEmpty(NONE)' 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'. See https://go.microsoft.com/fwlink/?linkid=2101038 for more information.


. 

Ah, ok - "DbSet" - if this is a database-driven EF LINQ query, then there are a few limitations.


Supported and Unsupported LINQ Methods (LINQ to Entities) - ADO.NET | Microsoft Learn


I'm not in front of an environment where I can replicate, but I know equality comparers sometimes have problems, so the issue might be here:


   .Where(perm => perm.UserID.Equals(userID)) 


Try changing that to:


   .Where(perm => perm.UserID == userID) 

I tried your change and I'm getting the same error.

Okay, then the next step would be to break it out into separate steps instead of chaining them together, to verify where it fails. So change this:


UserFunctionality   = mContext.Permissions
                  .Where(perm => perm.UserID.Equals(userID))
                  .Select(perm => perm.FunctionalityID)
                  .DefaultIfEmpty(Functionality.NONE)
                  .Max(),

Open in new window


to:


UserFunctionality   = getUserFunctionality(mContext);

Open in new window


and add a method called GetUserFunctionality where you break down the LINQ query into individual steps:


private xyz getUserFunctionality(xyz mContext) // <-- replace "xyz" with the appropriate types
{
    var step1 = mContext.Permissions.Where(perm => perm.UserID.Equals(userID));
    var step2 = step1.Select(perm => perm.FunctionalityID);
    var step3 = step2.DefaultIfEmpty(Functionality.NONE);
    var step4 = step3.Max();
    return step4;
}

Open in new window


Then step through each line with a debugger.

Same basic error, just presented a little differently:

The LINQ expression 'DbSet<Permission>()

    .Where(perm => perm.UserID.Equals(__userID_0))

    .Select(perm => perm.FunctionalityID)

    .DefaultIfEmpty(__p_1)' 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'. See https://go.microsoft.com/fwlink/?linkid=2101038 for more information.


The error you're showing indicates it's still chained together instead of separated into steps.

VS indicates to error was with step4

ASKER CERTIFIED SOLUTION
Avatar of gr8gonzo
gr8gonzo
Flag of United States of America image

Link to home
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
Start Free Trial

Thanks, the AsEnumerable() finally solved the problem:

var step3 = step2.AsEnumerable().DefaultIfEmpty(Functionality.NONE);


Just to make sure you understand -why- that worked, the LINQ expressions in this case were working against a database entity, so they were trying to execute the desired logic on the server side. 


Basically imagine that those LINQ expressions are trying to be translated into a database query that matches your expressions. However the DefaultIfEmpty was referring to an enum that wasn't defined on the server side, but rather on the client side.


By adding the AsEnumerable, you were ending the server-side execution and turning the results into a client-side enumerable, and then the DefaultIfEmpty was able to then run client-side successfully.


Does that make sense?

Thanks for the explanation and your time, that does make sense.