dotnet / efcore

EF Core is a modern object-database mapper for .NET. It supports LINQ queries, change tracking, updates, and schema migrations.
https://docs.microsoft.com/ef/
MIT License
13.78k stars 3.19k forks source link

System.InvalidOperationException when reading JSON #33822

Open doboczyakos opened 5 months ago

doboczyakos commented 5 months ago

System.InvalidOperationException Cannot get the value of a token type 'Null' as a string.

at System.Text.Json.ThrowHelper.ThrowInvalidOperationException_ExpectedString(JsonTokenType tokenType) at lambda_method2006(Closure, QueryContext, Object[], JsonReaderData) at lambda_method2005(Closure, QueryContext, DbDataReader, ResultContext, SingleQueryResultCoordinator) at Microsoft.EntityFrameworkCore.Query.Internal.SingleQueryingEnumerable1.AsyncEnumerator.MoveNextAsync() at Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.ToListAsync[TSource](IQueryable1 source, CancellationToken cancellationToken)

public class Foo { public required Foo2 Foo2{ get; init; } public required string Bar{ get; init; } }

public class Foo2 { ... }

builder.OwnsOne(m => m.Foo).ToJson().OwnsOne(foo=> foo.Foo2);

{..., "_TableSharingConcurrencyTokenConvention_RowVersion":null,"Foo2":{..., "_TableSharingConcurrencyTokenConvention_RowVersion":null}}

maumar commented 5 months ago

@doboczyakos which version of EF Core are you using? Also, please provide full standalone repro that shows the problem.

doboczyakos commented 5 months ago

@doboczyakos which version of EF Core are you using? Also, please provide full standalone repro that shows the problem.

EF Core 8.0.5

Sorry, I won't. I provided the entities. I got rid of owned entities and I use a simple Json ValueConverter instead. It's perfect.

davidnemeti commented 5 months ago

@doboczyakos which version of EF Core are you using? Also, please provide full standalone repro that shows the problem.

@maumar, here is a full standalone minimal repro (it was tested with EF Core 8.0.2 and 8.0.5, the results were the same):

void Main()
{
    using (var context = new TestContext())
    {
        context.Database.EnsureDeleted();
        context.Database.EnsureCreated();

        var theDerivedEntity = new TheDerivedEntity
        {
            Foo = new Foo
            {
                Bar = "hello"
            }
        };

        context.TheBaseEntities.Add(theDerivedEntity);
        context.SaveChanges();
    }

    using (var context = new TestContext())
    {
        // it will cause error: "Cannot get the value of a token type 'Null' as a string."
        context.TheBaseEntities.ToList();
    }
}

public class TestContext : DbContext
{
    public DbSet<TheBaseEntity> TheBaseEntities { get; set; } = null!;

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder
            .LogTo(Console.WriteLine)
            .UseSqlServer(@"Data Source=sqlserver; Initial Catalog=YYY; Persist Security Info=True; User ID=sa; Password=ThePassword; Encrypt=false");
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder
            .Entity<TheDerivedEntity>()
            .OwnsOne(m => m.Foo)
            .ToJson();
    }
}

public class TheBaseEntity
{
    public int Id { get; init; }

    [Timestamp, ConcurrencyCheck]
    public byte[] RowVersion { get; set; } = null!;
}

public class TheDerivedEntity : TheBaseEntity
{
    public required Foo Foo { get; init; }
}

public class Foo
{
    public required string Bar { get; init; }
}

Note that if we have a single TheEntity entity type with no TPH then there is no error:

public class TheEntity
{
    public int Id { get; init; }

    [Timestamp, ConcurrencyCheck]
    public byte[] RowVersion { get; set; } = null!;

    public required Foo Foo { get; init; }
}

The problem seems to be that when using TPH the TableSharingConcurrencyTokenConvention automatically puts a RowVersion byte array property into the owned entity type Foo, which the JSON deserializer cannot handle.

If we have a single TheEntity entity type, but we put an explicit byte array (does not even have to be a real rowversion with timestamp attribute) into the Foo entity type, then the error emerges:

public class TheEntity
{
    public int Id { get; init; }

    [Timestamp, ConcurrencyCheck]
    public byte[] RowVersion { get; set; } = null!;

    public required Foo Foo { get; init; }
}

public class Foo
{
    public required string Bar { get; init; }

    public byte[] ByteArray { get; init; } = null!; // causes problem
}

However, if we make the ByteArray property nullable, then the error goes away:

public class Foo
{
    public required string Bar { get; init; }

    public byte[]? ByteArray { get; init; }
}

Or, if we initialize the ByteArray property so it does not contain null value, then the error also goes away:

public class Foo
{
    public required string Bar { get; init; }

    public required byte[] ByteArray { get; init; }
}
var theEntity = new TheEntity
{
    Foo = new Foo
    {
        Bar = "hello",
        ByteArray = new byte[8]
    }
};
maumar commented 5 months ago

@davidnemeti thank you! I am able to reproduce this on my end. It also repros on our latest bits in main.

maumar commented 5 months ago

support for concurrency on JSON types is tracked here: https://github.com/dotnet/efcore/issues/29501