Skip to content

Fetching/reading an instance of an entity which stores a Primitive Collection and is also mapped as a TPH inherting type throws System.IndexOutOfRangeException #41

@hal-la

Description

@hal-la

Issue:

Primitive collections persistence works pretty well when set on classes that aren't mapped to a TPH inheritance scenario (haven't tested with TPT/TPC yet). When fetching/reading from an entity of a child class containing a Primitive Collection as a field, the exception IndexOutOfRangeExecption is thrown.

Exception w/ Stack Trace

Exception has occurred: CLR/System.IndexOutOfRangeException
An unhandled exception of type 'System.IndexOutOfRangeException' occurred in System.Private.CoreLib.dll: 'Index was outside the bounds of the array.'
at lambda_method63(Closure, ValueBuffer)
at System.Linq.Utilities.<>c__DisplayClass2_03.<CombineSelectors>b__0(TSource x) at System.Linq.Enumerable.IteratorSelectIterator2.TryGetFirst(Boolean& found)
at lambda_method62(Closure)
at kDg.FileBaseContext.Infrastructure.Query.FileBaseContextQueryExpression.ResultEnumerable.GetEnumerator()
at kDg.FileBaseContext.Infrastructure.Query.FileBaseContextShapedQueryCompilingExpressionVisitor.QueryingEnumerable1.Enumerator.MoveNextHelper() at kDg.FileBaseContext.Infrastructure.Query.FileBaseContextShapedQueryCompilingExpressionVisitor.QueryingEnumerable1.Enumerator.MoveNext()
at System.Linq.Enumerable.TryGetSingle[TSource](IEnumerable`1 source, Boolean& found)
at lambda_method61(Closure, QueryContext)
at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.ExecuteCore[TResult](Expression query, Boolean async, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.Execute[TResult](Expression query)
at Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryProvider.Execute[TResult](Expression expression)

Model Setup:

    public class Person
    {
        public int Id { get; set; }
        public string Username { get; set; }
    }
    public class Player : Person
    {
        public uint Level { get; set; }
        public List<string> Voicelines { get; set; } = [];
    }

DbContext Setup:

public class FileDbContext : DbContext
{
    public DbSet<Person> Persons { get; set; }
    private readonly string? _dbPath;

    public FileDbContext()
    {
        _dbPath = "local_db";
    }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder
            .UseFileBaseContextDatabase(
                _dbPath,
                applyServices: services =>
                {
                    services.ConfigureJsonSerializerOptions(options =>
                    {
                        options.WriteIndented = true;
                    });
                }
            )
            .EnableSensitiveDataLogging();

        base.OnConfiguring(optionsBuilder);
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        var jsonOptions = this.GetService<IServiceProvider>()
            .GetRequiredService<JsonSerializerOptions>();

        modelBuilder
            .Entity<Person>()
            .HasDiscriminator<string>("person_type")
            .HasValue<Person>("person_base")
            .HasValue<Player>("player");

        modelBuilder.Entity<Player>(e =>
        {
            e.Property(pl => pl.Voicelines)
                .HasConversion(
                    v => JsonSerializer.Serialize(v, jsonOptions),
                    v => JsonSerializer.Deserialize<List<string>>(v, jsonOptions) ?? new List<string>()
                );
        });
    }
}

Test:

using (var db = new FileDbContext())
{
    Player player =
        new()
        {
            Username = "CrazyFrog",
            Voicelines =
            [
                "People die if they are killed..",
                "Ze Healing is not as rewarding as the hurting...",
                "Just because you're correct doesn't mean you're right.."
            ]
        };
    db.Persons.Add(player);
    db.SaveChanges();
}

using (var db = new FileDbContext())
{
    Player player = (Player)db.Persons.First(); // IndexOutOfBounds thrown!
    foreach (var line in player.Voicelines)
    {
        Console.WriteLine(line);
    }
    // Similarly, calling e.g. db.Persons.First(); without casting throws this error.
}

The data will be saved, it's only when the entities are read from the db that the exception is thrown. Here's how it's stored:

[
  {
    "Id": 1,
    "Username": "CrazyFrog",
    "person_type": "player",
    "Level": 0,
    "Voicelines": "[\n  \u0022People die if they are killed..\u0022,\n  \u0022Ze Healing is not as rewarding as the hurting...\u0022,\n  \u0022Just because you\\u0027re correct doesn\\u0027t mean you\\u0027re right..\u0022\n]"
  }
]

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workinghelp wantedExtra attention is needed

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions