Custom LINQ extension method throws VerificationException: type argument IEnumerable`1[entity]' violates constraint of type parameter 'TCollection'

Viewed 1494

I've got some entities that looks like this:

public class MyEvent 
{
    // ...
    public IEnumerable<MyEncounter> Encounters { get; set; } // Navigation property
    // ...
}

public class MyEncounter
{
    public Guid MyEventID { get; set; }
    public MyEvent MyEvent { get; set; }  // Navigation property

    public Guid? MySpecialProperty { get; set; }

    public int Status { get; set; }
    //...
}

I want to add an extension method to do some filtering of the Encounters collection on MyEvent and return the value of MySpecialProperty for one of the MyEncounter instances (specifically, the instance where status = 1).

So, I wrote my extension method:

public static class MyExtensions
{
    public static Guid? GetMySpecialProperty(this IEnumerable<MyEncounter> encounters)
    {
        return encounters.Where(e => e.Status == 1)
                         .Select(e => e.MySpecialProperty)
                         .FirstOrDefault();
    }
}

Now, I'm ready to use my extension method. I've simplified this dramatically to remove stuff that should be irrelevant:

var qry = MyEventsContext.
    .Where(<some logic>)
    .Select(x => new MyViewModel
    {
        SpecialPropValue = x.Encounters.GetMySpecialProperty()   
    });

This compiles as expected, and I'm ready to run the query:

IList<MyViewModel> items = await qry.ToListAsync();

This is where things go wrong. As soon as .ToListAsync() is executed, I get this exception:

VerificationException: Method Microsoft.EntityFrameworkCore.Query.RelationalShapedQueryCompilingExpressionVisitor+CustomShaperCompilingExpressionVisitor.PopulateCollection: type argument 'System.Collections.Generic.IEnumerable`1[<fully qualified name of MyEncounter>]' violates the constraint of type parameter 'TCollection'.

The inner exception is only slightly more detailed:

ArgumentException: GenericArguments[0], 'System.Collections.Generic.IEnumerable`1[<fully qualified name of MyEncounter>]', on 'Void PopulateCollection[TCollection,TElement,TRelatedEntity](Int32, Microsoft.EntityFrameworkCore.Query.QueryContext, System.Data.Common.DbDataReader, Microsoft.EntityFrameworkCore.Query.Internal.ResultCoordinator, System.Func`3[Microsoft.EntityFrameworkCore.Query.QueryContext,System.Data.Common.DbDataReader,System.Object[]], System.Func`3[Microsoft.EntityFrameworkCore.Query.QueryContext,System.Data.Common.DbDataReader,System.Object[]], System.Func`3[Microsoft.EntityFrameworkCore.Query.QueryContext,System.Data.Common.DbDataReader,System.Object[]], System.Func`5[Microsoft.EntityFrameworkCore.Query.QueryContext,System.Data.Common.DbDataReader,Microsoft.EntityFrameworkCore.Query.Internal.ResultContext,Microsoft.EntityFrameworkCore.Query.Internal.ResultCoordinator,TRelatedEntity])' violates the constraint of type 'TCollection'.

What is this exception telling me, and is there a way to resolve it? As far as I can tell, I've created my extension method correctly.

Interestingly, if I create an extension method to just get the MyEncounters that match the criteria and do the Select/FirstOrDefault in the query, everything works as expected:
// Extension method
public static IEnumerable<MyEncounter> GetMySpecialEncounters(this IEnumerable<MyEncounter> encounters)
{
    return encounters.Where(e => e.Status == 1)
}

// Query
var qry = MyEventsContext.
    .Where(<some logic>)
    .Select(x => new MyViewModel
    {
        SpecialPropValue = x.Encounters.GetMySpecialEncounters()
                                       .Select(x => x.MySpecialProperty)
                                       .FirstOrDefault()   
    });

I was mistaken about the above. Using that extension method in the query results in this exception:
When called from 'VisitLambda', rewriting a node of type 'System.Linq.Expressions.ParameterExpression' must return a non-null value of the same type

What does work is doing all the logic inside the query, with no extension method at all:

// Query
var qry = MyEventsContext.
    .Where(<some logic>)
    .Select(x => new MyViewModel
    {
        SpecialPropValue = x.Encounters.Where(e => e.Status == 1)
                                       .Select(x => x.MySpecialProperty)
                                       .FirstOrDefault()   
    });
1 Answers

Invoking ToList on the x.Encounters works for me in 3.1 EF Core (though AsEnumerable() and ToArray() does not):

.Select(x => new MyViewModel
{
    SpecialPropValue = x.Encounters.ToList().GetMySpecialProperty()
});

The reason for such behavior is typing issue - CustomShaperCompilingExpressionVisitor.VisitExtension method tries to handle populating collection and tries to build expression tree to call PopulateCollection method which has generic constraint for collection type to be ICollection<TElement> but determines IEnumerable<TElement> and passes it to MakeGenericMethod call which fails cause IEnumerable is not ICollection (it is vice versa actually).

Related