olson.jeffery
olson.jeffery

Reputation: 13

EF Core: Can add and save parent entity, but fail to save child entities at all

I have a very basic scenario, outlined in the complete test case below. I am using .NET core 5.0.2, EF Core 5.0.3, running via vscode on win10. I am working with nullable reference types enabled, as well (as the code below illustrates).

If you jump all the way to the bottom and look at the unit test, it is failing based on the entities that should be added in the InjectRecords() method. The Child entries are never added.

I have tried all combinations of "add child via the parent's navigation collection, add child via directly adding to DbSet and setting parent FK as object OR integer key" and it only works when I reconfigure the model to destroy all of the relationships between Parent and Child and keep things reduced to weak references. There is obviously some very basic issue I am missing, because this isn't an interesting or expansive scenario at all. Very basic. I have also scoured google and SO for explanations, but have hit upon nothing to illustrate why this is not working.

Everything is in the test case below. On my system, when I run this, it fails starting at the second assertion. I would expect that all three assertions would pass (Pulling at Parents, pulling at Childs from the Childs DbSet, accessing the Childs via the Parent they were added to in InjectRecords()).

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Xunit;

namespace TIL.Tests
{

  public class Parent
  {
    public int Id { get; set; }
    public ICollection<Child> Childs => new List<Child>();
  }
  public class Child
  {
    public int Id { get; set; }

    public int ParentId { get; set; }
    public Parent Parent { get; set; } = default!;
  }

  public class TestDbContext : DbContext
  {
    public TestDbContext(DbContextOptions<TestDbContext> options) : base(options) { }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
      modelBuilder.Entity<Parent>()
        .HasMany(gd => gd.Childs)
        .WithOne(ge => ge.Parent);
    }

    public virtual DbSet<Child> Childs => Set<Child>();
    public virtual DbSet<Parent> Parents => Set<Parent>();
  }

  public static class TestDatabaseInitializer
  {
    public static string SqliteConnectionString = "DataSource=myshareddb;mode=memory;cache=shared";
    public static void ConfigureServices(IServiceCollection services)
    {
      var kaConn = new SqliteConnection(SqliteConnectionString);
      kaConn.Open();
      services.AddDbContext<TestDbContext>(options =>
      {
        options.UseSqlite(SqliteConnectionString);
      });
    }

    public async static Task DoCtxAction(DbContextOptions<TestDbContext> options, Func<TestDbContext, Task> action)
    {
      using (var ctx = new TestDbContext(options))
      {
        await action(ctx);
      }
    }

    public async static Task InjectRecords(IServiceProvider svcProvider)
    {
      var options = svcProvider.GetRequiredService<DbContextOptions<TestDbContext>>();
      await DoCtxAction(options, async (ctx) =>
      {
        await ctx.Database.EnsureCreatedAsync();

        var parent1 = new Parent { };
        var parent2 = new Parent { };
        ctx.Parents.AddRange(parent1, parent2);
        await ctx.SaveChangesAsync();
      });

      await DoCtxAction(options, async (ctx) =>
      {
        var parent1 = await ctx.Parents.FindAsync(1);
        var child1 = new Child
        {
        };
        var child2 = new Child
        {
        };
        parent1.Childs.Add(child1);
        parent1.Childs.Add(child2);
        await ctx.SaveChangesAsync();
      });
    }
  }


  public class DataLayerIntegrationTests
  {
    [Fact]
    public async Task DataLayerWorks()
    {
      var services = new ServiceCollection();
      TestDatabaseInitializer.ConfigureServices(services);
      using (var scope = services.BuildServiceProvider().CreateScope())
      {
        var serviceProvider = scope.ServiceProvider;
        try
        {
          await TestDatabaseInitializer.InjectRecords(serviceProvider);
        }
        catch (Exception e)
        {
          var f = e.Message;
        }

        var options = serviceProvider.GetRequiredService<DbContextOptions<TestDbContext>>();
        await TestDatabaseInitializer.DoCtxAction(options, async ctx => {
          var parents = await ctx.Parents.Include(x=>x.Childs).ToListAsync();
          Assert.Equal(2, parents.Count);

          var children = await ctx.Childs.ToListAsync();
          Assert.Equal(2, children.Count());

          Assert.Equal(2, parents.Where(x=>x.Id == 1).Single().Childs.Count());
        });
      }
    }
  }
}

Upvotes: 1

Views: 1474

Answers (2)

atiyar
atiyar

Reputation: 8305

Change the Parent model to -

public class Parent
{
    public Parent()
    {
        this.Childs = new List<Child>();
    }
    
    public int Id { get; set; }
    
    public ICollection<Child> Childs { get; set; }
}

so that the Childs collection gets initialized only once, during the Parent instantiation.

Upvotes: 0

David Browne - Microsoft
David Browne - Microsoft

Reputation: 89091

Your expression-bodied property is creating a new empty List<Child> on every call.
This

  public class Parent
  {
    public int Id { get; set; }
    public ICollection<Child> Childs => new List<Child>();
  }

is equivilent to

public class Parent
{
    public int Id { get; set; }
    public ICollection<Child> Childs 
    { 
        get
        {
            return new List<Child>();
        }
     } 
}

And instead should be an auto-initialized read-only property:

public class Parent
{
    public int Id { get; set; }
    public ICollection<Child> Childs { get; } = new List<Child>();
}

Upvotes: 2

Related Questions