Migrating SharePoint Libraries with Full Metadata, Version History, Check-In Comments, and Audit Logs (v2)

Overview

In this enhanced version of the FileMigrationService, we extend the migration logic to:

  • Preserve check-in comments from each version
  • Optionally publish the latest version as a major version
  • Generate a CSV audit log for reporting and validation

This version provides an auditable, production-ready migration path for complex SharePoint Online libraries.


1️⃣ New Capabilities

FeatureDescription
Check-in commentsCopies the comment entered during each check-in for traceability.
Major version publishingEnsures the latest file is visible to readers by publishing it after migration.
CSV loggingCreates an external CSV report with filename, version date, author, and migration status.

2️⃣ Enhanced Implementation

using Microsoft.SharePoint.Client;
using System;
using System.IO;
using System.Text;

namespace Contoso.MigrationTool
{
    public static class FileMigrationServiceV2
    {
        private static readonly string LogPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "MigrationLog.csv");

        static FileMigrationServiceV2()
        {
            // Create CSV header if not exists
            if (!File.Exists(LogPath))
                File.AppendAllText(LogPath, "Timestamp,FileName,VersionDate,Author,Status,Comment,Error\n");
        }

        public static void MigrateFileWithVersions(ClientContext sourceCtx, ClientContext targetCtx,
            string sourceFileUrl, string targetFolderUrl, bool publishAfterMigration = true)
        {
            try
            {
                var file = sourceCtx.Web.GetFileByServerRelativeUrl(sourceFileUrl);
                sourceCtx.Load(file, f => f.Author, f => f.ModifiedBy, f => f.TimeCreated,
                    f => f.TimeLastModified, f => f.Versions);
                sourceCtx.ExecuteQuery();

                Console.WriteLine($"Migrating file: {file.Name}");

                foreach (FileVersion version in file.Versions)
                {
                    try
                    {
                        sourceCtx.Load(version, v => v.Created, v => v.CreatedBy, v => v.CheckInComment);
                        var streamResult = version.OpenBinaryStream();
                        sourceCtx.ExecuteQuery();

                        byte[] bytes;
                        using (var ms = new MemoryStream())
                        {
                            streamResult.Value.CopyTo(ms);
                            bytes = ms.ToArray();
                        }

                        var folder = targetCtx.Web.GetFolderByServerRelativeUrl(targetFolderUrl);
                        targetCtx.Load(folder);
                        targetCtx.ExecuteQuery();

                        FileCreationInformation info = new FileCreationInformation
                        {
                            Content = bytes,
                            Url = file.Name,
                            Overwrite = true
                        };

                        var uploaded = folder.Files.Add(info);
                        targetCtx.ExecuteQuery();

                        var user = targetCtx.Web.EnsureUser(version.CreatedBy.LoginName);
                        targetCtx.Load(user);
                        targetCtx.Load(uploaded, f => f.ListItemAllFields);
                        targetCtx.ExecuteQuery();

                        uploaded.ListItemAllFields["Author"] = user;
                        uploaded.ListItemAllFields["Editor"] = user;
                        uploaded.ListItemAllFields["Created"] = version.Created;
                        uploaded.ListItemAllFields["Modified"] = version.Created;
                        uploaded.ListItemAllFields.Update();
                        targetCtx.ExecuteQuery();

                        // Optional check-in with preserved comment
                        if (!string.IsNullOrEmpty(version.CheckInComment))
                        {
                            uploaded.CheckIn(version.CheckInComment, CheckinType.MinorCheckIn);
                            targetCtx.ExecuteQuery();
                        }

                        LogSuccess(file.Name, version.Created, user.LoginName, version.CheckInComment);
                        Console.WriteLine($"   Version migrated: {version.Created:yyyy-MM-dd HH:mm}");
                    }
                    catch (Exception ex)
                    {
                        LogError(file.Name, DateTime.Now, ex.Message);
                        Console.WriteLine($"   Skipped version: {ex.Message}");
                    }
                }

                // Copy latest 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 latestInfo = new FileCreationInformation
                {
                    Content = currentBytes,
                    Url = file.Name,
                    Overwrite = true
                };

                var latest = targetFolder.Files.Add(latestInfo);
                targetCtx.ExecuteQuery();

                var author = targetCtx.Web.EnsureUser(file.Author.LoginName);
                var editor = targetCtx.Web.EnsureUser(file.ModifiedBy.LoginName);
                targetCtx.Load(author);
                targetCtx.Load(editor);
                targetCtx.Load(latest, f => f.ListItemAllFields);
                targetCtx.ExecuteQuery();

                latest.ListItemAllFields["Author"] = author;
                latest.ListItemAllFields["Editor"] = editor;
                latest.ListItemAllFields["Created"] = file.TimeCreated;
                latest.ListItemAllFields["Modified"] = file.TimeLastModified;
                latest.ListItemAllFields.Update();
                targetCtx.ExecuteQuery();

                if (publishAfterMigration)
                {
                    latest.CheckIn("Migrated file finalized", CheckinType.MajorCheckIn);
                    latest.Publish("Final major version after migration");
                    targetCtx.ExecuteQuery();
                }

                LogSuccess(file.Name, file.TimeLastModified, author.LoginName, "Current version");
                Console.WriteLine("   Current version migrated and published.");
            }
            catch (Exception ex)
            {
                LogError(sourceFileUrl, DateTime.Now, ex.Message);
                Console.WriteLine($"Error migrating file {sourceFileUrl}: {ex.Message}");
            }
        }

