Entity Framework 6 and Collections With DDD
If you start to work with Entity Framework 6 and a real Domain modeled following the SOLID principles and most common known rules of DDD (Domain Driven Design) you will also start to clash with some limits imposed by this ORM.
Let’s start with a classic example of a normal Entity that we define as UserRootAggregate. For this root aggregate we have defined some business rules as following:
- A User Entity is a root aggregate
- A User Entity can hold 0 or infinite amount of UserSettings objects
- A UserSetting can be created only within the context of a User root aggregate
- A UserSetting can be modified or deleted only within the context of a User root aggregate
- A UserSetting hold a reference to a parent User
Based on this normal DDD principles I will create the two following objects:
A User Entity is a root aggregate
/// My Root Aggregate public class User : IRootAggregate { public Guid Id { get; set; } /// A root aggregate can be created public User() { } }
A User Entity can hold 0 or infinite amount of UserSettings
public class User : IRootAggregate { public Guid Id { get; set; } public virtual ICollection<UserSetting> Settings { get; set; } public User() { this.Settings = new HashSet<Setting>(); } }
A UserSetting can be created or modified or deleted only within the context of a User root aggregate
public class UserSetting { public Guid Id { get; set; } public string Value { get; set; } public User User { get; set; } internal UserSetting(User user, string value) { this.Value = value; this.User = user; } } /// inside the User class public void CreateSetting(string value) { var setting = new UserSetting (this, value); this.Settings.Add(setting) } public void ModifySetting(Guid id, string value) { var setting = this.Settings.First(x => x.Id == id); setting.Value = value; } public void DeleteSetting(Guid id) { var setting = this.Settings.First(x => x.Id == id); this.Settings.Remove(setting); }
</ol>
So far so good, Now, considering that we have a Foreign Key between the table UserSetting and the table User we can easily map the relationship with this class:
public class PersonSettingMap : EntityTypeConfiguration<PersonSetting> { public PersonSettingMap() { HasRequired(x => x.User) .WithMany(x => x.Settings) .Map(cfg => cfg.MapKey("UserID")) .WillCascadeOnDelete(true); } }
Now below I want to show you the strange behavior of Entity Framework 6.
If you Add a child object and save the context Entity Framework will properly generate the INSERT statement:
using (DbContext context = new DbContext) { var user = context.Set<User>().First(); user.CreateSetting("my value"); context.SaveChanges(); }
If you try to UPDATE a child object, again EF is smart enough and will do the same UPDATE statement you would like to get issued:
using (DbContext context = new DbContext) { var user = context.Set<User>() .Include(x => x.Settings).First(); var setting = user.Settings.First(); setting.Value = "new value"; context.SaveChanges(); }
The problem occurs with the DELETE. Actually you would issue this C# statement and think that Entity Framework like any other ORM does already, will be smart enough to issue the DELETE statement …
using (DbContext context = new DbContext) { var user = context.Set<User>() .Include(x => x.Settings).First(); var setting = user.Settings.First(); user.DeleteSetting(setting.Id); context.SaveChanges(); }
But you will get a nice Exception has below:
_System.Data.Entity.Infrastructure.DbUpdateException: </p>
An error occurred while saving entities that do not expose foreign key properties for their relationships.
The EntityEntries property will return null because a single entity cannot be identified as the source of the exception.
Handling of exceptions while saving can be made easier by exposing foreign key properties in your entity types.
See the InnerException for details. —>
System.Data.Entity.Core.UpdateException: _**A relationship from the ‘UserSetting_User’ AssociationSet is in the ‘Deleted’ state.
</u>**_Given multiplicity constraints, a corresponding ‘UserSetting_User_Source’ must also in the ‘Deleted’ state.</em></blockquote>
So this means that EF does not understand that we want to delete the Child object. So inside the scope of our Database Context we have to do this:
using (DbContext context = new DbContext) { var user = context.Set<User>() .Include(x => x.Settings).First(); var setting = user.Settings.First(); user.DeleteSetting(setting);
// inform EF context.Entry(setting.Id).State = EntityState.Deleted;
context.SaveChanges(); }</pre>
I have searched a lot about this problem and actually you can read from the Entity Framework team that this is a feature that is still not available for the product: