Designing a Sitefinity RESTful WCF Service

RESTful services may seem a bit odd at first sight, especially when one is faced with designing such a service. In this topic we are going to give some guidelines for designing such services, warn about the common mistakes and point on the conventions that should be followed.

The following diagram outlines the steps for designing Sitefinity WCF RESTful web service.

Conceptual design

The first thing to do when designing a RESTful web service is to decide what kind of resource are we going to expose through the webservice. Remember, that with RESTful web services we are always working with resources.

For example, if we have products module, we could create following web services:

  • Products - provides CRUD methods for products
  • Categories - provides CRUD methods for categories
  • Suppliers - provides CRUD methods for suppliers
  • Customers - provides CRUD methods for customers
All these services would conceptually be correct, since each of them would expose one single type of resource.
After we have decided on the resource we wish to expose, the next thing we have to think about is the primary key of the resource and how will we access the resource (remember that every resource has to be able to be accessed through a unique URI). When we are dealing with a single primary key (e.g. integer or GUID), things are pretty simple, since our primary key is defined by only one value. However, when we are dealing with the resources that are defined by multiple keys, things to get a bit more complicated.

Let us consider following example:

We are designing a personal finance module, which consists of following resources:

  • Budget - represents users planning budget, which contains expense and income categories (e.g. "Familiy")
  • Category - represents an income or expense category which contains users planned items (e.g. "Food")
  • PlannedItem - represents a single planned item of income or expense that user has planned (e.g. "Bread")
The optimally designed RESTful API here will allow us to access the particular PlannedItem resource by specifying all of its primary key members, but will also allow us to get achieve hierarchical filtering. This is best illustrated by looking at the following requests we may allow.
  • /PlannedItems.svc/Family/Food/Bread - will return the planned expense for the "Bread"
  • /PlannedItems.svc/Family/Food - will return all planned expenses in the category "Food"
  • Items.svc/Family/Planned - will return all planned expenses and incomes in Budget "Family" regardless of the category to which they belong to
 Note

Notice the importance of the order in the specified URIs. We always have to start with the broadest possible part of the key and drill down to the more specific ones, in order to allow for this scenario.

URI Design

In WCF requests from the clients are matched to the actual web service methods through URI templates. Let us take a look at one sample method to see this concept in action.

 Note

ResourceEntry type is a type used to describe a localization resource in Sitefinity.

 Note

Notice how we are creating four web services for one module. Namely, since it the URIs for our web services should never contain a verbs (like Get, Save etc.), but only nouns (like Products, Suppliers) in conjunction with HTTP methods, we have to expose every resource through its own web service. This, however, provides for consistent and highly accessible API.

[WebGet(UriTemplate = "{cultureName}/{classId}/{key}/?provider={provider}", ResponseFormat = WebMessageFormat.Json)]
[OperationContract]
ResourceEntry GetResource(string cultureName, string classId, string key, string provider);

Let us start with the method for retrieving a single ResourceEntry object. The name of the actual method is irrelevant since the method will never actually be invoked by its name, so we can name it any way we see fit best for our internal class design.

Of course, for every method to be exposed as a web service method, in WCF we have to mark the method with the OperationContract attribute.

Finally, we get to the WebGet attribute (note that WebGet attribute is a shortcut for a more generic WebInvoke attribute, for the HTTP GET method).

WebGet attribute in our sample is specifying the UriTemplate, which when matched with a request will cause the GetResource method to be invoked. It also specifies that the response format should be serialized in JSON format (alternatively, we could have set the response format to XML).

While the subject of URI Templates is beyond of the scope of this article, we will outline few important things to know about them:
  • URI templates are the part of the URI defined after the base service URI. Take a look at following example:

    base service URI: www.test.com/Sitefinity/Services/Localization/LocalizationResources.svc

    URI template: /en-US/Labels/Cancel?provider=XmlResourcesProvider

    full URI: www.test.com/Sitefinity/Services/Localization/LocalizationResources.svc/en-US/Labels/Cancel?provider=XmlResourcesProvider

  • URI templates consist of two parts: path and query string. Path is everything in the template before the question mark (?) sign.

    path: /en-US/Labels/Cancel

    query string: ?provider=XmlResourcesProvider

  • Every URI template must be unique. URI templates must be unique in their path segments, meaning that query strings are not taken into account when determining if URI templates are different.

    URI template #1: /{cultureName}/{classId}/{key}

    URI template #2: /{cultureName}/{classId}/{key}/?provider={provider}

    URI template #1 and URI template #2 are considred to be identical.

