The following HTTP verbs may be used when creating RESTful services:
GET - Requests a specific representation of a resource
PUT - Create or update a resoure with the supplied representation
DELETE - Delete the specified resource
POST - Submit data to be processed by the identified resource
HEAD - Similar to GET but only retrieves headers
OPTIONS - Returns the methods supported by the identified resource
In this article I'll show how to build a "Notes" (a.k.a. "reminders") RESTful WCF service and consume it using jQuery / JSON from the client. We'll see how to create the service interface, what decorations (Attributes) are required, what's required in the web.config settings, how to make the jQuery $ajax calls to the service, and how to handle the server side logic with a small SQLite database. Everything in the downloadable solution is included - you need only unzip it and it should "work out of the box".
In order to create a WCF service, you can start with a standard ASP.NET Web Application project and add a new WCF Service.
To Create a complete WCF REST service, you start with the interface. Here's mine:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.ServiceModel.Web;
using System.Text;
using System.Web.Script.Services;
namespace RESTFulNotes
{
[ServiceContract]
public interface IService1
{
//GetNotes: method to get all Notes. In this UriTemplate there is tag querystring item ={tag} allows us to filter note by category name
[WebInvoke(Method = "POST", ResponseFormat = WebMessageFormat.Json, UriTemplate = "Notes?tag={tag}")]
[OperationContract]
Notes FindNotes(string tag);
[WebInvoke(Method = "POST", ResponseFormat = WebMessageFormat.Json, UriTemplate = "Notes/{ID}")]
[OperationContract]
Note GetNote(string ID);
[WebInvoke(Method = "PUT", ResponseFormat = WebMessageFormat.Json, UriTemplate = "Notes/{ID}")]
[OperationContract]
string PutNote(Note Noteobj, string ID);
[WebInvoke(Method = "DELETE", UriTemplate = "Notes/{ID}")]
[OperationContract]
void DeleteNote(string ID);
}
// Use a data contract as illustrated in the sample below to add composite types to service operations
[DataContract]
public class Note
{
[DataMember]
public int ID { get; set; }
[DataMember]
public string Category { get; set; }
[DataMember]
public string Subject { get; set; }
[DataMember]
public string NoteText { get; set; }
}
[CollectionDataContract]
public class Notes : List<Note>
{
public Notes() { }
public Notes(List<Note> Notes) : base(Notes) { }
}
}
Note that the WebInvoke decoration is the key item here. It defines the Method, the ResponseFormat, and the custom UriTemplate you want to use. With this information, the WCF runtime is able to handle everything - all you need to do is implement the methods in server-side code, and call them correctly from client script in your consumer page.
With the IService1 interface complete and all attributes correctly marked, we're now ready to implement our interface:
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Data.SQLite;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Text;
using System.ServiceModel.Web;
using System.Net;
using System.ServiceModel.Activation;
using System.Web;
namespace RESTFulNotes
{
[AspNetCompatibilityRequirements(RequirementsMode =AspNetCompatibilityRequirementsMode.Allowed)]
public class Service1 : IService1
{
private SQLiteConnection conn;
Service1()
{
conn = Global.conn;
}
#region IService1 Members
public Notes FindNotes(string tag)
{
WebOperationContext.Current.OutgoingResponse.StatusCode = HttpStatusCode.OK;
Notes notes = new Notes();
string Sql = "SELECT * FROM NOTE WHERE CATEGORY=@TAG";
SQLiteCommand cmd = new SQLiteCommand(Sql, conn);
cmd.Parameters.AddWithValue("@TAG", tag);
SQLiteDataAdapter da = new SQLiteDataAdapter(Sql, conn);
da.SelectCommand = cmd;
DataTable dt = new DataTable();
da.Fill(dt);
Note note;
if (dt.Rows.Count > 0)
{
foreach (DataRow row in dt.Rows)
{
note = new Note();
note.ID = Convert.ToInt32(row["ID"]);
note.Category = (string) row["Category"];
note.Subject = (string) row["Subject"];
note.NoteText = (string) row["NoteText"];
notes.Add(note);
}
}
return notes;
}
public Note GetNote(string ID)
{
string Sql = "SELECT * FROM NOTE WHERE ID=@ID";
SQLiteCommand cmd = new SQLiteCommand(Sql, conn);
cmd.Parameters.AddWithValue("@ID", int.Parse(ID));
SQLiteDataAdapter da = new SQLiteDataAdapter(Sql, conn);
da.SelectCommand = cmd;
DataTable dt = new DataTable();
da.Fill(dt);
Note note=null;
if (dt.Rows.Count > 0)
{
DataRow row = dt.Rows[0];
note = new Note();
note.ID = Convert.ToInt32(row["ID"]);
note.Category = (string)row["Category"];
note.Subject = (string)row["Subject"];
note.NoteText = (string)row["NoteText"];
}
WebOperationContext.Current.OutgoingResponse.StatusCode = HttpStatusCode.OK;
return note;
}
public string PutNote(Note note, string ID)
{
String Sql = "INSERT INTO NOTE (CATEGORY,SUBJECT,NOTETEXT) VALUES(@CATEGORY,@SUBJECT,@NOTETEXT)";
SQLiteCommand cmd = new SQLiteCommand(Sql, conn);
cmd.Parameters.AddWithValue("@CATEGORY", note.Category);
cmd.Parameters.AddWithValue("@SUBJECT", note.Subject);
cmd.Parameters.AddWithValue("@NOTETEXT", note.NoteText);
if(cmd.Connection.State ==ConnectionState.Closed)
cmd.Connection.Open();
cmd.ExecuteNonQuery();
cmd.CommandText = "SELECT MAX(ID) FROM NOTE";
var id = cmd.ExecuteScalar();
int retval = Convert.ToInt32(id);
cmd.Connection.Close();
WebOperationContext.Current.OutgoingResponse.StatusCode = HttpStatusCode.Created;
return "ID=" + retval.ToString();
}
public void DeleteNote(string ID)
{
WebOperationContext.Current.OutgoingResponse.StatusCode = HttpStatusCode.Found;
// delete the database row here
}
#endregion
}
}
The actual SQLite connection is defined in global.asax.cs:
public static SQLiteConnection conn;
void Application_Start(object sender, EventArgs e)
{
string cestring = "Data Source=" + HttpContext.Current.Server.MapPath("~/App_Data/Notes.db") + ";version=3";
conn = new SQLiteConnection(cestring);
}
This ensures that we get a path to the Notes.db SQLite database file no matter how we've deployed or unzipped the app.
The web.config requirements once the service methods are all implemented is as follows:
<?xml version="1.0"?>
<configuration>
<system.web>
<compilation debug="true" targetFramework="4.0" />
<authentication mode="Windows" />
<pages controlRenderingCompatibilityVersion="3.5" clientIDMode="AutoID"/>
</system.web>
<system.serviceModel>
<behaviors>
<serviceBehaviors>
<behavior name="ServiceBehavior">
<serviceMetadata httpGetEnabled="true"/>
<serviceDebug includeExceptionDetailInFaults="true"/>
</behavior>
</serviceBehaviors>
<endpointBehaviors>
<behavior name="EndpointBehavior">
<webHttp/>
</behavior>
</endpointBehaviors>
</behaviors>
<services>
<service behaviorConfiguration="ServiceBehavior" name="RESTFulNotes.Service1">
<endpoint address="" binding="webHttpBinding" contract="RESTFulNotes.IService1" behaviorConfiguration="EndpointBehavior">
</endpoint>
</service>
</services>
</system.serviceModel>
</configuration>
Now let's switch over to the Default.aspx page, where all the action is in client script:
<%@ Page Title="Home Page" Language="C#" MasterPageFile="~/Site.master" AutoEventWireup="true"
CodeBehind="Default.aspx.cs" Inherits="RESTFulNotes._Default" %>
<asp:Content ID="HeaderContent" runat="server" ContentPlaceHolderID="HeadContent">
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.5.1/jquery.min.js"></script>
<script src="Scripts/JSon.js" type="text/javascript"></script>
<script type="text/javascript">
var Type;
var Url;
var Data;
var ContentType;
var DataType;
var ProcessData;
var method;
//Generic function to call ASMX/WCF Service
function CallService() {
$.ajax({
type: Type, //GET,POST,PUT or DELETE verb
url: Url, // Location of the service
data: Data, //Data to be sent to server
contentType: ContentType, // content type sent to server
dataType: DataType, //Expected data format from server
processdata: ProcessData, //True or False
success: function (msg) { //On Successful service call
ServiceSucceeded(msg);
},
error: ServiceFailed // function When Service call fails
});
}
function ServiceFailed(result) {
alert('Service call failed: ' + result.status + '' + result.statusText);
Type = null; Url = null; Data = null; ContentType = null; DataType = null; ProcessData = null;
}
function GetNote() {
var ID = "5";
Type = "POST";
Url = "Service1.svc/Notes/" + ID;
//Data = '{"ID": "' + ID + '"}';
ContentType = "application/json; charset=utf-8";
DataType = "json";
ProcessData = false;
method = "GetNote";
CallService();
}
function FindNotes() {
Type = "POST";
var tag = "Chores";
Url = "Service1.svc/Notes?tag=" + tag;
Data = '{"tag": "' + tag + '"}';
ContentType = "application/json; charset=utf-8";
DataType = "json";
ProcessData = true;
method = "FindNotes";
CallService();
}
function CreateNote() {
Type = "PUT";
Url = "Service1.svc/Notes/0";
var msg2 = { "Category" : "Chores", "Subject": "To Remember", "NoteText": "Get Milk!" };
Data = JSON.stringify(msg2);
ContentType = "application/json; charset=utf-8";
DataType = "json";
ProcessData = true;
method = "CreateNote";
CallService();
}
function ServiceSucceeded(result) {
if (DataType == "json") {
if (method == "CreateNote") {
document.getElementById("display").innerHTML = "CREATED: " + result;
}
else if (method == "GetNote") {
resultObject = result.GetNoteResult;
var string = " ID:" + result.ID + "<br/>" + "CATEGORY: " + result.Category + "<br/>" + "SUBJECT: " + result.Subject + "<br/>NOTE: " + result.NoteText;
document.getElementById("display").innerHTML = string;
}
else if (method == "FindNotes") {
var string="";
resultObject = result;
for( result in resultObject){
string += " ID:" + resultObject[result].ID + "<br/>" + "CATEGORY: " + resultObject[result].Category + "<br/>" + "SUBJECT: " + resultObject[result].Subject + "<br/>NOTE: " + resultObject[result].NoteText + "<Br/>";
}
document.getElementById("display").innerHTML = string;
}
}
}
function ServiceFailed(xhr) {
alert("FAIL" +xhr.responseText);
if (xhr.responseText) {
var err = xhr.responseText;
if (err)
error(err);
else
error({ Message: "Unknown server error." })
}
return;
}
$(document).ready(
function () {
// CreateNote();
// GetNote();
FindNotes();
}
);
</script>
</asp:Content>
<asp:Content ID="BodyContent" runat="server" ContentPlaceHolderID="MainContent">
<div id="display">
</div>
</asp:Content>
You can see above that I have two script tags defined, one to the google jQuery CDN, and the other to Crockford's Json.js hosted locally.
The business logic in the last script section includes a "genericized" jQuery $ajax call:
var Type;
var Url;
var Data;
var ContentType;
var DataType;
var ProcessData;
var method;
//Generic function to call ASMX/WCF Service
function CallService() {
$.ajax({
type: Type, //GET,POST,PUT or DELETE verb
url: Url, // Location of the service
data: Data, //Data to be sent to server
contentType: ContentType, // content type sent to server
dataType: DataType, //Expected data format from server
processdata: ProcessData, //True or False
success: function (msg) { //On Successful service call
ServiceSucceeded(msg);
},
error: ServiceFailed // function When Service call fails
});
}
The above allows us to set parameters and make service calls for any method.
I have a ServiceSucceeded function that handles display of results for various method calls, and a ServiceFailed function to show any error messages.
$(document).ready {... } is where you can experiment with whatever function you want to try.
The other functions are just "set ups" for the CallService method. I did not create a fancy jQuery UI for this as it is outside the scope of the article. Ambitious programmers will want to use the jQuery Templating mechanism to populate grids, etc with results.
You can download the fully functional Visual Studio 2010 Solution which includes the standalone System.Data.Sqlite assembly (this is a mixed - mode assembly which has both the ADO.NET provider and the actual C++ database engine), as well as the sample Notes.db database.
|