Search code examples
c#.netentity-frameworkef-code-firsttable-per-type

Converting a EF CodeFirst Base class to a Inherited class (using table-per-type)


I am using EF Code First and have two classes defined as follows:

public class User
{
    public int Id { get; set; }
    public string Username { get; set; }
    public string Email { get; set; }
}

[Table("Visitors")]
public class Visitor : User
{
    public Visitor()
    {
        Favourites = new List<Building>();
    }
    public virtual IList<Building> Favourites { get; set; }
}

This uses Table-Per-Type inheritance and defines the DB schema as follows:

Users Table
    Id int PK
    Username nvarchar(max)
    Email nvarchar(max)
Visitors Table
    Id int PK (FK to Users table)

This is exactly how I wanted it to structure it. Now my question is, if I create a User object and save it to the DB, how will I later on be able to extend that into a Visitor (if I need to?) Will I need to delete the user and create a new Visitor or can I some how cast the user into a visitor object and the entry in the user table will remain intact and a new entry will be added to the visitor table referencing the user? Something like the below code?

Context.Set<User>().Add(new User(){Id=1, Username="Bob", Email="[email protected]"});
Context.SaveChanges();

//and elsewhere in the project I want to do this sort of thing:
Context.Set<Visitor>().Where(v=>v.Id == 1).FirstOrDefault().Favourites.Add(someFavouriteBuilding); //This obviously doesn't work, because the FirstOrDefault call returns null, so it will throw an exception
Context.SaveChanges();

//or maybe this can be modified slightly to work?:
var visitor = Context.Set<Visitor>().Where(v=>v.Id == 1).FirstOrDefault();
if (visitor==null)
{
    visitor = new Visitor(Context.Set<User>().Where(u=>u.Id == 1).FirstOrDefault()); // this contructor copies all the property values accross and returns a new object
}
visitor.Favourites.Add(someFavouriteBuilding); //This obviously doesn't work either
var entry = Context.Entry(visitor);
entry.State = EntityState.Modified;//here it throws this error: An object with the same key already exists in the ObjectStateManager. The ObjectStateManager cannot track multiple objects with the same key.
Context.SaveChanges();

I think the second approach in the above code may work if I can only attach it to the context correctly. Anyway, the code above is only to show you what I am trying to achieve. I know it will not work. Can anyone suggest a more elegent approach?

Thank you


Solution

  • You were almost there... The key is to detach the existing entity, then attach the new one.

    Here's an example:

    using System.Data;
    using System.Data.Entity;
    using System.Diagnostics;
    
    public class Animal
    {
        public long Id { get; set; }
    }
    
    public class Dog : Animal
    {
    }
    
    public class AnimalsContext : DbContext
    {
        public DbSet<Animal> Animals { get; set; }
    }
    
    
    public class Tester
    {
        public void Test()
        {
            var context = new AnimalsContext();
    
    
            var genericAnimal = new Animal();
            context.Animals.Add(genericAnimal);
            context.SaveChanges();
    
    
            // Make a new clean entity, but copy the ID (important!)
            var dog = new Dog { Id = genericAnimal.Id, };
    
            // Do the old switch-a-roo -- detach the existing one and attach the new one
            // NOTE: the order is important!  Detach existing FIRST, then attach the new one
            context.Entry(genericAnimal).State = EntityState.Detached;
            context.Entry(dog).State = EntityState.Modified;
            context.SaveChanges();
    
    
            var thisShouldBeADog = context.Animals.Find(genericAnimal.Id);
    
            // thisShouldBeADog is indeed a Dog!
            Debug.Assert(thisShouldBeADog is Dog);
    
            // And, of course, all the IDs match because it's the same entity
            Debug.Assert((genericAnimal.Id == dog.Id) && (dog.Id == thisShouldBeADog.Id));
        }
    }