Creating REST Services


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.

Just-enough REST Service Example

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.

image from book
Mashups

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.

image from book

Listing 22-9: Sample contact

image from book
      <contact >        <fName>Charlene</fName>        <lName>Locksley</lName>        <email>cl823@public.com</email>      </contact> 
image from book

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)

    image from book
    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.)

    image from book
    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)

    image from book
    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.)

    image from book
    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

image from book
      <%@ 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 
image from book

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

image from book
      <%@ 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 
image from book

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

image from book
      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 
image from book

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.

Open table as spreadsheet

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.

email

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

image from book
      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 
image from book

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

image from book
      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 
image from book

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.

A Pure REST Service Example

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.

Open table as spreadsheet

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.

/email

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

image from book
      Public Interface IHttpModule            ' Methods            Sub Dispose()            Sub Init(ByVal context As HttpApplication)      End Interface 
image from book

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

image from book
      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%">  image from book   

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

image from book
      <configuration>          <system.web>              <httpModules>                 <add name="ContactModule" type="ContactModule"/>              </httpModules>          </system.web>      </configuration> 
image from book

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.)

image from book
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.

image from book
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.

image from book
Figure 22-10

Figure 22-11 shows the resulting response.

image from book
Figure 22-11




Professional XML
Professional XML (Programmer to Programmer)
ISBN: 0471777773
EAN: 2147483647
Year: 2004
Pages: 215

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net