In additon to this, URI templates that have same number of segments, must be differentiated through a literal value.

Design URI templates for the CRUD operations

The ServiceContracts or WCF services are generally designed on the interface level, allowing for simple changes in the actual implementations. In the following code you can examine the implementation of the ILocalizationResources service (service which exposes ResourceEntry object, the new object which holds the localized resources in Sitefinity):

[ServiceContract]
[ServiceKnownType(typeof(XmlResourceEntry))]
public interface ILocalizationResources
{
    /// <summary>
    /// Gets the collection of <see cref="ResourceEntry"/> in JSON format.
    /// </summary>
    /// <param name="cultureName">
    /// Name of the culture for which the <see cref="CollectionService{ResourceEnty}"/> should be retrieved.
    /// </param>
    /// <param name="classId">
    /// The id of the class for which the <see cref="CollectionService{ResourceEnty}"/> should be retrieved.
    /// </param>
    /// <param name="provider">
    /// The name of the resource provider from which the resources should be retrived.
    /// </param>
    /// <param name="sort">
    /// The sort expression used to order the retrieved resources.
    /// </param>
    /// <param name="skip">
    /// The number of resources to skip before populating the collection (used primarily for paging).
    /// </param>
    /// <param name="take">
    /// The maximum number of resources to take in the collection (used primarily for paging).
    /// </param>
    /// <param name="filter">
    /// The filter expression in dynamic LINQ format used to filter the retrieved resources.
    /// </param>
    /// <returns>
    /// <see cref="CollectionContext{ResourceEnty}"/> object with resource entry items and other information about the retrieved collection.
    /// </returns>
    [WebHelp(Comment = "Gets a collection of all resources, with an option to retrieve all items for given culture or for given culture and class id. The results are returned in JSON format.")]
    [WebGet(UriTemplate = "{cultureName=null}/{classId=null}/?provider={provider}&sort={sort}&skip={skip}&take={take}&filter={filter}", ResponseFormat = WebMessageFormat.Json)]
    [OperationContract]
    CollectionContext<ResourceEntry> GetResources(string cultureName, string classId, string provider, string sort, int skip, int take, string filter);
 
    /// <summary>
    /// Gets the collection of <see cref="ResourceEntry"/> in XML format.
    /// </summary>
    /// <param name="cultureName">
    /// Name of the culture for which the <see cref="CollectionService{ResourceEnty}"/> should be retrieved.
    /// </param>
    /// <param name="classId">
    /// The id of the class for which the <see cref="CollectionService{ResourceEnty}"/> should be retrieved.
    /// </param>
    /// <param name="provider">
    /// The name of the resource provider from which the resources should be retrived.
    /// </param>
    /// <param name="sort">
    /// The sort expression used to order the retrieved resources.
    /// </param>
    /// <param name="skip">
    /// The number of resources to skip before populating the collection (used primarily for paging).
    /// </param>
    /// <param name="take">
    /// The maximum number of resources to take in the collection (used primarily for paging).
    /// </param>
    /// <param name="filter">
    /// The filter expression in dynamic LINQ format used to filter the retrieved resources.
    /// </param>
    /// <returns>
    /// <see cref="CollectionContext{ResourceEnty}"/> object with resource entry items and other information about the retrieved collection.
    /// </returns>
    [WebHelp(Comment = "Gets a collection of all resources, with an option to retrieve all items for given culture or for given culture and class id. The results are returned in XML format.")]
    [WebGet(UriTemplate = "xml/{cultureName=null}/{classId=null}/?provider={provider}&sort={sort}&skip={skip}&take={take}&filter={filter}", ResponseFormat = WebMessageFormat.Xml)]
    [OperationContract]
    CollectionContext<ResourceEntry> GetResourcesInXml(string cultureName, string classId, string provider, string sort, int skip, int take, string filter);
 
