Search code examples
c#asp.net-core-2.2entity-framework-plus

Getting an 'Cannot create an instance of an interface'-error with EF Plus IncludeFilter


Having an IList<Guid>, I wish to locate all matches from the context and then include a number of related tables, where outdated data are not included. Because of the size of the data, I try to use EF Plus IncludeFilter, to avoid loading all to memory and perform the filtering there. The problem occurs when I call ToListAsync() on the query. IncludeFilter (as far as I can see) then throws a System.MissingMethodException : Cannot create an instance of an interface exception.

The project is done in .NET Core 2.2, and I using Z.EntityFramework.Plus.EFCore 2.0.7

Sample project

This sample recreates the issue: https://dotnetfiddle.net/MYukHp

Data structure

The database structure centers around the Facts table which contains an immutable Guid, identifing each entry. Entries in all other tables link to this guid to bind together to a single entry. The other tables contain a DateTime ValidTo to track changes. No entries are ever updated or deleted. Instead, on change a new entry is made with ValidTo = DateTime.MaxValue, and the entry being updated has it's ValidTo set to DateTime.Now. This ensures that all changes are preserved historically.

Over time, the vast majority of data will be historical, so it's crucial that we can filter this out in the SQL query.

The data structure for the Fact-table is like this:

public class FactModel
{
    public Guid FactId { get; set; }
    public DateTime ValidFrom { get; set; }
    public DateTime ValidTo { get; set; }

    // Navigation properties
    public IEnumerable<PersonModel> Persons { get; set; }
    // Repeat for all other tables
}

All other tables inherits from a ModelBase, linking them to the Fact table.

public class ModelBase
{
    public Guid FactId { get; set; }    // Link to the Fact
    public FactModel Fact { get; set; }  // Navigation property
    public DateTime ValidFrom { get; set; } 
    public DateTime ValidTo { get; set; }   // ValidTo == DateTime.MaxValue -> active record
}

Example tables for Person and Patient

public class PersonModel : ModelBase
{
    public Guid PersonId { get; set; }    // Key - A new is created on every update
    public string FirstName { get; set; }  // data
    public string LastName { get; set; }   // data
}

public class PatientModel : ModelBase
{
    public Guid PatientId { get; set; }  // Key - A new is created on every update
    public Guid ActiveCompanyId { get; set; }  // Data
    public int HealthInsuranceGroup { get; set; } // Data
    public PatientStatusType Status { get; set; } // Data
}

Changing the parameter to IQueryable produces a new error: System.InvalidCastException : Unable to cast object of type System.Guid' to type 'System.String'

Call sequense

The call sequence is rather complex, but simplified we start by declaring the call parameter IQueryable<FactModel> facts. This is filtered by only adding Patients from the company the user is logged into. Then the search term is applied. Finally, the parameter is transformed into a list containing only the need guids, before calling AssignDataToPatientByFactId.

// Performing a search for an Address

IQueryable<FactModel> facts = null;
facts = _context.Facts.AsNoTracking().Where(p => p.Patients.Any(a => a.ActiveCompanyId == _identificationHandler.Identification.CompanyId));
facts = facts.AsNoTracking().Where(p => p.Addresses.Where(z => z.ValidTo == DateTime.MaxValue).Any(q => q.Street.Any() && q.Street.StartsWith(searchDTO.SearchString)));
return await AssignDataToPatient(facts.Select(x => x.FactId).ToList()), cancel);
public async Task<List<FactModel>> AssignDataToPatientByFactId(IList<Guid> factIds, CancellationToken cancel)
{
    return await _context.Facts.Where(x => factIds.Contains(x.FactId))
        .IncludeFilter(x => x.Patients.Where(c => c.ValidTo == DateTime.MaxValue))
        .IncludeFilter(x => x.Persons.Where(c => c.ValidTo == DateTime.MaxValue))
        .IncludeFilter(x => x.Communications.Where(c => c.ValidTo == DateTime.MaxValue))
        .IncludeFilter(x => x.Addresses.Where(c => c.ValidTo == DateTime.MaxValue))
        .ToListAsync(cancel);
}

So AssignDataToPatientByFactId takes a list of guids, finds all matching in the Facts-table and then adds the entries from the other tables where the ValidTo timestamp is Max. So all other entries should not be included.

Separating the code into several statements reveal that IncludeFilter seems to be working, but calling ToListAsync produces the error.


Solution

  • Disclaimer: I'm the owner of the project Entity Framework Plus

    The version v2.0.8 has been released fixing this issue.

    The issue was caused because the class Fact was using IEnumerable<T> properties which were not yet supported by our library.

    The Fiddle is now working as expected: https://dotnetfiddle.net/MYukHp