SharePoint for Developers Part 6 – Custom web services

Part 6 of the SharePoint for Developers series is posted to Channel9, this one focusing on creating custom web services in SharePoint.  For reference, here are the links to the previous 5 screencasts in this series so far.

In part 5, I showed how to create a custom field, use that field in a custom content type, create a list definition based on that content type, and finally create a list instance called “Annotations”.  The result looks like this when adding a new item:

image

The default view also displays the custom fields in our list.

image

In this session, we’ll show how to create a custom web service that exposes a very specific contract for our Annotations list, allowing us to select, insert, and update the data in our list. 

Why Didn’t You Use the Lists Web Service Instead of Rolling Your Own?

Sure, we could have used SharePoint’s Lists web service, but this service will be used often and I needed to make consumption of the service simple for my end users.  I want to constrain the type and amount of data being retrieved through a specific service contract, and I wanted to make it simple for consumers to use things like data binding when using this service.  The easier the service is to use, the more frequently people will use it. 

One of the benefits of providing a special-purpose service is that I can encourage consistency in how the list is accessed while reducing bandwidth.  For instance, you’ll notice the DeleteAnnotation method below needs to query for a particular item before deleting it.  If someone were using the Lists web service for this purpose, the equivalent functionality (query for an item, delete an item, retrieve the current list of items) would require at least 3 separate service calls instead of bundling the functionality into a single call.

A huge benefit of using web services in this scenario is that it provides a way to consume the data from both Microsoft and non-Microsoft applications via basic SOAP web services.  You can consume these services just as easily from a .NET application as you could a Java application, because the payload is just XML encoded as UTF-8, delivered over HTTP.  You can see the request and response payloads in the following screenshot.  There are many more benefits to this approach, I urge you to read the closing section below, “Why Would I Do This Instead of Simply Rolling My Own Service in ASP.NET?”.

image

Makes Sense… Now Show Us the Code!

As usual, I am not gong to walk through every step of how I created the solution.  The accompanying screencast walks you through the entire process from scratch, this post is simply making it easier for you to copy/paste the code into your environment.  I also recommend you take a look at Walkthrough: Creating a Custom Web Service.  This topic has the complete steps for creating your own custom web service in SharePoint and is where I pirated borrowed some of my code from.

I start by creating a new Template using Visual Studio Extensions for Windows SharePoint Services 1.3 called “AnnotationService.asmx”, and placed the resulting file in the Templates/LAYOUTS/Msdn.Web folder in my project.  Next, we create a class “AnnotationService.cs”.

The first method in our class is the GetAnnotations method, which allows our consumers to retrieve the current list of items.

         [WebMethod]
        public List<Annotation> GetAnnotations(string mediaPath)
        {
            List<Annotation> ret = new List<Annotation>();

            SPWeb site = SPContext.Current.Web;
            SPList list = site.Lists["Annotations"];
            foreach (SPListItem item in list.Items)
            {
                ret.Add(new Annotation
                {
                    AnnotationID = item["AnnotationID"].ToString(),
                    Comment = item[SPBuiltInFieldId.Title].ToString(),
                    MediaPath = new SPFieldUrlValue(item["MediaPath"].ToString()).Url,
                    TimeCode = item["TimeCode"].ToString()
                });
            }
            return ret;
        }

The cool part to note is how we can use the SPFieldUrlValue type to retrieve the data from a field of type “URL”.  Notice also the use of the SPBuiltInFieldId.Title field value.  We use this because our list contains a column “Title” defined by its parent content type.  We could have accessed it by simply using item[“Title”].ToString() instead of using the SPBuiltInField class, but I find this to be self-documenting to those who read the code later.

