REST, WCF 4, Forms Authentication, and Custom Clients (Part 2 of 3)

by Jason Williams31. December 2011 23:31

In the first part of this post, I discussed REST and how it compares to SOAP-based services. In this part, we’ll figure out how to create a REST service with WCF and what it takes to start thinking in a “RESTful” way when designing a REST service.

Design and Planning

A lot of services are used for basic CRUD operations. Create, Retrieve, Update, and Delete are tasks that any line-of-business service is going to need to perform. With a SOAP service, you might layout your service methods like this:

   1: [ServiceContract]
   2:public interface ICustomerService
   3:     {
   4:         [OperationContract]
   5:         ServiceResponse<Customer> AddCustomer(Customer cust);
   6:         [OperationContract]
   7:         IEnumerable<Customer> SearchCustomers(string searchTerm, int page, int recordsPerPage);
   8:         [OperationContract]
   9:         Customer GetCustomer(int customerId);
  10:         [OperationContract]
  11:         ServiceResponse<Customer> UpdateCustomer(Customer cust);
  12:         [OperationContract]
  13:         ServiceResponse<Customer> DeleteCustomer(int customerId);
  14:     }
  15:
  16:     [DataContract]
  17:public class Customer
  18:     {
  19:         [DataMember]
  20:public int CustomerId { get;set;}
  21:         [DataMember]
  22:public string FirstName { get; set; }
  23:
  24:         [DataMember]
  25:public string LastName { get; set; }
  26:     }
  27:
  28:     [DataContract]
  29:public class ServiceResponse<T>
  30:     {
  31:         [DataMember]
  32:public T StoredEntity { get;set; }
  33:         [DataMember]
  34:public bool Success { get;set; }
  35:         [DataMember]
  36:public string ErrorMessage { get;set;}
  37:     }

If you’re familiar with WCF, the attributes on these contracts should look familiar. By default, in a Visual Studio WCF Service Application (see VS 2010 Screenshot below), this contract will define a SOAP service with 5 distinct methods. For brevity’s sake, I have excluded the actual implementation of the service.

image_thumb1

The data contracts consist of a simple Customer Entity and a wrapper class to return some information back to the user about any errors that occurred during processing.

If you would like to learn more about WCF services, I encourage you to visit Basic WCF Programming to learn more about how this all fits together.

Getting Started With REST

To allow your WCF service to behave as if it were a REST service, you will need to add the following XML to the <system.serviceModel /> element in your web.config:

   1: <services>
   2:<service name="RESTDatabaseService.CustomerService">
   3:<endpoint kind="webHttpEndpoint" contract="RESTDatabaseService.ICustomerService" />
   4:</service>
   5: </services>

Once that is done, you’ll need to alter your service contract (ICustomerService, in this case) to read:

   1: [ServiceContract]
   2: public interface ICustomerService
   3: {
   4:     [OperationContract]
   5:     [WebInvoke(Method="POST")]
   6:     ServiceResponse<Customer> AddCustomer(Customer cust);
   7:
   8:     [OperationContract]
   9:     [WebInvoke(Method="GET", UriTemplate="/{searchTerm}?page={page}&pageSize={recordsPerPage}")]
  10:     IEnumerable<Customer> SearchCustomers(string searchTerm, int page, int recordsPerPage);
  11:
  12:     [OperationContract]
  13:     [WebInvoke(Method="GET", UriTemplate="/?id={customerId}")]
  14:     Customer GetCustomer(int customerId);
  15:
  16:     [OperationContract]
  17:     [WebInvoke(Method="PUT")]
  18:     ServiceResponse<Customer> UpdateCustomer(Customer cust);
  19:
  20:     [OperationContract]
  21:     [WebInvoke(Method="DELETE", UriTemplate="/?id={customerId}")]
  22:     ServiceResponse<Customer> DeleteCustomer(int customerId);
  23: }

Once those changes are made, you can now access your data via a URL. If you take a look at the WebInvoke attribute that is applied to each of the operations, you’ll notice that some of them define a UriTemplate. That template will dictate what has to come after the “CustomerService.svc” part of the URL for the operation to be invoked.

For instance, if I wanted to perform a search for everyone named “smith”, I would call the URL:

http://<server>/CustomerService.svc/smith?page=1&pageSize=10

You’ll notice the address for the service remains similar to what you would expect (starts with server name and contains /CustomerService.svc), but it ends with a string that matches the pattern in the UriTemplate argument on the SearchCustomers(…) service operation.

This works great. However, I’m not real happy with the CustomerService.svc, in the URL. It just doesn’t smell right. So, I want to change things to make my URL prettier -- like:

http://<server>/Customers/smith?page=1&pageSize=10

