In my ASP.NET MVC application, I am trying to retrieve all items in a list with the version history, and then cast them to a custom object. To do this, I am using Microsoft.SharePoint.
I was initially doing this the following way:
Util.GetSPItemCollectionWithHistory method:
public static SPListItemCollection GetSPItemCollectionWithHistory(string listName, SPQuery filterQuery)
{
using (SPSite spSite = new SPSite(sp_URL))
{
using (SPWeb spWeb = spSite.OpenWeb())
{
SPList itemsList = spWeb.GetList("/Lists/" + listName);
SPListItemCollection listItems = itemsList.GetItems(filterQuery);
return listItems;
}
}
}
GetSPObjectsWithHistory method:
protected static List<SPObjectWithHistory<T>> GetSPObjectsWithHistory(SPQuery query = null, List<string> filters = null)
{
List<SPObjectWithHistory<T>> resultsList = new List<SPObjectWithHistory<T>>();
Type objectType = typeof(T);
string listName = "";
query = query ?? Util.DEFAULT_SSOM_QUERY;
if (objectType == typeof(SPProject)) listName = Util.PROJECTS_LIST_NAME;
else if (objectType == typeof(SPTask)) listName = Util.TASKS_LIST_NAME;
else throw new Exception(String.Format("Could not find the list name for {0} objects.", objectType.Name));
SPListItemCollection results = Util.GetSPItemCollectionWithHistory(listName, query);
foreach (SPListItem item in results)
{
resultsList.Add(new SPObjectWithHistory<T>(item, filters));
}
return resultsList;
}
SPObjectWithHistory Class constructor:
public SPObjectWithHistory(SPListItem spItem, List<string> filters = null)
{
double.TryParse(spItem.Versions[0].VersionLabel, out _currentVersion);
History = new Dictionary<double, T>();
if (spItem.Versions.Count > 1)
{
for (int i = 1; i < spItem.Versions.Count; i++)
{
if (filters == null)
History.Add(double.Parse(spItem.Versions[i].VersionLabel), SPObject<T>.ConvertSPItemVersionObjectToSPObject(spItem.Versions[i]));
else
{
foreach (string filter in filters)
{
if (i == spItem.Versions.Count - 1 || (string)spItem.Versions[i][filter] != (string)spItem.Versions[i + 1][filter])
{
History.Add(double.Parse(spItem.Versions[i].VersionLabel), SPObject<T>.ConvertSPItemVersionObjectToSPObject(spItem.Versions[i]));
break;
}
}
}
}
}
}
This way the code works, but it is extremely slow on large lists. One of the lists has over 80000 items in it, and creating one SPObjectWithHistory item takes about 0.3 seconds, due to the logic in the constructor.
To speed up the process, I wanted to use Parallel.ForEach instead of a regular foreach.
My GetSPObjectsWithHistory was then updated to this:
protected static List<SPObjectWithHistory<T>> GetSPObjectsWithHistory(SPQuery query = null, List<string> filters = null)
{
ConcurrentBag<SPObjectWithHistory<T>> resultsList = new ConcurrentBag<SPObjectWithHistory<T>>();
Type objectType = typeof(T);
string listName = "";
query = query ?? Util.DEFAULT_SSOM_QUERY;
if (objectType == typeof(SPProject)) listName = Util.PROJECTS_LIST_NAME;
else if (objectType == typeof(SPTask)) listName = Util.TASKS_LIST_NAME;
else throw new Exception(String.Format("Could not find the list name for {0} objects.", objectType.Name));
List<SPListItem> results = Util.GetSPItemCollectionWithHistory(listName, query).Cast<SPListItem>().ToList();
Parallel.ForEach(results, item => resultsList.Add(new SPObjectWithHistory<T>(item, filters)));
return resultsList.ToList();
}
When I now try to run the application, however, I receive the following exception at the Parallel.ForEach:
Message: One or more errors occurred.
Type: System.AggregateException
StackTrace:
at System.Threading.Tasks.Task.ThrowIfExceptional(Boolean includeTaskCanceledExceptions)
at System.Threading.Tasks.Task.Wait(Int32 millisecondsTimeout, CancellationToken cancellationToken)
at System.Threading.Tasks.Parallel.ForWorker[TLocal](Int32 fromInclusive, Int32 toExclusive, ParallelOptions parallelOptions, Action'1 body, Action'2 bodyWithState, Func'4 bodyWithLocal, Func'1 localInit, Action'1 localFinally)
at System.Threading.Tasks.Parallel.ForEachWorker[TSource,TLocal](IEnumerable'1 source, ParallelOptions parallelOptions, Action'1 body, Action'2 bodyWithState, Action'3 bodyWithStateAndIndex, Func'4 bodyWithStateAndLocal, Func'5 bodyWithEverything, Func'1 localInit, Action'1 localFinally)
at System.Threading.Tasks.Parallel.ForEach[TSource](IEnumerable'1 source, Action'1 body)
at GetSPObjectsWithHistory(SPQuery query, List`1 filters) in...
InnerException:
Message: Attempted to make calls on more than one thread in single threaded mode. (Exception from HRESULT: 0x80010102 (RPC_E_ATTEMPTED_MULTITHREAD))
Type: Microsoft.SharePoint.SPException
StackTrace:
at Microsoft.SharePoint.SPGlobal.HandleComException(COMException comEx)
at Microsoft.SharePoint.Library.SPRequest.SetVar(String bstrUrl, String bstrName, String bstrValue)
at Microsoft.SharePoint.SPListItemVersionCollection.EnsureVersionsData()
at Microsoft.SharePoint.SPListItemVersionCollection.get_Item(Int32 iIndex)
at line of
double.TryParse(spItem.Versions[0].VersionLabel, out _currentVersion);in theSPObjectWithHistoryconstructor.InnerException:
Message: Attempted to make calls on more than one thread in single threaded mode. (Exception from HRESULT: 0x80010102 (RPC_E_ATTEMPTED_MULTITHREAD))
Type: System.Runtime.InteropServices.COMException
StackTrace:
at Microsoft.SharePoint.Library.SPRequestInternalClass.SetVar(String bstrUrl, String bstrName, String bstrValue)
at Microsoft.SharePoint.Library.SPRequest.SetVar(String bstrUrl, String bstrName, String bstrValue)
Would there be anyone who knows how I could get my code to work?
Thanks in advance!
Apparently, what I was trying to do is not possible. The
Microsoft.SharePointnamespace's SP objects are not thread safe, like @JeroenMostert stated.Since lazy loading was not an option for me, I decided to split my logic into batches (using
System.Threading.Task), which each execute the code from my original post (with theSPQuery.Querychanging for every batch). After that, the results from myGetSPObjectsWithHistoryare merged into a single list.