4

Here is a simple entity:

public class Customer : Entity
{
    public virtual Location Location { get; set; }
}

Now suppose we have a customer already:

var customer = new Customer() {Location = new Location("China")};

and now we want to update his location:

var customer = context.Customers.First(x => x.Location.Country == "China");
customer.Location = new Location("America");
context.SaveChanges();

and now when I look at the database, the location record "China" has not been deleted: the database now has two location records association with one customer record.

The reason for this issue is that I'm using virtual keyword on the Customer.Location property, and when I query the customer entity from database I didn't use Include method to load the Location property, and I also did not use any accesses to lazy load it. So EF can't track and know the China Location entity should be deleted.

I think the approach I used to the update virtual property is in line with intuition. I want update a property, then just use a update instruction "entity.xxx=...", add being forced to use some access of the property or method call while loading "entity.xxx" is not intuitive.

So I am looking for some better way to replace an entity's virtual property directly. Any suggestions?


Solutions update

I'm find two way to do this easy,

First you can use Identifying relation(recommend).

Another way you can Use ObjectContext.DeleteObject method, below is the example code:

public static class EFCollectionHelper
{
    public static void UpdateCollection<T, TValue>(this T target, 
Expression<Func<T, IEnumerable<TValue>>> memberLamda, TValue value)where T : Entity
    {
        var memberSelectorExpression = (MemberExpression)memberLamda.Body;
        var property = (PropertyInfo)memberSelectorExpression.Member;

        var oldCollection = memberLamda.Compile()(target);
        oldCollection.ClearUp();

        property.SetValue(target, value, null);
    }

    public static void ClearUp<T>(this IEnumerable<T> collection)
    {
        //Convert your DbContext to IObjectContextAdapter
        var objContext = ((IObjectContextAdapter) Program.DbContext).ObjectContext;
        for (int i = 0; i < collection.Count(); i++)
        {
            objContext.DeleteObject(collection.ElementAt(i));
        }
    }
}

And then you can simply write the code like:

customer.UpdateCollection(x => x.Locations, null);
Community
  • 1
  • 1
小广东
  • 1,151
  • 12
  • 20
  • When you say "the location record "China" has not been deleted: the database now has two location records" do you mean that the database now has 2 customer records, one with China and one with America as Locations? Or do you mean that the there are now two Location records, as it seems you are saying. If there are now two Location records, that is not necessarily wrong. If your Location table is a lookup type table that is supposed to have possible locations, you now have two possible locations in the table, and your customer record is now correct because it points at America (American). – DWright Apr 25 '15 at 15:58
  • @DWright, sorry for the confuse, the database now have two Location records association with one Customer record, and yes the customer.Location will points at America, but in database it will generate a Location record every time when I use "customer.Location = new Location()" to update customer location. – 小广东 Apr 25 '15 at 16:12
  • And another way, if the virtual property is a collection, then use "customer.collection=xxx..." will insert a new collection into database, and next time you get customer from database, "customer.collection" will be double items actually. – 小广东 Apr 25 '15 at 16:15

2 Answers2

4

Not completely sure what you want, but this is what i got.

The reason for you now getting two locations are because you use new Location("American"); you actually add a reference to a new location (EF don't know if China is used by another customer, and would never delete it, in that type of query)

Now if you said.

customer.Location.Country = "America"

The China would be overwritten by America, as we are now working with a specific Location's property.

Read the coments on the question so a little extras

If you want to update the location fully (new Location("Some new location")). Then you would do it like this.

Location oldLocation = customer.Location;
Location newLocation = new Location("America");
//Check if the new location country !exist
if(!context.Locations.Any(a=> a.Country == newLocation.Country))
{
    //If it don't exist then add it (avoiding the location duplicate)
    customer.Location = newLocation;
    //If its important to delete the old location then do this
    //(important to do after you removed the dependency, 
    //thats why its after the new location is added)
    context.Locations.Remove(oldLocation)
    //Finally Save the changes
    context.SaveChanges();
}
Thomas Andreè Wang
  • 3,379
  • 6
  • 37
  • 53
  • Yes I have what try your suggest actually, it work fine. But what I really want is "replace" not just "update", if I want to update a collection property of customer, because the new collection items count maybe not equal old collection, this solution will not work. – 小广东 Apr 25 '15 at 16:21
  • A database dont replace that whould require the database to find the data and run delete SQL query on it. – Thomas Andreè Wang Apr 25 '15 at 16:25
  • Exactly! so is there any way let the EF generate delete SQL query when I use "entity.xxxx=..."? – 小广东 Apr 25 '15 at 16:28
  • edited the answer. you can temp the entity before you change the location and delete it afterwards. (Alittle untested, just writing from my head, but it should work) – Thomas Andreè Wang Apr 25 '15 at 16:28
  • yes it should work, and now I trying research a more concise way to do this, thank you very much :) – 小广东 Apr 25 '15 at 16:50
1

Another way to update an entity is using the Entry.OriginalValues.SetValues method:

var currentLocation = context.Customers.First(x => x.Location.Country == "China").Select(c => c.Location);
context.Entry(currentLocation).OriginalValues.SetValues(newLocation) ;
context.SaveChanges();
Mohsen Esmailpour
  • 11,224
  • 3
  • 45
  • 66