Search code examples
entity-framework-coretable-splitting

Optional table splitting in Entity Framework Core


How do I get Entity Framework Core to handle optional table splitting?

I have these classes:

public class temp
{
    [Key, Required]
    public Guid tempid { get; set; }

    [Required(AllowEmptyStrings = true), MaxLength(50)]
    public string name { get; set; }

    [MaxLength(100)]
    public string filename { get; set; }

    [Required]
    public virtual tempContent tempContent { get; set; }

    public temp()
    {
        tempid = Guid.NewGuid();
    }

    public override string ToString()
    {
        return name;
    }

    public override bool Equals(object obj)
    {
        if (obj == null || GetType() != obj.GetType()) return false;

        temp other = (temp)obj;

        return tempid == other.tempid;
    }

    public override int GetHashCode()
    {
        return tempid.GetHashCode();
    }
}

public class tempContent
{
    [Key, Required]
    public Guid tempid { get; set; }

    public byte[] filecontents { get; set; }

    [Required]
    public virtual temp temp { get; set; }
}

Then I have:

public DbSet<temp> temps { get; set; }
public DbSet<tempContent> tempContents { get; set; }

I define the split with this setup code:

modelBuilder.Entity<tempContent>(o => o.ToTable("temps"));

modelBuilder.Entity<temp>(temp =>
{
    temp.HasOne(o => o.tempContent).WithOne(o => o.temp).HasForeignKey<temp>(o => o.tempid);
    temp.Navigation(o => o.tempContent).IsRequired();
});

When I try add an instance without the filename/content, like so:

temp = new temp();
temp.name = "Test";
db.Entry(temp).State = EntityState.Added;
await db.SaveChangesAsync();

I get :

Microsoft.EntityFrameworkCore.DbUpdateConcurrencyException: The database operation was expected to affect 1 row(s), but actually affected 0 row(s); data may have been modified or deleted since entities were loaded.


Solution

  • A few things. What you have defined is not table splitting, it is defining a one-to-one relationship. Table splitting specific to EF Core applies to using Owned entities rather than more open one-to-one. As far as the database schema is concerned, they can be identical, however how EF treats an owned relationship is a bit more constrained. Normally owned one-to-one entities use table splitting, which really should be considered "table sharing" in that the table is split between two related entities. A sub-set of columns in the parent table. Alternatively you can tell EF to move the owned entity into its own table so your desired Temp and TempContent tables. The key difference between Owned and regular one-to-one relationships is that an owned entity is never allowed to be used independently of the parent table. This makes sense as you should never really be using TempContent without the Temp. This means that there can be no DbSet<TempContent> in the DbContext.

    The relationship configuration would change slightly:

    modelBuilder.Entity<Temp>(temp =>
    {
        temp.OwnsOne(t => t.TempContent, tc => {tc,ToTable("tempContents")});
    });
    

    The next snag will be with the PK definitions for your Temp (and TempContents) tables. By default EF expects PKs that adopt the naming convention of either "Id", "{entityName}Id" or "{entityName}_Id" to be Identity columns. If you want to use client-side generated keys (not recommended) then you have to tell EF to expect the keys to be set:

    [Key, DatabaseGenerated(DatabaseGeneratedOption.None)]
    public Guid TempId { get; set; }
    

    Lastly, because you have an owned / required relationship between Temp and TempContent, when you add a new Temp, you "require" the content, so be sure to populate that on insert. As mentioned, use DbSet.Add to insert new entities. The only reason to be using EntityState is when dealing with detached entities or edge cases where you want to revert specific changes.

    temp = new Temp
    {
        name = fileName,
        tempContent = new TempContent { filecontents = fileData /* wherever you get the data for the file }
    };
    
    db.Temps.Add(temp);
    await db.SaveChangesAsync();
    

    Fix those main issues and you should be inserting records as expected.

    I would strongly recommend following standard C# coding conventions for naming with CamelCase for class and public member names. Entities can use attributes or config to translate down to other naming conventions used in the database.