To do this, you’ll need to add a Global.asax to your web site: In Solution Explorer, right-click the web-site project and choose Add –> New Item… (Ctrl + Shift + A), and choose the “Global Application Class” item. Leave its default name and click “Add.”

You will also need to add a reference to the “System.ServiceModel.Activation.dll” assembly.

On your service implementation (code in CustomerService.svc.cs), add the AspNetCompatibilityRequirements attribute to the service and require compatibility, like this:

   1: [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Required)]
   2: public class CustomerService : ICustomerService

You will also need to tell WCF to enable ASP.NET Compatibility by changing the web.config. In the <system.ServiceModel /> element, change the <serviceHostingEnvironment/ > element by adding the “aspNetCompatibilityEnabled” attribute and setting it to “true” as follows:

   1: <serviceHostingEnvironment multipleSiteBindingsEnabled="true" aspNetCompatibilityEnabled="true" />

Finally, in the new Global.asax file, add the following code:

   1: protected void Application_Start(object sender, EventArgs e)
   2: {
   3:     RegisterRoutes();
   4: }
   5:
   6: private void RegisterRoutes()
   7: {
   8:     var factory = new System.ServiceModel.Activation.WebServiceHostFactory();
   9:     System.Web.Routing.RouteTable.Routes.Add(new System.ServiceModel.Activation.ServiceRoute("customers", factory, typeof(CustomerService)));
  10: }

After all of that, you should be able to navigate your browser to:

http://<server>/Customers/smith?page=1&pageSize=10

Striving for True RESTful-ness

REST services are not just a different protocol for delivery, they are a different way to think about your services. Roy Fielding is typically credited with coining the term “REST” in his doctoral dissertation (chapter 5); in which he lays out his vision of Representational State Transfer (REST) as a service architecture.

One of the main differences between a SOAP/RPC-style of service and a RESTful one is that the RESTful service uses its data to guide the consumer around the service; opposed to a consumer downloading a WSDL and obtaining intimate details about a service. A RESTful service will provide hints about how it should be called, next. If you’ve done any web programming, it’s similar to providing links that your user can click on, or a post-back URL in a <form /> tag.

Because of this requirement, we need to think a little more about what gets returned to our consumers. We need to modify the data contracts to include some actions that can be taken on the data that gets returned.

As an example, I want to modify the SearchCustomers(…) method to not just return data, but to also return links to the next and previous pages of data.

So far, a GET call to “http://<server>/Customers/smith?page=1&pageSize=10” will result in data similar to this:

   1: <ArrayOfCustomer xmlns="http://schemas.datacontract.org/2004/07/RESTDatabaseService" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
   2:<Customer>
   3:<CustomerId>1</CustomerId>
   4:<FirstName>First1</FirstName>
   5:<LastName>Smith1</LastName>
   6:</Customer>
   7:<Customer>
   8:<CustomerId>2</CustomerId>
   9:<FirstName>First2</FirstName>
  10:<LastName>Smith2</LastName>
  11:</Customer>
  12:<Customer>
  13:<CustomerId>3</CustomerId>
  14:<FirstName>First3</FirstName>
  15:<LastName>Smith3</LastName>
  16:</Customer>
  17:<Customer>
  18:<CustomerId>4</CustomerId>
  19:<FirstName>First4</FirstName>
  20:<LastName>Smith4</LastName>
  21:</Customer>
  22:<Customer>
  23:<CustomerId>5</CustomerId>
  24:<FirstName>First5</FirstName>
  25:<LastName>Smith5</LastName>
  26:</Customer>
  27:<Customer>
  28:<CustomerId>6</CustomerId>
  29:<FirstName>First6</FirstName>
  30:<LastName>Smith6</LastName>
  31:</Customer>
  32:<Customer>
  33:<CustomerId>7</CustomerId>
  34:<FirstName>First7</FirstName>
  35:<LastName>Smith7</LastName>
  36:</Customer>
  37:<Customer>
  38:<CustomerId>8</CustomerId>
  39:<FirstName>First8</FirstName>
  40:<LastName>Smith8</LastName>
  41:</Customer>
  42:<Customer>
  43:<CustomerId>9</CustomerId>
  44:<FirstName>First9</FirstName>
  45:<LastName>Smith9</LastName>
  46:</Customer>
  47:<Customer>
  48:<CustomerId>10</CustomerId>
  49:<FirstName>First10</FirstName>
  50:<LastName>Smith10</LastName>
  51:</Customer>
  52: </ArrayOfCustomer>

