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

2

5

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

Phenothiazine answered 11/10, 2012 at 15:53 Comment(0)
S
12

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));
    }
}
Sonja answered 30/8, 2013 at 19:11 Comment(6)
I like your answer, but what is the purpose of retaining (re-using) the ID? Is that a different outcome to just creating a new Dog, and deleting the old Animal? Thanks.Rematch
Sorry for the necromancing but I have a question. What happen if the Id is generated by DataBase like an auto increment?Samons
@Rematch Yes, it will absolutely have a different outcome - it will be an entirely different entity. The goal is to reuse the same entity, AKA row in the table. Creating a new Entity (Dog) will create a new row in the table and deleting the old Animal will delete the original row.Sonja
@Samons That doesn't matter - id generation will only happen when the entity doesn't already have an id (i.e. an id with the .NET default long value of 0). Now, if you're making the Id property read-only to force auto-generation and not allow anyone to programatically set it... yeah, that's not going to work. Try exposing the setter as internal to allow it to be set.Sonja
this doesn't change the Discriminator to Dog though. It remains Animal. Why is that so?Jannery
Did anyone try this solution? It didn't work. It throws DbUpdateConcurrencyException. I'm using EF 6.Agler
R
0

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.

Rematch answered 26/9, 2014 at 19:45 Comment(1)
Ok thanks that's probably the better way to do it in some circumstances. What about if there is a lot of data associated with that User entry? Then you have to go and modify all the associated data. And then this functionality will break everything you update the database structure with another connection to the User table.Phenothiazine

© 2022 - 2024 — McMap. All rights reserved.