12

Yesterday I was working on a code refactor and came across an exception that I really couldn't find much information on. Here is the situation.

We have an a pair of EF entities that have a many to many relationship through a relation table. The objects in question look like this, leaving out the unnecessary bits.

public partial class MasterCode
{
    public int MasterCodeId { get; set; }
    ...

    public virtual ICollection<MasterCodeToSubCode> MasterCodeToSubCodes { get; set; }
}

public partial class MasterCodeToSubCodes
{
    public int MasterCodeToSubCodeId { get; set; }
    public int MasterCodeId { get; set; }
    public int SubCodeId { get; set; }
    ...
}

Now, I attempted to run a LINQ query against these entities. We use a lot of LINQ projections into DTOs. The DTO and the query follow. masterCodeId is a parameter passed in.

public class MasterCodeDto
{
    public int MasterCodeId { get; set; }
    ...

    public ICollection<int> SubCodeIds { get; set; }
}

(from m in MasterCodes
where m.MasterCodeId == masterCodeId
select new MasterCodeDto
{
    ...
    SubCodeIds = (from s in m.MasterCodeToSubCodes
                  select s.SubCodeId).ToList(),
    ...
}).SingleOrDefaultAsync();

The internal query throws the following exception

Expression of type 'System.Data.Entity.Infrastructure.ObjectReferenceEqualityComparer' cannot be used for constructor parameter of type 'System.Collections.Generic.IEqualityComparer`1[System.Int32]'

We have done inner queries like this before in other places in our code and not had any issues. The difference in this one is that we aren't new-ing up an object and projecting into it but rather returning a group of ints that we want to put in a list.

I have found a workaround by changing the ICollection on MasterCodeDto to IEnumerable and dropping the ToList() but I was never able to find out why I couldn't just select the ids and return them as a list.

Does anyone have any insight into this issue? Normally returning just an id field and calling ToList() works fine when it is not part of an inner query. Am I missing a restriction on inner queries that prevents an operation like this from happening?

Thanks.

Edit: To give an example of where this pattern is working I'll show you an example of a query that does work.

 (from p in Persons
 where p.PersonId == personId
 select new PersonDto
 {
     ...
     ContactInformation = (from pc in p.PersonContacts
                           select new ContactInformationDto
                           {
                               ContactInformationId = pc.PatientContactId,
                               ...
                           }).ToList(),
     ...
  }).SingleOrDefaultAsync();

In this example, we are selecting into a new Dto rather than just selecting a single value. It works fine. The issues seems to stem from just selecting a single value.

Edit 2: In another fun twist, if instead of selecting into a MasterCodeDto I select into an anonymous type the exception is also not thrown with ToList() in place.

Bradford Dillon
  • 1,750
  • 13
  • 24

1 Answers1

17

I think you stumbled upon a bug in Entity Framework. EF has some logic for picking an appropriate concrete type to materialize collections. HashSet<T> is one of its favorites. Apparently (I can't fully follow EF's source code here) it picks HashSet for ICollections and List for IEnumerable.

It looks like EF tries to create a HashSet by using the constructor that accepts an IEqualityComparer<T>. (This happens in EF'sDelegateFactory class, method GetNewExpressionForCollectionType.) The error is that it uses its own ObjectReferenceEqualityComparer for this. But that's an IEqualityComparer<object>, which can not be converted to an IEqualityComparer<int>.

In general I think it is best practice not to use ToList in LINQ queries and to use IEnumerable in collections in DTO types. Thus, EF will have total freedom to pick an appropriate concrete type.

Gert Arnold
  • 105,341
  • 31
  • 202
  • 291
  • This was similar to the conclusion that I came to while digging around inside the EF code. I think moving to IEnumerable is probably the best idea at this point to avoid this issue in the future. Thanks for confirming what I felt was the issue. – Bradford Dillon Aug 28 '14 at 15:40
  • Thank you. But i need ICollection because i want to use its methods like Add, Remove and so on. So any other hope ?? – Wahid Bitar Sep 07 '16 at 20:10
  • 1
    It is worth noting that while this bug appears to be still present in EF 6.2, IList works as an alternative to ICollection if you don't want to fall back on IEnumerable. – Stefan Aug 08 '18 at 14:44