EF Core delete data in many to many relationship - c#

I have the following tables:
public class Team
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
[Required]
public string Name { get; set; }
public virtual ICollection<UserTeam> UserTeams { get; set; }
}
public class User
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
[Required]
public string Name { get; set; }
public virtual ICollection<UserTeam> UserTeams { get; set; }
}
public class UserTeam
{
public long UserId { get; set; }
public User User { get; set; }
public long TeamId { get; set; }
public Team Team { get; set; }
}
the many to many relationship is defined in context:
modelBuilder.Entity<UserTeam>()
.HasKey(bc => new { bc.UserId, bc.TeamId });
modelBuilder.Entity<UserTeam>()
.HasOne(bc => bc.User)
.WithMany(b => b.UserTeams)
.HasForeignKey(bc => bc.UserId)
.OnDelete(DeleteBehavior.Restrict);
modelBuilder.Entity<UserTeam>()
.HasOne(bc => bc.Team)
.WithMany(c => c.UserTeams)
.HasForeignKey(bc => bc.TeamId)
.OnDelete(DeleteBehavior.Restrict);
I am trying to delete some users from a team with the following code:
public async Task RemoveUsersFromTeam(int teamId, List<long> users)
{
Team existingTeam = await dbContext.Team.Include(x => x.UserTeams).FirstOrDefaultAsync(x => x.Id == teamId);
foreach (var user in users)
{
existingTeam.UserTeams.Remove(new UserTeam { UserId = user });
}
await dbContext.SaveAsync();
}
but this query is not deleting the users that I pass. Anyone knows why this happens?

You can delete objects by Id using the new and attach method, or by passing the actual entity.
Passing Entity
public async Task RemoveUsersFromTeam(int teamId, List<long> userIds)
{
Team existingTeam = await dbContext.Team.Include(x => x.UserTeams).FirstOrDefaultAsync(x => x.Id == teamId);
foreach (var userId in userIds)
{
var userTeam = existingTeam.UserTeams.FirstOrDefault(x => x.UserId == userId);
if(userTeam != null)
{
existingTeam.UserTeams.Remove(userTeam);
}
}
await dbContext.SaveAsync();
}
Delete By Id
public async Task RemoveUsersFromTeam(int teamId, List<long> userIds)
{
Team existingTeam = await dbContext.Team.FirstOrDefaultAsync(x => x.Id == teamId);
foreach (var userId in userIds)
{
var userTeam = new UserTeam { UserId = userId });
dbContext.UserTeams.Attach(userTeam);
existingTeam.UserTeams.Remove(userTeam);
}
await dbContext.SaveAsync();
}
Option two does not require selecting UserTeams from the database and will be slightly more efficient in that respect. However option one may be more understandable. You should choose which best fits your situation.
Personally I prefer option two, as include will select the whole entity and in some cases that could be a lot more than necessary.

Related

How to save changes properly in code-first without navigation property?

