Overview of Common Coding Issues with the SharePoint Object Model
As more developers write custom code by using the SharePoint Object Model, they encounter common issues that can affect application performance. This article attempts to address some of those issues, and recommends ways to identify and correct them.
The following areas reflect the main issues encountered by developers as they write custom code by using the SharePoint object model:
1/ Disposing of SharePoint objects
2/ Caching data and objects
3/ Writing code that is scalable
Caching Data and Objects
Many developers are starting to use the Microsoft .NET Framework caching objects (for example, System.Web.Caching.Cache) to help make better use of memory and increase overall system performance. But, many objects are not "thread safe" and caching those objects can lead to application crashes and unexpected or unrelated user errors.
Caching SharePoint Objects That Are Not Thread Safe
Developers are trying to increase performance and memory usage by caching SPListItemCollection objects that are returned from queries. In general, this is a good practice but the SPListItemCollection object contains an embedded SPWeb object that is not thread safe and should not be cached. For example, assume the SPListItemCollection object is cached in thread A. Then, as other threads try to read it, the application can fail or behave strangely because the object is not thread safe.
For more information, see the Microsoft.SharePoint.SPWeb class.
Not Using Thread Synchronization
Some developers are not aware that they are running in a multi-threaded environment (by default, Internet Information Services is multi-threaded) or how to manage that environment. The following code example shows how some developers are caching Microsoft.SharePoint.SPListItemCollection objects.
C#
public void CacheData()
{
SPListItemCollection oListItems;
oListItems = (SPListItemCollection)Cache["ListItemCacheName"];
if(oListItems == null)
{
oListItems = DoQueryToReturnItems();
Cache.Add("ListItemCacheName", oListItems, ..);
}
}
In the previous code example, the problem is that if the query to get the data takes 10 seconds, you could have many users hitting that page at the same time, all running the same query and trying to update the same cache object at the same time. This can cause performance issues because the same query might be running 10, 50, or 100 times and can cause crashes because multiple threads are trying to update the same object at the same time, especially on multi-process, hyper-threaded computers. To fix this, you must change the code as follows.
C#
public void CacheData()
{
SPListItemCollection oListItems;
lock(this) {
oListItems = (SPListItemCollection)Cache["ListItemCacheName"];
if(oListItems == null)
{
oListItems = DoQueryToReturnItems();
Cache.Add("ListItemCacheName", oListItems, ..);
}
}
}
Note:
It is possible to increase performance slightly by placing the lock inside the if(oListItems == null) code block. When you do this, you do not need to suspend all threads while checking to see if the data is already cached. Depending on how long it takes the query to return the data, there is still the possibility that more than one user might be running the query at the same time. This is especially true if you are running on multiprocessor computers. Remember that the more processors running and the longer the query takes to run, the more likely putting the lock in the if() code block will cause problems. While the previous example might cause a slight performance hit, it is the only way to ensure that you will not have multiple queries running at the same time.
This code suspends all other threads in a critical section running in Internet Information Services, and prevents other threads from accessing the cached object until it is completely built.
The previous example addresses the thread synchronization issue; however, it is still not correct because it is caching an object that is not thread safe. To address thread safety, you could cache a DataTable object that is created from the SPListItemCollection object. For example, you would modify the previous example as follows.
C#
public void CacheData()
{
DataTable oDataTable;
SPListItemCollection oListItems;
lock(this)
{
oDataTable = (DataTable)Cache["ListItemCacheName"];
if(oDataTable == null)
{
oListItems = DoQueryToReturnItems();
oDataTable = oListItems.GetDataTable();
Cache.Add("ListItemCacheName", oDataTable, ..);
}
}
}
Your code then gets the data from the DataTable object. For more information and examples of using the DataTable object, and other good ideas for developing SharePoint applications, see Tips and Tricks for Developing with Windows SharePoint Services.
Writing Code That Is Scalable
Some developers are not aware that they need to write their code to be scalable for handling multiple users at the same time. A good example of this is creating custom navigation information for all sites and subsites on each page or as part of a master page. For example, if you have a SharePoint site on a corporate intranet and each department has its own site with many subsites, your code might resemble the following.
C#
public void GetNavigationInfoForAllSitesAndWebs()
{
foreach(SPSite oSPSite in SPContext.Current.Site.WebApplication.Sites)
{
using(SPWeb oSPWeb = oSPSite.RootWeb)
{
AddAllWebs(oSPWeb );
}
}
}
C#
public void AddAllWebs(SPWeb oSPWeb)
{
foreach(SPWeb oSubWeb in oSPWeb.Webs)
{
//.. Code to add items ..
AddAllWebs(oSubWeb);
oSubWeb.Dispose();
}
}
While the previous code disposes of objects properly, it still causes problems because the code is going through the same lists over and over. For example, if you have 10 site collections and an average of 20 sites or subsites per site collection, you would iterate through the same code 200 times. For a small number of users this might not cause bad performance. But, as you add more users to the system, the problem gets worse. Table 2 shows this.
Table 2. Iterations increase as the number of users increase
Users Iterations
10 2000
50 10000
100 200000
250 500000
The code executes for each user that hits the system, but the data remains the same for everyone. The impact of this can vary depending on what the code is doing. In some cases, repeating code over and over might not cause a performance problem; however, in the previous example the system has to create a COM object (SPSite or SPWeb objects are created when retrieved from their collections), retrieve data from the object, and then dispose of it for each item in the collection. This creates a lot of performance overhead.
How can you make this code more scalable or fine-tuned for a multiple user environment? This can be a hard question to answer, and it depends on what the application is designed to do. There are a few things that you need to take into consideration when asking how to make code more scalable:
Is the data static (seldom changes), somewhat static (changes occasionally), or dynamic (constantly changing)?
Is the data the same for all users, or does it change? For example, does it change depending on the user who is logged on, the part of the site being accessed, or the time of year (seasonal information)?
Is the data easily accessible or does it require a long time to return the data? For example, is it returning from a long-running SQL query or from remote databases that can have some network latency in the data transfers?
Is the data public or does it require a higher level of security?
What is the size of the data?
Is the SharePoint site on a single server or on a server farm?
Depending on how you answer the previous questions, there are several different ways you can make your code more scalable and handle multiple users. The intent of this article is not to provide answers for all of the questions or scenarios but to provide a few ideas that you can apply to your specific needs. The following sections offer a few areas of consideration.
Caching Raw Data
You can cache your data by using the System.Web.Caching.Cache object. This object requires that you query the data one time and store it in the cache for access by other users.
If your data is static, you can set up the cache to load the data once and not expire until the application is restarted, or to load once a day to ensure data freshness. You can create the cache item when the application starts, when the first user session starts, or when the first user tries to access that data.
If your data is somewhat static, you can set up the cached items to expire within a certain number of seconds, minutes, or hours after it is created. This enables you to refresh your data within a timeframe that is acceptable to your users. Even if the data is cached for only 30 seconds, under heavy loads you will still see an increase of performance because you are running the code only once every 30 seconds instead of multiple times a second for every user hitting the system.
Be sure to take into consideration the issues outlined previously in Caching Data and Objects.
Building Data Before Displaying It
Think about how your cached data will be used. If this data is used to make run-time decisions, putting it into a DataSet or DataTable object might be the best way to store it. You can then query those objects for the data to make run-time decisions. If the data is being used to display a list, table, or formatted page to the user, consider building a display object and storing that object in the cache. At run time, you need only to retrieve the object from the cache and call its render function to display its contents. You could also store the rendered output, but this can lead to security issues and the cached item could be quite large, causing a lot of page swapping or memory fragmentation.
Caching for a Single Server or Server Farm
Depending on how your SharePoint site is set up, you might have to address some caching issues differently. If your data must be the same on all servers at all times, then you must ensure that the same data is cached on each server. One way to ensure this is to create the cached data and store it on a common server or in an SQL database. Again, you must consider how long it takes to access the data and any security issues of the data being stored on a common server.
You could also create business-layer objects that cache data on a common sever, and then access that data by different interprocess communications available in networking objects or APIs.
Conclusion
To ensure that your SharePoint system performs at its best, you need to be able to answer the following questions about the code you write:
Does my code properly dispose of SharePoint objects?
Does my code cache objects properly?
Does my code cache the correct types of objects?
Does my code use thread synchronization when necessary?
Does my code work as efficiently for 1000 users as it does for 10 users?
If you consider these issues when you write your code, you will find that your SharePoint system runs more efficiently and that your users have a much better experience. You can also help to prevent unexpected failures and errors in your system.