Tuesday, April 29, 2014

SharePoint 2013: Create a Custom WCF REST Service Hosted in SharePoint

SharePoint 2013 provides a robust Representational State Transfer (REST) interface that allows any technology that supports standard REST capabilities to interact with SharePoint (sites, libraries, lists, etc).  In addition to the built-in SharePoint REST API, you can create your own custom Windows Communication Foundation (WCF) REST services that are hosted in SharePoint.  In this example, we'll explore the steps necessary to create a SharePoint-hosted WCF service with a REST interface that is deployed in a SharePoint solution (.wsp).

Download the source (50.8 KB)

Step-by-Step Instructions

  1. Run Visual Studio 2013 as an Administrator.
  2. Create a new project named Barkes.Services.Presidents using the SharePoint 2013 - Empty Project template from the Visual C# SharePoint Solutions category.
  3. Ensure the Deploy as farm solution option is selected.
  4. After the project is created, right-click on the project in the Solution Explorer, then Add -> SharePoint Mapped Folder.
  5. On the Add SharePoint Mapped Folder dialog, select the ISAPI folder and click OK.
  6. Right-click on the ISAPI folder in the Solution Explorer, then Add -> New Item.
  7. On the Add New Item dialog, select Text File from the General category and enter  PresidentsService.svc as the name, then click Add.  Make sure to change the default file extension from txt to svc.
     
  8. Right-click on the ISAPI folder in the Solution Explorer, then Add -> New Item.
  9. On the Add New Item dialog, select Code File from the Code category and enter PresidentsService.svc.cs as the name, then click Add.
  10. Right-click on the ISAPI folder in the Solution Explorer, then Add -> New Item.
  11. On the Add New Item dialog, select Interface from the Code category and enter IPresidentsService.cs as the name, then click Add.
  12. Add the required assembly references by right-clicking References, then Add Reference from the Solution Explorer.
  13. On the Reference Manager dialog, select Framework and check System.Runtime.Serialization, System.ServiceModel and System.ServiceModel.Web, then click OK.
  14. By default Visual Studio does not support token replacements in .SVC files.  In order to use the $SharePoint.Project.AssemblyFullName$ token, right-click on the project in Solution Explorer, then Unload Project.  If you are prompted to save the project, select yes.
  15. Right-click the project in the Solution Explorer, then Edit Barkes.Services.Presidents.csproj.
  16. In the first PropertyGroup (toward the top of the project file), add the TokenReplacementFileExtensions element beneath the SandboxedSolution element and set its value to svc.  Don't forget to save the changes to the project file.
    <PropertyGroup>
        ...
        <SandboxedSolution>False</SandboxedSolution>
        <TokenReplacementFileExtensions>svc</TokenReplacementFileExtensions>
    </PropertyGroup>
  17. After you've made the required manual project changes, right-click the project and select Reload Project.
  18. Open PresidentsService.svc and enter the following service declaration.  Note that the use of the SharePoint-specific MultipleBaseAddressWebServiceHostFactory replaces the need to specify endpoint configurations in a web.config.
    <%@ ServiceHost Language="C#" Debug="true"
        Service="Barkes.Services.Presidents.PresidentsService, $SharePoint.Project.AssemblyFullName$"
        CodeBehind="PresidentsService.svc.cs"
        Factory="Microsoft.SharePoint.Client.Services.MultipleBaseAddressWebServiceHostFactory,
        Microsoft.SharePoint.Client.ServerRuntime, Version=15.0.0.0, Culture=neutral,
        PublicKeyToken=71e9bce111e9429c" %>
  19. Open IPresidentsService.cs and enter the following interface definition, with associated ServiceContract and OperationContracts.
    1. using System;
      using System.Collections.Generic;
      using System.ServiceModel;
      using System.ServiceModel.Web;
      using Barkes.Services.Presidents.Model;
      
      namespace Barkes.Services.Presidents
      {
          [ServiceContract]
          interface IPresidentsService
          {
              [OperationContract]
              [WebGet(UriTemplate = "GetAllPresidents",
                  ResponseFormat = WebMessageFormat.Json)]
              List<President> GetAllPresidents();
      
              [OperationContract(Name = "GetPresidentsByLastName")]
              [WebGet(UriTemplate = "GetPresidentsByLastName/{lastName}",
                  ResponseFormat = WebMessageFormat.Json)]
              List<President> GetPresidentsByName(string lastName);
      
              [OperationContract(Name = "GetPresidentsByLastFirstName")]
              [WebGet(UriTemplate = "GetPresidentsByLastFirstName/{lastName}/{firstName}",
                  ResponseFormat = WebMessageFormat.Json)]
              List<President> GetPresidentsByName(string lastName, string firstName);
      
              [OperationContract]
              [WebGet(UriTemplate = "GetPresidentById/{id}",
                  ResponseFormat = WebMessageFormat.Json)]
              President GetPresidentById(string id);
      
              [OperationContract]
              [WebInvoke(Method = "POST", UriTemplate = "AddPresident",
                  RequestFormat = WebMessageFormat.Json,
                  ResponseFormat = WebMessageFormat.Json)]
              bool AddPresident(President president);
          }
      }
      
  20. Open PresidentsService.svc.cs and enter the following code to implement the service interface.
    1. using Microsoft.SharePoint.Client.Services;
      using System;
      using System.Collections.Generic;
      using System.Linq;
      using System.ServiceModel.Activation;
      using Barkes.Services.Presidents.Model;
      
      namespace Barkes.Services.Presidents
      {
          [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Required)]
          public class PresidentsService : IPresidentsService
          {
              #region Private Members
      
              private List<President> _presidents;
              private List<President> Presidents
              {
                  get
                  {
                      // If there aren't any presidents in our list, populate with samples
                      _presidents = _presidents ?? new List<President>(SampleData.SamplePresidents);
                      return _presidents;
                  }
              }
      
              #endregion
      
              #region IPresidentsService Implementation
      
              public List<President> GetAllPresidents()
              {
                  return Presidents;
              }
      
              public List<President> GetPresidentsByName(string lastName)
              {
                  return GetPresidentsByName(lastName, string.Empty);
              }
      
              public List<President> GetPresidentsByName(string lastName, string firstName)
              {
                  var query = from President p in Presidents
                              where p.LastName.ToLower().Contains(lastName.ToLower())
                                 && (string.IsNullOrWhiteSpace(firstName) 
                                      ? true 
                                      : p.FirstName.ToLower().Contains(firstName.ToLower()))
                              select p;
      
                  return query.ToList();
              }
      
              public President GetPresidentById(string id)
              {
                  var query = from President p in Presidents
                              where p.Id == id
                              select p;
      
                  return query.FirstOrDefault();
              }
      
              public bool AddPresident(President president)
              {
                  Presidents.Add(president);
                  return true;
              }
      
              #endregion
      
          }
      }
  21. Add a new folder named Model to the project by right-clicking on the project and selecting Add, then New Folder.
  22. Add a new class in the Model folder named President.cs and enter the following class definition, with associated DataContract and DataMembers.
    1. using System.Runtime.Serialization;
      
      namespace Barkes.Services.Presidents.Model
      {
          [DataContract]
          public class President
          {
              [DataMember]
              public string Id { get; set; }
      
              [DataMember]
              public string LastName { get; set; }
      
              [DataMember]
              public string FirstName { get; set; }
      
              [DataMember]
              public string EmailAddress { get; set; }
          }
      }
  23. Add a new class in the Model folder named PresidentsData.cs and enter the following sample data code.  In a production application, this would typically come from a database.  The presidents array is purposely abbreviated for readability - all the presidents are in the complete source.
    1. namespace Barkes.Services.Presidents.Model
      {
          public static class SampleData
          {
              // This array is purposely abbreviated for readability in this article.
              // The complete list of presidents is available in the source download.
              public static President[] SamplePresidents = new President[]
              {
                  new President { 
                      Id =  "1", FirstName = "George", LastName = "Washington", 
                      EmailAddress = "gwashington@email.com" },
                  new President { 
                      Id =  "2", FirstName = "John", LastName = "Adams", 
                      EmailAddress = "jadams@email.com" },
                  new President { 
                      Id =  "3", FirstName = "Thomas", LastName = "Jefferson", 
                      EmailAddress = "tjefferson@email.com" },
                  new President { 
                      Id =  "4", FirstName = "James", LastName = "Madison", 
                      EmailAddress = "jmadison@email.com" },
                  new President { 
                      Id =  "5", FirstName = "James", LastName = "Monroe", 
                      EmailAddress = "jmonroe@email.com" },
                  new President { 
                      Id = "43", FirstName = "George W.", LastName = "Bush", 
                      EmailAddress = "gbush@email.com" },
                  new President { 
                      Id = "44", FirstName = "Barack", LastName = "Obama", 
                      EmailAddress = "bobama@email.com" },
              };
          }
      }
  24. Now you're ready to build the solution and deploy the WSP.  After deployment, you'll find the PresidentsService.svc service declaration in the 15 hive at C:\Program Files\Common Files\microsoft shared\Web Server Extensions\15\ISAPI\BarkesServices.

Call the Service from Managed Code

There are a number of different options (tools, libraries, etc) and articles available to help you consume a WCF REST service from managed code.  The following is an excerpt from a Visual Studio unit test project that calls the service to return all presidents.   The custom JSON helper class used to simplify the object (de)serialization is shown below as well.

  1. // Be sure to update the url to point to the Presidents Service in your SP farm.
    string url = "http://sp13.dev/_vti_bin/BarkesServices/PresidentsService.svc/GetAllPresidents";
    string response = CallService(url);
    List<President> presidents = JsonHelper.Deserialize<List<President>>(response);
    
    private string CallService(string serviceUrl)
    {
        WebClient client = new WebClient();
        client.UseDefaultCredentials = true;
        client.Headers["Content-type"] = "application/json";
        client.Encoding = Encoding.UTF8;
        string response = response = client.DownloadString(serviceUrl);
    
        return response;
    }
  1. public class JsonHelper
    {
        public static string Serialize<T>(T obj)
        {
            MemoryStream stream = new MemoryStream();
            DataContractJsonSerializer serializer = new DataContractJsonSerializer(typeof(T));
            serializer.WriteObject(stream, obj);
            stream.Position = 0;
            StreamReader reader = new StreamReader(stream);
            return reader.ReadToEnd();
        }
    
        public static T Deserialize<T>(string data)
        {
            if (string.IsNullOrWhiteSpace(data)) return default(T);
            DataContractJsonSerializer serializer = new DataContractJsonSerializer(typeof(T));
            MemoryStream stream = new MemoryStream(Encoding.UTF8.GetBytes(data));
            return (T)serializer.ReadObject(stream);
        }
    }

