efcore icon indicating copy to clipboard operation
efcore copied to clipboard

Update nested Json with generic lists - System.ArgumentNullException: 'Value cannot be null. Arg_ParamName_Name'

Open FM1973 opened this issue 1 year ago • 5 comments

Hello.

I´m trying to update a Json-Column with nested complex objects. I can create the inital object without a hassle. When I try to update (add a new object to a list inside the Json-Field) I get an exception:

System.ArgumentNullException: 'Value cannot be null. Arg_ParamName_Name'

   at System.ThrowHelper.ThrowArgumentNullException(ExceptionArgument argument) in System\ThrowHelper.cs:line 247
   at System.Collections.Generic.Dictionary`2.FindValue(TKey key) in System.Collections.Generic\Dictionary.cs:line 1036
   at System.Collections.Generic.Dictionary`2.TryGetValue(TKey key, TValue& value) in System.Collections.Generic\Dictionary.cs:line 1388
   at Microsoft.EntityFrameworkCore.Update.ModificationCommand.<GenerateColumnModifications>g__HandleJson|41_4(List`1 columnModifications, <>c__DisplayClass41_0& ) in Microsoft.EntityFrameworkCore.Update\ModificationCommand.cs:line 517
   at Microsoft.EntityFrameworkCore.Update.ModificationCommand.GenerateColumnModifications() in Microsoft.EntityFrameworkCore.Update\ModificationCommand.cs:line 302
   at Microsoft.EntityFrameworkCore.Update.ModificationCommand.<>c.<get_ColumnModifications>b__33_0(ModificationCommand command) in Microsoft.EntityFrameworkCore.Update\ModificationCommand.cs:line 146
   at Microsoft.EntityFrameworkCore.Internal.NonCapturingLazyInitializer.EnsureInitialized[TParam,TValue](TValue& target, TParam param, Func`2 valueFactory) in Microsoft.EntityFrameworkCore.Internal\NonCapturingLazyInitializer.cs:line 16
   at Microsoft.EntityFrameworkCore.Update.ModificationCommand.get_ColumnModifications() in Microsoft.EntityFrameworkCore.Update\ModificationCommand.cs:line 146
   at Microsoft.EntityFrameworkCore.Update.Internal.CommandBatchPreparer.<CreateCommandBatches>d__10.MoveNext() in Microsoft.EntityFrameworkCore.Update.Internal\CommandBatchPreparer.cs:line 80
   at Microsoft.EntityFrameworkCore.Update.Internal.CommandBatchPreparer.<BatchCommands>d__8.MoveNext() in Microsoft.EntityFrameworkCore.Update.Internal\CommandBatchPreparer.cs:line 63
   at Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.<ExecuteAsync>d__9.MoveNext() in Microsoft.EntityFrameworkCore.Update.Internal\BatchExecutor.cs:line 111
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.<SaveChangesAsync>d__111.MoveNext() in Microsoft.EntityFrameworkCore.ChangeTracking.Internal\StateManager.cs:line 851
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.<SaveChangesAsync>d__115.MoveNext() in Microsoft.EntityFrameworkCore.ChangeTracking.Internal\StateManager.cs:line 927
   at Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal.SqlServerExecutionStrategy.<ExecuteAsync>d__7`2.MoveNext()
   at Microsoft.EntityFrameworkCore.DbContext.<SaveChangesAsync>d__63.MoveNext() in Microsoft.EntityFrameworkCore\DbContext.cs:line 351
   at Microsoft.EntityFrameworkCore.DbContext.<SaveChangesAsync>d__63.MoveNext() in Microsoft.EntityFrameworkCore\DbContext.cs:line 375
   at EfTestJson.Data.ApplicationDbContext.<SaveChangesAsync>d__7.MoveNext() in D:\Projects\EFCore\EfTestJson\EfTestJson\Data\ApplicationDbContext.cs:line 31
   at Program.<<Main>$>d__0.MoveNext() in D:\Projects\EFCore\EfTestJson\EfTestJson\Program.cs:line 86
   at Program.<Main>(String[] args)

I´m using the .ToJson function. The configuration looks like this:

internal sealed class PollConfiguration : IEntityTypeConfiguration<Poll>
{
    public void Configure(EntityTypeBuilder<Poll> builder)
    {
        builder.ToTable("Polls");

        builder.HasKey(c => c.Id);
        builder.Property(c => c.Title).HasMaxLength(255).IsRequired();
        builder.Property(c => c.Description).HasMaxLength(1000).IsRequired(false);
        builder.Property(c => c.Start).IsRequired(false);
        builder.Property(c => c.End).IsRequired(false);
        builder.Property(c => c.Status).IsRequired(true);
        builder.OwnsMany(c => c.Categories, d =>
        {
            d.ToJson();
            d.OwnsMany(e => e.Tasks, e =>
            {
                e.ToJson();
                e.OwnsMany(f => f.TasksUsers, g =>
                {
                    g.ToJson();
                    g.Property(f => f.Percent).HasPrecision(18, 2);
                });
            });
        });
        builder.Property(c => c.CreationDate).IsRequired(false);
        builder.Property(c => c.LastUpdate).IsRequired(false);
        builder.Property(c => c.Version).IsRowVersion();
    }
}

So there is a poll object. This contains a list of "categories" stored as Json. Each category has a list of tasks. The tasks have a list of "TaskUsers".

The workflow is like this:

  • the user creates a poll (works)
  • later the user adds a category (works - the category does not contain tasks at this time)
  • then the user adds a task to a category - this is when the exception occurs

You can find a repo here: https://github.com/FM1973/EfTestJson.git

What am I missing? Any help is appreciated. Thanks!

PS: when I add a category already containing tasks there is no error. When I try to add another task later, the error occurs again.

Provider and version information

EF Core version: 8.0.4 Database provider: Microsoft.EntityFrameworkCore.SqlServer Target framework: .NET 8 Operating system: Windows 10 IDE: Visual Studio 2022 17.9.6

FM1973 avatar Apr 23 '24 07:04 FM1973

Just for info: what does work is to get the list of categories in a local variable and add a task to this local varaible. Then I set the categories list of the poll class to a new List<PollCategory> and save the changes (all categories gone). Then I set the polls categories list to the local variable I created before and save the changes.

This is a workaround, but a very bad one.

FM1973 avatar Apr 23 '24 13:04 FM1973

@FM1973 try the following configuration instead:

internal sealed class PollConfiguration : IEntityTypeConfiguration<Poll>
{
    public void Configure(EntityTypeBuilder<Poll> builder)
    {
        builder.ToTable("Polls");

        builder.HasKey(c => c.Id);
        builder.Property(c => c.Title).HasMaxLength(255).IsRequired();
        builder.Property(c => c.Description).HasMaxLength(1000).IsRequired(false);
        builder.Property(c => c.Start).IsRequired(false);
        builder.Property(c => c.End).IsRequired(false);
        builder.Property(c => c.Status).IsRequired(true);
        builder.OwnsMany(c => c.Categories, d =>
        {
            d.ToJson();
            d.OwnsMany(e => e.Tasks, e =>
            {
                e.OwnsMany(f => f.TasksUsers, g =>
                {
                    g.Property(f => f.Percent).HasPrecision(18, 2);
                });
            });
        });
        builder.Property(c => c.CreationDate).IsRequired(false);
        builder.Property(c => c.LastUpdate).IsRequired(false);
        builder.Property(c => c.Version).IsRowVersion();
    }
}

Basically only make call to ToJson() on the top level.

maumar avatar Apr 24 '24 23:04 maumar

Problem here is that if inner OwnsMany contains a ToJson call, the navigation gets mapped to a Tasks JSON column in the database model, instead of the top level JSON column (Categories). Instead, it should be a noop

maumar avatar Apr 24 '24 23:04 maumar

Oh man! One should read the docs. Sorry to have wasted your time. Works like a charm now! Thanks!

FM1973 avatar Apr 25 '24 05:04 FM1973

We still should fix this, so that other people don't get hit by this. Will re-open, but it's not a high priority, given the easy workaround

maumar avatar Apr 25 '24 22:04 maumar

after a bug fixing war I found this thank you

mfgok avatar Jun 03 '24 21:06 mfgok