Entity Framework proper way to replace collection

2019-04-21 03:29发布

问题:

Suppose a customer has many phone numbers and a phone number has only one customer.

public class PhoneNumber : IValueObject {
  public string Number {get; set;}
  public string Type {get; set;}
}

public class Customer : IEntity {
   public ICollection<PhoneNumber> phones {get; private set;} //ew at no encapsulated collection support
   public void SetPhones(params PhoneNumber[] phones) {
       this.phones.Clear();
       this.phones.AddRange(phones);
   }
}

If I do an EF mapping like this and run it, every time I set phone numbers it will create new PhoneNumbers but not delete the old ones. There are no other entities referencing phone numbers, I don't even expose it on my dbcontext, is there a way to tell EF that Customer owns PhoneNumbers completely and therefore if phone numbers were removed from the collection they should be deleted?

I realize that there's a dozen ways to hack around this problem, but this isn't a weird edge case, what's the "right" way to handle this.

回答1:

I had the exact same question :)

This answer on identifying relationships solved my issue.

Note: You have to load the collection (eagerly, explicitly or lazily) so that it can be tracked before setting the new values and calling save. Otherwise you will not be replacing the collection but, just be adding to it.

For example:

var entity = unitOfWork.EntityRepository.GetById(model.Id);
// I have something like this to load collection because
// I don't have the collection's entities exposed to the context
unitOfWork.EntityRepository.LoadCollection(entity, e => e.CollectionProperty);
entity.CollectionProperty = newCollectionValuesList;
unitOfWork.Save();

This will remove the previous collection values from the 'collection table' and only add the newly set values.

Hope that helps.



回答2:

First (optional):

I recommend you to make

public ICollection<PhoneNumber> phones {get; private set;}

a virtual property, to let the Entity Framework know that it should be lazy loaded (even if you do not have Lazy Load enabled, it is a good practice).

public virtual ICollection<PhoneNumber> phones {get; private set;}

Second:

Add an Inverse Navigation Property on your PhoneNumber class (it will be required in order to achieve the solution I give you below):

public class PhoneNumber : IValueObject {
  public string Number {get; set;}
  public string Type {get; set;}

  public virtual Customer {get; set;}
}

public class Customer : IEntity {
   public ICollection<PhoneNumber> phones {get; private set;} //ew at no encapsulated collection support
   public void SetPhones(params PhoneNumber[] phones) {
       this.phones.Clear();
       this.phones.AddRange(phones);
   }
}

Third (Possible solution for your problem):

Remove the PhoneNumber objects from Context instead of doing so from the Customer:

public ICollection<PhoneNumber> phones {get; private set;} //ew at no encapsulated collection support
   public void SetPhones(params PhoneNumber[] phones) {
       Context.PhoneNumbers.RemoveRange(this.phones);
       this.phones.AddRange(phones);
   }
}