Programmatically working with document libraries, a common issue I've come across is how to explicitly set the value of the Modified by and Created by fields displayed in the SharePoint UI. Being able to do so is useful during (lightweight) migration of documents or when creating minor versions from any application not impersonating the actual SharePoint user. In those cases, we might wish to override the default behavior where Modified by and Created by are set to the credentials under which CSOM is executing and the Modified and Created timestamps are set to the current date and time.
To run the demo code, first locate a document library in SharePoint with support for minor versions enabled and upload any document to it. What we're after is the ID of the item rather than creating the item in code. Then adjust the sample code accordingly by replacing <tokens> with actual values.
Next I define metadata describing each minor version. In this case it's hardcoded, but it might as well have come from another system.
class VersionMetadata { public double Version { get; set; } public string CreatedBy { get; set; } public DateTime CreatedAt { get; set; } public string ModifiedBy { get; set; } public DateTime ModifiedAt { get; set; } public string Content { get; set; } } class SettingVersionMetadata { Uri _site = new Uri("https://<tenant>.sharepoint.com/<site>"); string _user = "<user>@<tenant>.onmicrosoft.com"; string _password = "<password>"; string _documentLibrary = "<library>"; int _documentId = <id>; List<VersionMetadata> _versions = new List<VersionMetadata> { new VersionMetadata { Version = 0.2, CreatedBy = "<user1>", CreatedAt = DateTime.Now.AddDays(-5), ModifiedBy = "<user2>", ModifiedAt = DateTime.Now.AddDays(-4), Content = "0.2" }, new VersionMetadata { Version = 0.3, CreatedBy = "<user3>", CreatedAt = DateTime.Now.AddDays(-3), ModifiedBy = "<user4>", ModifiedAt = DateTime.Now.AddDays(-2), Content = "0.3" } }; // subsequent code in post goes here }
During testing, I find it helpful to recreate the ClientContext between SharePoint interactions. It makes the code more explicit and prevents any residual state being left over from earlier calls, affecting later calls positively or negatively.
void RunCodeWithNewClientContext(Action<ClientContext, ListItem> callback) { var securePassword = new SecureString(); _password.ToCharArray().ToList().ForEach(securePassword.AppendChar); var credentials = new SharePointOnlineCredentials(_user, securePassword); using (var ctx = new ClientContext(_site) { Credentials = credentials }) { var web = ctx.Web; var list = ctx.Web.Lists.GetByTitle(_documentLibrary); var item = list.GetItemById(_documentId); ctx.Load(item, f => f.File); ctx.ExecuteQuery(); callback(ctx, item); } }
Now, using RunCodeWithNewClientContext, passing in lambda expressions to execute, I first checkout the list item, then update its content and checks it back in, causing a new minor version to be created.
The last part, updating Modified by, Created by and the corresponding timestamp fields, doesn't create a new minor version. It operates on the most recent version which doesn't have to be checked out in advance for the update to work. The update will cause the current minor version to be checked in, though. If I'd checked out the item myself, I must also check it back in before modifying the modifier and author fields or the update is lost.
void ProcessMinorVersion(VersionMetadata vm) { // check out item RunCodeWithNewClientContext((ctx, item) => { if (item.File.CheckOutType == CheckOutType.None) item.File.CheckOut(); ctx.ExecuteQuery(); }); // modify file contents RunCodeWithNewClientContext((ctx, item) => { var bytes = new byte[vm.Content.Length * sizeof(char)]; Buffer.BlockCopy(vm.Content.ToCharArray(), 0, bytes, 0, bytes.Length); item.File.SaveBinary(new FileSaveBinaryInformation { Content = bytes }); ctx.ExecuteQuery(); }); // check in item RunCodeWithNewClientContext((ctx, item) => { item.File.CheckIn("Checked in by tool", CheckinType.MinorCheckIn); ctx.ExecuteQuery(); }); // modify latest minor version RunCodeWithNewClientContext((ctx, item) => { const string template = "i:0#.f|membership|{0}@<tenant>.onmicrosoft.com"; var modifiedBy = ctx.Web.EnsureUser(string.Format(template, vm.ModifiedBy)); var createdBy = ctx.Web.EnsureUser(string.Format(template, vm.CreatedBy)); ctx.Load(modifiedBy); ctx.Load(createdBy); ctx.ExecuteQuery(); item["Editor"] = modifiedBy.Id; var modifiedByField = new ListItemFormUpdateValue { FieldName = "Modified_x0020_By", FieldValue = modifiedBy.Id.ToString() }; item["Author"] = createdBy.Id; var createdByField = new ListItemFormUpdateValue { FieldName = "Created_x0020_By", FieldValue = createdBy.Id.ToString() }; item["Modified"] = vm.ModifiedAt.ToUniversalTime(); item["Created"] = vm.CreatedAt.ToUniversalTime(); // it doesn't matter if you add both modifiedByField and createdByField. // As long as the list is non-empty all changes appear to carry over. var updatedValues = new List<ListItemFormUpdateValue> { modifiedByField, createdByField }; item.ValidateUpdateListItem(updatedValues, true, "Ignored on explicit checkin/checkout"); ctx.ExecuteQuery(); }); }
Finally, here's how to kick off the sample:
public void Run() { _versions .OrderBy(m => m.Version) .ToList() .ForEach(ProcessMinorVersion); }
Apparent from this sample is that updating any field on the item follows the traditional approach. But Modified by and Created by are special UI fields whose updated values doesn't automatically get propagated to the UI.