An Honest look at SharePoint Web Services
- by juanlarios
INTRODUCTION
If you are a SharePoint developer you know that there are two basic ways to develop against SharePoint. 1) The object Model 2) Web services.
SharePoint object model has the advantage of being quite rich. Anything you can do through the SharePoint UI as an administrator or end user, you can do through the object model. In fact everything that is done through the UI is done through the object model behind the scenes. The major disadvantage to getting at SharePoint this way is that the code needs to run on the server. This means that all web parts, event receivers, features, etc… all of this is code that is deployed to the server.
The second way to get to SharePoint is through the built in web services. There are many articles on how to manipulate web services, how to authenticate to them and interact with them. The basic idea is that a remote application or process can contact SharePoint through a web service. Lots has been written about how great these web services are. This article is written to document the limitations, some of the issues and frustrations with working with SharePoint built in web services. Ultimately, for the tasks I was given to , SharePoint built in web services did not suffice.
My evaluation of SharePoint built in services was compared against creating my own WCF Services to do what I needed. The current project I'm working on right now involved several "integration points". A remote application, installed on a separate server was to contact SharePoint and perform an task or operation. So I decided to start up Visual Studio and built a DLL and basically have 2 layers of logic. An integration layer and a data layer. A good friend of mine pointed me to SOLID principles and referred me to some videos and tutorials about it. I decided to implement the methodology (although a lot of the principles are common sense and I already incorporated in my coding practices). I was to deliver this dll to the application team and they would simply call the methods exposed by this dll and voila! it would do some task or operation in SharePoint.
SOLUTION
My integration layer implemented an interface that defined some of the basic integration tasks that I was to put together. My data layer was about the same, it implemented an interface with some of the tasks that I was going to develop. This gave me the opportunity to develop different data layers, ultimately different ways to get at SharePoint if I needed to. This is a classic SOLID principle. In this case it proved to be quite helpful because I wrote one data layer completely implementing SharePoint built in Web Services and another implementing my own WCF Service that I wrote. I should mention there is another layer underneath the data layer. In referencing SharePoint or WCF services in my visual studio project I created a class for every web service call. So for example, if I used List.asx. I created a class called "DocumentRetreival" this class would do the grunt work to connect to the correct URL, It would perform the basic operation of contacting the service and so on. If I used a view.asmx, I implemented a class called "ViewRetrieval" with the same idea as the last class but it would now interact with all he operations in view.asmx. This gave my data layer the ability to perform multiple calls without really worrying about some of the grunt work each class performs. This again, is a classic SOLID principle.
So, in order to compare them side by side we can look at both data layers and with is involved in each. Lets take a look at the "Create Project" task or operation. The integration point is described as , "dll is to provide a way to create a project in SharePoint". Projects , in this case are basically document libraries. I am to implement a way in which a remote application can create a document library in SharePoint. Easy enough right? Use the list.asmx Web service in SharePoint. So here we go! Lets take a look at the code.
I added the List.asmx web service reference to my project and this is the class that contacts it:
class DocumentRetrieval
{
private ListsSoapClient _service;
d private bool _impersonation;
public DocumentRetrieval(bool impersonation, string endpt)
{
_service = new ListsSoapClient();
this.SetEndPoint(string.Format("{0}/{1}", endpt, ConfigurationManager.AppSettings["List"]));
_impersonation = impersonation;
if (_impersonation)
{
_service.ClientCredentials.Windows.ClientCredential.Password = ConfigurationManager.AppSettings["password"];
_service.ClientCredentials.Windows.ClientCredential.UserName = ConfigurationManager.AppSettings["username"];
_service.ClientCredentials.Windows.AllowedImpersonationLevel =
System.Security.Principal.TokenImpersonationLevel.Impersonation;
}
private void SetEndPoint(string p)
{
_service.Endpoint.Address = new EndpointAddress(p);
}
/// <summary>
/// Creates a document library with specific name and templateID
/// </summary>
/// <param name="listName">New list name</param>
/// <param name="templateID">Template ID</param>
/// <returns></returns>
public XmlElement CreateLibrary(string listName, int templateID, ref ExceptionContract exContract)
{
XmlDocument sample = new XmlDocument();
XmlElement viewCol = sample.CreateElement("Empty");
try
{
_service.Open();
viewCol = _service.AddList(listName, "", templateID);
}
catch (Exception ex)
{
exContract = new ExceptionContract("DocumentRetrieval/CreateLibrary", ex.GetType(), "Connection Error", ex.StackTrace, ExceptionContract.ExceptionCode.error);
}finally
{
_service.Close();
}
return viewCol;
}
}
There was a lot more in this class (that I am not including) because i was reusing the grunt work and making other operations with LIst.asmx, For example, updating content types, changing or configuring lists or document libraries.
One of the first things I noticed about working with the built in services is that you are really at the mercy of what is available to you. Before creating a document library (Project) I wanted to expose a IsProjectExisting method. This way the integration or data layer could recognize if a library already exists. Well there is no service call or method available to do that check. So this is what I wrote:
public bool DocLibExists(string listName, ref ExceptionContract exContract)
{
try
{
var allLists = _service.GetListCollection();
return allLists.ChildNodes.OfType<XmlElement>().ToList().Exists(x => x.Attributes["Title"].Value ==listName);
}
catch (Exception ex)
{
exContract = new ExceptionContract("DocumentRetrieval/GetList/GetListWSCall", ex.GetType(), "Unable to Retrieve List Collection", ex.StackTrace, ExceptionContract.ExceptionCode.error);
}
return false;
}
This really just gets an XMLElement with all the lists. It was then up to me to sift through the clutter and noise and see if Document library already existed. This took a little bit of getting used to. Now instead of working with code, you are working with XMLElement response format from web service. I wrote a LINQ query to go through and find if the attribute "Title" existed and had a value of the listname then it would return True, if not False. I didn't particularly like working this way. Dealing with XMLElement responses and then having to manipulate it to get at the exact data I was looking for. Once the check for the DocLibExists, was done, I would either create the document library or send back an error indicating the document library already existed.
Now lets examine the code that actually creates the document library. It does what you are really after, it creates a document library. Notice how the template ID is really an integer. Every document library template in SharePoint has an ID associated with it. Document libraries, Image Library, Custom List, Project Tasks, etc… they all he a unique integer associated with it. Well, that's great but the client came back to me and gave me some specifics that each "project" or document library, should have. They specified they had 3 types of projects. Each project would have unique views, about 10 views for each project. Each Project specified unique configurations (auditing, versioning, content types, etc…) So what turned out to be a simple implementation of creating a document library as a repository for a project, turned out to be quite involved.
The first thing I thought of was to create a template for document library. There are other ways you can do this too. Using the web Service call, you could configure views, versioning, even content types, etc… the only catch is, you have to be working quite extensively with CAML. I am not fond of CAML. I can do it and work with it, I just don't like doing it. It is quite touchy and at times it is quite tough to understand where errors were made with CAML statements. Working with Web Services and CAML proved to be quite annoying. The service call would return a generic error message that did not particularly point me to a CAML statement syntax error, or even a CAML error. I was not sure if it was a security , performance or code based issue. It was quite tough to work with. At times it was difficult to work with because of the way SharePoint handles metadata. There are "Names", "Display Name", and "StaticName" fields. It was quite tough to understand at times, which one to use. So it took a lot of trial and error. There are tools that can help with CAML generation. There is also now intellisense for CAML statements in Visual Studio that might help but ultimately I'm not fond of CAML with Web Services.
So I decided on the template. So my plan was to create create a document library, configure it accordingly and then use The Template Builder that comes with the SharePoint SDK. This tool allows you to create site templates, list template etc… It is quite interesting because it does not generate an STP file, it actually generates an xml definition and a feature you can activate and make that template available on a site or site collection. The first issue I experienced with this is that one of the specifications to this template was that the "All Documents" view was to have 2 web parts on it. Well, it turns out that using the template builder , it did not include the web parts as part of the list template definition it generated. It backed up the settings, the views, the content types but not the custom web parts. I still decided to try this even without the web parts on the page. This new template defined a new Document library definition with a unique ID. The problem was that the service call accepts an int but it only has access to the built in library int definitions. Any new ones added or created will not be available to create. So this made it impossible for me to approach the problem this way.
I should also mention that one of the nice features about SharePoint is the ability to create list templates, back them up and then create lists based on that template. It can all be done by end user administrators. These templates are quite unique because they are saved as an STP file and not an xml definition. I also went this route and tried to see if there was another service call where I could create a document library based no given template name. Nope! none.
After some thinking I decide to implement a WCF service to do this creation for me. I was quite certain that the object model would allow me to create document libraries base on a template in which an ID was required and also templates saved as STP files. Now I don't want to bother with posting the code to contact WCF service because it's self explanatory, but I will post the code that I used to create a list with custom template.
public ServiceResult CreateProject(string name, string templateName, string projectId)
{
string siteurl = SPContext.Current.Site.Url;
Guid webguid = SPContext.Current.Web.ID;
using (SPSite site = new SPSite(siteurl))
{
using (SPWeb rootweb = site.RootWeb)
{
SPListTemplateCollection temps = site.GetCustomListTemplates(rootweb);
ProcessWeb(siteurl, webguid, web => Act_CreateProject(web, name, templateName, projectId, temps));
}//SpWeb
}//SPSite
return _globalResult;
}
private void Act_CreateProject(SPWeb targetsite, string name, string templateName, string projectId, SPListTemplateCollection temps) {
var temp = temps.Cast<SPListTemplate>().FirstOrDefault(x => x.Name.Equals(templateName));
if (temp != null)
{
try
{
Guid listGuid = targetsite.Lists.Add(name, "", temp);
SPList newList = targetsite.Lists[listGuid];
_globalResult = new ServiceResult(true, "Success", "Success");
}
catch (Exception ex)
{
_globalResult = new ServiceResult(false, (string.IsNullOrEmpty(ex.Message) ? "None" : ex.Message + " " + templateName), ex.StackTrace.ToString());
}
}
private void ProcessWeb(string siteurl, Guid webguid, Action<SPWeb> action) {
using (SPSite sitecollection = new SPSite(siteurl)) {
using (SPWeb web = sitecollection.AllWebs[webguid]) {
action(web);
}
}
}
This code is actually some of the code I implemented for the service. there was a lot more I did on Project Creation which I will cover in my next blog post. I implemented an ACTION method to process the web. This allowed me to properly dispose the SPWEb and SPSite objects and not rewrite this code over and over again.
So I implemented a WCF service to create projects for me, this allowed me to do a lot more than just create a document library with a template, it now gave me the flexibility to do just about anything the client wanted at project creation. Once this was implemented , the client came back to me and said, "we reference all our projects with ID's in our application. we want SharePoint to do the same". This has been something I have been doing for a little while now but I do hope that SharePoint 2010 can have more of an answer to this and address it properly. I have been adding metadata to SPWebs through property bag. I believe I have blogged about it before. This time it required metadata added to a document library. No problem!!! I also mentioned these web parts that were to go on the "All Documents" View. I took the opportunity to configure them to the appropriate settings. There were two settings that needed to be set on these web parts. One of them was a Project ID configured in the webpart properties. The following code enhances and replaces the "Act_CreateProject " method above:
private void Act_CreateProject(SPWeb targetsite, string name, string templateName, string projectId, SPListTemplateCollection temps) {
var temp = temps.Cast<SPListTemplate>().FirstOrDefault(x => x.Name.Equals(templateName));
if (temp != null)
{
SPLimitedWebPartManager wpmgr = null;
try
{
Guid listGuid = targetsite.Lists.Add(name, "", temp);
SPList newList = targetsite.Lists[listGuid];
SPFolder rootFolder = newList.RootFolder;
rootFolder.Properties.Add(KEY, projectId);
rootFolder.Update();
if (rootFolder.ParentWeb != targetsite)
rootFolder.ParentWeb.Dispose();
if (!templateName.Contains("Natural"))
{
SPView alldocumentsview = newList.Views.Cast<SPView>().FirstOrDefault(x => x.Title.Equals(ALLDOCUMENTS));
SPFile alldocfile = targetsite.GetFile(alldocumentsview.ServerRelativeUrl);
wpmgr = alldocfile.GetLimitedWebPartManager(PersonalizationScope.Shared);
ConfigureWebPart(wpmgr, projectId, CUSTOMWPNAME);
alldocfile.Update();
}
if (newList.ParentWeb != targetsite)
newList.ParentWeb.Dispose();
_globalResult = new ServiceResult(true, "Success", "Success");
}
catch (Exception ex)
{
_globalResult = new ServiceResult(false, (string.IsNullOrEmpty(ex.Message) ? "None" : ex.Message + " " + templateName), ex.StackTrace.ToString());
}
finally
{
if (wpmgr != null)
{
wpmgr.Web.Dispose();
wpmgr.Dispose();
}
}
}
}
private void ConfigureWebPart(SPLimitedWebPartManager mgr, string prjId, string webpartname)
{
var wp = mgr.WebParts.Cast<System.Web.UI.WebControls.WebParts.WebPart>().FirstOrDefault(x => x.DisplayTitle.Equals(webpartname));
if (wp != null)
{
(wp as ListRelationshipWebPart.ListRelationshipWebPart).ProjectID = prjId;
mgr.SaveChanges(wp);
}
}
This Shows you how I was able to set metadata on the document library. It has to be added to the RootFolder of the document library, Unfortunately, the SPList does not have a Property bag that I can add a key\value pair to. It has to be done on the root folder. Now everything in the integration will reference projects by ID's and will not care about names. My, "DocLibExists" will now need to be changed because a web service is not set up to look at property bags. I had to write another method on the Service to do the equivalent but with ID's instead of names. The second thing you will notice about the code is the use of the Webpartmanager. I have seen several examples online, and also read a lot about memory leaks, The above code does not produce memory leaks. The web part manager creates an SPWeb, so just dispose it like I did.
CONCLUSION
This is a long long post so I will stop here for now, I will continue with more comparisons and limitations in my next post. My conclusion for this example is that Web Services will do the trick if you can suffer through CAML and if you are doing some simple operations. For Everything else, there's WCF!
**** fireI apologize for the disorganization of this post, I was on a bus on a 12 hour trip to IOWA while I wrote it, I was half asleep and half awake, hopefully it makes enough sense to someone.