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:

ObjectDefault AuthorDefault EditorDefault CreatedDefault Modified
📄 FileCurrent userCurrent userNowNow
📁 FolderCurrent userCurrent userNowNow

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:

  1. Upload first, then modify metadata — SharePoint only accepts Author and Editor updates once the object already exists.
  2. Use ListItemAllFields.Update() or UpdateOverwriteVersion() — this bypasses version incrementing.
  3. Use EnsureUser() on the target context to map users correctly.
  4. For folders, explicitly load their ListItemAllFields and set metadata after creation.
  5. 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:

ObjectCreated ByModified ByCreatedModified
Folder Aoriginal useroriginal userpreservedpreserved
Document.docxoriginal useroriginal userpreservedpreserved

🚫 Common Pitfalls

IssueCauseFix
All files show current user as authorSetting Author before uploadAlways upload first, then update
Folders still have your nameMetadata not updated after creationCall CopyFolderMetadata() explicitly
Missing versionsOnly current file copiedLoop through file.Versions
Errors on EnsureUser()User not found in target siteEnsure both sites share the same user directory or tenant

💡 Best Practices

  1. Always load only the fields you need (Load(..., f => f.Author, f => f.Editor)).
  2. Use Update() only when you want to create a new version; otherwise, prefer UpdateOverwriteVersion().
  3. Execute queries in small batches to avoid throttling.
  4. For large migrations, implement progress logging (e.g., via a JobState object or console output).
  5. Consider retry logic for transient CSOM errors (429, 503).

🧾 Summary

TaskMethodResult
Copy file versionsMigrateFileWithVersions()Preserves all version history
Copy folder metadataCopyFolderMetadata()Preserves Author, Editor, Created, Modified
Guarantee user mappingEnsureUser()Links users from source to target
Prevent overwriting metadataUpdateOverwriteVersion()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.

Edvaldo Guimrães Filho Avatar

Published by