Introduction to Web API - Working with Data - ASP.NET MVC 5 with Bootstrap and Knockout.js (2015)

ASP.NET MVC 5 with Bootstrap and Knockout.js (2015)

Part II. Working with Data

Chapter 8. Introduction to Web API

Web API was briefly mentioned in Chapter 1 because Visual Studio provides a template for automatically creating a Web API application. Web API allows you to build RESTful web applications. When using Web API in combination with the MVC architecture pattern, the Controller is often the entry point for the Resource (Model) being interacted with. The View with Web API is often a JSON or XML representation of the resource.

This chapter will demonstrate the Web API by enhancing the CRUD interaction with the authors that the previous two chapters have been focusing on. In this chapter, the listing of authors will be updated to perform the sorting and paging of authors via a Web API controller. Likewise, adding and editing an author will also interact with the same Web API controller. Previously, new HTML pages were returned, but when Web API is integrated, the HTML will be updated to use Knockout data bindings. These will be dynamically updated by the result of an AJAX request to a Web API endpoint, which will prevent full-page reloads.

Installing Web API

In Chapter 1, when the BootstrapIntroduction project was first created, Web API was not included. This means it now needs to be added via the NuGet Package Manager. If you wish to avoid the visual NuGet Package Manager, a console utility is also available. To install packages via the console, click Tools → NuGet Package Manager → Package Manager Console. In the console window, enter Install-Package Microsoft.AspNet.WebApi to install the Web API package.

When a new project is created with Web API, Visual Studio scaffolds several additional pieces that, when installed via the NuGet Package Manager, are not set up. Let’s configure those now.

Example 8-1 is a new file called WebApiConfig and should be added to the App_Start folder. This file is very similar to the RouteConfig that was explored in Chapter 1 with the default routing.

Example 8-1. WebApiConfig

using System;

using System.Collections.Generic;

using System.Linq;

using System.Web.Http;

namespace BootstrapIntroduction

{

public static class WebApiConfig

{

public static void Register(HttpConfiguration config)

{

// Web API configuration and services

// Web API routes

config.MapHttpAttributeRoutes();

config.Routes.MapHttpRoute(

name: "DefaultApi",

routeTemplate: "api/{controller}/{id}",

defaults: new { id = RouteParameter.Optional }

);

}

}

}

Just like RouteConfig, this creates a new default route that will allow the common HTTP verbs associated with a RESTful application to work out of the box. A key difference is that all URLs are prefixed with api before the controller and action.

Next, in the root folder of the project, the Global.asax.cs file requires a minor update (as shown in Example 8-2) to configure the newly added Web API routes.

Example 8-2. Updated Global.asax.cs

using BootstrapIntroduction.DAL;

using System;

using System.Collections.Generic;

using System.Data.Entity;

using System.Linq;

using System.Web;

using System.Web.Http;

using System.Web.Mvc;

using System.Web.Optimization;

using System.Web.Routing;

namespace BootstrapIntroduction

{

public class MvcApplication : System.Web.HttpApplication

{

protected void Application_Start()

{

AreaRegistration.RegisterAllAreas();

GlobalConfiguration.Configure(WebApiConfig.Register);

FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);

RouteConfig.RegisterRoutes(RouteTable.Routes);

BundleConfig.RegisterBundles(BundleTable.Bundles);

var bookContext = new BookContext();

Database.SetInitializer(new BookInitializer());

bookContext.Database.Initialize(true);

}

}

}

If you create a new project and include the Web API framework at the same time, these steps are not needed because Visual Studio will automatically configure this.

And finally, a new AuthorsController can be created. Prior to creating the file for the new AuthorsController, create a new folder called api inside of the Controllers folder. Once created, right-click the new folder and add a new Controller. This time, select Web API 2 Controller - Empty to finish creating the empty AuthorsController (as shown in Example 8-3).

Example 8-3. Empty AuthorsController

using System;

using System.Collections.Generic;

using System.Linq;

