How To Use A PUT / PATCH Request to Alter Model List Within a Model

Viewed 22

My HTTP requests cannot successfully add new players to my team within my code. Say I do a POST Request and add a team and a list of players within it. I then would try either a POST or a PATCH Request to add a new player to my team but it fails. My code is below:

    public class Team
    {
        public long Id { get; set; }
        public string Name { get; set; }
        public string Location { get; set; }
        public IList<Player> Players { get; set; } = new List<Player>();
    }
    public class Player
    {
        public long Id { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
    }
        [HttpPatch]
        public async Task<ActionResult<Team>> AddPlayerToTeam(long id, List<Player> player)
        {
            var teams = await _context.Teams.Include(t => t.Players).ToListAsync();
            var team = (from selectedTeam in teams where selectedTeam.Id == id select 
                selectedTeam).SingleOrDefault();

            if (id != team.Id)
            {
                return BadRequest();
            }

            team.Players.Clear();
            team.Players = player

            _context.Update(team);
            await _context.SaveChangesAsync();

            return NoContent();
        }
        [HttpPut("{id}")]
        public async Task<IActionResult> PutTeam(long id, Team team)
        {
            if (id != team.Id)
            {
                return BadRequest();
            }


            _context.Teams.Update(team);

            try
            {
                await _context.SaveChangesAsync();
            }
            catch (DbUpdateConcurrencyException)
            {
                if (!TeamExists(id))
                {
                    return NotFound();
                }
                else
                {
                    throw;
                }
            }

            return NoContent();
        }

How can I add more Players to my team.Players?

1 Answers

A golden rule of working with collections in EF. Never replace the collection reference. EF tracks changes via proxies so code like:

team.Players = player

blasts the proxy.

To update the player collection you likely want to deal with references to existing player records. The passed in Players collection are not tracked entities, they will be deserialized blocks of data representing players. As such you could simplify your call to just pass a collection of Player IDs rather than detached player entities.

Firstly:

var teams = await _context.Teams.Include(t => t.Players).ToListAsync();
var team = (from selectedTeam in teams where selectedTeam.Id == id select 
            selectedTeam).SingleOrDefault();

Don't do this. You are loading ALL teams with All players into memory...

var team = _context.Teams.Include(t => t.Players).Single(t => t.Id == id);

Instead, this loads the 1 team with it's players.

For the meat of the problem, work within the collection to add and remove player associations. The following code can be condensed down into fewer lines, but I left it expanded to make it easier to follow the process.

// Get IDs for existing and updated player references to determine who has been added or removed.
var existingPlayerIds = team.Players.Select(p => p.Id).ToList();
var updatedPlayerIds = players.Select(p => p.Id).ToList();

var playerIdsToAdd = updatedPlayerIds.Except(existingPlayerIds).ToList();
var playerIdsToRemove = existingPlayerIds.Except(updatedPlayerIds).ToList();

if(playerIdsToRemove.Any())
{
    var playersToRemove = team.Players.Where(p => playerIdsToRemove.Contains(p.Id)).ToList();
    foreach(var player in playersToRemove)
        team.Players.Remove(player);
}
if(playerIdsToAdd.Any())
{
    var playersToAdd = _context.Players.Where(p => playerIdsToAdd.Contains(p.Id)).ToList();
    foreach(var player in playersToAdd)
        team.Players.Add(player);
}
_context.SaveChanges();

If your Team.Players is defined as List<Player> you can use AddRange/RemoveRange, though typically I recommend leaving collection references as ICollection<Player>.

As you can see, the "players" collection is only needed to get the IDs of the players we currently want associated to a team. We could just replace that with a collection called playerIds which would take the place of the updatedPlayerIds. This would greatly reduce the amount of data sent over the wire for a request like this.

On a final note, a method like this should probably be named something like UpdateTeamPlayers or ReplaceTeamPlayers rather than AddPlayerToTeam. AddPlayerToTeam implies adding a single player to an existing team. AddPlayersToTeam would imply adding several players to an existing team. (appending to, not replacing the current player roster)

Related