Keep multiple objects with the same key in DbContext - c#

I am trying to create ASP.NET MVC web application using Entity Framework Code First.
I have two simple models
public class Assignment
{
public int Id { get; set; }
[ForeignKey("Initiator")]
public int InititatorId { get; set; }
public virtual User Initiator { get; set; }
public virtual List<User> Debtors { get; set; }
public virtual List<User> Responsibles { get; set; }
public Assignment()
{
Debtors = new List<User>();
Responsibles = new List<User>();
}
}
public class User
{
public int Id { get; set; }
public string Name { get; set; }
public string City { get; set; }
}
and my DbContext looks like
public class AssignmentContext : DbContext
{
public DbSet<Assignment> Assignments { get; set; }
public DbSet<User> Users { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Assignment>()
.HasRequired(r => r.Initiator)
.WithMany()
.WillCascadeOnDelete(false);
modelBuilder.Entity<Assignment>()
.HasMany(m => m.Responsibles)
.WithMany()
.Map(m =>
{
m.MapLeftKey("AssignmentId");
m.MapRightKey("UserId(Responsible)");
m.ToTable("AssignmentResponsibles");
});
modelBuilder.Entity<Assignment>()
.HasMany(m => m.Debtors)
.WithMany()
.Map(m =>
{
m.MapLeftKey("AssignmentId");
m.MapRightKey("UserId(Debtor)");
m.ToTable("AssignmentDebtors");
});
}
Is there a way to keep the same User instance in the Lists Debtors and Responsibles?
Because I'm getting the error when I'm trying to save changes.
An object with the same key already exists in the ObjectStateManager. The ObjectStateManager cannot track multiple objects with the same key.
I need to do this because it is required by the application logic.
Thanks in advance.
Update:
Here on HttpPost the error appears (The same error in Console app sample):
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit([Bind(Include = "Id,Text,CreationDate,InititatorId,Debtors,Responsibles")] Assignment assignment)
{
if (ModelState.IsValid)
{
assignment.Debtors.RemoveAll(r => r.Name == "Roman");
db.Entry(assignment).State = EntityState.Modified; // HERE!
db.SaveChanges();
return RedirectToAction("Index");
}
ViewBag.InititatorId = new SelectList(db.Users, "Id", "Name", assignment.InititatorId);
return View(assignment);
}
Debtors and Responsbiles elements from the view are passed by hidden input.

Why don't you just get the data first instead of directly attach it to the dbcontext, the same assignment with the same id might have been attached locally within the same context.
instead of these
assignment.Debtors.RemoveAll(r => r.Name == "Roman");
db.Entry(assignment).State = EntityState.Modified; // HERE!
db.SaveChanges();
try these
var assignmentDb = db.Assignments.FirstOrDefault(asg => asg.Id == assignment.Id);
assignmentDb.Debtors.RemoveAll(r => r.Name == "Roman");
db.SaveChanges();

Related

How to create an Add Friend functionality between two individual user accounts in the same table in asp.net core identity

I am developing a mini social media web app and I use ASP.NET Identity to create and manage user accounts.
I want to add another user account as a friend to my account. I could succesfully do that but the problem is when I checked my added friend's account, there is no update in his friends list. It's empty.
Here is my User class inherited from IdentityUser,
public class AppUser : IdentityUser
{
public AppUser()
{
this.Friends = new HashSet<AppUser>();
}
public int Age { get; set; }
public string Sex { get; set; }
public string City { get; set; }
public string Education { get; set; }
public string Description { get; set; }
public string? FriendOfUserId { get; set; }
public virtual AppUser FriendOf { get; set; }
public ICollection<AppUser> Friends { get; set; }
}
My DbContext class,
public class ApplicationDbContext : IdentityDbContext<AppUser>
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options)
{
}
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder.Entity<AppUser>(x =>
{
x
.HasMany(x => x.Friends)
.WithOne(x => x.FriendOf)
.HasForeignKey(x => x.FriendOfUserId);
});
}
public DbSet<AppUser> Users { get; set; }
}
My Controller Class method to add friend,
public async Task<IActionResult> AddFriend(string id)
{
var addFriend = await context.Users.Include(u => u.Friends).FirstOrDefaultAsync(u => u.Id == id);
var user = await userManager.GetUserAsync(this.User);
var u = await context.Users.Include(u => u.Friends).FirstOrDefaultAsync(u => u.Id == user.Id);
user.FriendOf = addFriend;
user.Friends.Add(addFriend);
await context.SaveChangesAsync();
return Redirect("/");
}
I think you're not modeling your entity correctly. Since an user can have a list of friends, and also be a friend of other users, I guess you need to capture the latter part in the model.
Since this is a many-to-many relationship, and EF Core still hasn't supported it without declaring an entity to represent the join table, you need to defind that entity as well:
public class AppUser : IdentityUser
{
public int Age { get; set; }
public string Sex { get; set; }
public string City { get; set; }
public string Education { get; set; }
public string Description { get; set; }
public ICollection<AppUserFriendship> FriendsOf { get; set; }
public ICollection<AppUserFriendship> Friends { get; set; }
}
public class AppUserFriendship
{
public string UserId { get; set; }
public AppUser User { get; set; }
public string UserFriendId { get; set; }
public AppUser UserFriend { get; set; }
}
And then you need to configure their relationships:
public class ApplicationDbContext : IdentityDbContext<AppUser>
{
...
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder.Entity<AppUserFriendship>(b =>
{
b.HasKey(x => new { x.UserId, x.UserFriendId };
b.HasOne(x => x.User)
.WithMany(x => x.Friends)
.HasForeignKey(x => x.UserId)
.OnDelete(DeleteBehavior.Restrict);
b.HasOne(x => x.UserFriend)
.WithMany(x => x.FriendsOf)
.HasForeignKey(x => x.UserFriendId)
.OnDelete(DeleteBehavior.Restrict);
});
}
public DbSet<AppUser> Users { get; set; }
}
Note OnDelete(DeleteBehavior.Restrict). You have to set it to something other than DeleteBehavior.Cascade which is the default to prevent the cascade deletion.
Disclaimer: I wrote all by hand. Never test it.
You will need to use the Include function.
// What you have is fine.
var friend = context.Users.Select ( u => u == id );
// this is what needs to occur in await userManager.GetUserAsync(this.User);
// Without the include the Navigation Property will not be tracked.
var user = context.Users
.Select ( u => u == id )
.Include ( u => u.Friends );
user.Friends.Add ( friend );
context.SaveChanges ();
Check out loading related data.
https://learn.microsoft.com/en-us/ef/core/querying/related-data
EDIT:
Take a look at this post it is a duplicate of this one.
Many-to-many self referencing relationship
EDIT 2:
So the issue you are having is that you are still trying to model this as one table. Think about how this would be structured in a SQL database. How could a single table contain a collection (Friend) in a single column? To accomplish this we will create a new table to model the relationship between AppUsers.
public class Friendship
{
// Foreign Key
Int32 MeId { get; set; }
// Foreign Key
Int32 FriendId { get; set; }
// Navigation Property
AppUser Me { get; set; }
// Navigation Property
AppUser Friend { get; set; }
}
public class AppUser : IdentityUser
{
public AppUser()
{
this.Friends = new HashSet<AppUser>();
}
// Primary Key. Is this defined in IdentityUser?
public int Id { get; }
public int Age { get; set; }
public string Sex { get; set; }
public string City { get; set; }
public string Education { get; set; }
public string Description { get; set; }
// This is considered a Navigation Property
public ICollection<Friendship> Friends { get; set; }
}
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder.Entity<AppUser>(x =>
{
x.HasPrimaryKey ( x => x.Id );
});
builder.Entity<Friendship>( f =>
{
f.HasKey ( f => new { f.MeId, f.FriendId } );
f
.HasOne( f => f.Me )
.WithMany ( u => u.Friends )
.HasForeignKey( f => f.MeId );
f
.HasOne( f => f.Friend )
.WithMany ( u => u.Friends )
.HasForeignKey( f => f.FriendId );
});
}
At this point you should be able to query the join table for friendships.
public void AddFriend ( AppUser user, AppUser friend ) {
var trackedUser = context.AppUsers
.Select ( u => u.Id == user.Id )
.Include ( u => u.Friends );
.FirstOrDefault ();
trackedUser.Friends.Add ( new Friendship () {
MeId = user.Id,
FriendId = friend.Id
});
context.SaveChanges ();
}

