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
| Feature | Description |
|---|---|
| Check-in comments | Copies the comment entered during each check-in for traceability. |
| Major version publishing | Ensures the latest file is visible to readers by publishing it after migration. |
| CSV logging | Creates 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
| Enhancement | Description |
|---|---|
| Check-In Comments | Every historical version retains its original comment, visible in SharePoint version history. |
| Publish Final Version | The newest upload is automatically published as a major version (optional). |
| CSV Log | Each migrated file and version is recorded with timestamp, author, and status for auditability. |
| Error Logging | Errors 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 Enhancement | Purpose |
|---|---|
| Parallel migration | Use Task.Run() with throttling to increase performance on large libraries. |
| PnP.Core SDK integration | Replace CSOM with modern, async SDK for scalability and modern auth. |
| Progress tracking UI | Integrate with WPF or MAUI front-end to display migration progress in real time. |
| Incremental mode | Skip already migrated files by comparing Modified timestamps. |
| Custom CSV path | Parameterize 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:
- Copy all versions of
DesignPlan.pdf - Preserve authors, timestamps, and check-in comments
- Publish the latest version
- Log all actions to
MigrationLog.csv
7️⃣ Key Takeaways
| Capability | Supported |
|---|---|
| Folder structure recreation | ✅ |
| Version history migration | ✅ |
| Check-in comments | ✅ |
| Major version publishing | ✅ |
| Author/Editor preservation | ✅ |
| Timestamps preserved | ✅ |
| CSV audit log | ✅ |
8️⃣ Microsoft Learn References
- Check In and Publish Files using CSOM
- FileVersion Class (CSOM)
- PnP Core SDK Overview
- SharePoint Online Throttling Guidance
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.