That is because an IEnumerable, when serialized by the DataContractSerializer, becomes an ArrayOf XML element, by default. Instead of returning a straight IEnumerable, we need to create a type that contains the returned elements, and includes the links we want to provide to the consumer. That type might look like this:

   1: [DataContract(Name="PagedServiceResponse")]
   2: public class PagedServiceResponse<T>
   3: {
   4:public PagedServiceResponse()
   5:     {
   6:         Results = new List<T>();
   7:     }
   8:
   9:     [DataMember]
  10:public string PreviousPage { get; set; }
  11:
  12:     [DataMember]
  13:public string NextPage { get; set; }
  14:
  15:     [DataMember]
  16:public List<T> Results { get; set; }
  17: }

If we return that type; filled with Customer objects, we’ll see output like this:

   1: <PagedServiceResponse xmlns="http://schemas.datacontract.org/2004/07/RESTDatabaseService" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
   2:<NextPage>http://localhost:63608/customers/Smith?page=4&amp;pageSize=10</NextPage>
   3:<PreviousPage>http://localhost:63608/customers/Smith?page=2&amp;pageSize=10</PreviousPage>
   4:<Results>
   5:<Customer>
   6:<CustomerId>21</CustomerId>
   7:<FirstName>First21</FirstName>
   8:<LastName>Smith21</LastName>
   9:</Customer>
  10:<Customer>
  11:<CustomerId>22</CustomerId>
  12:<FirstName>First22</FirstName>
  13:<LastName>Smith22</LastName>
  14:</Customer>
  15:<Customer>
  16:<CustomerId>23</CustomerId>
  17:<FirstName>First23</FirstName>
  18:<LastName>Smith23</LastName>
  19:</Customer>
  20:<Customer>
  21:<CustomerId>24</CustomerId>
  22:<FirstName>First24</FirstName>
  23:<LastName>Smith24</LastName>
  24:</Customer>
  25:<Customer>
  26:<CustomerId>25</CustomerId>
  27:<FirstName>First25</FirstName>
  28:<LastName>Smith25</LastName>
  29:</Customer>
  30:<Customer>
  31:<CustomerId>26</CustomerId>
  32:<FirstName>First26</FirstName>
  33:<LastName>Smith26</LastName>
  34:</Customer>
  35:<Customer>
  36:<CustomerId>27</CustomerId>
  37:<FirstName>First27</FirstName>
  38:<LastName>Smith27</LastName>
  39:</Customer>
  40:<Customer>
  41:<CustomerId>28</CustomerId>
  42:<FirstName>First28</FirstName>
  43:<LastName>Smith28</LastName>
  44:</Customer>
  45:<Customer>
  46:<CustomerId>29</CustomerId>
  47:<FirstName>First29</FirstName>
  48:<LastName>Smith29</LastName>
  49:</Customer>
  50:<Customer>
  51:<CustomerId>30</CustomerId>
  52:<FirstName>First30</FirstName>
  53:<LastName>Smith30</LastName>
  54:</Customer>
  55:</Results>
  56: </PagedServiceResponse>

We’re almost there! The last thing we should probably do is provide a direct link to the individual customer record details. To do this, we’ll create a new domain object like this:

   1: [DataContract(Name="Customer")]
   2: public class LinkedCustomer : Customer
   3: {
   4:public LinkedCustomer(Customer c)
   5:     {
   6:this.FirstName = c.FirstName;
   7:this.LastName = c.LastName;
   8:this.CustomerId = c.CustomerId;
   9:     }
  10:     [DataMember]
  11:public string Link { get; set; }
  12: }