    /// <summary>
    /// Gets the single resource entry in JSON format.
    /// </summary>
    /// <param name="cultureName">
    /// Name of the culture to which the resource is defined for.
    /// </param>
    /// <param name="classId">
    /// The id of the class to which the resource belongs to.
    /// </param>
    /// <param name="key">
    /// The key of the resource.
    /// </param>
    /// <param name="provider">
    /// The name of the resource provider from which the <see cref="ResourceEntry"/> should be retrieved.
    /// </param>
    /// <returns>ResourceEntry object.</returns>
    [WebHelp(Comment = "Gets a single resource entry in JSON format.")]
    [WebGet(UriTemplate = "{cultureName}/{classId}/{key}/?provider={provider}", ResponseFormat = WebMessageFormat.Json)]
    [OperationContract]
    ResourceEntry GetResource(string cultureName, string classId, string key, string provider);
 
    /// <summary>
    /// Gets the single resource entry in XML format.
    /// </summary>
    /// <param name="cultureName">
    /// Name of the culture to which the resource is defined for.
    /// </param>
    /// <param name="classId">
    /// The id of the class to which the resource belongs to.
    /// </param>
    /// <param name="key">
    /// The key of the resource.
    /// </param>
    /// <param name="provider">
    /// The name of the resource provider from which the <see cref="ResourceEntry"/> should be retrieved.
    /// </param>
    /// <returns>ResourceEntry object.</returns>
    [WebHelp(Comment = "Gets a single resource entry in XML format.")]
    [WebGet(UriTemplate = "xml/{cultureName}/{classId}/{key}/?provider={provider}", ResponseFormat = WebMessageFormat.Xml)]
    [OperationContract]
    ResourceEntry GetResourceInXml(string cultureName, string classId, string key, string provider);
 
    /// <summary>
    /// Saves the resource and returns the saved version of the resources in JSON format.
    /// </summary>
    /// <remarks>
    /// If the resource to be saved does not exist, new resource will be created. If the resource, 
    /// however, does exist the existing resource will be update.
    /// </remarks>
    /// <param name="propertyBag">
    /// The array of ResourceEntry properties that should be persisted. The first array contains 
    /// properties, while the second array holds property name in its first dimension and 
    /// property value in its second dimension.
    /// </param>
    /// <param name="cultureName">
    /// Name of the culture for which the resource should be saved.
    /// </param>
    /// <param name="classId">
    /// The id of the class for which the resource should be saved.
    /// </param>
    /// <param name="key">
    /// The key of the resource for which the resource should be saved.
    /// </param>
    /// <param name="provider">
    /// The name of the resource provider on which the <see cref="ResourceEntry"/> should be saved.
    /// </param>
    /// <returns>
    /// Newly created or updated <see cref="ResourceEntry"/> object in JSON format.
    /// </returns>
    [WebHelp(Comment = "Adds a new resource entry or updates an existing one and then returns the resource in JSON format.")]
    [WebInvoke(Method = "PUT", UriTemplate = "{cultureName}/{classId}/{key}/?provider={provider}", ResponseFormat = WebMessageFormat.Json)]
    [OperationContract]
    ResourceEntry SaveResource(string[][] propertyBag, string cultureName, string classId, string key, string provider);
 