EF Core FindAsync returns only root object

I have a many to many relation and I am using ASP.Net Core2.2 MVC with EF.
From what I read I understand that at the current time I have to create a CLR class for join table.
My code:
Models:
public class Post
{
public Post()
{
Categories= new List<PostCategory>();
}
public int Id { get; set; }
[Required]
public string Title { get; set; }
[Required]
public string Description { get; set; }
public List<PostCategory> Categories { get; set; }
}
public class Category
{
public Category()
{
Posts= new List<PostCategory>();
}
public int Id { get; set; }
[Required]
public string Name { get; set; }
public ICollection<PostCategory> Posts { get; set; }
}
public class PostCategory
{
public int PostId { get; set; }
public Post Post { get; set; }
public int CategoryId { get; set; }
public Category Category { get; set; }
}
DBContext:
public class MyDBContext:DbContext
{
public MyDBContext()
{
}
public MyDBContext(DbContextOptions<MyDBContext> options) : base(options)
{
}
public DbSet<Post> Post { get; set; }
public DbSet<Category> Category { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<PostCategory>()
.HasKey(x => new { x.PostId, x.CategoryId });
modelBuilder.Entity<PostCategory>()
.HasOne(x => x.Post)
.WithMany(m => m.Categories)
.HasForeignKey(x => x.PostId);
modelBuilder.Entity<PostCategory>()
.HasOne(x => x.Category)
.WithMany(e => e.Posts)
.HasForeignKey(x => x.CategoryId);
}
}
Create Post Code:
var categories= new List<PostCategory>();
foreach (var category in model.SelectedCategories)
{
categories.Add(new PostCategory()
{
CategoryId= category
});
}
model.Post.Categories.AddRange(categories);
_context.Post.Add(model.Post);
await _context.SaveChangesAsync();
When I create a post I can see in database that I have Post data, and in PostCategory table I can see PostId CategoryId as they should be.
My issue is when I try to get a post data using:
var post = await _context.Post.FindAsync(id);
post.Categories count is always 0, what am I missing?
You need to write your query as follows to eager load the related Categories with Post:
var post = await _context.Post.Include(p => p.Categories).FirstOrDefaultAsync(p => p.Id == id);
Here is the more details about Loading Related Data in EF core
The accepted answer will work but may not be as performant - when using FindAsync if the entity is already being tracked you'll get that copy rather than going to the database
In order to have that and load related data you need to do it manually
for example:
context.Entry(post)
.Reference(p => p.Categories)
.Load();
references:
https://learn.microsoft.com/en-us/ef/core/querying/related-data/explicit#explicit-loading
https://learn.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext.findasync?view=efcore-7.0#overloads