using System.Net;

using System.Net.Http;

using System.Web.Http;

namespace BootstrapIntroduction.Controllers.Api

{

public class AuthorsController : ApiController

{

}

}

Much like the regular MVC controllers that were created earlier, Web API controllers are classes that extend a base ApiController class instead of the Controller class. Like the Controller class, the ApiController contains a lot of core methods that will help bind and execute the custom controller code and return for output.

Updating the List of Authors

In Chapter 5, the list of authors was sorted and paged via an MVC controller. This meant that each time a link was clicked, the entire HTML would be refreshed. In this chapter, a Web API controller will be used not to return back a full HTML page, but to return only an updated list of authors with the sorting and paging applied.

The Index view will contain new Knockout bindings that will then automatically update the list of authors when the AJAX call is successfully completed.

Example 8-4 is the Web API AuthorsController with a Get function that accepts the QueryOptions as input from the URL. The code to sort the authors and page through them is identical to the MVC AuthorsController.

Example 8-4. Get AuthorsController

using System;

using System.Collections.Generic;

using System.Data;

using System.Data.Entity;

using System.Data.Entity.Infrastructure;

using System.Linq;

using System.Linq.Dynamic;

using System.Net;

using System.Net.Http;

using System.Web.Http;

using System.Web.Http.Description;

using BootstrapIntroduction.DAL;

using BootstrapIntroduction.Models;

using BootstrapIntroduction.ViewModels;

namespace BootstrapIntroduction.Controllers.Api

{

public class AuthorsController : ApiController

{

private BookContext db = new BookContext();

// GET: api/Authors

public ResultList<AuthorViewModel> Get([FromUri]QueryOptions queryOptions)

{

var start = (queryOptions.CurrentPage - 1) * queryOptions.PageSize;

var authors = db.Authors.

OrderBy(queryOptions.Sort).

Skip(start).

Take(queryOptions.PageSize);

queryOptions.TotalPages =

(int)Math.Ceiling((double)db.Authors.Count() / queryOptions.PageSize);

AutoMapper.Mapper.CreateMap<Author, AuthorViewModel>();

return new ResultList<AuthorViewModel>

{

QueryOptions = queryOptions,

Results = AutoMapper.Mapper.Map<List<Author>, List<AuthorViewModel>>

(authors.ToList())

};

}

protected override void Dispose(bool disposing)

{

if (disposing)

{

db.Dispose();

}

base.Dispose(disposing);

}

}

}

Example 8-4 demonstrates some immediate differences between an MVC controller and a Web API controller. The QueryOptions input parameter in both controllers comes from URL parameters; however, they are attributed differently in the controllers. An MVC controller defines it as Form, and a Web API controller defines it as FromUri.

Second, the MVC controller would typically finish an action by returning via a call to the View function. With Web API, the object that you wish to return is returned as-is. In this example, a new class called ResultList (shown in Example 8-5) of type AuthorViewModel is returned. Based on the request made to the Web API controller, the results will be encoded as JSON or XML. Knockout works really well with JSON, so that is what will be used.

Example 8-5 is a new ViewModel called ResultList. This class should be added to the previously created ViewModels folder. This class contains a generic property called List<T> that allows this class to be reused for other listing pages. In Example 8-4, the ResultList was created with a type of AuthorViewModel.

Example 8-5. ResultList ViewModel

using Newtonsoft.Json;

using System;

using System.Collections.Generic;

using System.Linq;

using System.Web;

namespace BootstrapIntroduction.ViewModels

{

public class ResultList<T>

{

[JsonProperty(PropertyName="queryOptions")]

public QueryOptions QueryOptions { get; set; }

[JsonProperty(PropertyName = "results")]

public List<T> Results { get; set; }

}

}

Along with the generic list property, the ResultList contains a second property for the QueryOptions. In Chapter 5, the QueryOptions were returned to the Index view in the ViewBag. In this example, they are bound in the ResultList ViewModel. This model will be used by Knockout to dynamically update the user interface (UI) when the authors are sorted or paged.