Next up is our method to insert a new row.

         [WebMethod]
        public List<Annotation> InsertAnnotation(string comment, string timeCode, string mediaPath)
        {
            SPWeb site = SPContext.Current.Web;
            site.AllowUnsafeUpdates = true;

            SPList list = site.Lists["Annotations"];
            SPListItem item = list.Items.Add();
            item[SPBuiltInFieldId.Title] = comment;
            item["AnnotationID"] = Guid.NewGuid().ToString();
            item["TimeCode"] = timeCode;
            item["MediaPath"] = mediaPath;

            item.Update();
            site.AllowUnsafeUpdates = false;

            return GetAnnotations(mediaPath);
        }

Of note here is the use of AllowUnsafeUpdates.  The reason for this code block is that, without it, you receive a SoapException when the .Update() method is called.  To be completely transparent here, I am not convinced that this is the only way to accomplish this, and recognize the risk in opening up the site for unsafe updates even for a brief period of time.  Even though our code is only allowing unsafe updates for a brief window, imagine if this service is called many, many times… that means there are many, many more time windows where a hacker could potentially squeeze in a malicious cross-site scripting payload via a GET request.  You should also recognize that the AllowUnsafeUpdates=false call should be performed in a finally block, ensuring that it is always executed.  In short, this is definitely not offered as a best practice, but rather offered as a workaround that you should investigate further and be familiar with the consequences before you copy/paste and deploy.

Next, our Delete method.

         [WebMethod]
        public List<Annotation> DeleteAnnotation(string annotationID, string mediaPath)
        {
            SPWeb site = SPContext.Current.Web;
            site.AllowUnsafeUpdates = true;

            SPList list = site.Lists["Annotations"];

            SPQuery query = new SPQuery();
            query.Query = "<Where><And><Eq><FieldRef Name=\"AnnotationID\" /><Value Type=\"Text\">" 
                        + annotationID + 
                        "</Value></Eq><Eq><FieldRef Name=\"MediaPath\" /><Value Type=\"Text\">" 
                        + mediaPath + "</Value></Eq></And></Where>";

            SPListItemCollection items = list.GetItems(query);
            items[0].Delete();
            site.AllowUnsafeUpdates = false;

            return GetAnnotations(mediaPath);

        }

The thing to point out here is the inline XML.  This is Collaborative Application Markup Language (CAML), as a SharePoint developer you should start to familiarize yourself with CAML.  Then you should make sure you have downloaded U2U’s CAML Query Builder as demonstrated in the video because it makes the job of generating CAML so much easier.  This allows us to retrieve a single item rather than iterating through the entire list.  Once we have the item, it is simple to call .Delete().  The caveats for AllowUnsafeUpdates apply here as well.

Now that we have the code-behind implementation, let’s revisit the AnnotationService.asmx file that we created earlier.  VSeWSS 1.3 will generate some text in the file that we need to replace.

 <%@ WebService Class="Msdn.Web.AnnotationService, Msdn.Web, Version=1.0.0.0, Culture=neutral, PublicKeyToken=asdf1234qwer5687" %>

Notice the PublicKeyToken in the 5-part type name above.  The way to retrieve this value for your assembly is to make sure that you’ve signed the assembly and compiled, creating a strongly-named assembly.  Then you open up the Visual Studio Command Prompt, navigate to the folder where your assembly lives, and use the sn.exe utility.

 sn.exe -Tp Msdn.Web.dll

A handy trick is to simply map sn.exe in Visual Studio as an external tool.  This is one of the prime differences between coding for ASP.NET and coding for SharePoint.  In ASP.NET, you typically use code-behind files because that’s what Visual Studio generates.  We don’t use code-behind files here, instead we put the code in the Global Assembly Cache and reference the code using the 5-part name.  As a .NET developer, you should be intimately familiar with How the Runtime Locates Assemblies and Working with Assemblies and the Global Assembly Cache.  By far the best reference material I have read on this is Jeff Richter’s excellent book, CLR via C#.  I contend that this is required reading for all .NET developers.

Creating the DISCO and WSDL Files

