jamebob
jamebob

Reputation: 322

Create null enumerable within Linq to Entities query

I am working on an ASP.NET MVC project which uses Entity Framework.

I need to project values pulled from the database into a PropertyValue type, which looks like the following:

public class PropertyValue {
    public string StringValue { get; set; }
    public bool? BoolValue { get; set; }
    public int? IntValue { get; set; }
}

Most of the time, this is easy enough. To filter all users with "First Name" = "John" and "Is Archived" = true, I could do:

usersQuery
    .Where(u => 
        new PropertyValue {
            StringValue = u.FirstName,
            BoolValue = null,
            IntValue = null
        }.StringValue == "John")
    .Where(u =>
        new PropertyValue {
            StringValue = null,
            BoolValue = u.IsArchived,
            IntValue = null
        }.BoolValue == true);

Obviously this is a ridiculous looking query, but I'm constructing these queries piece by piece based on user input. These queries need to be combinable, which is why I have to explicitly set all the unused properties of PropertyValue to null, and I have to set them all in the same order. If I don't do this, I'll get an error from Entity Framework saying that PropertyValue exists in two structurally incompatible initializations within the query.

System.NotSupportedException: The type 'UMS.Utilities.PropertyValue' appears in two structurally incompatible initializations within a single LINQ to Entities query. A type can be initialized in two places in the same query, but only if the same properties are set in both places and those properties are set in the same order.

This isn't a problem, I can just make sure all the properties are explicitly set to null. The problem arises when I have to add another property to my PropertyValue type, to be able to retrieve a list of IDs (in my case, retrieving all selected roles for the user, i.e. "Admin", "Guardian", "Teacher", "Student")

I've modified my PropertyValue class to be able to store a list of Guids:

public class PropertyValue {
    public string StringValue { get; set; }
    public bool? BoolValue { get; set; }
    public int? IntValue { get; set; }
    public IEnumerable<Guid> GuidValues { get; set; }
}

I can now query for user roles like so:

usersQuery
    .Select(u => new PropertyValue {
        StringValue = null,
        BoolValue = null,
        IntValue = null,
        GuidValues = u.UserRoles.Select(ur => ur.Role_ID)
    });

That works great. Pulling first names would now look like this:

usersQuery
    .Select(u => new PropertyValue {
        StringValue = u.FirstName,
        BoolValue = null,
        IntValue = null,
        GuidValues = null
    });

But I get the following error:

System.NotSupportedException: Unable to create a null constant value of type 'System.Collections.Generic.IEnumerable`1[[System.Guid, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]'. Only entity types, enumeration types or primitive types are supported in this context.

So I can't create a null enumerable within EF. But I can't just ignore the initialization of the property, or else I get the structurally incompatible error. I've tried doing GuidValues = Enumerable.Empty<Guid>() instead, but I can't do that in an EF context either.

How can I get around this issue and create a null or empty enumerable within an EF projection?

Upvotes: 3

Views: 2434

Answers (1)

Ivan Stoev
Ivan Stoev

Reputation: 205799

I'm not quite sure about the use case. But speaking generally, I agree that it's annoying EF limitation, especially the reasoning behind the structurally incompatible initializations requirement, which forces us to use an ugly workaround I'm going to propose you.

Shortly, there is no way to workaround the null enumerable assignment other than not including the assignment into projection. The next is an attempt to workaround the structurally incompatible initializations problem. It works in most cases except in Concat / Union scenarios.

Start by defining a couple subclasses:

public class StringPropertyValue : PropertyValue { }
public class BoolPropertyValue : PropertyValue { }
public class IntPropertyValue : PropertyValue { }
public class GuidsPropertyValue : PropertyValue { }

The names does not really matter because no special constraints are put on those classes, the only important thing is they to be different. We'll use those classes for projection instead of PropertyValue.

Then we'll need another simple class like this:

public class Property
{
    public PropertyValue Value { get; set; }
}

It's needed to workaround another EF limitation - unsupported casts. In order to project a PropertyValue, instead of unsupported

(PropertyValue)new StringPropertyValue { StringValue = .. }

we will perform the cast by using the ugly but supported

new Property { Value = new StringPropertyValue { StringValue = ... } }.Value

And that's it.

Here are the equivalent working versions of your examples:

(1)

usersQuery
    .Where(u => new Property { Value = new StringPropertyValue {
            StringValue = u.FirstName
        }}.Value.StringValue == "John")
    .Where(u => new Property { Value = new BoolPropertyValue { 
            BoolValue = u.IsArchived
        }}.Value.BoolValue == true);

(2)

usersQuery
    .Select(u => new Property { Value = new GuidsPropertyValue {
        GuidValues = u.UserRoles.Select(ur => ur.Role_ID)
    }}.Value);

(3)

usersQuery
    .Select(u => new Property { Value = new StringPropertyValue {
        StringValue = u.FirstName
    }}.Value);

Upvotes: 3

Related Questions