        private static void LogSuccess(string fileName, DateTime versionDate, string author, string comment)
        {
            string line = $"{DateTime.Now:yyyy-MM-dd HH:mm},{fileName},{versionDate:yyyy-MM-dd HH:mm},{author},Success,{comment},\n";
            File.AppendAllText(LogPath, line, Encoding.UTF8);
        }

        private static void LogError(string fileName, DateTime versionDate, string error)
        {
            string line = $"{DateTime.Now:yyyy-MM-dd HH:mm},{fileName},{versionDate:yyyy-MM-dd HH:mm},,Error,,{error}\n";
            File.AppendAllText(LogPath, line, Encoding.UTF8);
        }
    }
}


3️⃣ How It Improves Version 1

EnhancementDescription
Check-In CommentsEvery historical version retains its original comment, visible in SharePoint version history.
Publish Final VersionThe newest upload is automatically published as a major version (optional).
CSV LogEach migrated file and version is recorded with timestamp, author, and status for auditability.
Error LoggingErrors are appended to the same log with detailed messages.

4️⃣ CSV Output Example

Timestamp,FileName,VersionDate,Author,Status,Comment,Error
2025-10-14 11:42,DesignPlan.pdf,2024-06-18 10:11,user@contoso.com,Success,"Initial draft",
2025-10-14 11:42,DesignPlan.pdf,2024-09-05 14:22,user@contoso.com,Success,"Final check-in before release",
2025-10-14 11:42,DesignPlan.pdf,2024-09-05 14:22,user@contoso.com,Success,Current version,


5️⃣ Recommended Extensions

Future EnhancementPurpose
Parallel migrationUse Task.Run() with throttling to increase performance on large libraries.
PnP.Core SDK integrationReplace CSOM with modern, async SDK for scalability and modern auth.
Progress tracking UIIntegrate with WPF or MAUI front-end to display migration progress in real time.
Incremental modeSkip already migrated files by comparing Modified timestamps.
Custom CSV pathParameterize the CSV path per migration job or subsite.

6️⃣ Example Invocation

FileMigrationServiceV2.MigrateFileWithVersions(
    sourceCtx,
    targetCtx,
    "/sites/Engineering/Shared Documents/DesignPlan.pdf",
    "/sites/Archive/Shared Documents/2024",
    publishAfterMigration: true
);

This will:

  1. Copy all versions of DesignPlan.pdf
  2. Preserve authors, timestamps, and check-in comments
  3. Publish the latest version
  4. Log all actions to MigrationLog.csv

7️⃣ Key Takeaways

CapabilitySupported
Folder structure recreation
Version history migration
Check-in comments
Major version publishing
Author/Editor preservation
Timestamps preserved
CSV audit log

8️⃣ Microsoft Learn References


Conclusion

With these improvements, the migration tool now provides full-fidelity document transfer for SharePoint Online, preserving every detail of the file’s lifecycle—from author to version history to final publication.

It’s a ready foundation for enterprise migrations requiring auditable, compliant, and metadata-intact document transitions.

Edvaldo Guimrães Filho Avatar

Published by