I have problem to design to split config without navigation property. it makes each work/transaction call SaveChanges twice. I dont know it effect performance or not. I see alot tutorial only call SaveChanges one time only. Is there a new ways entityframework to design code-first without navigation property?
Program
using (ApplicationDbContext context = new())
{
using (var contextTransaction = context.Database.BeginTransaction())
{
var account = new Account
{
AccountId = "stack237762"
};
context.Accounts.Add(account);
context.SaveChanges();
context.Users.Add(new User
{
Username = "Admin",
Password = "PassAdmin",
AccountId = account.Id
});
context.SaveChanges();
contextTransaction.Commit();
}
}
Entities
public class User
{
public int Id { get; set; }
public string Username { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty;
public int AccountId { get; set; }
}
public class Account
{
public int Id { get; set; }
public string AccountId { get; set; } = string.Empty;
}
Context
public class UserEntityConfig : IEntityTypeConfiguration<User>
{
public void Configure(EntityTypeBuilder<User> builder)
{
builder.HasKey("Id");
builder.Property(t => t.Id)
.ValueGeneratedOnAdd();
builder.Property(t => t.Username)
.IsRequired();
builder.Property(t => t.Password)
.IsRequired();
builder.HasOne<Account>()
.WithOne()
.HasForeignKey<User>(u => u.AccountId);
}
}

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

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

How to write eagerly loading query on many to many relation with additional filtering and sorting

I need to create a eager loading query that will perform these tasks:
Get parent Entity by id
Filter child entities by criteria
Sort child list of entities
My non-eager query looks like this:
var company = _dbContext.Companies.FirstOrDefault(c => c.Id == companyId);
if (company != null)
{
company.CompanyProducts =
company.CompanyProducts
.Where(cp => cp.IsBuyable && cp.Product.IsPublished)
.OrderByDescending(c => c.Product.PublishDate)
.ThenBy(c => c.Product.Name)
.ToList();
}
return company;
Where Entities have this structure:
public class Company
{
public long Id { get; set; }
public string Name { get; set; }
[ForeignKey("CompanyId")]
public virtual ICollection<CompanyProduct> CompanyProducts { get; set; }
}
public class CompanyProdcut
{
public long Id { get; set; }
public long CompanyId { get; set; }
public long ProductId { get; set; }
public bool IsBuyable { get; set; }
public virtual Company Company { get; set; }
public virtual Product Product { get; set; }
}
public class Product
{
public long Id { get; set; }
public string Name { get; set; }
public DateTime PublishDate { get; set; }
public bool IsPublished { get; set; }
[ForeignKey("ProductId")]
public virtual ICollection<CompanyProduct> CompanyProducts { get; set; }
}
public class MyDbContext : DbContext
{
public MyDbContext() : base("name=connectionString")
{
Database.Log = s => System.Diagnostics.Debug.WriteLine(s);
}
public virtual IDbSet<Product> Products { get; set; }
public virtual IDbSet<Company> Companies { get; set; }
public virtual IDbSet<CompanyProduct> CompanyProducts { get; set; }
}
This is classic many to many relation that uses an entity in the middle to store addtional data.
Company -|---------|< CompanyProduct >|---------|- Product
How in this case I can rewrite the non-eager query to eager query, that will do everything in one SQL call? I tried to do it myself using the .Include() but I failed.
Any ideas?
I wouldn't consider it the best approach but you can have a .Include() statement in the two positions I have shown below. If you include it in the first query then I think you'd be fine to use your method for the second one but I havn't tested to not certain about that.
The reason why you can't use it in the positions in your example is becasue it can only be used on a IQueryable() object, of which Company and ICollection is not.
var _dbContext = new MyDbContext();
var company = _dbContext.Companies.Include("").SingleOrDefault(c => c.Id == companyId);
var products = _dbContext.Companies.Include("").SingleOrDefault(c => c.Id == companyId)
?.CompanyProducts
.Where(cp => cp.IsBuyable && cp.Product.IsPublished)
.OrderByDescending(c => c.Product.PublishDate)
.ThenBy(c => c.Product.Name)
.ToList();
if(company != null)
{
company.CompanyProducts = products;
}
return company;
var company = _dbContext.Companies.Include(x => x.CompanyProducts).Includex => x.CompanyProducts.Select(y => y.Company).Includex => x.CompanyProducts.Select(y => y.Product).FirstOrDefault(c => c.Id == companyId);
if (company != null)
{
company.CompanyProducts =
company.CompanyProducts
.Where(cp => cp.IsBuyable && cp.Product.IsPublished)
.OrderByDescending(c => c.Product.PublishDate)
.ThenBy(c => c.Product.Name)
.ToList();
}
return company;
To get the lambda include make sure to add reference to System.Data.Entity.
https://msdn.microsoft.com/en-us/library/dn176380(v=vs.113).aspx

Fetch root entity with their junction tables and include their principal tables

I have a Test entity:
public class Test
{
public Test()
{
PupilsTests = new HashSet<PupilTest>();
TestTypeTests = new HashSet<TestTypeTest>();
SchoolclassTests = new HashSet<SchoolclassTest>();
SubjectTests = new HashSet<SubjectTest>();
}
public int Id { get; set; }
public DateTime Date { get; set; }
public int Number { get; set; }
public ISet<PupilTest> PupilsTests { get; set; }
public ISet<TestTypeTest> TestTypeTests { get; set; }
public ISet<SchoolclassTest> SchoolclassTests { get; set; }
public ISet<SubjectTest> SubjectTests { get; set; }
public GradingKey ScoreGradeKey { get; set; }
public Schoolyear Schoolyear { get; set; }
public int SchoolyearId { get; set; }
}
I need to fetch all Test entities filtered by schoolyearId including the junction tables SchoolclassTests, SubjectTests, TestTypeTests.
But with these junction tables I also have to include their principal tables Schoolclass, Subject, TestType.
This is what I tried:
public async Task<IEnumerable<Test>> GetTestsAsync(int schoolyearId)
{
return await context.Tests.Where(t => t.SchoolyearId == schoolyearId)
.Include(t => t.SchoolclassTests)
.Include(t => t.SubjectTests)
.Include(t => t.TestTypeTests)
// How to include all 3 Principal tables? ThenInclude does not workk
// over all the 3...
.ToListAsync();
}
Whatever combinations I try for .Include or ThenInclude I never get all 3 principal tables with the junction tables in ONE query.
How can I do that?
Using Select method, you can include the principal tables of your join tables:
public async Task<IEnumerable<Test>> GetTestsAsync(int schoolyearId)
{
return await context.Tests.Where(t => t.SchoolyearId == schoolyearId)
.Include(t => t.SchoolclassTests.Select(s => s.Schoolclass))
.Include(t => t.SubjectTests.Select(s => s.Subject))
.Include(t => t.TestTypeTests.Select(t => t.TestType))
.ToListAsync();
}

Categories