Finally, we get the following result:

   1: <PagedServiceResponse xmlns="http://schemas.datacontract.org/2004/07/RESTDatabaseService" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
   2:<NextPage>http://localhost:63608/customers/Smith?page=4&amp;pageSize=10</NextPage>
   3:<PreviousPage>http://localhost:63608/customers/Smith?page=2&amp;pageSize=10</PreviousPage>
   4:<Results>
   5:<Customer>
   6:<CustomerId>21</CustomerId>
   7:<FirstName>First21</FirstName>
   8:<LastName>Smith21</LastName>
   9:<Link>http://localhost:63608/customers/?id=21</Link>
  10:</Customer>
  11:<Customer>
  12:<CustomerId>22</CustomerId>
  13:<FirstName>First22</FirstName>
  14:<LastName>Smith22</LastName>
  15:<Link>http://localhost:63608/customers/?id=22</Link>
  16:</Customer>
  17:<Customer>
  18:<CustomerId>23</CustomerId>
  19:<FirstName>First23</FirstName>
  20:<LastName>Smith23</LastName>
  21:<Link>http://localhost:63608/customers/?id=23</Link>
  22:</Customer>
  23:<Customer>
  24:<CustomerId>24</CustomerId>
  25:<FirstName>First24</FirstName>
  26:<LastName>Smith24</LastName>
  27:<Link>http://localhost:63608/customers/?id=24</Link>
  28:</Customer>
  29:<Customer>
  30:<CustomerId>25</CustomerId>
  31:<FirstName>First25</FirstName>
  32:<LastName>Smith25</LastName>
  33:<Link>http://localhost:63608/customers/?id=25</Link>
  34:</Customer>
  35:<Customer>
  36:<CustomerId>26</CustomerId>
  37:<FirstName>First26</FirstName>
  38:<LastName>Smith26</LastName>
  39:<Link>http://localhost:63608/customers/?id=26</Link>
  40:</Customer>
  41:<Customer>
  42:<CustomerId>27</CustomerId>
  43:<FirstName>First27</FirstName>
  44:<LastName>Smith27</LastName>
  45:<Link>http://localhost:63608/customers/?id=27</Link>
  46:</Customer>
  47:<Customer>
  48:<CustomerId>28</CustomerId>
  49:<FirstName>First28</FirstName>
  50:<LastName>Smith28</LastName>
  51:<Link>http://localhost:63608/customers/?id=28</Link>
  52:</Customer>
  53:<Customer>
  54:<CustomerId>29</CustomerId>
  55:<FirstName>First29</FirstName>
  56:<LastName>Smith29</LastName>
  57:<Link>http://localhost:63608/customers/?id=29</Link>
  58:</Customer>
  59:<Customer>
  60:<CustomerId>30</CustomerId>
  61:<FirstName>First30</FirstName>
  62:<LastName>Smith30</LastName>
  63:<Link>http://localhost:63608/customers/?id=30</Link>
  64:</Customer>
  65:</Results>
  66: </PagedServiceResponse>

Of course, this type can be modified to be re-used by the GetCustomer(…) method so that it also includes the URLs for saving the item back to the service and deleting it. Providing feedback to your service consumer, in this manner, allows you to change your service layout, later; while not affecting the consumer’s ability to use the service. This is because the consumer never has to hard-code any URL information; instead they pull it from the message returned by the service.

Pick Your Formatting

So far, we have a service that will respond very well to basic HTTP calls and return very useful information – in XML. However, what if a JavaScript consumer calls the service and wants JSON messages back, instead? Fortunately, because of the way WCF is designed, you can control this with only a few lines of configuration.

In the web.config, add the following XML to the <system.serviceModel /> element:

   1: <standardEndpoints>
   2:<webHttpEndpoint>
   3:<standardEndpoint automaticFormatSelectionEnabled="true" helpEnabled="true" />
   4:</webHttpEndpoint>
   5: </standardEndpoints>

The “automaticFormatSelectionEnabled” is the real magic, here. This instructs WCF to inspect the HTTP headers for Accept and Content-Type; to determine what kind of formatting the consumer is asking for. After that, WCF picks the correct serializer, for you, and you don’t have to worry about it. Cool, eh?!

Another nicety I should mention is the “helpEnabled" attribute. This enables consumers to navigate to http://<server>/Customers/help and see documentation for your service. Here’s a screenshot of what comes back in a browser:

image_thumb2

In essence, as a consumer, you end up with an interactive document that helps you format your messages and URLs and gives you examples of what returns back from the service. This is incredibly useful and once again, it’s all done for you!

Summary

So, what have we done so far? In Part 1 of this post we saw some simple differences between SOAP and REST architectures. In this post, we saw how simple it is to convert a basic WCF service into a REST service; using a couple lines of configuration, and the addition of the WebInvoke attribute to the service contract.

We also saw how we can use ASP.NET routing to make the service URL more “pretty” and get rid of the .svc extension on the service, itself. Additionally, to stay true to the way REST services are suppose to work, we saw one example of how we might add links to other service actions in our return messages. Just because your service is accessed via a URL and an HTTP verb doesn’t make it truly RESTful. You have to think about your consumers in a different way than if they had downloaded all of the details of your service from a WSDL.

Lastly, we looked at the formatting of the data that comes out of the service. It’s very simple to enable flexible formatting; with only a few lines of configuration. Additionally, it’s just as easy to provide online help and documentation to your service consumers using the “helpEnabled” configuration attribute.

In the upcoming Part 3 of this post, we’ll add a client / consumer to the service and, just to make things more interesting, tack on some security to make this hypothetical service a little more real-world-ready.

Tags: ,

Development | REST | WCF

Comments are closed

About

Jason Williams is a .NET developer in Lincoln, Nebraska.

The name "Centrolutions" came out of a long search for a domain name. The goal was to create a name that conveyed an ideology of writing software centered (Centr--) on a solution (--olutions) for a particular problem. In other words, it was the only name in a long list that wasn't already registered on the internet.

If you're looking for the products I have for sale, you should go here.