简体   繁体   中英

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="bob@mail.bob"});
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

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));
    }
}

Accepted answer didn't work for me. Either by using multiple contexts per database, or because EF6 somehow ignores this.

Only solution that worked for me is the one I described in this answer: https://stackoverflow.com/a/28380804/2424989

I would create a new record and delete the old record. I don't think you should effectively change the class of an existing object, nor delete and create records attempting to reuse the database key.

Database primary keys should be meaningless. If you need to assign a meaningful ID to your records then add a new field for that. Think of your stack overflow ID, I bet that's not the primary key in their database.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM