Skip to content

Instantly share code, notes, and snippets.

@BlaiseGratton
Last active November 8, 2016 01:00
Show Gist options
  • Save BlaiseGratton/cb06e9665bfc421a245ee6b44e85e6d0 to your computer and use it in GitHub Desktop.
Save BlaiseGratton/cb06e9665bfc421a245ee6b44e85e6d0 to your computer and use it in GitHub Desktop.

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:

The POST-GET

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);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment