Let's assume we have 3 models that look roughly like this:
An Artist
class:
public class Artist
{
[Key]
public int Id { get; set; }
public int BornIn { get; set; }
public string ArtistName { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public virtual List<Album> Albums { get; set; }
}
An Album
class:
public class Artist
{
[Key]
public int Id { get; set; }
public int YearReleased { get; set; }
public string Title { get; set; }
public Artist ReleasedBy { get; set; }
public virtual List<Songs> Songs { get; set; }
}
A Song
class:
public class Song
{
[Key]
public int Id { get; set; }
public Artist PerformedBy { get; set; }
public string Title { get; set; }
public Album ReleasedOn { get; set; }
public int Duration { get; set; }
}
Visual Studio will scaffold RESTful WebAPI endpoints for models. A controller generated in this way will perform the common CRUD operations that you'd expect on just that model
A default model controller scaffolded by Visual Studio can look like this:
public class ArtistsController : ApiController
{
private ApplicationDbContext db = new ApplicationDbContext();
// GET: api/Artists
public IQueryable<Artist> GetArtists()
{
return db.Artists;
}
// GET: api/Artists/5
[ResponseType(typeof(Artist))]
public IHttpActionResult GetArtist(int id)
{
Artist artist = db.Artists.Find(id);
if (artist == null)
{
return NotFound();
}
return Ok(artist);
}
// PUT: api/Artists/5
[ResponseType(typeof(void))]
public IHttpActionResult PutArtist(int id, Artist artist)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
if (id != artist.Id)
{
return BadRequest();
}
db.Entry(artist).State = EntityState.Modified;
try
{
db.SaveChanges();
}
catch (DbUpdateConcurrencyException)
{
if (!ArtistExists(id))
{
return NotFound();
}
else
{
throw;
}
}
return StatusCode(HttpStatusCode.NoContent);
}
// POST: api/Artists
[ResponseType(typeof(Artist))]
public IHttpActionResult PostArtist(Artist artist)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
db.Artists.Add(artist);
db.SaveChanges();
return CreatedAtRoute("DefaultApi", new { id = artist.Id }, artist);
}
// DELETE: api/Artists/5
[ResponseType(typeof(Artist))]
public IHttpActionResult DeleteArtist(int id)
{
Artist artist = db.Artists.Find(id);
if (artist == null)
{
return NotFound();
}
db.Artists.Remove(artist);
db.SaveChanges();
return Ok(artist);
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
db.Dispose();
}
base.Dispose(disposing);
}
private bool ArtistExists(int id)
{
return db.Artists.Count(e => e.Id == id) > 0;
}
}
The principles of REST can keep your code organized and understandable and enforce good model structure.
www.myapp.com/api/artists/ + GET => a list of all artists
www.myapp.com/api/artists/5 + GET => the artist model with the ID of 5 (what happens when that model doesn't exist?)
www.myapp.com/api/artists/ + POST + new object => if valid, creates a new instance of Artist and saves it to the db
www.myapp.com/api/artists/5 + PUT + updated object => if valid, updates existing artist model and saves it to the db
www.myapp.com/api/artists/5 + DELETE => if artist exists, delete that artist from the db
www.myapp.com/api/artists/ + DELETE => delete the entire collection
Which of these are nullipotent and idempotent?
How can/should query strings affect HTTP requests? When are they most common?
www.myapp.com/api/artists?yearborn=1954 + GET
www.myapp.com/api/artists?page=3 + GET
We can generate controllers for the other models as well... but what happens when we need to do more complex actions involving more than one model type?
RESTful URLs express relations between models almost like accessing properties of an object
www.myapp.com/api/artists/5/songs + GET => a list of songs by artist with ID of 5
www.myapp.com/api/artists/5/albums + GET => a list of albums released by artist 5
www.myapp.com/api/artists/5/albums/2/songs + GET => a list of songs on album 2 by artist 5
www.myapp.com/api/albums + GET => a list of all albums on the site
www.myapp.com/api/artists/5/albums + POST + model data => if valid, adds a new album to this artist
How do we handle custom routes in our WebAPI controllers? Better to be explicit with our two friends:
-Route Helper
-Explicit Http method tags
For example:
[HttpPost]
[Route("/api/artists/{artistId}/albums")]
public IHttpActionResult AddAlbumToArtist(int artistId, Album album)
{
//use repo to check if artist exists, then create album and associate it with artist
//also create new song instances from the post body
//send back response
}
From the client side:
angular.module('omgMyApp', []).controller('artistController', function($scope, apiService) {
$scope.artistId = 5;
$scope.newAlbum = {
yearReleased: 1999,
title: "FanMail",
songs: [{...}, {...}, {...}]
};
$scope.addAlbum = function() {
apiService.postAlbumToArtist($scope.artistId, $scope.newAlbum).then(function(success) {
// you did it!
}, function(error) {
// uh-oh
});
};
}).service('apiService', function($http) {
const service = this;
service.postAlbumToArtist = (artistId, album) => $http.post('/api/artists/${artistId}/albums', album);
});
Keep in mind that it is entirely possible to not follow these guidelines and still perform CRUD operations. The result is less readability and the obfuscation of your API's intent.
For example:
Given a query operation that needs restrictions (such as filter parameters), you will sometimes see an endpoint accepting a posted filter object.
filterParams = {
yearPublished: 2000,
genres: [fiction, sci-fi, space]
}
E.g. www.books.com/api/books + POST + filterParams
How does this violate REST? How would you change it?
www.books.com/api/books?yearPublished=2000&genres=fiction,sci-fi,space + GET
It is possible to heavily violate HTTP protocol - but nothing is stopping you
www.books.com/api/createauthorbook/twice?id=22 + GET
[HttpGet]
[Route("/api/createauthorbook/{times}")]
public IHttpActionResult DeletePublisher(string times)
{
int id = int.Parse(Request.GetQueryNameValuePairs()["id"]);
if (times == "twice")
{
id *= 2;
}
repo.DeletePublisherById(id);
return Created("DefaultApi", new Author());
}
This one isn't nearly as bad, but it's still bad
www.books.com/api/createbook?title=mytitle&author=Bryan&publishedIn=1990 + GET
[HttpGet]
[Route("/api/createbook")]
public IHttpActionResult AddBook()
{
var params = Request.GetQueryNameValuePairs();
Book newBook = new Book
{
Title = params["title"],
Author = params["author"],
PublishedIn = params["publishedIn"]
};
repo.AddBook(newBook);
return Created("DefaultApi", newBook);
}
This is much better expressed as a POST: ``` newBook = { title: "myTitle", author: "Bryan", publishedIn: 1990 } ```
www.books.com/api/books + POST + newBook
[HttpPost]
[Route("/api/books")]
public IHttpActionResult AddBook(Book newBook)
{
repo.AddBook(newBook);
return Created("DefaultApi", newBook);
}