Migrating document libraries between SharePoint sites is a common scenario during restructuring, modernization, or automation. However, one of the biggest challenges is preserving original metadata — specifically:
- Author (Created By)
- Editor (Modified By)
- Created
- Modified
By default, when uploading files or creating folders through CSOM, SharePoint automatically assigns the current authenticated user as both Author and Editor.
📘 Preserving File and Folder Metadata During SharePoint Library Migration (CSOM Approach)
Migrating document libraries between SharePoint sites is a common scenario during restructuring, modernization, or automation. However, one of the biggest challenges is preserving original metadata — specifically:
- Author (Created By)
- Editor (Modified By)
- Created
- Modified
By default, when uploading files or creating folders through CSOM, SharePoint automatically assigns the current authenticated user as both Author and Editor.
This behavior breaks data lineage, historical accuracy, and audit integrity.
This article explains how to fully preserve original metadata for both files and folders, including version history, using pure CSOM (Client-Side Object Model), without relying on PnP Framework or third-party tools.
🧩 Problem Overview
When you upload files or create folders programmatically, SharePoint automatically does the following:
| Object | Default Author | Default Editor | Default Created | Default Modified |
|---|---|---|---|---|
| 📄 File | Current user | Current user | Now | Now |
| 📁 Folder | Current user | Current user | Now | Now |
If you try to set Author or Editor directly during upload, you’ll get an error:
“The property or field ‘Author’ has not been initialized.”
or the update will silently fail, because SharePoint doesn’t allow overriding system fields during a normal add operation.
🧠 Core Principle
To successfully preserve metadata:
- Upload first, then modify metadata — SharePoint only accepts
AuthorandEditorupdates once the object already exists. - Use
ListItemAllFields.Update()orUpdateOverwriteVersion()— this bypasses version incrementing. - Use
EnsureUser()on the target context to map users correctly. - For folders, explicitly load their
ListItemAllFieldsand set metadata after creation. - For files with versions, recreate each version manually, applying the correct user and timestamps.
⚙️ Copying Files and Versions with Metadata
Below is a simplified version of a method that preserves file versions, author, editor, and dates across libraries:
public static void MigrateFileWithVersions(ClientContext sourceCtx, ClientContext targetCtx,
string sourceFileUrl, string targetFolderUrl)
{
var file = sourceCtx.Web.GetFileByServerRelativeUrl(sourceFileUrl);
sourceCtx.Load(file, f => f.Name, f => f.TimeLastModified, f => f.Author, f => f.Versions);
sourceCtx.ExecuteQuery();
// Copy historical versions first
foreach (FileVersion version in file.Versions)
{
sourceCtx.Load(version, v => v.Created, v => v.CreatedBy, v => v.CheckInComment);
var stream = version.OpenBinaryStream();
sourceCtx.ExecuteQuery();
byte[] bytes;
using (var ms = new MemoryStream())
{
stream.Value.CopyTo(ms);
bytes = ms.ToArray();
}
var folder = targetCtx.Web.GetFolderByServerRelativeUrl(targetFolderUrl);
targetCtx.Load(folder);
targetCtx.ExecuteQuery();
var user = targetCtx.Web.EnsureUser(version.CreatedBy.LoginName);
targetCtx.Load(user);
targetCtx.ExecuteQuery();
var newFile = new FileCreationInformation { Content = bytes, Url = file.Name, Overwrite = true };
var uploaded = folder.Files.Add(newFile);
targetCtx.Load(uploaded, f => f.ListItemAllFields);
targetCtx.ExecuteQuery();
uploaded.ListItemAllFields["Created"] = version.Created;
uploaded.ListItemAllFields["Modified"] = version.Created;
uploaded.ListItemAllFields["Author"] = user;
uploaded.ListItemAllFields["Editor"] = user;
uploaded.ListItemAllFields.Update();
targetCtx.ExecuteQuery();
}
// Copy current version
var currentStream = file.OpenBinaryStream();
sourceCtx.ExecuteQuery();
byte[] currentBytes;
using (var ms = new MemoryStream())
{
currentStream.Value.CopyTo(ms);
currentBytes = ms.ToArray();
}
var targetFolder = targetCtx.Web.GetFolderByServerRelativeUrl(targetFolderUrl);
targetCtx.Load(targetFolder);
targetCtx.ExecuteQuery();
var currentFile = new FileCreationInformation { Content = currentBytes, Url = file.Name, Overwrite = true };
var uploadedCurrent = targetFolder.Files.Add(currentFile);
targetCtx.Load(uploadedCurrent, f => f.ListItemAllFields);
targetCtx.ExecuteQuery();
var currentUser = targetCtx.Web.EnsureUser(file.Author.LoginName);
targetCtx.Load(currentUser);
targetCtx.ExecuteQuery();
uploadedCurrent.ListItemAllFields["Created"] = file.TimeLastModified;
uploadedCurrent.ListItemAllFields["Modified"] = file.TimeLastModified;
uploadedCurrent.ListItemAllFields["Author"] = currentUser;
uploadedCurrent.ListItemAllFields["Editor"] = currentUser;
uploadedCurrent.ListItemAllFields.Update();
targetCtx.ExecuteQuery();
}
✅ Key points:
- Every version is recreated in sequence.
EnsureUser()guarantees that each author/editor exists in the target site.- Metadata fields are set after upload using
ListItemAllFields.Update().
📁 Preserving Folder Metadata
Folders require a separate approach.
SharePoint treats folders as items with ListItemAllFields, but without versions.
You must explicitly copy their metadata after creating or retrieving them.
Example helper:
private void CopyFolderMetadata(ClientContext srcCtx, ClientContext tgtCtx, string sourceFolderUrl, string targetFolderUrl)
{
var srcFolder = srcCtx.Web.GetFolderByServerRelativeUrl(sourceFolderUrl);
srcCtx.Load(srcFolder, f => f.ListItemAllFields);
srcCtx.ExecuteQuery();
var item = srcFolder.ListItemAllFields;
var tgtFolder = tgtCtx.Web.GetFolderByServerRelativeUrl(targetFolderUrl);
tgtCtx.Load(tgtFolder, f => f.ListItemAllFields);
tgtCtx.ExecuteQuery();
FieldUserValue author = item["Author"] as FieldUserValue;
FieldUserValue editor = item["Editor"] as FieldUserValue;
if (author != null)
{
var srcUser = srcCtx.Web.GetUserById(author.LookupId);
srcCtx.Load(srcUser, u => u.LoginName);
srcCtx.ExecuteQuery();
var ensuredAuthor = tgtCtx.Web.EnsureUser(srcUser.LoginName);
tgtCtx.Load(ensuredAuthor);
tgtCtx.ExecuteQuery();
tgtFolder.ListItemAllFields["Author"] = ensuredAuthor;
}
if (editor != null)
{
var srcUser = srcCtx.Web.GetUserById(editor.LookupId);
srcCtx.Load(srcUser, u => u.LoginName);
srcCtx.ExecuteQuery();
var ensuredEditor = tgtCtx.Web.EnsureUser(srcUser.LoginName);
tgtCtx.Load(ensuredEditor);
tgtCtx.ExecuteQuery();
tgtFolder.ListItemAllFields["Editor"] = ensuredEditor;
}
if (item.FieldValues.ContainsKey("Created"))
tgtFolder.ListItemAllFields["Created"] = item["Created"];
if (item.FieldValues.ContainsKey("Modified"))
tgtFolder.ListItemAllFields["Modified"] = item["Modified"];
tgtFolder.ListItemAllFields.Update();
tgtCtx.ExecuteQuery();
}
Call this method immediately after creating or ensuring the folder exists.
It will synchronize all metadata fields.
🧪 Testing and Validation
After migration, validate results by enabling the “Modified By” and “Created By” columns in the target library view.
Expected results:
| Object | Created By | Modified By | Created | Modified |
|---|---|---|---|---|
| Folder A | original user | original user | preserved | preserved |
| Document.docx | original user | original user | preserved | preserved |
🚫 Common Pitfalls
| Issue | Cause | Fix |
|---|---|---|
| All files show current user as author | Setting Author before upload | Always upload first, then update |
| Folders still have your name | Metadata not updated after creation | Call CopyFolderMetadata() explicitly |
| Missing versions | Only current file copied | Loop through file.Versions |
Errors on EnsureUser() | User not found in target site | Ensure both sites share the same user directory or tenant |
💡 Best Practices
- Always load only the fields you need (
Load(..., f => f.Author, f => f.Editor)). - Use
Update()only when you want to create a new version; otherwise, preferUpdateOverwriteVersion(). - Execute queries in small batches to avoid throttling.
- For large migrations, implement progress logging (e.g., via a
JobStateobject or console output). - Consider retry logic for transient CSOM errors (429, 503).
🧾 Summary
| Task | Method | Result |
|---|---|---|
| Copy file versions | MigrateFileWithVersions() | Preserves all version history |
| Copy folder metadata | CopyFolderMetadata() | Preserves Author, Editor, Created, Modified |
| Guarantee user mapping | EnsureUser() | Links users from source to target |
| Prevent overwriting metadata | UpdateOverwriteVersion() | Writes without creating new versions |
🧰 Tools and Environment
- Microsoft.SharePoint.Client.dll
- Microsoft.SharePoint.Client.Runtime.dll
- Authentication: Interactive (MSAL) or App-Only
- Works in: .NET Framework, .NET 6+, WPF, Console, Azure Functions (with adjustments)
🏁 Conclusion
Preserving Author, Editor, and timestamps during migration is crucial for maintaining historical accuracy and audit traceability in SharePoint environments.
By carefully controlling when and how metadata is applied — uploading first, then updating system fields through ListItemAllFields — it’s possible to clone libraries, files, and folders with 100% fidelity to the original state.