    /// <summary>
    /// Saves the resource and returns the saved version of the resources in XML format.
    /// </summary>
    /// <remarks>
    /// If the resource to be saved does not exist, new resource will be created. If the resource, 
    /// however, does exist the existing resource will be update.
    /// </remarks>
    /// <param name="propertyBag">
    /// The array of ResourceEntry properties that should be persisted. The first array contains 
    /// properties, while the second array holds property name in its first dimension and 
    /// property value in its second dimension.
    /// </param>
    /// <param name="cultureName">
    /// Name of the culture for which the resource should be saved.
    /// </param>
    /// <param name="classId">
    /// The id of the class for which the resource should be saved.
    /// </param>
    /// <param name="key">
    /// The key of the resource for which the resource should be saved.
    /// </param>
    /// <param name="provider">
    /// The name of the resource provider on which the <see cref="ResourceEntry"/> should be saved.
    /// </param>
    /// <returns>
    /// Newly created or updated ResourceEntry object in XML format.
    /// </returns>
    [WebHelp(Comment = "Adds a new resource entry or updates an existing one and then returns the resource in XML format.")]
    [WebInvoke(Method = "PUT", UriTemplate = "xml/{cultureName}/{classId}/{key}/?provider={provider}", ResponseFormat = WebMessageFormat.Xml)]
    [OperationContract]
    ResourceEntry SaveResourceInXml(string[][] propertyBag, string cultureName, string classId, string key, string provider);
 
    /// <summary>
    /// Deletes the resource entry.
    /// </summary>
    /// <param name="cultureName">
    /// Name of the culture.
    /// </param>
    /// <param name="classId">
    /// The class id.
    /// </param>
    /// <param name="key">
    /// The key.
    /// </param>
    /// <param name="provider">
    /// The name of the resource provider from which the resource entry should be deleted.
    /// </param>
    [WebHelp(Comment = "Deletes a resource entry.")]
    [WebInvoke(Method = "DELETE", UriTemplate = "{cultureName}/{classId}/{key}/?provider={provider}", ResponseFormat = WebMessageFormat.Json)]
    [OperationContract]
    void DeleteResource(string cultureName, string classId, string key, string provider);
     
    ...
}

There are several important things to note here: HTTP methods:
  • HTTP GET method is used for retrieving resource entries
  • HTTP PUT method is used for creating new resource entries or updating existing ones
  • HTTP DELETE method is used for deleting ResourceEntries
While, technically, you can design an API that will use only POST method, you will end up with a service which is not RESTful. Following the above outlined conventions provides the other developers with the ability to start using your API in very short time, without the need to learn or remember your API.

Response formats

In Sitefinity, all OperationContracts that return data support JSON and XML as response formats. Prefix your templates with /xml/ when you are providing an XML response. By convention, Sitefinity returns data in JSON format.

Documentation

You should provide the full XML documentation of all the operational contracts, so that the implementers know how to implement your interface. In addition to this, you should also provide the OperationContract WebHelp with a brief summary of what service does, so that the web service consumers know how to use your service.

Convention assurance

The conventions play a major role in the Sitefinity client binding model as well as they assure that the consistency across the API. Sitefinity comes with numerous ClientBinder controls which automate the process of client side binding with different controls and these ClientBinder controls rely on certain conventions in the WCF service design. Following list outlins these conventions:

  • For every OperationContract (web service method) make sure you provide a "provider" query string parameter and use it in your implementation when instantiating the manager class.
  • For every OperationContract (web service method) that retrieves a collection of items, make sure you provide "sort", "skip" , "take" and "filter" query string parameters. "Sort" parameter should be used to order the collection. "Skip" and "Take" are used to return a subset of items, where "Filter" is a filter expression in dynamic LINQ form.
  • For every OperationContract (web service method) that retrieves a collection of items, return an object of type CollectionContext<T>, where you pass the type of the resource as a generic argument. Make sure you set the total number of items on CollectionContext object in order to facilitate possible client paging scenarios.
  • For every OperationContract (web service method) which returns some data, make sure you provide both XML and JSON implementations.
  • The only path segments in your URI templates should be "/xml/" literal which specifies that returned value should be in XML format and data keys. Do not implement arbitrarily path segments. (Path segments are the parts of the UriTemplate path prior to the question mark sign (?) spearated by the slash sign (/).
  • You can use any query string parameters that you may find useful, however, note that the built-in ClientBinder controls can take advantage only of the above mentioned QueryString keys ("provider", "sort", "skip", "take" and "filter").
Now that we have explained the WCF RESTful web service design, in next article we are going to examine the implementation of such service.

Related topics:

Feedback

How useful is this article?

Tell us more

Submit
Your message was successfully sent.

We appreciate your feedback.

Your message could not be sent.

OK