As part of last week’s post on Aurelia’s Event Aggregator I needed a way to update existing contacts. This was a hole in my existing API and it was time to fill it. See last week’s post for usage on the client side this post will only be covering the server side.
First off here is the code I added to the ContactsController in the API folder of my project. I thought the overview would be helpful a helpful context as we break down the different parts of the function.
[HttpPut("{id}")] public async Task<IActionResult> Update(int id, [FromBody] Contact updatedContact) { if (updatedContact == null || updatedContact.Id != id) { return HttpBadRequest(); } var contact = await GetContacts() .Where(c => c.Id == id).FirstOrDefaultAsync(); if (contact == null) { return HttpNotFound(); } if (!ModelState.IsValid) { return new BadRequestObjectResult(ModelState); } _dbContext.Entry(contact).State = EntityState.Detached; _dbContext.Update(updatedContact); updatedContact.UserId = User.GetUserId(); await _dbContext.SaveChangesAsync(); return CreatedAtRoute("GetById", new { controller = "Contacts", id = updatedContact.Id }, updatedContact); }
First off the Update function is decorated to only handle HttpPut actions with an ID. The function itself of course requires an ID. The FormBody attribute on the updatedContact parameter tells the framework to build a contact object from the request body.
[HttpPut("{id}")] public async Task<IActionResult> Update(int id, [FromBody] Contact updatedContact)
Next if the updatedContact was unable to be built from the request body or if the ID passed to the update function doesn’t match the ID in the updatedContact then a bad request status is returned.
if (updatedContact == null || updatedContact.Id != id) { return HttpBadRequest(); }
Next I query the database for a matching contact. This query is limited to the current users so it should help prevent a contact for the wrong user being updated. If no matching contact is found then not found status is returned.
var contact = await GetContacts() .Where(c => c.Id == id).FirstOrDefaultAsync(); if (contact == null) { return HttpNotFound(); }
Next check to see if the mode state is valid and return bad request object result if it is not.
if (!ModelState.IsValid) { return new BadRequestObjectResult(ModelState); }
This next bit was a little bit of a challenge. Since entity framework was queried to see if the updated contact already exists I was getting an error when trying to use the DB context’s update function. One way to address this error would have been to update the contact pulled from the database with the values from the updated contact from the request body. If I had of gone this route something like automapper would have made the job fairly easy. Instead I used the DB context entry on the contact returned from the database to set its state to detached which tells the context to stop tracking that item. After that the context’s update function worked like a charm and I could save changes based on the updatedContact parameter.
_dbContext.Entry(contact).State = EntityState.Detached; _dbContext.Update(updatedContact); updatedContact.UserId = User.GetUserId(); await _dbContext.SaveChangesAsync();
Finally I returned the client the updated contact. This is the same thing I did for post and may or may not be the best thing to do on a put.
return CreatedAtRoute("GetById", new { controller = "Contacts", id = updatedContact.Id }, updatedContact);
I am not sure how much of this is the “correct” way to handle a put. After seeing how some of this is handled when using razor with anti-forgery tokens and the ability to only allow certain fields to be updated this implementation seems lacking. This is a jumping off point, but I need to do more research on how to properly secure an API.