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

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]"
  }
]

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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