To make it easier to provide consistency to the Knockout ViewModel, the Index view in the original MVC AuthorsController will also be updated to leverage the new ResultList ViewModel. Example 8-6 contains an updated Index function from the AuthorsController that constructs the new ResultList ViewModel just like Example 8-4 did in the Web API AuthorsController.

Example 8-6. Index action

public ActionResult Index([Form] QueryOptions queryOptions)

{

var start = (queryOptions.CurrentPage - 1) * queryOptions.PageSize;

var authors = db.Authors.

OrderBy(queryOptions.Sort).

Skip(start).

Take(queryOptions.PageSize);

queryOptions.TotalPages =

(int)Math.Ceiling((double)db.Authors.Count() / queryOptions.PageSize);

AutoMapper.Mapper.CreateMap<Author, AuthorViewModel>();

return View(new ResultList<AuthorViewModel>

{

QueryOptions = queryOptions,

Results = AutoMapper.Mapper.Map<List<Author>,

List<AuthorViewModel>>(authors.ToList())

});

}

The QueryOptions that were previously passed via the ViewBag have been moved into the ResultList ViewModel. This will force the Index.cshtml view to require minor changes to accomodate the new model being bound and where the QueryOptions are retrieved from.

Example 8-7 contains a fully updated Index view. It contains the previously described changes, as well as several others that implement Knockout bindings to perform the previous sorting and paging that were happening via regular HTML links. These links have been updated to generate Knockout bindings that will execute an AJAX request to the Web API and dynamically update the list of authors.

Example 8-7. Index view

@using BootstrapIntroduction.ViewModels

@model ResultList<AuthorViewModel>

@{

ViewBag.Title = "Authors";

var queryOptions = Model.QueryOptions;

}

<h2>Authors</h2>

<p>@Html.ActionLink("Create New", "Create")</p>

<div style="display: none" data-bind="visible: pagingService.entities().length > 0">

<table class="table table-bordered table-striped">

<thead>

<tr>

<th>@Html.BuildKnockoutSortableLink("First Name", "Index", "firstName")</th>

<th>@Html.BuildKnockoutSortableLink("Last Name", "Index", "lastName")</th>

<th>Actions</th>

</tr>

</thead>

<tbody data-bind="foreach: pagingService.entities">

<tr>

<td data-bind="text: firstName"></td>

<td data-bind="text: lastName"></td>

<td>

<a data-bind="attr: { href: '@Url.Action("Details")/' + id }"

class="btn btn-info">Details</a>

<a data-bind="attr: { href: '@Url.Action("Edit")/' + id }"

class="btn btn-primary">Edit</a>

<a data-bind="click: $parent.showDeleteModal,

attr: { href: '@Url.Action("Delete")/' + id }"

class="btn btn-danger">Delete</a>

</td>

</tr>

</tbody>

</table>

@Html.BuildKnockoutNextPreviousLinks("Index")

</div>

<div style="display: none" data-bind="visible: pagingService.entities().length == 0"

class="alert alert-warning alert-dismissible" role="alert">

<button type="button" class="close" data-dismiss="alert">

<span aria-hidden="true">×</span><span class="sr-only">Close</span>

</button>

There are no authors to display. Click @Html.ActionLink("here", "Create")

to create one now.

</div>

@section Scripts {

@Scripts.Render("/Scripts/Services/PagingService.js",

"/Scripts/ViewModels/AuthorIndexViewModel.js")

<script>

var viewModel = new AuthorIndexViewModel(@Html.HtmlConvertToJson(Model));

ko.applyBindings(viewModel);

</script>

}

This code will not compile just yet because new files and HtmlHelper extensions need to be created. Prior to reviewing those, let’s go through the several important changes to the Index view.