How to solve Code First from database many-to-many mapping in Entity Framework?

Bellow code snippet showing my scenario:
[Table("User")]
public partial class UserModel
{
public UserModel()
{
UserRole = new HashSet<UserRoleModel>();
}
public int UserID { get; set; }
public string FullName { get; set; }
public virtual ICollection<UserRoleModel> UserRole { get; set; }
}
[Table("UserRole")]
public partial class UserRoleModel
{
public UserRoleModel()
{
User = new HashSet<UserModel>();
}
public int RoleID { get; set; }
public string RoleName { get; set; }
public virtual ICollection<UserModel> User { get; set; }
}
Now within OnModelCreating(DbModelBuilder modelBuilder) EF Generate code like bellow
modelBuilder.Entity<UserModel>()
.HasMany(e => e.UserRole)
.WithMany(e => e.User)
.Map(m => m.ToTable("UserRoleMapping").MapLeftKey("UserID").MapRightKey("UserRoleID"));
now this is fine add / insert data into UserRoleMapping table. But how to
Get / Update data from UserRoleMapping table ?
I try to solve this issue following create-code-first-many-to-many the post and come-up with third class with join entity
public partial class UserRoleMappingModel
{
[Key, Column(Order = 0)]
public Guid UserId { get; set; }
public UserModel User { get; set; }
[Key, Column(Order = 1)]
public int RoleId { get; set; }
public UserRoleModel UserRole { get; set; }
}
then add public virtual ICollection<UserRoleMappingModel> UserRoleMapping { get; set; } in both the UserModel and UserRoleModel class
But when I try to GET value from database using bellow code
var results = _userRepository.GetAll()
.Include(r => r.UserRoleMapping
.Select(s => s.UserRole))
.SingleOrDefault(e => e.ID == id);
It throws ERROR
"An error occurred while executing the command definition. See the
inner exception for details.System.Data.SqlClient.SqlException
(0x80131904): Invalid object name 'dbo.UserRoleMappingModel'.\r\n
Even I tried bellow Configuration within OnModelCreating, but nothing work as expected
modelBuilder.Entity<UserRoleMappingModel>()
.HasKey(e => new { e.UserId, e.RoleId });
Your class UserRoleMappingModel has no Table-Attribute. Bcause of this, EF searches for a Table UserRoleMappingModel instead von UserRoleMapping.
You have to choose: Either map the n-to-n relationship and don't access the mapping-table or load the table to access the values in it.
As workaround you could implement a Not-Mapped column:
[Table("User")]
public partial class UserModel
{
public UserModel()
{
UserRole = new HashSet<UserRoleModel>();
}
public int UserID { get; set; }
public string FullName { get; set; }
public virtual ICollection<UserRoleMappingModel> Mappings { get; set; }
public virtual ICollection<UserRoleModel> UserRole
{
get
{
return this.Mappings.Select(s => s.UserRole);
}
}
}
AS per GertArnold response I solve the issue in bellow way.
1st Remove below settings
modelBuilder.Entity<UserModel>()
.HasMany(e => e.UserRole)
.WithMany(e => e.User)
.Map(m => m.ToTable("UserRoleMapping").MapLeftKey("UserID").MapRightKey("UserRoleID"));
2nd Add bellow settings
modelBuilder.Entity<UserRoleMappingModel>()
.HasKey(e => new { e.UserId, e.RoleId });
3rd add table property in Mapping Model
[Table("UserRoleMapping")]
public partial class UserRoleMappingModel
{
[Key, Column(Order = 0)]
public Guid UserId { get; set; }
public UserModel User { get; set; }
[Key, Column(Order = 1)]
public int RoleId { get; set; }
public UserRoleModel UserRole { get; set; }
}
4th Create a Mapping Repository
IUserRoleMappingRepository
5th a simple get Method (Problem Solved)
var results = _userRoleMappingRepository.SearchFor(e => e.UserId == id)
.Select(s => new
{
s.UserId,
s.UserRoleId,
s.UserRole.RoleName
})
.FirstOrDefault();
Point to be noted : using bellow query I able to get result but unable to serialize with Newtonsoft.Json due to self referencing issue
var results = _userRepository.GetAll()
.Include(r => r.UserRoleMapping
.Select(s => s.UserRole))
.SingleOrDefault(e => e.ID == id);
Try bellow JsonSerializerSettingssetting alternatively but unable to serialize sucessfully
PreserveReferencesHandling = PreserveReferencesHandling.All / Object
ReferenceLoopHandling = ReferenceLoopHandling.Serialize / Ignore

Entity Framework Many to many between custom entity and Identity user never inserts row

I have a Many to Many relationship defined between my custom class (Department) and my User class based on Identity 2 defined as below. This generates an extra table, which is all good. However this table is never populated, and from my Profiler i can see that it never creates and executes the SQL to insert the row. What am i doing wrong?
public class Department
{
public int DepartmentId { get; set; }
[DisplayName("Navn")]
public string Name { get; set; }
[DisplayName("Mindst acceptable antal skemaer")]
public int ApprovalThreshold { get; set; }
public List<ATVisit> Atvisits { get; set; }
public bool IsActive { get; set; }
public ICollection<Actionplan> ActionPlans { get; set; }
public int? QuestionSheetTemplateIdFK { get; set; }
[ForeignKey("QuestionSheetTemplateIdFK")]
public QuestionSheetTemplate QuestionSheetTemplate { get; set; }
public virtual List<User> Users { get; set; }
}
public class User : IdentityUser
{
public virtual List<Department> Departments { get; set; }
public async Task<ClaimsIdentity> GenerateUserIdentityAsync(UserManager<User> manager)
{
// Note the authenticationType must match the one defined in CookieAuthenticationOptions.AuthenticationType
var userIdentity = await manager.CreateIdentityAsync(this, DefaultAuthenticationTypes.ApplicationCookie);
// Add custom user claims here
return userIdentity;
}
}
I even tried with Fluent API:
modelBuilder.Entity<User>()
.HasMany(x => x.Departments)
.WithMany(c => c.Users);
modelBuilder.Entity<Department>()
.HasMany(x => x.Users)
.WithMany(c => c.Departments);
I am using repositories, and because of this i have to find the User in the Department Repository, otherwise i'll get an error telling me it comes from 2 different Contexts. This is my Department Repository method for updating:
public bool FindUserAndAddToDepartment(Department entity, string userId)
{
var user = context.Set<User>().Where(x => x.Id == userId).FirstOrDefault();
entity.Users.Add(user);
return true;
}
This is the method that handles everything
public ActionResult EditUser(UserViewModel model)
{ //vi starter med at fjerne alle Claims der er department
List<Department> depList = new List<Department>();
var user = UserManager.FindById(model.Id);
if (user.Departments == null)
user.Departments = new List<Department>();
foreach (var selectedDepartment in model.SelectedDepartments)
{
depList.Add(departmentRepo.GetById(selectedDepartment));
}
foreach (var d in depList)
{
if (user.Departments.Where(x => x.DepartmentId == d.DepartmentId).SingleOrDefault() == null)
{
departmentRepo.FindUserAndAddToDepartment(d, user.Id);
departmentRepo.Update(d);
}
}
departmentRepo.Commit();
}

Saving changes to EF with children

I have build reporting system where user can select data and display as report. Report is saved in three tables (split entity). But when I try to edit report and save again I get following error:
An entity object cannot be referenced by multiple instances of IEntityChangeTracker
My Entity:
public class Report
{
[Key]
public int ReportId { get; set; }
public string Title { get; set; }
public int? DateRange { get; set; }
public int Layout { get; set; }
public DateTime? DateFrom { get; set; }
public DateTime? DateTo { get; set; }
public int OwnerId { get; set; }
public DateTime DateCreated { get; set; }
public bool Active { get; set; }
public virtual List<ReportCharts> ReportCharts { get; set; }
public virtual List<ReportElements> ReportElements { get; set; }
}
My EF repository:
//Save Report to Database
public void Save(Report report)
{
assignSettingsToEntity(report);
assignElementsToEntity(report);
assignChartsToEntity(report);
int found = Reports
.Select(r => r.ReportId)
.Where(id => id == report.ReportId)
.SingleOrDefault();
if (found == 0)
{
context.Reports.Add(report);
}
else
{
context.Entry(report).State = EntityState.Modified; // Here I get error
}
context.SaveChanges();
}
My DBContext
class EFDbContext : DbContext
{
//Get Lines data from Lines table
public DbSet<Line> Lines { get; set; }
//Get Shifts data from Shifts table
public DbSet<Shift> Shifts { get; set; }
//Get list of Charts from Charts table
public DbSet<Graph> Graphs { get; set; }
//Get Reports data from Reports table
public DbSet<Report> Reports { get; set; }
// Report entity mapping
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Report>().Property(t => t.ReportId).HasColumnName("ReportId");
modelBuilder.Entity<Report>().Property(t => t.Title).HasColumnName("Title");
modelBuilder.Entity<Report>().Property(t => t.DateRange).HasColumnName("DateRange");
modelBuilder.Entity<Report>().Property(t => t.Layout).HasColumnName("Layout");
modelBuilder.Entity<Report>().Property(t => t.DateFrom).HasColumnName("DateFrom");
modelBuilder.Entity<Report>().Property(t => t.DateTo).HasColumnName("DateTo");
modelBuilder.Entity<Report>().Property(t => t.OwnerId).HasColumnName("OwnerId");
modelBuilder.Entity<Report>().Property(t => t.DateCreated).HasColumnName("DateCreated");
modelBuilder.Entity<Report>().Property(t => t.Active).HasColumnName("Active");
modelBuilder.Entity<Report>().HasMany(t => t.ReportElements).WithRequired().HasForeignKey(c => c.ReportId).WillCascadeOnDelete(true);
modelBuilder.Entity<Report>().HasMany(t => t.ReportCharts).WithRequired().HasForeignKey(p => p.ReportId).WillCascadeOnDelete(true);
modelBuilder.Entity<ReportElements>().Property(c => c.ElementName).HasColumnName("ElementName");
modelBuilder.Entity<ReportElements>().HasKey(c => new { c.ReportId, c.ElementName, c.Active });
modelBuilder.Entity<ReportCharts>().Property(p => p.ChartId).HasColumnName("ChartId");
modelBuilder.Entity<ReportCharts>().HasKey(c => new { c.ReportId, c.ChartId, c.Active });
}
}
Thanks for any help.
Entity Framework attaches every entity to one context and all changes can only be performed on the context it is attached to it, you are probably using multiple context. You should detach and attach entity obtained from different context.
Otherwise, the best practice is not to use multiple instances of context, only keep one context throughout your changes.
Sounds like the object was attached to another context and not detached.
//Save Report to Database
public void Save(Report report)
{
using(EFDbContext context=new EFDbContext ())
{
assignSettingsToEntity(report);
assignElementsToEntity(report);
assignChartsToEntity(report);
int found = context.Reports
.Select(r => r.ReportId)
.Where(id => id == report.ReportId)
.SingleOrDefault();
// Insert Flow
if (found == 0)
{
context.Reports.Add(report);
}
// Update flow
else
{
context.Reports.Attach(report);
context.Entry(report).State = EntityState.Modified;
}
context.SaveChanges();
}
}

Categories