For those who don't want to be limited to the existing REST services, you can create your own services. Another possible reason for creating your own services is to expose legacy data. Just as you expose this data using Web services, you can expose it via REST services to provide a fairly interoperable means of making the data available to others.
The first step in creating a REST service is to decide what resources you want to provide with this service, that is, the entities maintained by the service. For example, in a Web log application, the resources are the posts and comments in the system. In a shopping application, the resources are the items to buy.
The next step in defining a REST service is to identify the HTTP verbs and URLs that you will use. Here is where you decide whether to create a pure REST service, or a just-enough REST service. By limiting yourself to GET and POST, your service can be called via a browser. Unless your service is read-only, you probably want to add some parameter to differentiate the various requests. Alternately, calling the PUT and DELETE methods of a pure REST service is more difficult for users accessing your service than using a simple browser interface.
To demonstrate how to create a just-enough REST interface, I'll create a simple contact management system. While this is a fairly simple solution, it shows many of the mechanics needed by REST services.
As described previously, the first step in creating a REST service is to identify the resources managed by the system. In this case, the resources will be contacts (in a simple XML layout). With the exception of requesting a list of contacts, all exchanges will consist of individual entries. Listing 22-9 shows an example entry in the system.
![]() |
With the number of REST services growing, it was only a matter of time before people started to combine them, creating what are called mashups. For example, they used the location information from a Flickr user search as input to a Yahoo Geocode, plotting the result on a Google map. Many of these applications are listed at http://www.programmableweb.com.
![]() |
Listing 22-9: Sample contact
![]() |
<contact > <fName>Charlene</fName> <lName>Locksley</lName> <email>cl823@public.com</email> </contact>
![]() |
Now that the resources have been defined, the next step is to define the HTTP verbs and URLs that are used by the system. The service is intended for use from a browser, so both HTTP GET and HTTP POST are supported by the system. As this service is intended to provide read/write access to the contacts, the service needs a way of adding, updating, and deleting entries. The list that follows shows some of the URLs that are supported by the system. For each of the URLs, only the query string is shown. All of the requests are made to a single URL on the system: (http://www.localhost/restcontacts/rest.ashx)
q ?method=getcontact-Returns a list of all the contacts entered into the system. (See Figure 22-4)
Figure 22-4
q ?method=getcontact&id=3-Returns an individual contact from the system. based on the id requested. The contact is formatted like the XML shown in Listing 22-9. (See Figure 22-5.)
Figure 22-5
q ?method=getcontact&email=user@server.com-Returns an individual contact from the system. based on a search of the e-mail addresses. The contact is formatted as the XML shown above.
q http://www.?method=insertcontact&fname=name&lname=name&email=address-Inserts a new entry in the list of contacts. Returns the newly added contact, with the assigned id value. (See Figure 22-6)
Figure 22-6
q ?method=insertcontact-Inserts a new entry in the list of contacts. This form is intended for POST calls, and the request body should include the contact information as the XML above, without the id attribute. Returns the newly added contact, with the assigned id value.
q http://www.?method=updatecontact&id=3&fname=new&lname=new&email=new-Updates one of the existing contacts in the system, returning the updated contact. (See Figure 22-7.)
Figure 22-7
q ?method=updatecontact-Updates one of the existing contacts in the system. This form is intended for use in POST requests. The request body should contain the updated contact information as the XML above (without the id value). Returns the updated contact.
q ?method=deletecontact&id=3-Deletes a contact from the system, returning the deleted contact as XML.
Because all the methods for this service are accessible from GET requests, testing the service can be done using a browser. Figures 22-4 through 22-7 show how to access the various methods of the service.
The REST service you'll be creating is based on an ASP.NET HTTP Handler. HTTP Handlers are files in ASP.NET that respond to requests. Although you could create this using an ASP.NET page, doing so doesn't return a complete page, but only a block of XML. Using the handler is a cleaner scenario. The simplest HTTP handler is a file with the extension ashx as shown in Listing 22-10.
Listing 22-10: A basic HTTP handler in ASP.NET
![]() |
<%@ WebHandler Language="VB" %> Imports System Imports System.Web Public Class Handler : Implements IHttpHandler Public Sub ProcessRequest(ByVal context As HttpContext) _ Implements IHttpHandler.ProcessRequest End Sub Public ReadOnly Property IsReusable() As Boolean _ Implements IHttpHandler.IsReusable Get Return False End Get End Property End Class
![]() |
The bulk of the work in creating an HTTP handler is in the code in the ProcessRequest method. This method is responsible for building the appropriate response content and setting the correct content type for the response. The IsReusable method is a marker method to determine if the Web server can use this HTTP handler for multiple requests. Unless you are certain your ProcessRequest method is completely safe for multiple threads, it is generally safer to have IsReusable return false.
Listing 22-11 shows the implementation of the RestHandler.
Listing 22-11: The RestHandler
![]() |
<%@ WebHandler Language="VB" %> Imports System Imports System.Web Imports ProXml.Samples.Rest Public Class RestHandler : Implements IHttpHandler Public Sub ProcessRequest(ByVal context As HttpContext) _ Implements IHttpHandler.ProcessRequest Dim req As HttpRequest = context.Request Dim resp As HttpResponse = context.Response Dim method As String = "unknown" resp.ContentType = "application/xml" If req("method") IsNot Nothing Then method = req("method").ToLower() End If Select Case method Case "getcontact" Dim contact As Contact If req("id") IsNot Nothing Then 'they are looking for a single user Dim id As Integer = Int32.Parse(req("id")) contact = ContactManager.GetContact(id) If contact IsNot Nothing Then resp.Write(contact.ToString()) Else Throw New HttpException(404, _ "Could not find Contact with that ID") End If ElseIf req("email") IsNot Nothing Then 'search by email contact = ContactManager.GetContact(req("email")) If contact IsNot Nothing Then resp.Write(contact.ToString()) Else Throw New HttpException(404, _ "Could not find Contact") End If Else 'return all users Dim Contacts() = ContactManager.GetContacts() resp.Write("<contacts>") For Each c As Contact In Contacts resp.Write(c.ToString()) Next resp.Write("</contacts>") End If Case "insertcontact" If req("fname") Is Nothing Then 'try reading from the body resp.Write(ContactManager.InsertContact(req.InputStream)) Else resp.Write(ContactManager.InsertContact(req("fname"), _ req("lname"), req("email"))) End If Case "updatecontact" If req("id") IsNot Nothing Then Dim id As Integer = Int32.Parse(req("id")) If req("fname") Is Nothing Then 'contact is in the body resp.Write(ContactManager.UpdateContact(id, _ req.InputStream)) Else resp.Write(ContactManager.UpdateContact(id, _ req("fName"), req("lName"), req("email"))) End If End If Case "deletecontact" If req("id") IsNot Nothing Then Dim id As Integer = Int32.Parse(req("id")) resp.Write(ContactManager.DeleteContact(id)) End If Case Else End Select End Sub Public ReadOnly Property IsReusable() As Boolean _ Implements IHttpHandler.IsReusable Get Return False End Get End Property End Class
![]() |
Although the code is lengthy, it is primarily the code for dispatching requests. The method parameter on the query string identifies which action to perform. However, each method includes multiple possible actions (such as the various getcontact methods). The handler delegates the actual changes to the data to the various static methods on the ContactManager class (see Listing 22-13). All these methods return one or more Contact objects (see Listing 22-12).
Listing 22-12: The Contact class
![]() |
Imports System.Text Imports System.Xml Public Class Contact #Region "Properties" Private _id As Integer Private _firstName As String Private _lastName As String Private _email As String Public Property ID() As Integer Get Return _id End Get Friend Set(ByVal value As Integer) _id = value End Set End Property Public Property FirstName() As String Get Return _firstName End Get Set(ByVal value As String) _firstName = value End Set End Property Public Property Lastname() As String Get Return _lastName End Get Set(ByVal value As String) _lastName = value End Set End Property Public Property EMail() As String Get Return _email End Get Set(ByVal value As String) _email = value End Set End Property #End Region #Region "Public Methods" Public Overrides Function ToString() As String Dim sb As New StringBuilder() Dim ws As New XmlWriterSettings With ws .CheckCharacters = True .CloseOutput = True .ConformanceLevel = ConformanceLevel.Fragment .Encoding = Encoding.UTF8 .OmitXmlDeclaration = True End With Using w As XmlWriter = XmlWriter.Create(sb, ws) w.WriteStartElement("contact") w.WriteAttributeString("id", Me.ID) w.WriteElementString("fName", Me.FirstName) w.WriteElementString("lName", Me.Lastname) w.WriteElementString("email", Me.EMail) w.WriteEndElement() 'contact End Using Return sb.ToString() End Function #End Region End Class
![]() |
The Contact class is a simple class, with only a few properties. The only notable part of the class is the ToString method that returns the XML representation of the class. The handler calls this when the contact is output.
The contacts will be stored in a database. This database includes only a single table. The structure of the contacts table is as follows.
Column | Type | Size | Description |
---|---|---|---|
id | int | n/a | Identity column used as the primary key. |
fName | nvarchar | 50 | First name of the contact. |
lName | nvarchar | 50 | Last name of the contact. |
| nvarchar | 50 | E-mail address of the contact. |
The ContactManager class (Listing 22-13) performs the bulk of the work of the handler in a set of static methods. Of note are the two methods intended for use by POST requests.
Listing 22-13: The Contact Manager class
![]() |
Imports System.Data.SqlClient Imports System Imports System.Collections.Generic Public Class ContactManager Public Shared Function GetContacts() As Contact() Dim result As New List(Of Contact) Dim c As Contact Dim reader As SqlDataReader Dim sql As String = "SELECT id, fname, lname, email FROM Contacts" reader = DAL.ExecuteSqlText(sql) While reader.Read() c = New Contact c.ID = reader.GetInt32(0) c.FirstName = reader.GetString(1) c.Lastname = reader.GetString(2) c.EMail = reader.GetString(3) result.Add(c) End While reader.Close() Return result.ToArray() End Function Public Shared Function GetContact(ByVal id As Integer) As Contact Dim result As Contact = Nothing Dim reader As SqlDataReader Dim sql As String = _ "SELECT id, fname, lname, email FROM Contacts WHERE SELECT id, fname, lname, email " & _ "FROM Contacts WHERE email='{0}'", _ email) reader = DAL.ExecuteSqlText(sql) While reader.Read() result = New Contact result.ID = reader.GetInt32(0) result.FirstName = reader.GetString(1) result.Lastname = reader.GetString(2) result.EMail = reader.GetString(3) End While reader.Close() Return result End Function Public Shared Function InsertContact(ByVal firstName As String, _ ByVal lastName As String, ByVal email As String) As Contact Dim result As Contact = Nothing result = GetContact(email) If result Is Nothing Then 'user does not exist, we can create Dim sql As String = _ String.Format("INSERT INTO Contacts(fname, lname, email) " & _ "VALUES ('{0}', '{1}', '{2}')", _ firstName, lastName, email) If DAL.Execute(sql) Then result = GetContact(email) End If End If Return result End Function Public Shared Function InsertContact(ByVal block As IO.Stream) As Contact Dim result As Contact = Nothing Dim firstName As String = String.Empty Dim lastName As String = String.Empty Dim email As String = String.Empty Using r As Xml.XmlReader = Xml.XmlReader.Create(block) While r.Read() If r.IsStartElement() AndAlso Not r.IsEmptyElement Then Select Case r.Name.ToLower Case "fname" r.Read() firstName = r.Value Case "lname" r.Read() lastName = r.Value Case "email" r.Read() email = r.Value End Select End If End While End Using result = InsertContact(firstName, lastName, email) Return result End Function Public Shared Function UpdateContact(ByVal id As Integer, _ ByVal firstName As String, ByVal lastName As String, _ ByVal email As String) As Contact Dim result As Contact = GetContact(id) If result IsNot Nothing Then Dim sql As String = _ String.Format("UPDATE Contacts SET fname='{0}', " & _ "lname='{1}', email='{2}' WHERE id={3}", _ firstName, lastName, email, id) If DAL.Execute(sql) Then result = GetContact(id) End If End If Return result End Function Public Shared Function UpdateContact(ByVal id As Integer, _ ByVal block As IO.Stream) As Contact Dim result As Contact = GetContact(id) Dim firstName As String = String.Empty Dim lastName As String = String.Empty Dim email As String = String.Empty Using r As Xml.XmlReader = Xml.XmlReader.Create(block) While r.Read() If r.IsStartElement() AndAlso Not r.IsEmptyElement Then Select Case r.Name.ToLower Case "fname" r.Read() firstName = r.Value Case "lname" r.Read() lastName = r.Value Case "email" r.Read() email = r.Value End Select End If End While End Using result = UpdateContact(id, firstName, lastName, email) Return result End Function Public Shared Function DeleteContact(ByVal id As Integer) dim result as Contact = nothing Dim c As Contact = GetContact(id) If c IsNot Nothing Then Dim sql = String.Format("DELETE FROM Contacts WHERE id={0}", id) If DAL.Execute(sql) Then result = c End If End If Return result End Function End Class
![]() |
The code is mostly simple SQL processing and, ideally, is used to call stored procedures. The most notable methods are the two intended for use with POST requests (InsertContact and UpdateContact). When posting data using HTTP, the body of the request contains information. This data is encoded via a MIME type. The most common MIME type for POST data is application/x-www-form-urlencoded if an HTML form sends the information. This format looks similar to a query string, but is contained in the body of the request to avoid the 2K limit on the length of a query string. Most server-side tools process this form of request to create a hash table, just as they create query string variables. Alternately, the POST body could contain the XML representation submitted (and the MIME type of application/xml). In this case, the code extracts the values from the XML for handling.
The actual data access is isolated from the ContactManager class in the simple DAL class (see Listing 22-14).
Listing 22-14: The Data Access Layer class
![]() |
Imports System.Data.SqlClient Imports System.Configuration Imports System.Data Public Class DAL Private Shared _conn As SqlClient.SqlConnection Shared Sub New() Dim connectionstring As String connectionstring = _ ConfigurationManager.ConnectionStrings("contactsConnection").ConnectionString _conn = New SqlClient.SqlConnection(connectionstring) End Sub Public Shared Function ExecuteQuery(ByVal query As String, _ ByVal parms As SqlParameter()) As SqlDataReader Dim result As SqlDataReader = Nothing Dim cmd As New SqlCommand(query, _conn) cmd.CommandType = CommandType.StoredProcedure cmd.Parameters.AddRange(parms) If _conn.State <> ConnectionState.Open Then _conn.Open() End If result = cmd.ExecuteReader(CommandBehavior.CloseConnection) Return result End Function Public Shared Function ExecuteSqlText(ByVal sql As String) _ As System.Data.SqlClient.SqlDataReader Dim result As SqlDataReader = Nothing Dim cmd As New SqlCommand(sql, _conn) cmd.CommandType = CommandType.Text If _conn.State <> ConnectionState.Open Then _conn.Open() End If result = cmd.ExecuteReader(CommandBehavior.CloseConnection) Return result End Function Public Shared Function Execute(ByVal sql As String) As Boolean Dim result As Boolean = False Dim cmd As New SqlCommand(sql, _conn) Try If _conn.State <> ConnectionState.Open Then _conn.Open() End If result = CBool(cmd.ExecuteNonQuery()) Finally If _conn.State = ConnectionState.Open Then _conn.Close() End If End Try Return result End Function End Class
![]() |
The connection string stored in the web.config file for the RestContacts project is as follows. Note that the connectionString attribute should be all on a single line in the web.config file. The connection string points to a database stored in the app_data directory of the project. Therefore, the AttachDbFilename attribute is used to identify the database file. In addition, the |DataDirectory| marker is used. This string is used to represent the current Web application's app_data directory.
<connectionStrings> <add name="contactsConnection" providerName="System.Data.SqlClient" connectionString="Data Source=.\SQLEXPRESS; AttachDbFilename=|DataDirectory|contacts.mdf; Integrated Security=True; User Instance=True"/> </connectionStrings>
The DAL class provides a number of static methods for executing various SQL statements, including stored procedures and simple SQL text.
To demonstrate creating a pure REST interface, I'll convert the example used in the just-enough REST interface and make it function as a pure REST interface as well.
The following table describes the URLs that the service exposes. Notice that we are overloading the POST request for those clients that do not support the PUT verb.
URL | Verb | Response |
---|---|---|
/all | GET | Returns a list of all available contacts entered into the system. |
/# | GET | Returns a specific contact from the system by searching on id. |
| GET | Returns a specific contact from the system by searching on e-mail. |
/ | POST | Inserts a new contact into the system, returning the contact (with assigned id). |
/# | POST | Updates a contact with new information. The contact must exist in the system. |
/# | PUT | Updates a contact with new information. Contact must exist in the system. |
/# | DELETE | Deletes the contact from the system. The contact must exist in the system. |
Core to many pure REST applications is a URL rewriting module. This URL rewriter converts the URLs in the system into the actions that perform the tasks. In the case of the contact manager service, it rewrites a URL such as http://www.server/restcontacts/23 as http://www.server/restcontacts/rest.ashx?method=getcontact&id=23. The actual rewriting may be performed by the Web server or by the server-side code. Apache (via the mod_rewrite module) has excellent URL rewriting capabilities, and many systems using Apache employ this module to define regular expressions to rewrite URLs. IIS, on the other hand, has relatively weak URL rewriting capabilities, limited to static mappings. To supplement this system of URL rewriting, you can write an ISAPI module, an ASP.NET HTTP Module, or virtual path provider. For the pure REST contact manager, I create a simple HTTP module to rewrite the URLs.
An ASP.NET HTTP Module is a class that implements the System.Web.IHttpModule interface. This interface (see Listing 22-15) provides two methods. The Init method is called when IIS loads the HTTP Module. In it, you connect event handlers to process the request at the appropriate stage in the page lifetime. The Dispose method is called when IIS is unloading the class and is used to perform any cleanup needed by the module, such as closing files or database handles.
Listing 22-15: System.Web.IHttpModule
![]() |
Public Interface IHttpModule ' Methods Sub Dispose() Sub Init(ByVal context As HttpApplication) End Interface
![]() |
In addition to these two methods, the HTTP Module also needs to have handlers for the stages in the page lifetime that it processes. For the REST module, the AuthenticateRequest stage is chosen to process the URL rewriting. This event is fired early enough in the process of a request so that security can be added later without changing the behaviour. The BeginRequest event also works in this case, but if authentication is added to the system, the module processes based on the rewritten URL, not the desired one.
The actual implementation of the HTTP module is shown in Listing 22-16. It adds a single handler to process the AuthenticateRequest event. This event is used to rewrite the request URL.
Listing 22-16: REST HTTP module
![]() |
Imports Microsoft.VisualBasic Imports System.Text Imports System.Xml Imports System.Net Imports System.IO Public Class ContactModule Implements IHttpModule Public Sub Dispose() Implements System.Web.IHttpModule.Dispose 'this space left intentionally blank End Sub Public Sub Init(ByVal context As System.Web.HttpApplication) _ Implements System.Web.IHttpModule.Init 'set up connection to Application events AddHandler context.AuthenticateRequest, _ AddressOf Me.AuthenticateRequest End Sub Private Sub AuthenticateRequest (ByVal sender As Object, ByVal e As EventArgs) Dim app As HttpApplication = CType(sender, HttpApplication) Rewrite(app.Request.Path, app) End Sub Protected Sub Rewrite(ByVal requestedPath As String, _ ByVal app As HttpApplication) 'You would probably want to make these mappings via ' a separate configuration section in web.config Dim context As HttpContext = app.Context Dim method As String = app.Request.HttpMethod.ToLower Dim tail As String = _ requestedPath.Substring(app.Request.ApplicationPath.Length) If tail.StartsWith("/") Then tail = tail.Substring(1) End If If Not File.Exists(app.Server.MapPath(requestedPath)) Then 'tail should have the id or name to work with 'and method the HTTP verb Select Case method Case "get" If IsNumeric(tail) Then context.RewritePath("~/rest.ashx", False, _ "method=getcontact&all" Then 'special case to retrieve all contacts context.RewritePath("~/rest.ashx", False, _ "method=getcontact", False) Else 'assuming an email search context.RewritePath("~/rest.ashx", False, _ "method=getcontact&email=" & tail, False) End If Case "post" If IsNumeric(tail) Then 'overriding POST to also work as a PUT, ' for those clients without PUT support context.RewritePath("~/rest.ashx", False, _ "method=updatecontact&~/rest.ashx", False, _ "method=insertcontact", False) End If Case "put" context.RewritePath("~/rest.ashx", False, _ "method=updatecontact&delete" context.RewritePath("~/rest.ashx", False, _ "method=deletecontact&BlueLine" border="0" cellspacing="0" cellpadding="0" width="100%">
The Rewrite method, like the main method in the earlier REST service, is primarily a dispatcher. In this case, if the file cannot be found on disk, the method determines the HTTP verb used and any trailing query parameters. It then uses the RewritePath method of the HttpContext to rewrite the URL to the just-enough equivalent.
After the HTTP Module is completed, it must be added to the web.config file for the virtual root it will work with. Listing 22-17 shows the code to be added for the ContactModule.
Listing 22-17: Adding HTTP module to web.config
![]() |
<configuration> <system.web> <httpModules> <add name="ContactModule" type="ContactModule"/> </httpModules> </system.web> </configuration>
![]() |
Because of the format of the URLs, one last step is required before using the HTTP module. Because the requests do not have an extension, IIS would normally attempt to process these requests. You must map these requests to be processed by ASP.NET or you will receive 404 errors. Under IIS 5.1 (Windows XP), this is done by adding a handler for the .* extension (see Figure 22-8.)
Figure 22-8
The wildcard mapping is used for IIS 6.0 in Windows Server 2003 (see Figure 22-9). Notice that the Check That File Exists is unchecked. This is necessary because the majority of requests that the REST sample supports do not actually exist as files on disk.
Figure 22-9
Testing the pure REST service is slightly more difficult than testing the just-enough service, because you must create the HTTP PUT and DELETE verbs. In addition, the body of the request must be set for some of the requests. Therefore, it is a good idea to get an HTTP debugging tool, such as Fiddler or curl (see the Resources list at the end of the chapter). These tools let you create requests against your service, set the POST/PUT body, and trace the results, including the HTTP headers. Figure 22-10 shows a test of inserting a new contact into the system using Fiddler.
Figure 22-10
Figure 22-11 shows the resulting response.
Figure 22-11