First, there was an array of authors contained in the Knockout ViewModel. This has been replaced with a new observableArray called entities under the pagingService object. The pagingService is a new JavaScript class that can be reused across different HTML views to allow easy paging and sorting of your data. The entities is an observableArray, which means whenever this array changes, Knockout will automatically update any data bindings that reference it. When changing the sort order, the list of authors will be dynamically redrawn with the results of the AJAX call from the Web API controller.

Next, the previously created HtmlHelper extension methods that helped build the sortable link and the next/previous page links have been updated to call a new method. They contain the same name with Knockout injected after the word Build to identify that these methods will build Knockout-specific links.

The final change in the Index view is that the Scripts.Render call has been updated to include the new PagingService file that will be created in Example 8-9.

Example 8-8 contains the newly created HtmlHelper extension methods that create the Knockout data-bound links for sorting and paging.

Example 8-8. HtmlHelperExtension

using BootstrapIntroduction.ViewModels;

using Newtonsoft.Json;

using System.Web;

using System.Web.Mvc;

using System.Web.Mvc.Html;

public static class HtmlHelperExtensions

{

// other functions removed for an abbreviated example

public static MvcHtmlString BuildKnockoutSortableLink(this HtmlHelper htmlHelper,

string fieldName, string actionName, string sortField)

{

var urlHelper = new UrlHelper(htmlHelper.ViewContext.RequestContext);

return new MvcHtmlString(string.Format(

"<a href=\"{0}\" data-bind=\"click: pagingService.sortEntitiesBy\"" +

" data-sort-field=\"{1}\">{2} " +

"<span data-bind=\"css: pagingService.buildSortIcon('{1}')\"></span></a>",

urlHelper.Action(actionName),

sortField,

fieldName));

}

public static MvcHtmlString BuildKnockoutNextPreviousLinks(

this HtmlHelper htmlHelper, string actionName)

{

var urlHelper = new UrlHelper(htmlHelper.ViewContext.RequestContext);

return new MvcHtmlString(string.Format(

"<nav>" +

" <ul class=\"pager\">" +

" <li data-bind=\"css: pagingService.buildPreviousClass()\">" +

" <a href=\"{0}\" data-bind=\"click: pagingService.previousPage\">

Previous</a></li>" +

" <li data-bind=\"css: pagingService.buildNextClass()\">" +

" <a href=\"{0}\" data-bind=\"click: pagingService.nextPage\">Next

</a></li></li>" +

" </ul>" +

"</nav>",

@urlHelper.Action(actionName)

));

}

// other functions removed for an abbreviated example

}

These two functions are quite similar to their counterparts (the non-Knockout versions) in that they return a new MvcHtmlString to perform the sorting or paging. The non-Knockout versions leveraged the QueryOptions to construct a full URL. These functions instead leverage the Knockoutclick data binding. The click data binding allows you to specify a function to call inside your Knockout ViewModel.

The BuildKnockoutSortableLink binds the click to the sortEntitiesBy function within the aforementioned PagingService class. Inside this link, the sorting icon is leveraging another Knockout data binding called css. The results of the buildSortIcon function in the PagingService returns the appropriate class names to build the sort icon. The buildSortIcon is a computedObservable function, which means that when Knockout detects a change in any observed property within the function, it will re-execute the function to update what it is data bound to. This will allow for the sorting link to change each time you alter the sort order.

The BuildKnockoutNextPreviousLinks works quite similarly to the sortable link function. The previous and next links are data bound to the click event that calls the previousPage and nextPage functions, respectively, from the PagingService class. Both links also contain a css data binding to mark them as disabled when the previous and next links are unavailable.

Example 8-9 contains the new PagingService JavaScript class. For organization purposes, I have created a new Services folder inside of the Scripts folder and added the PagingService.js file here.

Example 8-9. PagingService

