Press "Enter" to skip to content

How to get list item field versions data in Office365 / SharePoint Online

The good old times I used to do farm (full trust) solutions for SharePoint on-premise and there was very easy way to get list item versions data and work with it. Nowadays the Office365 / SharePoint Online offers limited support for this through the CSOM/JSOM framework, but it is not possible to get data for custom list item field.

If the aim is JSOM and javascript solution there is a workaround solution called SPservices (http://spservices.codeplex.com) that wraps the html to get the versions history data, but I have not tested it.

If the case is getting the data through the CSOM then can by executed code from the client library to get limited versions information:

var id = new Guid(item["UniqueId"].ToString());
var file1 = clientContext.Web.GetFileById(id);
clientContext.Load(file1);
clientContext.Load(file1.Versions);
clientContext.ExecuteQuery();

var versions = file1.Versions;

The above code will provide data for the following list item fields (for every version): CheckInComment,Created,CreatedBy,ID,IsCurrentVersion,Length,Size,Url,VersionLabel

If the case is to get custom field or field that does not fall in the above list of fields then there is a way to get all versions data for particular SharePoint / Office365 list item field and it is by exposing the Lists.asmx web service.

I have created console program as example and can be cloned from https://github.com/OneBitSoftware/Core.ListItemFieldVersions.

Create console app and add reference to the Microsoft.SharePoint.Client.

Then add web reference to the “_vti_bin/Lists.asmx” web service. The bellow images demonstrate how this can be done in the console app.

Select your project file and from the context menu select Add->Service Reference…

then 

 

Finally the Web Reference should look like this in your solution browser:

Once Web Reference is setup authentication and web service wsdl methods handler can be created.

My VersionsHandler.cs looks like:

using System;
using System.Net;
using System.Security;
using System.Xml;
using Core.ListItemFieldVersions.ListsASMX;
using Microsoft.SharePoint.Client;

namespace Core.ListItemFieldVersions
{
    public class VersionsHandler
    {
        private const string ListsServiceUrl = "/_vti_bin/Lists.asmx";
        private Lists lists = null;
        public string TenantUrl{get;set;}
        public String User { get; set; }
        public String Password { get; set; }
        public string Domain { get; set; }
        public string MySiteHost { get; set; }
        private Lists _lists
        {
            get
            {
                if (lists == null)
                {
                    if (!String.IsNullOrEmpty(TenantUrl))
                    {
                        this.lists = new Lists();
                        lists.Url = TenantUrl + ListsServiceUrl;
                        lists.UseDefaultCredentials = false;
                        lists.CookieContainer = new CookieContainer();
                        lists.CookieContainer.Add(GetFedAuthCookie(CreateSharePointOnlineCredentials()));
                        return lists;
                    }
                    else if (this.User.Length > 0 && this.Password.Length > 0 && this.Domain.Length > 0 && this.MySiteHost.Length > 0)
                    {
                        this.lists = new Lists();
                        lists.Url = this.MySiteHost + ListsServiceUrl;
                        NetworkCredential credential = new NetworkCredential(this.User, this.Password, this.Domain);
                        CredentialCache credentialCache = new CredentialCache();
                        credentialCache.Add(new Uri(this.MySiteHost), "NTLM", credential);
                        lists.Credentials = credentialCache;
                        return lists;
                    }
                    else
                    {
                        throw new Exception("Please specify an authentication provider or specify domain credentials");
                    }
                }
                else
                {
                    return this.lists;
                }
            }
        }

        public XmlNode GetVersionCollection(string listId, string itemId, string fieldName)
        {
            return _lists.GetVersionCollection(listId, itemId, fieldName);
        }

        private SharePointOnlineCredentials CreateSharePointOnlineCredentials()
        {
            var spoPassword = new SecureString();
            foreach (char c in Password)
            {
                spoPassword.AppendChar(c);
            }
            return new SharePointOnlineCredentials(User, spoPassword);
        }

        private Cookie GetFedAuthCookie(SharePointOnlineCredentials credentials)
        {
            string authCookie = credentials.GetAuthenticationCookie(new Uri(this.TenantUrl));
            if (authCookie.Length > 0)
            {
                return new Cookie("SPOIDCRL", authCookie.TrimStart("SPOIDCRL=".ToCharArray()), String.Empty, new Uri(this.TenantUrl).Authority);
            }
            else
            {
                return null;
            }
        }
    }   
}

Once we have the handler class, the program can send authentication cookie and access the methods of the Lists.asmx. Username and password are required in order to do this and maybe this is one of the downsides. I have added my credentials in the app.config file in plain text, but in production the password should be at least encrypted.

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <configSections>
        <sectionGroup name="applicationSettings" 
type="System.Configuration.ApplicationSettingsGroup, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" > <section name="Core.ListItemFieldVersions.Properties.Settings"
type="System.Configuration.ClientSettingsSection, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
requirePermission="false" /> </sectionGroup> </configSections> <startup> <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" /> </startup> <applicationSettings> <Core.ListItemFieldVersions.Properties.Settings> <setting name="Core_ListItemFieldVersions_ListsASMX_Lists" serializeAs="String"> <value>https://yoursite.sharepoint.com/_vti_bin/Lists.asmx</value>
</setting> </Core.ListItemFieldVersions.Properties.Settings> </applicationSettings> <appSettings> <add key="TenantUrl" value="https://yoursite.sharepoint.com/sites/rootsite" />
<add key="AdminUser" value="yoursite@yourcompany.onmicrosoft.com" /> <add key="Password" value="your_password" /> </appSettings> </configuration>

Now get to the goal... getting all the versions of the "Title" field of a list item. This is the Program.cs:

using System;
using System.Configuration;
using System.Security;
using System.Xml;
using Microsoft.SharePoint.Client;

namespace Core.ListItemFieldVersions
{
    class Program
    {
        private static Web _web;
        private static ClientContext _context;

        static void Main(string[] args)
        {
            _context = new ClientContext(ConfigurationManager.AppSettings["TenantUrl"]);
            string userName = ConfigurationManager.AppSettings["AdminUser"];
            var passWord = new SecureString();
            foreach (char c in ConfigurationManager.AppSettings["Password"].ToCharArray())
            {
                passWord.AppendChar(c);
            }
            _context.Credentials = new SharePointOnlineCredentials(userName, passWord);
            _web = _context.Web;
            var list = _context.Web.Lists.GetByTitle("Retention Rules");
            var query = new CamlQuery();
            query.ViewXml =
              @"<View Scope='RecursiveAll'>  
                            <Query> 
                               <Where><Eq><FieldRef Name='FSObjType' /><Value Type='Integer'>0</Value></Eq></Where> 
                            </Query> 
                            <RowLimit>5000</RowLimit> 
                      </View>";
            var listItems = list.GetItems(query);
            _context.Load(_web);
            _context.Load(list);
            _context.Load(listItems);
            _context.ExecuteQuery();

            var versionsHandler = new VersionsHandler();
            versionsHandler.User = userName;
            versionsHandler.Password = ConfigurationManager.AppSettings["Password"];
            versionsHandler.TenantUrl = ConfigurationManager.AppSettings["TenantUrl"];
            
            if(listItems.Count == 0) throw new ArgumentException("No list items");

            var listId = list.Id.ToString();
            var itemId = listItems[0].Id.ToString();

            var versionNodes = versionsHandler.GetVersionCollection(listId, itemId, "Title");
            foreach (XmlNode node in versionNodes.ChildNodes)
            {
                if (node.Attributes != null)
                {
                    var title = node.Attributes["Title"].Value;
                    var modified = node.Attributes["Modified"].Value;

                    Console.WriteLine("Modified: {0}, Title: {1}", modified, title);
                }
            }
            Console.WriteLine("Hit any key to end.");
            Console.ReadKey();
        }
    }
}

The interesting moment is where we invoke the handler that would invoke the web service and retrieve data in xml format:

var versionNodes = versionsHandler.GetVersionCollection(listId, itemId, "Title");

This is how you can get version history data for a list item field. It is also useful when you have "AppenOnly" note field and you have to get all the comments.
Another downside is that it is very slow if you have big set of fields or big set of list items, but at least is a way to get the data.