I am currently working on a project using the latest version of Entity Framework and I have come across an issue which I can not seem to solve.
When it comes to updating existing objects, I can fairly easily update the object properties ok, until it comes to a property which is a reference to another class.
In the below example I have a class called Foo, which stores various properties, with 2 of these being instances of other classes
public class Foo
{
public int Id {get; set;}
public string Name {get; set;}
public SubFoo SubFoo {get; set}
public AnotherSubFoo AnotherSubFoo {get; set}
}
When I use the below Edit() method, I pass in the object I wish to update and I can manage to get the Name to properly update, however I have not managed to find a way in which to get the properties of the SubFoo to change. For example, if the SubFoo class has a property of Name, and this has been changed and is different between my DB and the newFoo, it does not get updated.
public Foo Edit(Foo newFoo)
{
var dbFoo = context.Foo
.Include(x => x.SubFoo)
.Include(x => x.AnotherSubFoo)
.Single(c => c.Id == newFoo.Id);
var entry = context.Entry<Foo>(dbFoo);
entry.OriginalValues.SetValues(dbFoo);
entry.CurrentValues.SetValues(newFoo);
context.SaveChanges();
return newFoo;
}
Any help or pointers would be greatly appreciated.
UPDATE:
Based on the comment by Slauma I have modified my method to
public Foo Edit(Foo newFoo)
{
var dbFoo = context.Foo
.Include(x => x.SubFoo)
.Include(x => x.AnotherSubFoo)
.Single(c => c.Id == newFoo.Id);
context.Entry(dbFoo).CurrentValues.SetValues(newFoo);
context.Entry(dbFoo.SubFoo).CurrentValues.SetValues(newFoo.SubFoo);
context.SaveChanges();
return newFoo;
}
When running this now, I get the error:
The entity type Collection`1 is not part of the model for the current
context.
To try and get around this, I added code to try to attach the newFoo subclasses to the context, but this through an error saying that the ObjectManager already had an entity the same:
An object with the same key already exists in the ObjectStateManager.
The ObjectStateManager cannot track multiple objects with the same
key
CurrentValues.SetValues only updates scalar properties but no related entities, so you must do the same for each related entity:
public Foo Edit(Foo newFoo)
{
var dbFoo = context.Foo
.Include(x => x.SubFoo)
.Include(x => x.AnotherSubFoo)
.Single(c => c.Id == newFoo.Id);
context.Entry(dbFoo).CurrentValues.SetValues(newFoo);
context.Entry(dbFoo.SubFoo).CurrentValues.SetValues(newFoo.SubFoo);
context.Entry(dbFoo.AnotherSubFoo).CurrentValues.SetValues(newFoo.AnotherSubFoo);
context.SaveChanges();
return newFoo;
}
If the relationship could have been removed altogether or have been created you also need to handle those cases explicitly:
public Foo Edit(Foo newFoo)
{
var dbFoo = context.Foo
.Include(x => x.SubFoo)
.Include(x => x.AnotherSubFoo)
.Single(c => c.Id == newFoo.Id);
context.Entry(dbFoo).CurrentValues.SetValues(newFoo);
if (dbFoo.SubFoo != null)
{
if (newFoo.SubFoo != null)
{
if (dbFoo.SubFoo.Id == newFoo.SubFoo.Id)
// no relationship change, only scalar prop.
context.Entry(dbFoo.SubFoo).CurrentValues.SetValues(newFoo.SubFoo);
else
{
// Relationship change
// Attach assumes that newFoo.SubFoo is an existing entity
context.SubFoos.Attach(newFoo.SubFoo);
dbFoo.SubFoo = newFoo.SubFoo;
}
}
else // relationship has been removed
dbFoo.SubFoo = null;
}
else
{
if (newFoo.SubFoo != null) // relationship has been added
{
// Attach assumes that newFoo.SubFoo is an existing entity
context.SubFoos.Attach(newFoo.SubFoo);
dbFoo.SubFoo = newFoo.SubFoo;
}
// else -> old and new SubFoo is null -> nothing to do
}
// the same logic for AnotherSubFoo ...
context.SaveChanges();
return newFoo;
}
You eventually also need to set the state of the attached entities to Modified if the relationship has been changed and the scalar properties as well.
Edit
If - according to your comment - Foo.SubFoo is actually a collection and not only a reference you will need something like this to update the related entities:
public Foo Edit(Foo newFoo)
{
var dbFoo = context.Foo
.Include(x => x.SubFoo)
.Include(x => x.AnotherSubFoo)
.Single(c => c.Id == newFoo.Id);
// Update foo (works only for scalar properties)
context.Entry(dbFoo).CurrentValues.SetValues(newFoo);
// Delete subFoos from database that are not in the newFoo.SubFoo collection
foreach (var dbSubFoo in dbFoo.SubFoo.ToList())
if (!newFoo.SubFoo.Any(s => s.Id == dbSubFoo.Id))
context.SubFoos.Remove(dbSubFoo);
foreach (var newSubFoo in newFoo.SubFoo)
{
var dbSubFoo = dbFoo.SubFoo.SingleOrDefault(s => s.Id == newSubFoo.Id);
if (dbSubFoo != null)
// Update subFoos that are in the newFoo.SubFoo collection
context.Entry(dbSubFoo).CurrentValues.SetValues(newSubFoo);
else
// Insert subFoos into the database that are not
// in the dbFoo.subFoo collection
dbFoo.SubFoo.Add(newSubFoo);
}
// and the same for AnotherSubFoo...
db.SaveChanges();
return newFoo;
}
Just thought I would post the link below as it really helped me understand how to update related entities.
Updating related data with the entity framework in an asp net mvc application
NOTE: I did change the logic slightly that is shown in the UpdateInstructorCourses function to suit my needs.
You can also call the tables independently
MyContext db = new MyContext
// I like using asynchronous calls in my API methods
var OldFoo = await db.Foo.FindAsync(id);
var OldAssociateFoo = db.AssociatedFoo;
var NewFoo = OldFoo;
var NewAssociatedFoo = OldAssociatedFoo;
NewFoo.SomeValue = "The Value";
NewAssociatedFoo.OtherValue = 20;
db.Entry(OldFoo).CurrentValues.SetValues(NewFoo);
db.Entry(OldAssociatedFoo).CurrentValues.SetValues(NewAssociatedFoo);
await db.SaveChangesAsync();
Related
I am using Entity Framework Core 5 in a .NET 5.0 web app. I have two classes that are joined in a many-to-many relationship. I am using 'direct' approach, as described here. This mean that I do not have joining tables explicitly defined in code; instead, EF has inferred the relationship from the following schema:
public class User
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int ID { get; set; }
public ICollection<Group> Groups { get; set; }
}
public class Group
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int ID { get; set; }
public ICollection<User> Users { get; set; }
}
I wish to be able to update a 'Group' or 'User' by simply providing EF with a new object. For example, if I provide EF with a 'Group' object that has the same ID as a Group already in the database, but it now has an extra 'User' in the Users collection, then I would expect EF to update the 'UserGroup' table that it has made behind the scenes with a new record to represent this relationship.
Here is what I have tried:
1.
public void Update(Group group)
{
Group oldGroup = _context.Groups
.Include(g => g.Users)
.First(g => g.ID == group.ID);
_context.Entry(oldGroup).CurrentValues.SetValues(group);
_context.SaveChanges();
}
Result: Saves changes to any props belonging to the Group object, but does not affect any relationships.
2.
public void Update(Group group)
{
Group oldGroup = _context.Groups
.Include(g => g.Users)
.First(g => g.ID == group.ID);
oldGroup.Users = group.Users;
_context.Entry(oldGroup).CurrentValues.SetValues(group);
_context.SaveChanges();
}
Result: Exception.
System.InvalidOperationException: 'The instance of entity type 'User' cannot be tracked because another instance with the same key value for {'ID'} is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached. Consider using 'DbContextOptionsBuilder.EnableSensitiveDataLogging' to see the conflicting key values.'
I added some extension methods for Intersect and LeftComplementRight from here and tried calculating the diffs myself.
public void Update(Group group)
{
Group oldGroup = _context.Groups
.Include(g => g.Users)
.First(g => g.ID == group.ID);
var oldUsers = oldGroup.Users;
var newUsers = group.Users;
var toBeRemoved = oldUsers.LeftComplementRight(newUsers, x => x.ID);
var toBeAdded = newUsers.LeftComplementRight(oldUsers, x => x.ID);
var toBeUpdated = oldUsers.Intersect(newUsers, x => x.ID);
foreach (var u in toBeAdded)
{
oldGroup.Users.Add(u);
}
foreach (var u in toBeRemoved)
{
oldGroup.Users.Remove(u);
}
_context.Entry(oldGroup).CurrentValues.SetValues(group);
_context.SaveChanges();
}
Result: Same exception as above.
It was at this point I realised that the 'User' objects that compose the 'Users' collection had the 'Groups' object instantiated and populated with self-references back to the Group object. I realised that this was probably confusing EF, so I tried this:
4.
public void Update(Group group)
{
Group oldGroup = _context.Groups
.Include(g => g.Users)
.First(g => g.ID == group.ID);
var oldUsers = oldGroup.Users;
var newUsers = group.Users;
var toBeRemoved = oldUsers.LeftComplementRight(newUsers, x => x.ID);
var toBeAdded = newUsers.LeftComplementRight(oldUsers, x => x.ID);
var toBeUpdated = oldUsers.Intersect(newUsers, x => x.ID);
foreach (var u in toBeAdded)
{
u.Groups = null;
oldGroup.Users.Add(u);
}
foreach (var u in toBeRemoved)
{
u.Groups = null;
oldGroup.Users.Remove(u);
}
_context.Entry(oldGroup).CurrentValues.SetValues(group);
_context.SaveChanges();
}
This works, however it seems like I'm doing far too much work. I anticipate having multiple many-to-many relationships throughout this project, and I don't want to have to duplicate this code on every update method. I suppose I could create an Extension method, but I feel like EF should be able to handle this common usecase.
Am I missing something?
You can try this. Basically just replaces the entities (the ones with matching IDs) with the currently tracked instances. The new entities in the list are automatically persisted when you save. And the entities which were present in the original list but aren't present in the new list, will be automatically removed.
public void Update(Group updatedGroup)
{
Group group = _context.Groups
.Include(g => g.Users)
.First(g => g.ID == updatedGroup.ID);
group.Users = updatedGroup.Users
.Select(u => group.Users.FirstOrDefault(ou => ou.ID == u.ID) ?? u)
.ToList();
// Do other changes on group as needed.
_context.SaveChanges();
}
Yeah, it's kind of ridiculous that there is no simple method to replace a list of entities in this way. Although it gets less ridiculous if you consider that normally we don't even get entity instances in our update-like methods, because usually we get the changes through DTOs, so we couldn't just replace a list to begin with.
Let me know if it doesn't work (it's late here). :)
This question already has answers here:
EF Core Second level ThenInclude missworks
(2 answers)
Closed 2 years ago.
I have Request class, which has two collections (Details and RequestHistoryRecords). Detail in Details collection also have DetailHistoryRecords collection.
I would like to delete the whole request with all referenced collections (by foreaching all items and delete them one by one). But I have issue to load all the collections at once.
classes
public class Request
{
public IList<Detail> Details { get; private set; } = new List<Detail>();
public IList<RequestHistory> RequestHistoryRecords { get; private set; } = new List<RequestHistory>();
}
public class Detail
{
public IList<DetailHistory> DetailHistoryRecords { get; private set; } = new List<DetailHistory>();
}
db context definition
// 1-n relationship
modelBuilder.Entity<Request>()
.HasMany(x => x.Details)
.WithOne();
// 1-n relationship
modelBuilder.Entity<Request>()
.HasMany(x => x.RequestHistoryRecords)
.WithOne();
// 1-n relationship
modelBuilder.Entity<Detail>()
.HasMany(x => x.DetailHistoryRecords)
.WithOne();
I tried to load all the collections with Include and ThenInclude method, but I do not see DetailHistoryRecords collection, I don't know why. Am I on the right track or should I do the loading completely differently ?
var request = _context.Request
.Include(x => x.Details) // Works
.ThenInclude(details => details./*I do not see DetailHistoryRecords here ?! */)
.Include(x => x.RequestHistoryRecords) // Works
.Single(x => x.Id == id);
AS I can see Details and RequestHistoryRecords are not depend on each other. you can remove below line in query.
.ThenInclude(details => details./*I do not see DetailHistoryRecords here ?! */)
Call below code
var request = _context.Request
.Include(x => x.Details) // Works
.Include(x => x.RequestHistoryRecords) // Works
.Single(x => x.Id == id);
Sample models.
public class Root
{
public string Id { get; private set; }
public ICollection<Child> Children { get; private set; }
}
public class Child
{
public string Id { get; private set; }
public string RootId { get; private set; }
public string Code { get; private set; }
public string Name { get; private set; }
}
Constraints.
Child has the RootId and Code property as its unique key. This means that each Root object is only allowed to have as many Child objects as long as no two or more Child contains the same code.
Sample query
Get all Root records with Child that have Code equals A100.
Sample List Data Containing two Root objects
Root1 with 2 children, one having a code A100 and the other A200.
Root2 with 2 children, one having a code A100 and the other A500.
The current query that I am doing right now is get all the Root records first along with all their children. Then, iterate each of the records and remove all of its children that doesn't have the same code that I am querying. The problem with this approach is when the database grows, it will have an impact on this method since I am retrieving all children when all I need is one for each Root objects.
Sample code
var records = context.Roots
.Include(x => x.Children)
.Where(x => x.Children.Any(y => y.Code == "A100"))
.ToList();
foreach (var root in records)
{
foreach (var child in root.Children)
{
if (!child.Code == "A100")
{
root.Children.Remove(child);
}
}
}
My models have their property setters set to private following DDD principles. So I cannot do linq projections using the Select() command like the following.
var records = context.Roots
.Include(x => x.Children)
.Where(x => x.Children.Any(y => y.Code == "A100"))
.Select(x => new Root{...})
.ToList();
Using the constructor is also not ideal in my case because I am setting the state of each object to Created during instantation as part of the design of each model.
Edit 1
I could use the constructor in the LINQ projection using Select() but my problem is, in all of my models, there is a property called State where I update in various points in my model depending on what occurred. In the constructor part, I update it to a Create state to imply the fact the a new model was created. So if I am going to create a constructor just so I could create an instance of the model from the database, that would lead to confusion because I am just retrieving an already existing record from the database and if I am going to use the constructor, the code, during the instantiation will mark the model as Created which is not what I want because it will create a new meaning in my design.
Edit 2
My apologies for not making myself clear enough. My problem is on this part of the query.
Part 1.
var records = context.Roots
.Include(x => x.Children)
.Where(x => x.Children.Any(y => y.Code == "A100"))
.ToList();
So I won't need to arrive on this part.
Part 2
foreach (var root in records)
{
foreach (var child in root.Children)
{
if (!child.Code == "A100")
{
root.Children.Remove(child);
}
}
}
Now based on the constraints I mentioned.
Constraint 1. Not using public setters, so I cannot use this.
var records = context.Roots
.Include(x => x.Children)
.Where(x => x.Children.Any(y => y.Code == "A100"))
.Select(x => new Root{...})
.ToList();
Constaint 2. Not using constructor
var records = context.Roots
.Include(x => x.Children)
.Where(x => x.Children.Any(y => y.Code == "A100"))
.Select(x => new Root(...))
.ToList();
The bottom line is, is there a query that I can use or any other method get the records I want, straight from the database without doing the second part of the query?
Try traditional LINQ so you will not more need to remove children manually and project your query result to the anonymous object.
var result = (from root in context.Roots.Include(x => x.Children)
from child in root.Children
where child.Code == "A100"
select new
{
Id = root.Id,
Children = child
}).ToList();
Unless you have some kind of sorting in your data storage that you can use you still have to "retrieve" items to look at them. And if you want a copy of your data with the result instead of modifying your context data, you need some kind of cloning. So in my opinion - considering your constraints - it is best to only keep references on the resulting Rootand Child items:
var l = new List<Tuple<Root, Child>>();
foreach(var p in context.Roots.Include(x => x.Children))
{
foreach(var c in p.Children)
{
if(c.Code == "A100")
{
l.Add(Tuple.Create(p, c));
break;
}
}
}
That way, you only look at the children and the root items once, and only check children until you found your item. The resulting list of tuples contain references to your respective Root and Child items without modifying them, so don't use the Children property of your referenced Root items.
For simplicity, let's guess there are two entities:
public class Entity
{
public string Value { get; set; }
public ChildEntity Child { get; set; }
}
public class ChildEntity
{
public string Value { get; set; }
}
I need to find all entities where either Value or Child.Value are insensitive like specified string query.
That's what I have by now:
Entity entity = null;
ChildEntity child = null;
var nhibernateQuery = session
.QueryOver(() => entity)
.JoinAlias(() => entity.Child, () => child);
if (!string.IsNullOrWhiteSpace(query))
{
nhibernateQuery = nhibernateQuery
.Where(
Restrictions.Or(
Restrictions.On(() => entity).IsInsensitiveLike(query),
Restrictions.On(() => child).IsInsensitiveLike(query)
)
);
}
return nhibernateQuery.List().ToArray();
I get the NullReferenceException - it seems like Restrictions.On does not handle alias correctly.
Another approach that I have tried is .JoinQueryOver() which is suggested by this post:
return session
.QueryOver<Entity>()
.Where(Restrictions.InsensitiveLike("Value", query))
.JoinQueryOver(e => e.Child)
.Where(Restrictions.InsensitiveLike("Value", query));
This thing works except for one thing: it returns all items where both Value and Child.Value are like query. I need the same thing, but with or logic.
What should be done to make it work? I would like to use .QueryOver(), either with or without aliases, but without .CreateCriteria(), but will appreciate if you help me with any working solution.
The problem has been solved by using NHibernate LINQ .Query<>().
It can resolve all joins and relations by itself.
At the same time, .Contains() method is translated into case-insensitive LIKE statement for MS SQL which suits me needs.
var nhibernateQuery = session
.Query<Entity>();
if (!string.IsNullOrWhiteSpace(query))
{
nhibernateQuery = nhibernateQuery
.Where(e => e.Value.Contains(query) || e.Child.Value.Contains(query));
}
return nhibernateQuery.ToArray();
I'll try explain simply my Entity Framework model. I have a User object which has a collection of zero or more UserInterest objects. Each user interest object has only three properties, unique ID, User Id and Description.
Whenever the user updates the User object, it should also update the related UserInterest objects but because these are free form (ie not part of a list of allowed interests), I want the user to pass in a list of type "string" to the webmethod of the names of all their interests. The code would ideally then look at the users existing list of interests, remove any that were no longer relevant and add in new ones and leave the ones which already exist.
My object model definitions
[Table("User")]
public class DbUser {
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int UserId { get; set; }
public virtual IList<DbUserInterest> Interests { get; set; }
}
[Table("UserInterest")]
public class DbUserInterest : IEntityComparable<DbUserInterest>
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public long Id { get; set; }
public virtual DbUser User { get; set; }
public int? UserId { get; set; }
}
The context Fluent mappings
modelBuilder.Entity<DbUser>()
.HasKey(u => u.UserId);
modelBuilder.Entity<DbUser>()
.Property(u => u.UserId)
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
modelBuilder.Entity<DbUser>()
.HasMany(u => u.Interests)
.WithRequired(p => p.User)
.WillCascadeOnDelete(true);
modelBuilder.Entity<DbUserInterest>()
.HasKey(p => p.Id);
modelBuilder.Entity<DbUserInterest>()
.Property(p => p.Id)
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
modelBuilder.Entity<DbUserInterest>()
.HasRequired(p => p.User)
.WithMany(u => u.Interests)
.WillCascadeOnDelete(true);
And lastly my webmethod code and repository method to do the update
public UpdateUserProfileDetailsResponse UpdateUserProfileDetails(UpdateUserProfileDetailsRequest request)
{
try
{
var dbItem = _userDataRepository.GetItem(request.Header.UserId);
dbItem.Interests.Clear();
foreach (var dbInterest in request.UserInterests)
dbItem.Interests.Add(new DbUserInterest { Name = dbInterest, UserId = dbItem.UserId});
_userDataRepository.UpdateItem(dbItem);
_userDataRepository.Save();
}
catch (Exception ex)
{
}
}
public override bool UpdateItem(DbUser item)
{
var dbItem = GetItem(item.UserId);
if (dbItem == null)
throw new DataRepositoryException("User not found to update", "UserDataRepository.UpdateItem");
var dbInterests = Work.Context.UserInterests.Where(b => b.UserId == item.UserId).ToList();
var interestsToRemove = (from interest in dbInterests let found = item.Interests.Any(p => p.IsSame(interest)) where !found select interest).ToList();
var interestsToAdd = (from interest in item.Interests let found = dbInterests.Any(p => p.IsSame(interest)) where !found select interest).ToList();
foreach (var interest in interestsToRemove)
Work.Context.UserInterests.Remove(interest);
foreach (var interest in interestsToAdd)
{
interest.UserId = item.UserId;
Work.Context.UserInterests.Add(interest);
}
Work.Context.Entry(dbItem).State = EntityState.Modified;
return Work.Context.Entry(dbItem).GetValidationResult().IsValid;
}
When I run this, at the Repository.Save() line I get the exception
Assert.IsTrue failed. An unexpected error occurred: An error occurred while updating the entries. See the inner exception for details.
But interestingly in the webmethod if I comment out the line dbItem.Interests.Clear(); it doesn't throw an error, although then of course you get duplicates or extra items as it thinks everything is a new interest to add. However removing this line is the only way I can get the code to not error
Before, I had the UserId property of the Interest object set to non nullable and then the error was slightly different, something about you cannot change the relationship of a foreign key entity that is non nullable, which is why I changed the property to nullable but still no go.
Any thoughts?
You can't just clear the collection and then try to rebuild it. EF doesn't work that way. The DbContext keeps track of all of the objects that were brought back from the database in its Change Tracker. Doing it your way will of course cause duplicates because EF sees that they're not in the Change Tracker at all so they must be brand new objects necessitating being added to the database.
You'll need to either do the add/remove logic in your UpdateUserProfileDetails method, or else you have to find a way to pass request.UserInterests into your UpdateItem method. Because you need to adjust the existing entities, not the ones found on the request (which EF thinks are new).
you could try in this way
remove
dbItem.Interests.Clear();
then
foreach (var dbInterest in request.UserInterests){
if(dbItem.Interests.Any()){
if (dbItem.Interests.Count(i=> i.Name==dbInterest && i.UserId==dbItem.UserId) == 0){
dbItem.Interests.Add(new DbUserInterest { Name = dbInterest, UserId = dbItem.UserId});
}
}
}