måndag 14 september 2009

Some tips for working with URLs in the SharePoint Object Model

Ok, so concistency is not one of the strenghts of the SharePoint OM.

Here's a couple of tips that might just save you from some headache...

1. SPListItem.Url does not make any sense

A typical value is "Lists/MyList/31_.000" where 31 is the list ID, and 000 somehow represents the version number. Of course, if you prepend the web URL and open this in a web browser, you get a 404. Great. If you want to send the user to the item display form, you need to get the name of the display form from the content type, and the item ID and build the URL manually.

2. Don't trust the documentation

The documentation has this to say about SPWeb.GetList(string):

public SPList GetList(string strUrl);
// Summary:
// Returns the list with the specified site-relative URL.
// Parameters:
// strUrl:
// A string that contains the site-relative URL for a list, for example, /Lists/Announcements.
// Returns:
// A Microsoft.SharePoint.SPList object representing the list.

Which is great, except it's completely wrong. The URL must be server relative or absolute - not site relative.

3. Be extremely careful not to mix up Absolute, Server, Site and Web Relative URLs

For a subweb of a site which is in the root web of a server, a site relative and a server relative URL are equal, which makes it easy to confuse them. If they are confused, the code will fail as soon as it tries to open a web under any site collection which is not in the root path of the server.

(Similar things can happen if a Site/Web relative or Server/Web relative URL is confused as well.)

Here's an example how not to do it, right out of one of the Microsoft assemblies using Reflector.

In one of the content database tables (regarding alerts), they've named two columns "siteUrl" and "webUrl". These names are confusing, most of all because siteUrl is not the siteUrl at all; the column contains only server URLs! Also, the values in "webUrl" are server relative.

Then, the developer of Microsoft.Office.Server.Search.Query.SearchAlertHandler.OnNotification in assembly Microsoft.Office.Server.Search, assumed that the "siteUrl" was the URL of the site (false), and that webUrl was a site relative URL (also false). S/he wrote these lines:

using (SPSite site = new SPSite(alertHandlerParams.siteUrl)){
SPWeb web = site.AllWebs[alertHandlerParams.webUrl];
// most of the code is here

(They forget to dispose that web, so the code is leaking memory, but that's not the main issue here.)

The SPWebCollection, most commonly returned by SPSite.AllWebs[] and SPWeb.GetSubwebsForCurrentUser(), has an indexer which accepts a site relative string. That makes sense, so ok.

But with webUrl actually being a server relative URL, that line will fail and throw an exception for any web whose parent site is not located in that the root of the server. It wont fail if the site is in the root of the server, because then a site relative and server relative web URL are the same.


Search-based alerts are completely broken for any web not being a subweb of the root site collection. They will inevitably fail with a "There is no web named ..." in the ULS log and no alerts will ever be sent. This bug exists in at least both SP1 and SP2 of MOSS 2007.

The lesson is of course that it is extremely important to always ensure if you are passing an absolute or a server, site or web relative URL, and make sure that matches what is expected!

By the way, Microsoft are aware of this issue, and they where already aware of it when I contacted them about it a couple of months ago. Appearantly, having all features in working order is not a high priority. :-(

PS. I mixed up site and server quite a few times myself just when writing this entry. I hope I got it right now though... :-)

4. URLs can change, so use other identifiers if possible

Web URLs are not static. For example, they can be changed by administrators. If possible, try not to handle any URL as something constant! That is, avoid specifying URLs in permanent configuration files, web part properties, etc. Whenever possible, use GUIDs or other static identifiers.

If you have the option to chose between for example a SiteRelative or a WebRelative url, use the one which is most general for your purpose. For specifying a list in the same web, for example, the web relative URL would be best, site relative URL second best, and so on. (Again, concider using other identifiers before resorting to URLs.)

Even worse of course is to use "Title" or "DisplayName" as identifiers, as they are even more likely to be changed in the future than the URL. Also, unlike any other identifier, they are never guaranteed to be unique!

5. Use SPUtility, SPUrlUtility, HttpUtility if you can

Don't re-invent the wheel! SPUtility, SPUrlUtility and HttpUtility contains several methods for common operations with URLs, such as concatenating and splitting a URL string. Feel free to check them out in Reflector to see what they do if it's a least bit unclear.

torsdag 3 september 2009

A random WSS 3.0 SP2 issue

I found this by accident:


"Document library event handlers that use object model code without explicit impersonation fail with the "Cannot complete this action" error message after Windows SharePoint Services Service Pack 2 is installed"

"All document library event handlers must perform explicit impersonation to use Windows SharePoint Services object model calls."

"This behavior is by design."

I'll just leave it at that...

torsdag 27 augusti 2009

A little bit about SPListItemCollection...

SPList list = SPContext.Current.Web.Lists["MyList"];
SPListItem item = list.Items.GetItemById(7);
SPList list = SPContext.Current.Web.Lists["MyList"];
SPListItem item = list.GetItemById(7);

Using the "Items" field of an SPList will always create an SPListItemCollection and populate it with all items from the database, which can take a long time if the list contains many items.
Calling GetItemById() directly on the SPList does not require this and can finish in a fraction of the time, even for a list with thousands of items.
To go further into this...
If performance is important (or if you are working with lists with many items), you should (or even must) avoid the "Items" field of SPList completely. Instead use SPQuery and SPList.GetItems(SPQuery) to create your own SPListItemCollection, and limit its size by setting these fields on the SPQuery:
- "ViewFields" - specify only the fields you need.
- Row Limit - set the maximum number of items you need
- Query - of course, to make the collection only contain items which you need.

For example,

SPListItem item = list.Items.Add(...);
SPListItem item = list.GetItems(new SPQuery { RowLimit = 0 }).Add(...);
Another example,

itemCount = list.Items.Count;
itemCount = list.GetItems(new SPQquery { ViewFields = "" }).Count;

Much of this is usually not required of course, but for lists which contains hundreds of items the difference will likely be noticable.

By the way, many of the OM methods use SPQuerys "behind the curtain". For example, SPList.GetItemById() creates an SPQuery with RowLimit = 1 and a "where eq" query searching for the specified ID, and the constructor of SPListItemCollection (which for some reason is marked internal) simply just takes an SPList and an SPQuery as parameters.

Hope this is of use to anyone!