Updating many-to-many links with direct relationships - c#

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). :)

Related

How to retrieve a list of record including only one child each?

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.

Linq select group by where syntax

I have these classes:
User {
//identity user class
public IList<Proyecto> Proyectos { get; set; }
public bool IsImportant { get; set }
}
Proyecto
{
public Guid Id { get; set; }
// more properties here
}
What I am trying to do is to group by userId all the projects they have created and where IsImportant is true.
I have tried this
var usuariosRobotVmInDb =await _dbContext.Users.Include(p=>p.Proyectos)
.groupby(u=>u.id)
.Where(u => u.IsImportant== True)
.Select(x=> new usuariosRobotViewModel
{
Id=x.Id,
Name=x.Name,
LastName=x.LastName,
Phone=x.Phone,
Email=x.Email,
proyectosVms=x.Proyectos.Select(a=>new ProyectosVm {Id=a.Id,Date=a.Date})
}).ToListAsync();
But it seems I can not use a groupBy and a where clause...How can I fix this? thanks
As you use navigation properties and the projects are already properties of your User class then you do not need to group by anything. Just retrieve the users where Important is true:
var result = await _dbContext.Users.Include(p => p.Proyectos)
.Where(u => u.IsImportant);
(And of course you can add the projection to the usuariosRobotViewModel object)
The problem with LINQ is that developers are still thinking in the SQL way and trying to convert it to LINQ Which sometimes doesn't work and sometimes is not optimal. In your case, you're using a navigation property when you're including the Proyectos object, and LINQ already knows the relationship between the User and Proyectos objects, so you don't need to group or do anything else:
var usuariosRobotVmInDb = await _dbContext.Users.Include(p => p.Proyectos)
.Where(u => u.IsImportant) //Same as u.IsImportant == true
.Select(x => new usuariosRobotViewModel {
Id = x.Key,
Nombre = x.Nombre,
...
}).ToListAsync();

Adding Many To Many relationships between classes in EF Core

I'm working on a code-first database utilizing EF Core, and am having issues getting the relationships between entities quite right.
I have a model called Customer, that represents some basic customer information, as well as a model called User that represents a user of our site. These Users will have access to many Customers, and Customers will have many Users that have access to them (Many to Many relationship).
However, whenever I try to add existing Customers to a User's list, I get an error indicating that the Customer already exists.
// retrieves the current list of Customers
var current = GetCustomersByUserId(userId.ToString())?.Select(x => x.Id).ToList();
var toAdd = customers.Where(x => !current.Any(y => y.Equals(x.Id)));
customers.ForEach(x => x.Id = _data.Customers.First(y => y.SalesOrg.Equals(x.SalesOrg) && y.DistributionChannel.Equals(y.DistributionChannel) && y.Division.Equals(x.Division)).Id);
var affil = _data.Users.FirstOrDefault(x => x.Id.Equals(userId));
if (affil == null)
{
affil = new User() { UserId = userId, Customers = customers, Id = Guid.NewGuid() };
// Where the error occurs
_data.Users.Add(affil);
}
else
{
affil.Customers.AddRange(customers);
}
_data.SaveChanges();
return customers;
In the context OnModelCreating method I have the following:
modelBuilder.Entity<User>()
.HasMany(x => x.Customers);
modelBuilder.Entity<Customer>()
.HasMany(x => x.Users);
and in each of the classes, I reference the other as a property like so:
public List<Customer> Customers { get; set; }
public List<User> Users { get; set; }
Many-to-many relationships without an entity class to represent the join table are not yet supported on EF Core 2.1.
But you can include an entity class for the join table and mapping two separate one-to-many relationships. For example say you are creating a class UserCustomer your code in OnModelCreating would bee:
modelBuilder.Entity<UserCustomer>()
.HasKey(t => new {....});
modelBuilder.Entity<UserCustomer>()
.HasOne(uc => uc.User)
.WithMany(u => u.UsersCustomers )
.HasForeignKey(.. => ....);
modelBuilder.Entity<UserCustomer >()
.HasOne(uc => uc.Customer)
.WithMany(c => c.UsersCustomers)
.HasForeignKey(.. => .....);

Cannot update Entity Framework virtual list objects

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});
}
}
}

Entity framework, problems updating related objects

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();

Categories

Resources