Dealing with composite primary keys and EntityFramework scaffolded controllers

A recent uni assignment has required me to implement an ASP.NET Core REST API using EntityFrameworkCore on a pre-existing database. Whilst EF Core tends to generate controllers for models with single primary keys just fine, I’ve found that it has no support for composite primary keys. This requires you to alter your controller classes to add the support manually, which in my case involved the following;

  • Alter the paths of the individual Get, Put, and Delete endpoints to contain two (or more, depending on how many keys are in your composite) parameters, and then alter the method parameters to match. This requires careful consideration as there are many different ways you can structure the parameters in your route.
  • Alter every usage of _context.nameOfModelHere.FindAsync to contain all components of your primary key. Look at the model’s Entity definition in your database context class, specifically the usage of entity.HasKey, to determine the order in which to list the components in your key to FindAsync.
  • Alter the nameOfModelHereExists method to take the correct amount of parameters for your key. Adjust the body of the method accordingly to check all parts of the key; I just added && at the end of the existing equality expression, and added more equality expressions.
    • Alter all usages of nameOfModelHereExists appropriately.
  • In the Put method, alter the second if statement to check all the components of your primary key, rather than just id.
  • In the Post method, adjust the CreatedAtAction call in the return statement to contain all the components of your primary key.
  • I would also recommend updating all the auto-generated comments that EntityFramework put in to keep things consistent.

Below is my implementation of these steps made to a controller titled BookCopies. The components of the primary key are int CopyCount and string ISBN.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using ProjectAPI.Models;
namespace ProjectAPI.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class BookCopiesController : ControllerBase
{
private readonly ProjectDatabaseContext _context;
public BookCopiesController(ProjectDatabaseContext context)
{
_context = context;
}
// GET: api/BookCopies
[HttpGet]
public IEnumerable<BookCopy> GetBookCopy()
{
return _context.BookCopy;
}
// GET: api/BookCopies/isbn/copyCount
[HttpGet("{isbn}/{copyCount}")]
public async Task<IActionResult> GetBookCopy([FromRoute] string isbn, [FromRoute] int copyCount)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
var bookCopy = await _context.BookCopy.FindAsync(copyCount, isbn);
if (bookCopy == null)
{
return NotFound();
}
return Ok(bookCopy);
}
// PUT: api/BookCopies/isbn/copyCount
[HttpPut("{isbn}/{copyCount}")]
public async Task<IActionResult> PutBookCopy([FromRoute] string isbn, [FromRoute] int copyCount, [FromBody] BookCopy bookCopy)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
if (copyCount != bookCopy.CopyCount && isbn != bookCopy.Isbn)
{
return BadRequest();
}
_context.Entry(bookCopy).State = EntityState.Modified;
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!BookCopyExists(isbn, copyCount))
{
return NotFound();
}
else
{
throw;
}
}
return NoContent();
}
// POST: api/BookCopies
[HttpPost]
public async Task<IActionResult> PostBookCopy([FromBody] BookCopy bookCopy)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
_context.BookCopy.Add(bookCopy);
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateException)
{
if (BookCopyExists(bookCopy.Isbn, bookCopy.CopyCount))
{
return new StatusCodeResult(StatusCodes.Status409Conflict);
}
else
{
throw;
}
}
return CreatedAtAction("GetBookCopy", new { copyCount = bookCopy.CopyCount, isbn = bookCopy.Isbn }, bookCopy);
}
// DELETE: api/BookCopies/isbn/copyCount
[HttpDelete("{isbn}/{copyCount}")]
public async Task<IActionResult> DeleteBookCopy([FromRoute] string isbn, [FromRoute] int copyCount)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
var bookCopy = await _context.BookCopy.FindAsync(copyCount, isbn);
if (bookCopy == null)
{
return NotFound();
}
_context.BookCopy.Remove(bookCopy);
await _context.SaveChangesAsync();
return Ok(bookCopy);
}
private bool BookCopyExists(string isbn, int copyCount)
{
return _context.BookCopy.Any(e => e.CopyCount == copyCount && e.Isbn == isbn);
}
}
}

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s