Call the Service from JQuery

The following demonstrates how to call the service from a Script Editor Web Part using simple HTML, JavaScript and JQuery.

  1. <script src="http://sp13.dev/SiteAssets/jquery-1.10.2.min.js"></script>
    
    <h2>SharePoint 2013: Consume a custom WCF REST service hosted in SharePoint 2013.</h2>
    <h3>This is a quick sample to demonstrate calling a custom SharePoint-hosted WCF REST service from a
        Script Editor Web Part using simple HTML, JavaScript and JQuery.
    </h3>
    
    <div>
        <br />
        <p id="message">Loading presidents...</p>
    </div>
    
    <div id="resultsPanel"></div>
    
    <script type="text/javascript">
        $(document).ready(function () {
            getPresidentsData();
        });
      
    function getPresidentsData() {
        var serviceUri = _spPageContextInfo.webAbsoluteUrl +
            "/_vti_bin/BarkesServices/PresidentsService.svc/GetAllPresidents";
        $.ajax({
            type: "GET",
            contentType: "application/json",
            url: serviceUri,
            dataType: "json",
            success:
                function (response) {
                    showPresidentsList(response);
                    $('#message').html("<a href=" + serviceUri + ">" + serviceUri + "</a>");
                },
            error:
                function (err) {
                    alert(err);
                }
        });
    }
    
    function showPresidentsList(presidentsData) {
        $.each(presidentsData, function () {
            $('#resultsPanel').append($(this)[0].Id + ' - ');
            $('#resultsPanel').append($(this)[0].FirstName + ' ');
            $('#resultsPanel').append($(this)[0].LastName + ' (');
            $('#resultsPanel').append($(this)[0].EmailAddress + ')');
            $('#resultsPanel').append('<br><br>');
        });
    }
    </script>

Results Screenshots

Calling the Presidents Service from a Script Editor Web Part using simple HTML, JavaScript and JQuery.  Of course you can use the resulting JSON data with Backbone.js, Knockout and a variety of JavaScript/JQuery grids (JS Grid, simpleGrid, jqGrid, etc).

Interacting with the Presidents Service in Fiddler:



Download the source (50.8 KB)

For more SharePoint articles and samples, check out the Software Development Outpost.

12 comments:

  1. Outstanding article!

    ReplyDelete
  2. This is well done...thanks!

    ReplyDelete
  3. Excellent article! Thx

    ReplyDelete
  4. This is by far the best tutorial for custom SP Web Services I have ever seen. And it works!:) (which is not always the case)

    ReplyDelete
  5. I agree with the other comments - this is by far the best article I've read about this so far. One minor thing I noted, in case anyone else runs across it, is there's no mention in Step 12 of creating the "BarkesServices" folder under the "ISAPI" folder. For anyone trying this on their own, don't forget to create a sub-folder (with a name you pick) under ISAPI for your web service.

    ReplyDelete
  6. Fantastic! Really well done article! Thank you

    ReplyDelete
  7. I am glad I found this article. Saved me a ton of time! Agree with the others.

    ReplyDelete
  8. Thanks for the positive feedback. Several people have reported that the source link was broken. Since Atlassian and BitBucket combined, the repo was set to private. I updated the repo settings and the source link should be working again.

    ReplyDelete
  9. Glad the source link is back up! Perfect timing for me. Huge help!

    ReplyDelete
  10. This is a great article. I've been searching everywhere for a simple walkthrough like this.

    ReplyDelete