function PagingService(resultList) {

var self = this;

self.queryOptions = {

currentPage: ko.observable(),

totalPages: ko.observable(),

pageSize: ko.observable(),

sortField: ko.observable(),

sortOrder: ko.observable()

};

self.entities = ko.observableArray();

self.updateResultList = function (resultList) {

self.queryOptions.currentPage(resultList.queryOptions.currentPage);

self.queryOptions.totalPages(resultList.queryOptions.totalPages);

self.queryOptions.pageSize(resultList.queryOptions.pageSize);

self.queryOptions.sortField(resultList.queryOptions.sortField);

self.queryOptions.sortOrder(resultList.queryOptions.sortOrder);

self.entities(resultList.results);

};

self.updateResultList(resultList);

self.sortEntitiesBy = function (data, event) {

var sortField = $(event.target).data('sortField');

if (sortField == self.queryOptions.sortField() &&

self.queryOptions.sortOrder() == "ASC")

self.queryOptions.sortOrder("DESC");

else

self.queryOptions.sortOrder("ASC");

self.queryOptions.sortField(sortField);

self.queryOptions.currentPage(1);

self.fetchEntities(event);

};

self.previousPage = function (data, event) {

if (self.queryOptions.currentPage() > 1) {

self.queryOptions.currentPage(self.queryOptions.currentPage() - 1);

self.fetchEntities(event);

}

};

self.nextPage = function (data, event) {

if (self.queryOptions.currentPage() < self.queryOptions.totalPages()) {

self.queryOptions.currentPage(self.queryOptions.currentPage() + 1);

self.fetchEntities(event);

}

};

self.fetchEntities = function (event) {

var url = '/api/' + $(event.target).attr('href');

url += "?sortField=" + self.queryOptions.sortField();

url += "&sortOrder=" + self.queryOptions.sortOrder();

url += "&currentPage=" + self.queryOptions.currentPage();

url += "&pageSize=" + self.queryOptions.pageSize();

$.ajax({

dataType: 'json',

url: url

}).success(function (data) {

self.updateResultList(data);

}).error(function () {

$('.body-content').prepend('<div class="alert alert-danger">

<strong>Error!</strong> There was an error fetching the data.</div>');

});

};

self.buildSortIcon = function (sortField) {

return ko.pureComputed(function () {

var sortIcon = 'sort';

if (self.queryOptions.sortField() == sortField) {

sortIcon += '-by-alphabet';

if (self.queryOptions.sortOrder() == "DESC")

sortIcon += '-alt';

}

return 'glyphicon glyphicon-' + sortIcon;

});

};

self.buildPreviousClass = ko.pureComputed(function () {

var className = 'previous';

if (self.queryOptions.currentPage() == 1)

className += ' disabled';

return className;

});

self.buildNextClass = ko.pureComputed(function () {

var className = 'next';

if (self.queryOptions.currentPage() == self.queryOptions.totalPages())

className += ' disabled';

return className;

});

}

The PagingService class starts by creating two properties: the queryOptions and the entities array. The queryOptions makes all of its child properties observable. This will be used to dynamically update the sort icons and build the AJAX URL to update the data. The entities array will contain the list of authors.

The updateResultList function is then defined that accepts a resultList model and sets all of the observables that were just defined. This function is then immediately called afterward to populate the observables with the input parameter to the PagingService class. This function will also be used after the AJAX calls to update all of the observables with the results from the Web API controller.

The sortEntitiesBy, previousPage, and nextPage functions are defined next. These functions update the affected queryOptions properties to perform the sorting and paging, respectively. sortEntitiesBy sets the sortOrder and sortField passed from the link that is clicked. It then resets thecurrentPage to 1. The previousPage and nextPage functions decrement and increment the currentPage property, respectively. Both functions also perform a check to prevent going below and above the minimum and maximum pages. And finally, all three functions call the sharedfetchEntities function.

The fetchEntities function builds the URL to call using the href attribute from the link that was clicked. Then the url variable is updated to set the various queryOptions. An AJAX request is then made to the URL. On success, the updateResultList function is called with the results of the AJAX request to update the observed properties. When the properties are updated, Knockout will automatically update the sort icons, list of authors, and paging links dynamically. If an error occurs with the AJAX request, an alert is added to notify the user they should try again.

The final three functions, buildSortIcon, buildPreviousClass, and buildNextClass, are created as pureComputed functions. The buildSortIcon accesses the sortField and sortOrder observed properties from the queryOptions variable. Whenever these properties are updated, any UI element that is data bound to the function will be redrawn with the updated results of the function. The buildPreviousClass and buildNextClass work similarily, but they are updated whenever the currentPage property on the queryOptions variable is updated.

Example 8-10 is an updated AuthorIndexViewModel. The only change is that there is no longer an array of authors. Instead, a new pagingService variable is instantiated with the new PagingService class passing in the serialized resultList from the Index view.

Example 8-10. Updated AuthorIndexViewModel

function AuthorIndexViewModel(resultList) {

var self = this;

self.pagingService = new PagingService(resultList);

self.showDeleteModal = function (data, event) {

self.sending = ko.observable(false);

$.get($(event.target).attr('href'), function (d) {

$('.body-content').prepend(d);

$('#deleteModal').modal('show');

ko.applyBindings(self, document.getElementById('deleteModal'));

});

};

self.deleteAuthor = function (form) {

self.sending(true);

return true;

};

};

When using Knockout, I like leveraging Web API controllers to return back only JSON data instead of the full HTML to build the list of authors. Knockout makes it really simple to dynamically update the UI by data binding to observable properties, arrays, or computed functions.

Updating the Add/Edit Authors Form

Updating the add and edit is much simpler than updating the list of authors. Most of the effort in the previous section was about maintaining the user interface. This is not required for the add and edit form, because on success, the user was redirected back to the list of authors, and on error, an alert message was dynamically added.

That will remain the same. The minor updates in the JavaScript ViewModel will involve updating the AJAX request type and changing the contentType (shown in Example 8-11). The rest will remain the same on the JavaScript side of things.

The MVC controller that was scaffolded in Chapter 5 will be updated to remove the automatically generated form post version of the Create and Edit actions. Similar actions will be created in the Authors Web API controller.

Example 8-11 contains an updated validateAndSave function from the AuthorFormViewModel. It removes the previous inline if statement for the URL and moves it to the AJAX request type property. The contentType is changed from a standard form post to be of type application/json. The data property has been updated to leverage the similar ko.toJS to be ko.toJSON. It works quite similarily to the former, but it encodes the JavaScript variable into valid JSON to send to the server.

Example 8-11. Updated validateAndSave function

self.validateAndSave = function (form) {

if (!$(form).valid())

return false;

self.sending(true);

// include the anti forgery token

self.author.__RequestVerificationToken = form[0].value;

$.ajax({

url: '/api/authors',

type: (self.isCreating) ? 'post' : 'put',

contentType: 'application/json',

data: ko.toJSON(self.author)

})

.success(self.successfulSave)

.error(self.errorSave)

.complete(function () { self.sending(false) });

};

Previously, when an author was being created, the AJAX request was going to a different URL than when the author was being edited. When interacting with a RESTful API, the URL stays consistent; instead, the request type changes. When you are adding, the type is post. When you are editing, the request type is changed to a put. Similarily, if you were to implement a delete action, the request type would be delete and the URL would remain the same.

Example 8-12 is an updated Authors Web API controller. Two new functions Post and Put have been added that accept the AuthorViewModel as input.

Example 8-12. Updated Web API AuthorsController

using System;

using System.Collections.Generic;

using System.Data;

using System.Data.Entity;

using System.Data.Entity.Infrastructure;

using System.Linq;

using System.Linq.Dynamic;

using System.Net;

using System.Net.Http;

using System.Web.Http;

using System.Web.Http.Description;

using BootstrapIntroduction.DAL;

using BootstrapIntroduction.Models;

using BootstrapIntroduction.ViewModels;

namespace BootstrapIntroduction.Controllers.Api

{

public class AuthorsController : ApiController

{

private BookContext db = new BookContext();

// GET: api/Authors

public ResultList<AuthorViewModel> Get([FromUri]QueryOptions queryOptions)

{

var start = (queryOptions.CurrentPage - 1) * queryOptions.PageSize;

var authors = db.Authors.

OrderBy(queryOptions.Sort).

Skip(start).

Take(queryOptions.PageSize);

queryOptions.TotalPages =

(int)Math.Ceiling((double)db.Authors.Count() / queryOptions.PageSize);

AutoMapper.Mapper.CreateMap<Author, AuthorViewModel>();

return new ResultList<AuthorViewModel>

{

QueryOptions = queryOptions,

Results = AutoMapper.Mapper.Map<List<Author>, List<AuthorViewModel>>

(authors.ToList())

};

}

// PUT: api/Authors/5

[ResponseType(typeof(void))]

public IHttpActionResult Put(AuthorViewModel author)

{

if (!ModelState.IsValid)

{

return BadRequest(ModelState);

}

AutoMapper.Mapper.CreateMap<AuthorViewModel, Author>();

db.Entry(AutoMapper.Mapper.Map<AuthorViewModel, Author>(author)).State

= EntityState.Modified;

db.SaveChanges();

return StatusCode(HttpStatusCode.NoContent);

}

// POST: api/Authors

[ResponseType(typeof(AuthorViewModel))]

public IHttpActionResult Post(AuthorViewModel author)

{

if (!ModelState.IsValid)

{

return BadRequest(ModelState);

}

AutoMapper.Mapper.CreateMap<AuthorViewModel, Author>();

db.Authors.Add(AutoMapper.Mapper.Map<AuthorViewModel, Author>(author));

db.SaveChanges();

return CreatedAtRoute("DefaultApi", new { id = author.Id }, author);

}

protected override void Dispose(bool disposing)

{

if (disposing)

{

db.Dispose();

}

base.Dispose(disposing);

}

}

}

These functions are almost identical to the scaffolded MVC AuthorsController in that they use AutoMapper to convert the ViewModel to a data model and save it using the Entity Framework DbContext.

The key difference is that neither function returns an HTML view. The Put function returns an empty result and sets the HTTP Status Code to NoContent. The Post function returns an updated AuthorViewModel with the id property set with the newly created value from the database.

HTTP STATUS CODES

RESTful applications rely heavily on HTTP Status Codes to provide the integrator with feedback of the API request. Three main levels are commonly used:

Successful 2xx

The common successful requests are 200 OK, 201 Created, and 204 No Content. Any request in the 200s is used to identify that the API request was successful.

Client Error 4xx

The common client error requests are 400 Bad Request (the input data was not valid), 401 Unauthorized, 404 Not Found, and 405 Method Not Allowed. Any request in the 400s is used to identify that the API integrator is doing something incorrectly. It’s quite common for the body of the response to contain a helpful error message to fix the problem prior to resubmitting the same request.

Server Error 5xx

The common server error requests are 500 Internal Server Error, 501 Not Implemented, and 503 Service Unavailable (often used for rate-limiting the number of requests to an API). Any request in the 500s is used to identify that an error occurred on the server and the API integrator should try his request again. Similar to 400 level requests, it is quite common for the body of the response to contain a helpful error message identifying what the problem is.

To avoid unnecessary extra code, I removed the Create and Edit functions from the MVC AuthorsController that perform the saving of data to the database. I left the two functions that display the form to the user.

Summary

This chapter demonstrated using Web API controllers to only return JSON data from the server that gets data bound to observable Knockout properties. I think it nicely demonstrates how Knockout is capable of updating multiple UI elements when one or more observable properties are changed. It’s a much smoother user interface to dynamically update the table of authors without the need for a full-page reload of the entire HTML.

If you would like to further explore Web API controllers, I would suggest that you try converting the delete modal to use two other common Web API functions. The first is an overloaded Get (by id) that returns an individual AuthorViewModel instead of a list. The second is a Delete method that also accepts an ID and deletes the author from the database.