3

My Situation

Hi, my client has a Sharepoint Server 2013 on Premise. He wants to write metadata on multiple files. More specifically he wants to choose a folder and then the application should write the same value in a column for every file/folder in that folder.

What I already did

I wrote a CAML-query that returns all files/folder below the chosen folder. I then write the value for each item and update it.

What I want to do

I want to write an application where my client can choose a folder. After that the software should update all files under that folder. I want to write this application using C# and CSOM.

My Problem

This is very slow. With multiple thousands of files per folder it takes hours just for one folder to complete.

My Questions

Is there a way to speed things up?
Can CAML-Query also update values?
What is the recommended way of doing what I want to do?
Is there another way of achieving what my client wants?

Thanks for your help.

Fidel
  • 7,027
  • 11
  • 57
  • 81
Simon Balling
  • 481
  • 1
  • 5
  • 14

1 Answers1

3

You could take advantage of feature called Request Batching, which in turn could dramatically affect the performance by minimizing the number of messages that are passed between the client and the server.

From the documentation:

The CSOM programming model is built around request batching. When you work with the CSOM, you can perform a series of data operations on the ClientContext object. These operations are submitted to the server in a single request when you call the ClientContext.BeginExecuteQuery method.

For comparison lets demonstrate two ways the updating of multiple list items:

Standard way:

foreach (var item in items)
{
   item["LastReviewed"] = DateTime.Now;
   item.Update();
   ctx.ExecuteQuery();
}

Batched update:

foreach (var item in items)
{
   item["LastReviewed"] = DateTime.Now;
   item.Update();
}
ctx.ExecuteQuery();

Note: in the second approach only a single batched request is submitted to the server

Complete example

var listTitle = "Documents";
var folderUrl = "Archive/2013";

using (var ctx = GetContext(url, userName, password))
{
     //1. Get list items operation
     var list = ctx.Web.Lists.GetByTitle(listTitle);
     var items = list.GetListItems(folderUrl);
     ctx.Load(items);
     ctx.ExecuteQuery();


     //2. Update list items operation
     var watch = Stopwatch.StartNew();
     foreach (var item in items)
     {
           if(Convert.ToInt32(item["FSObjType"]) == 1) //skip folders
               continue;

           item["LastReviewed"] = DateTime.Now;
           item.Update();
           //ctx.ExecuteQuery();
           Console.WriteLine("{0} has been updated", item["FileRef"]);
     }
     ctx.ExecuteQuery();  //execute batched request
     watch.Stop();
     Console.WriteLine("Update operation completed: {0}", watch.ElapsedMilliseconds); 

     Console.ReadLine();
}

where GetListItems is the extension method for getting list items located under a specific folder:

using System.Linq;
using Microsoft.SharePoint.Client;

namespace SharePoint.Client.Extensions
{
    public static class ListCollectionExtensions
    {

        /// <summary>
        /// Get list items located under specific folder 
        /// </summary>
        /// <param name="list"></param>
        /// <param name="relativeFolderUrl"></param>
        /// <returns></returns>
        public static ListItemCollection GetListItems(this List list, string relativeFolderUrl)
        {
            var ctx = list.Context;
            if (!list.IsPropertyAvailable("RootFolder"))
            {
                ctx.Load(list.RootFolder, f => f.ServerRelativeUrl);
                ctx.ExecuteQuery();
            }
            var folderUrl = list.RootFolder.ServerRelativeUrl + "/" + relativeFolderUrl;
            var qry = CamlQuery.CreateAllItemsQuery();
            qry.FolderServerRelativeUrl = folderUrl;
            var items = list.GetItems(qry);
            return items;
        }
    }
}
Vadim Gremyachev
  • 57,952
  • 20
  • 129
  • 193
  • 1
    Thank you for your help. I ran into 2 problems (maybe they are one) with your code. First I got an error stating that the request is too big and it should not be bigger then about 2Mb. I then build a counter that executes the query after X items. Which is definitely faster than my first appoach. is there a way to get the max amount of updates programmatically so i can get the best performance? the other error was similar but it statet that I use too many resources. – Simon Balling Jan 19 '16 at 14:20
  • 1
    Simon, that's indeed an expectable error when the request size is getting too big (2 Mb by default) and your approach (splitting batched requests) is correct. I will look into how to properly determine the current size of request and get back to you – Vadim Gremyachev Jan 19 '16 at 14:42
  • Cool, thanks. What exactly does the line "if (!list.IsPropertyAvailable("RootFolder"))" do? I want to also include the root folder in the update. How can I achieve this? – Simon Balling Jan 19 '16 at 14:56
  • This expression in human language means: let's check if `RootFolder` property of List object is loaded (`FolderServerRelativeUrl` property has the following format `/[web]/[list or library]/[folder]` and `list.RootFolder.ServerRelativeUrl` is intededed for getting list/library url ), if not then request it from the server – Vadim Gremyachev Jan 19 '16 at 15:03
  • Thanks. One more question: my library is calles "Archive" and my folder is "Group1". So my folderUrl would be "Archive/Group1/" .With the current code, i will update all items below "Group1".What I want to do is also updating the folder "Group1". How can i achieve this? – Simon Balling Jan 20 '16 at 08:49