If you’ve created ASMX web services before, you might think you are done here. WRONG.  When you host a custom .asmx file in SharePoint, the infrastructure has some additional goodies to support hosting the web service in multiple sites.  For instance, your service (once deployed) is accessible from the root web, its child site, or any of the child sites under that root web.  Further, that first line of code in each of the methods above, SPContext.Current.Web, points to the current site, meaning the service operates relative to the site that it is being accessed from.  That makes automatic WSDL generation a little harder, so SharePoint requires that you add 2 more files: yourservicenameWSDL.aspx and yourservicenameDISCO.aspx.

I’m not going to go through all the steps or even list the code here, because the steps and code are already well-documented in the SDK, “Walkthrough: Creating a Custom Web Service.”  This is where I unashamedly copied and pasted the code at the top of the WSDL and DISCO files from that you see in the screencast.

When I was first building the demo for this screencast, I tried to skip this step, thinking that the ASMX would automagically create the WSDL, as usual, and that I am not going to provide discovery of services via DISCO so I didn’t need the DISCO.aspx file.  When you try to add a service reference in Visual Studio, it asks SharePoint for the WSDL, and SharePoint intercepts this call and looks for the WSDL.aspx and DISCO.aspx files.  If they don’t exist, you receive a nice exception and your proxy code is not generated.  This part threw me for awhile until I fought the debugger for about a day and learned how SharePoint uses these files under the hood.  Learn from others’ laziness… Consider the creation of the DISCO.aspx and WSDL.aspx files as mandatory steps when creating your own ASMX services. 

Why Would I Do This Instead of Simply Rolling My Own Service in ASP.NET?

This screencast, in particular, made me learn a ton about developing for SharePoint because there were so many issues that I needed to work through.  Using CAML, creating and deleting list items, working with SharePoint’s web services infrastructure… there’s a lot of infrastructure that we are taking advantage of here.  The amount of code we write is quite minimal, but the benefits are huge.  The list that we are writing to uses a custom field called TimeCodeField.  This field provides its own validation for data stored in this field, we didn’t need to create our own abstraction layer or data access mapping.  We don’t need to concern ourselves with creating and maintaining our own data storage, because SharePoint’s lists infrastructure handles this for us.  We don’t need to concern ourselves with deploying our service every time someone creates a new site, SharePoint handles that for us as well. 

This series has been building up to an application that provides the ability for an end user to pause a video (rendered through a Silverlight media player) and capture annotations about the video at a particular time segment.  You can imagine that, in a media company, there very well could be additional actions taken when someone creates an annotation such as automatically sending a formatted email to an advertiser (“The text formatting at time 00:00:28.4500000 does not comply with our standards for broadcast”).  This is the real power and benefit of leveraging SharePoint as an application platform: we can incrementally add value to our solution without requiring us to build that functionality into the service.  In a traditional ASP.NET application, you would likely add new functionality in a business logic layer, requiring a change in database storage, changes to the UI to display the new functionality, and changes to the service layer to expose the data to remote consumers.  With SharePoint, you could simply attach workflows to the content type, and any updates to the list via our web service will enjoy this new functionality without making any changes to the web service.  Or you might create a new content type and attach that to an existing list. 

This is a hugely powerful concept, and strongly demonstrates why you should consider using SharePoint as a development platform.  The separation of fields and content types and the ability to apply workflows to content types is huge.  This separation provides the framework for reusable components that can be easily assembled by end users through the web UI instead of requiring developers to code more functionality.  This lets you focus on creating the really cool building blocks and focusing on how to add value rather than writing monotonous forms code that performs CRUD operations against yet another custom database.  It provides a single, consistent platform that provides the web UI, services, and storage within a single platform.

The possibilities here are staggering, because it truly changes the way we think about building applications.

For More Information

SharePoint for Developers Part 6 – custom web services (screencast)

Code Practices - getting\setting values from\to the lookup and the hyperlink fields

U2U’s CAML Query Builder

What You Need to Know About Unsafe Updates (Part 1)

How the Runtime Locates Assemblies

Working with Assemblies and the Global Assembly Cache

Walkthrough: Creating a Custom Web Service