3

What is the best way to move child entities from one parent entity to another? Is there a method inside the ObjectContext or DbContext that allows us to accomplish this?

public class Person
{
   public int PersonId
   public ICollection<Car> Cars
}

public class Car
{
   public int CarId
   public string Color
}

EDIT: I'm currently using EF 4.0 model first with POCO template.

svick
  • 236,525
  • 50
  • 385
  • 514
madatanic
  • 1,760
  • 3
  • 16
  • 28
  • Very much dependant on how you using EF (e.g. code first, schema first etc)... are you using POCO, proxies etc. E.g. if you're using the template generated entities or proxies you should just be able to change the relationship in memory and save. – James Gaunt Feb 21 '12 at 17:23
  • @JamesGaunt: Thanks James, I have editted my question. – madatanic Feb 21 '12 at 17:25
  • Did you try removing from one collection and adding to another? – usr Feb 21 '12 at 17:51
  • @usr: I haven't tried that yet. I just wanted to get opinions from the audience, also best practices. – madatanic Feb 21 '12 at 17:56
  • If you're using POCO with proxies then you the right thing to do is just edit the collection in memory and save. If you're not using proxies - you can ask the framework to manually detect changes (context.DetectChanges()). – James Gaunt Feb 21 '12 at 18:08

2 Answers2

2

I'd say what you want to accomplish is changing the owner of the car in this example. If there are no serious cons against adding a back reference to Person in the Car i'd go with something like:

public class Car
{
   ...
   public virtual Person Owner { get; protected set; }

   public void ChangeOwner(Person newOwner)
   {
          // perform validation and then 
          Owner = newOwner;
          // maybe perform some further domain-specific logic
   }
}

NOTE: the protected setter is to enforce calling the ChangeOwner method by external consumers. EF wil be able to set it properly thanks to the autogenerated proxies for POCO classes (assume you use them).

EDIT: In case there is no possibility to add a back reference to Person, you still have have the same goal looking from the domain logic perspective. You just want to change owner of a car. Such operation involves two entites so i'd probably go with a method placed somewhere outside the entity (regardless of where it should be placed in a well designed system):

public void ChangeCarOwner(Person originalOwner, Person newOwner, int carId)
{
    Car car = originalOwner.RemoveCarOwnership(carId);
    newOwner.AddCarOwnership(car);
}

public class Person
{
    ...
    public Car RemoveCarOwnership(int carId)
    {
        Car car = this.Cars.Single(c => c.Id == carId);
        this.Cars.Remove(car);
        return car;
    }
}

This is just a conceptual piece of code and it most certainly can be written better (making sure the old owner actually owns the car etc.), but i just wanted to present an idea of how would i approach it. I also ommited the implementation of AddCarOwnership cause i suppose it's pretty strainghtforward. I introduced those methods cause adding and removing ownership may trigger some further logic "inside" a particular person.

dmusial
  • 1,504
  • 15
  • 14
  • if Car does not have a navigation property Owner, how would you approach this? – madatanic Feb 23 '12 at 00:26
  • So basically, you remove "Car" from one person and add that car to another person. That's what I ended up doing. – madatanic Feb 24 '12 at 22:58
  • If you have lazy loading enabled, as it is by default, then `this.Cars.Remove(car);` has the side effect that it will load all cars owned by this person from the database. Need to be aware of this as it can become a performance issue in some cases. – Jonathan Wood Oct 08 '15 at 23:14
1

With modern EFCore, you can do this very simply by Attaching the new Parent entity, which contains Children with IDs in them. It will reassign the FK of the child (or create it if no ID is specified), and create the new Person, all in one go

Ex:

var newOwner = new Person { 
    Cars = new List<Car> { 
        new Car { carId = theCarToMove.carId } 
    } 
};
Context.Attach(newOwner);
await Context.SaveChangesAsync();

Beware that Attach can cause problems if your Context isn't truly transient, but as a bandaid you could always clear the ChangeTracker before attempting an Attach

EDIT: After trying this, I found that for some DB providers, it doesn't work directly. Instead, try:

foreach(var car in carsToMove)
{
    Context.Attach(car);
    car.Owner = newOwner;
}
Context.Attach(newOwner);
await Context.SaveChangesAsync();

Order matters when using Attach. The SQL query built by EFCore builds in reverse of the order you set it up in C#. If you attach the newOwner before the cars, the CREATE query is the last thing in the SQL, after the UPDATE for the cars. If this is the case, the cars can't UPDATE to the new OwnerId, because the newOwner did not have an ID at that point in the query. I believe this is also what's happening with the first code block, with some providers

D Mentia
  • 11
  • 3