Skip to content

InvalidCastException when using ThenInclude on IReadOnlyCollection #463

@rainerllera

Description

@rainerllera

Hey all!

First of all thank you for your work in this library.

After upgrading from Ardalis.Specification v8.1.0 to v9.0.1, we started seeing an InvalidCastException when using ThenInclude on a navigation property of type IReadOnlyCollection with a cast in the lambda. We use a lot of these throughout our codebase.

This worked as expected in v8 but throws at runtime in v9.

Here's a minimal reproduction in a console app: (or you can try it in .net fiddle)

using Ardalis.Specification;
using Ardalis.Specification.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Linq;
using System;

public class Certificate
{
    public int Id { get; set; }
    public string Name { get; set; } = "";
	public Course Course { get; set; } 
	public int CourseId { get; set; }
}

public class SpecialCertificate : Certificate
{
	public int? CertificateTemplateId { get; set; }
    public CertificateTemplate CertificateTemplate { get; set; } = new();
}



public class CertificateTemplate
{
    public int Id { get; set; }
    public string TemplateName { get; set; } = "";
}

public class Course
{
    private List<Certificate> _certs = new();
    public int Id { get; set; }
	//public List<Certificate> Certificates => _certs; // Replacing IReadOnlyCollection with List<T> works :)
    public IReadOnlyCollection<Certificate> Certificates => _certs;

    public void AddCertificate(Certificate cert) => _certs.Add(cert);
}

// ------------ DbContext -------------
public class AppDbContext : DbContext
{
    public DbSet<Course> Courses => Set<Course>();
    public DbSet<Certificate> Certificates => Set<Certificate>();
    public DbSet<CertificateTemplate> CertificateTemplates => Set<CertificateTemplate>();

    protected override void OnConfiguring(DbContextOptionsBuilder options)
        => options.UseInMemoryDatabase("TestDb");

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Course>()
            .HasMany(c=>c.Certificates)
            .WithOne(c=>c.Course)
            .HasForeignKey(c=>c.CourseId);
		
		modelBuilder.Entity<Certificate>()
			.ToTable("Certificates");
			
		modelBuilder.Entity<SpecialCertificate>()
			.ToTable("Certificates");
    }
}

// ------------ Specification -------------
public class CourseWithCertificatesSpec : Specification<Course>
{
    public CourseWithCertificatesSpec()
    {
        Query.Include(c => c.Certificates)
             .ThenInclude(c => (c as SpecialCertificate)!.CertificateTemplate); // triggers the crash in v9
		
    }
}

// ------------ Program -------------
class Program
{
    static void Main()
    {
        using var db = new AppDbContext();

        var course = new Course();
        course.AddCertificate(new SpecialCertificate { CertificateTemplate = new CertificateTemplate { TemplateName = "Test Template" } });
        db.Courses.Add(course);
        db.SaveChanges();

        try
        {
            var spec = new CourseWithCertificatesSpec();
            var courseWithCerts = db.Courses
                .WithSpecification(spec)
                .FirstOrDefault();

            Console.WriteLine("Query succeeded.");
        }
        catch (Exception ex)
        {
            Console.WriteLine("Exception during query:");
            Console.WriteLine(ex);
        }
    }
}

Running this code will throw:


Exception during query:
System.InvalidCastException: Unable to cast object of type 'System.Linq.Expressions.Expression1`1[System.Func`2[System.Collections.Generic.IReadOnlyCollection`1[Certificate],CertificateTemplate]]' to type 'System.Linq.Expressions.Expression`1[System.Func`2[Certificate,CertificateTemplate]]'.
   at lambda_method108(Closure, IQueryable, LambdaExpression)
   at Ardalis.Specification.EntityFrameworkCore.IncludeEvaluator.GetQuery[T](IQueryable`1 query, ISpecification`1 specification)
   at Ardalis.Specification.EntityFrameworkCore.SpecificationEvaluator.GetQuery[T](IQueryable`1 query, ISpecification`1 specification, Boolean evaluateCriteriaOnly)
   at Ardalis.Specification.EntityFrameworkCore.DbSetExtensions.WithSpecification[TSource](IQueryable`1 source, ISpecification`1 specification, ISpecificationEvaluator evaluator)
   at Program.Main()

I think it's related with the way you determine whether to use the _thenIncludeAfterEnumerableMethodInfo in IncludeEvaluator.cs but not sure how to debug further.

Also, as noted in the code, if you replace IReadOnlyCollection with List, for example, the query works properly as it did in v8.

Let me know if you need any more info. And thanks again!

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingquestionFurther information is requested

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions