I am working with .NET 5 and using EF Core and MSSQL
for database.
I have 2 classes that have many to many relationship between them. Those were migrated from models to DB. example of DB tables
Migration:
migrationBuilder.CreateTable(
name: "TournamentUser",
columns: table => new
{
TournamentsId = table.Column<int>(type: "int", nullable: false),
UsersId = table.Column<int>(type: "int", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_TournamentUser", x => new { x.TournamentsId, x.UsersId });
table.ForeignKey(
name: "FK_TournamentUser_Tournaments_TournamentsId",
column: x => x.TournamentsId,
principalTable: "Tournaments",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_TournamentUser_Users_UsersId",
column: x => x.UsersId,
principalTable: "Users",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
Models:
public class Tournament
{
[Key]
public int Id { get; set; }
[Required, MaxLength(20)]
public string Name { get; set; }
[Required]
public int MaxPlayers { get; set; }
public int PlayersCount { get; set; }
[Required]
public DateTime Start { get; set; }
public bool IsStarted { get; set; }
public bool IsEnded { get; set; }
public List<User> Users { get; set; }
}
public class User
{
[Key]
public int Id { get; set; }
[Required, MaxLength(15)]
public string Username { get; set; }
[Required]
public string Password { get; set; }
[Required, MaxLength(20)]
public string Name { get; set; }
[Required, MaxLength(20)]
public string Surname { get; set; }
[JsonIgnore]
public List<Tournament> Tournaments { get; set; }
}
So as I try to update Tournaments model
[HttpPut]
public async Task<ActionResult<Tournament>> Put([FromBody] Tournament tournament)
{
_context.Tournaments.Update(tournament);
await _context.SaveChangesAsync();
return CreatedAtAction(nameof(GetTournament), new { id = tournament.Id }, tournament);
}
I get this error
Microsoft.Data.SqlClient.SqlException (0x80131904): Violation of PRIMARY KEY constraint 'PK_TournamentUser'. Cannot insert duplicate key in object 'dbo.TournamentUser'. The duplicate key value is (1, 1).
What could be possible solutions for this? I would like to keep my many to many table udpated as Tournament's User
can be removed or added each time I update Tournament
.
EDIT:
Following shows the tournament object data -
The Solution:
To update the Tournament
entity only use the following approach -
_context.Entry(tournament).State = EntityState.Modified;
await _context.SaveChangesAsync();
The Explanation:
The Update()
method marks the entity as Modified
if it has an Id
(auto generated primary key) value, and as Added
if it doesn't have an Id
value. Same is true for any related entities if you call the method with an entity graph (entity with related entities). Then on the next SaveChanges()
call, EF generates update and/or insert command for the entity (or entities) based on whether they are marked as Modified
or Added
. The point is - the Update()
method does not only update an entity, it can insert new entities too.
In your case, looking at the models, EF can tell Tournament
and User
are in a many-to-many relationship, and there is a joining entity TournamentUser
at database level. But since you are receiving the entity graph from outside, they are not tracked by EF and it cannot tell whether the TournamentUser
entities for this tournament and the related users already exist in database or not.
As you are calling the Update()
method with the entity graph, the tournament, the related users, and the joining entity relating them (TournamentUser
) all becomes subject for update operation. And EF tries to create the joining entity just like it would create/insert a new entity without an Id
value.
EF generates insert command for a TournamentUser
entity and update commands for Tournament
and User
entities. But at database level, since the TournamentUser
link already exists you get the Violation of PRIMARY KEY
error.
The -
_context.Entry(tournament).State = EntityState.Modified;
suggested above explicitly marks only the tournament as Modified
. Therefore, EF doesn't bother to look at any related entities, or to infer any relationship, and only an update command is generated for the tournament.
Try to remove users from your torment object if you want to update tournment.
public async Task<ActionResult<Tournament>> Put([FromBody] Tournament tournament)
{
tournament.Users=null;
_context.Tournaments.Update(tournament);
await _context.SaveChangesAsync();
.....
}
Or if you want to add an user to your tournament
var existing= _context
.Tournaments
.Include(u=>u.Users)
.FirstOrDefault(t=> t.Id== tournament.Id);
if(existing==null) return ...error
if( existing.Users==null) existing.User=new List<User>();
if( existing.Users.Any(u=>u.Id=tournment.User.Id) return ...error
existing.Users.Add(tournment.User);
_context.Entry(existing).State = EntityState.Modified;
_context.SaveChanges();
It looks ackward but you are paying for not adding many-to-many relantionship table explicitly. If you use UserTournment table code will be shorter.
You can try this. I never tried it. Let me know if it works:
var existing = _context
.Tournaments
.Include(u=>u.Users)
.FirstOrDefault(t=> t.Id== tournament.Id);
if(existing==null) return ...error
_context.Entry(existing).CurrentValues.SetValues(tournment);
_context.SaveChanges();
I have found a solution from this post
I have modified the code a bit, since originally it was removing users from table and I got the result I wanted
public async Task<ActionResult<Tournament>> Put([FromBody] Tournament tournament)
{
var _tournament = _context.Tournaments.Include(t => t.Users).FirstOrDefault(t => t.Id == tournament.Id);
_context.Entry(_tournament).CurrentValues.SetValues(tournament);
var _users = _tournament.Users.ToList();
// Adds new Users
foreach (var tournamentUser in tournament.Users)
{
if (_users.All(i => i.Id != tournamentUser.Id))
{
_tournament.Users.Add(tournamentUser);
}
}
// Removes old Users
foreach (var tournamentUser in _users)
{
if (tournament.Users.FirstOrDefault(tu => tu.Id == tournamentUser.Id) == null)
{
_tournament.Users.Remove(tournamentUser);
}
}
await _context.SaveChangesAsync();
return CreatedAtAction(nameof(GetTournament), new { id = tournament.Id }, tournament);
}
Not sure if it's not over complex but it works
User contributions licensed under CC BY-SA 3.0