feat(04-01): add Phase 4 models, interfaces, BulkOperationRunner, and test scaffolds
- Install CsvHelper 33.1.0 and Microsoft.Graph 5.74.0 (main + test projects) - Add 14 core model/enum files (BulkOperationResult, BulkMemberRow, BulkSiteRow, TransferJob, FolderStructureRow, SiteTemplate, SiteTemplateOptions, TemplateLibraryInfo, TemplateFolderInfo, TemplatePermissionGroup, ConflictPolicy, TransferMode, CsvValidationRow) - Add 6 service interfaces (IFileTransferService, IBulkMemberService, IBulkSiteService, ITemplateService, IFolderStructureService, ICsvValidationService) - Add BulkOperationRunner with continue-on-error and cancellation support - Add BulkResultCsvExportService stub (compile-ready) - Add test scaffolds: BulkOperationRunnerTests (5 passing), BulkResultCsvExportServiceTests (2 passing), CsvValidationServiceTests (6 skipped), TemplateRepositoryTests (4 skipped)
This commit is contained in:
18
SharepointToolbox/Core/Models/BulkMemberRow.cs
Normal file
18
SharepointToolbox/Core/Models/BulkMemberRow.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using CsvHelper.Configuration.Attributes;
|
||||
|
||||
namespace SharepointToolbox.Core.Models;
|
||||
|
||||
public class BulkMemberRow
|
||||
{
|
||||
[Name("GroupName")]
|
||||
public string GroupName { get; set; } = string.Empty;
|
||||
|
||||
[Name("GroupUrl")]
|
||||
public string GroupUrl { get; set; } = string.Empty;
|
||||
|
||||
[Name("Email")]
|
||||
public string Email { get; set; } = string.Empty;
|
||||
|
||||
[Name("Role")]
|
||||
public string Role { get; set; } = string.Empty; // "Member" or "Owner"
|
||||
}
|
||||
35
SharepointToolbox/Core/Models/BulkOperationResult.cs
Normal file
35
SharepointToolbox/Core/Models/BulkOperationResult.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
namespace SharepointToolbox.Core.Models;
|
||||
|
||||
public class BulkItemResult<T>
|
||||
{
|
||||
public T Item { get; }
|
||||
public bool IsSuccess { get; }
|
||||
public string? ErrorMessage { get; }
|
||||
public DateTime Timestamp { get; }
|
||||
|
||||
private BulkItemResult(T item, bool success, string? error)
|
||||
{
|
||||
Item = item;
|
||||
IsSuccess = success;
|
||||
ErrorMessage = error;
|
||||
Timestamp = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public static BulkItemResult<T> Success(T item) => new(item, true, null);
|
||||
public static BulkItemResult<T> Failed(T item, string error) => new(item, false, error);
|
||||
}
|
||||
|
||||
public class BulkOperationSummary<T>
|
||||
{
|
||||
public IReadOnlyList<BulkItemResult<T>> Results { get; }
|
||||
public int TotalCount => Results.Count;
|
||||
public int SuccessCount => Results.Count(r => r.IsSuccess);
|
||||
public int FailedCount => Results.Count(r => !r.IsSuccess);
|
||||
public bool HasFailures => FailedCount > 0;
|
||||
public IReadOnlyList<BulkItemResult<T>> FailedItems => Results.Where(r => !r.IsSuccess).ToList();
|
||||
|
||||
public BulkOperationSummary(IReadOnlyList<BulkItemResult<T>> results)
|
||||
{
|
||||
Results = results;
|
||||
}
|
||||
}
|
||||
24
SharepointToolbox/Core/Models/BulkSiteRow.cs
Normal file
24
SharepointToolbox/Core/Models/BulkSiteRow.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using CsvHelper.Configuration.Attributes;
|
||||
|
||||
namespace SharepointToolbox.Core.Models;
|
||||
|
||||
public class BulkSiteRow
|
||||
{
|
||||
[Name("Name")]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
[Name("Alias")]
|
||||
public string Alias { get; set; } = string.Empty;
|
||||
|
||||
[Name("Type")]
|
||||
public string Type { get; set; } = string.Empty; // "Team" or "Communication"
|
||||
|
||||
[Name("Template")]
|
||||
public string Template { get; set; } = string.Empty;
|
||||
|
||||
[Name("Owners")]
|
||||
public string Owners { get; set; } = string.Empty; // comma-separated emails
|
||||
|
||||
[Name("Members")]
|
||||
public string Members { get; set; } = string.Empty; // comma-separated emails
|
||||
}
|
||||
8
SharepointToolbox/Core/Models/ConflictPolicy.cs
Normal file
8
SharepointToolbox/Core/Models/ConflictPolicy.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace SharepointToolbox.Core.Models;
|
||||
|
||||
public enum ConflictPolicy
|
||||
{
|
||||
Skip,
|
||||
Overwrite,
|
||||
Rename
|
||||
}
|
||||
25
SharepointToolbox/Core/Models/CsvValidationRow.cs
Normal file
25
SharepointToolbox/Core/Models/CsvValidationRow.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
namespace SharepointToolbox.Core.Models;
|
||||
|
||||
public class CsvValidationRow<T>
|
||||
{
|
||||
public T? Record { get; }
|
||||
public bool IsValid => Errors.Count == 0;
|
||||
public List<string> Errors { get; }
|
||||
public string? RawRecord { get; }
|
||||
|
||||
public CsvValidationRow(T record, List<string> errors)
|
||||
{
|
||||
Record = record;
|
||||
Errors = errors;
|
||||
}
|
||||
|
||||
private CsvValidationRow(string rawRecord, string parseError)
|
||||
{
|
||||
Record = default;
|
||||
RawRecord = rawRecord;
|
||||
Errors = new List<string> { parseError };
|
||||
}
|
||||
|
||||
public static CsvValidationRow<T> ParseError(string? rawRecord, string error)
|
||||
=> new(rawRecord ?? string.Empty, error);
|
||||
}
|
||||
28
SharepointToolbox/Core/Models/FolderStructureRow.cs
Normal file
28
SharepointToolbox/Core/Models/FolderStructureRow.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
using CsvHelper.Configuration.Attributes;
|
||||
|
||||
namespace SharepointToolbox.Core.Models;
|
||||
|
||||
public class FolderStructureRow
|
||||
{
|
||||
[Name("Level1")]
|
||||
public string Level1 { get; set; } = string.Empty;
|
||||
|
||||
[Name("Level2")]
|
||||
public string Level2 { get; set; } = string.Empty;
|
||||
|
||||
[Name("Level3")]
|
||||
public string Level3 { get; set; } = string.Empty;
|
||||
|
||||
[Name("Level4")]
|
||||
public string Level4 { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Builds the folder path from non-empty level values (e.g. "Admin/HR/Contracts").
|
||||
/// </summary>
|
||||
public string BuildPath()
|
||||
{
|
||||
var parts = new[] { Level1, Level2, Level3, Level4 }
|
||||
.Where(s => !string.IsNullOrWhiteSpace(s));
|
||||
return string.Join("/", parts);
|
||||
}
|
||||
}
|
||||
27
SharepointToolbox/Core/Models/SiteTemplate.cs
Normal file
27
SharepointToolbox/Core/Models/SiteTemplate.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
namespace SharepointToolbox.Core.Models;
|
||||
|
||||
public class SiteTemplate
|
||||
{
|
||||
public string Id { get; set; } = Guid.NewGuid().ToString();
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string SourceUrl { get; set; } = string.Empty;
|
||||
public DateTime CapturedAt { get; set; }
|
||||
public string SiteType { get; set; } = string.Empty; // "Team" or "Communication"
|
||||
public SiteTemplateOptions Options { get; set; } = new();
|
||||
public TemplateSettings? Settings { get; set; }
|
||||
public TemplateLogo? Logo { get; set; }
|
||||
public List<TemplateLibraryInfo> Libraries { get; set; } = new();
|
||||
public List<TemplatePermissionGroup> PermissionGroups { get; set; } = new();
|
||||
}
|
||||
|
||||
public class TemplateSettings
|
||||
{
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string Description { get; set; } = string.Empty;
|
||||
public int Language { get; set; }
|
||||
}
|
||||
|
||||
public class TemplateLogo
|
||||
{
|
||||
public string LogoUrl { get; set; } = string.Empty;
|
||||
}
|
||||
10
SharepointToolbox/Core/Models/SiteTemplateOptions.cs
Normal file
10
SharepointToolbox/Core/Models/SiteTemplateOptions.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace SharepointToolbox.Core.Models;
|
||||
|
||||
public class SiteTemplateOptions
|
||||
{
|
||||
public bool CaptureLibraries { get; set; } = true;
|
||||
public bool CaptureFolders { get; set; } = true;
|
||||
public bool CapturePermissionGroups { get; set; } = true;
|
||||
public bool CaptureLogo { get; set; } = true;
|
||||
public bool CaptureSettings { get; set; } = true;
|
||||
}
|
||||
8
SharepointToolbox/Core/Models/TemplateFolderInfo.cs
Normal file
8
SharepointToolbox/Core/Models/TemplateFolderInfo.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace SharepointToolbox.Core.Models;
|
||||
|
||||
public class TemplateFolderInfo
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string RelativePath { get; set; } = string.Empty;
|
||||
public List<TemplateFolderInfo> Children { get; set; } = new();
|
||||
}
|
||||
9
SharepointToolbox/Core/Models/TemplateLibraryInfo.cs
Normal file
9
SharepointToolbox/Core/Models/TemplateLibraryInfo.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace SharepointToolbox.Core.Models;
|
||||
|
||||
public class TemplateLibraryInfo
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string BaseType { get; set; } = string.Empty; // "DocumentLibrary", "GenericList"
|
||||
public int BaseTemplate { get; set; }
|
||||
public List<TemplateFolderInfo> Folders { get; set; } = new();
|
||||
}
|
||||
8
SharepointToolbox/Core/Models/TemplatePermissionGroup.cs
Normal file
8
SharepointToolbox/Core/Models/TemplatePermissionGroup.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace SharepointToolbox.Core.Models;
|
||||
|
||||
public class TemplatePermissionGroup
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Description { get; set; } = string.Empty;
|
||||
public List<string> RoleDefinitions { get; set; } = new(); // e.g. "Full Control", "Contribute"
|
||||
}
|
||||
13
SharepointToolbox/Core/Models/TransferJob.cs
Normal file
13
SharepointToolbox/Core/Models/TransferJob.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
namespace SharepointToolbox.Core.Models;
|
||||
|
||||
public class TransferJob
|
||||
{
|
||||
public string SourceSiteUrl { get; set; } = string.Empty;
|
||||
public string SourceLibrary { get; set; } = string.Empty;
|
||||
public string SourceFolderPath { get; set; } = string.Empty; // relative within library
|
||||
public string DestinationSiteUrl { get; set; } = string.Empty;
|
||||
public string DestinationLibrary { get; set; } = string.Empty;
|
||||
public string DestinationFolderPath { get; set; } = string.Empty;
|
||||
public TransferMode Mode { get; set; } = TransferMode.Copy;
|
||||
public ConflictPolicy ConflictPolicy { get; set; } = ConflictPolicy.Skip;
|
||||
}
|
||||
7
SharepointToolbox/Core/Models/TransferMode.cs
Normal file
7
SharepointToolbox/Core/Models/TransferMode.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace SharepointToolbox.Core.Models;
|
||||
|
||||
public enum TransferMode
|
||||
{
|
||||
Copy,
|
||||
Move
|
||||
}
|
||||
36
SharepointToolbox/Services/BulkOperationRunner.cs
Normal file
36
SharepointToolbox/Services/BulkOperationRunner.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using SharepointToolbox.Core.Models;
|
||||
|
||||
namespace SharepointToolbox.Services;
|
||||
|
||||
public static class BulkOperationRunner
|
||||
{
|
||||
/// <summary>
|
||||
/// Runs a bulk operation with continue-on-error semantics, per-item result tracking,
|
||||
/// and cancellation support. OperationCanceledException propagates immediately.
|
||||
/// </summary>
|
||||
public static async Task<BulkOperationSummary<TItem>> RunAsync<TItem>(
|
||||
IReadOnlyList<TItem> items,
|
||||
Func<TItem, int, CancellationToken, Task> processItem,
|
||||
IProgress<OperationProgress> progress,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var results = new List<BulkItemResult<TItem>>();
|
||||
for (int i = 0; i < items.Count; i++)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
progress.Report(new OperationProgress(i + 1, items.Count, $"Processing {i + 1}/{items.Count}..."));
|
||||
try
|
||||
{
|
||||
await processItem(items[i], i, ct);
|
||||
results.Add(BulkItemResult<TItem>.Success(items[i]));
|
||||
}
|
||||
catch (OperationCanceledException) { throw; }
|
||||
catch (Exception ex)
|
||||
{
|
||||
results.Add(BulkItemResult<TItem>.Failed(items[i], ex.Message));
|
||||
}
|
||||
}
|
||||
progress.Report(new OperationProgress(items.Count, items.Count, "Complete."));
|
||||
return new BulkOperationSummary<TItem>(results);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using CsvHelper;
|
||||
using SharepointToolbox.Core.Models;
|
||||
|
||||
namespace SharepointToolbox.Services.Export;
|
||||
|
||||
public class BulkResultCsvExportService
|
||||
{
|
||||
public string BuildFailedItemsCsv<T>(IReadOnlyList<BulkItemResult<T>> failedItems)
|
||||
{
|
||||
using var writer = new StringWriter();
|
||||
using var csv = new CsvWriter(writer, CultureInfo.InvariantCulture);
|
||||
|
||||
csv.WriteHeader<T>();
|
||||
csv.WriteField("Error");
|
||||
csv.WriteField("Timestamp");
|
||||
csv.NextRecord();
|
||||
|
||||
foreach (var item in failedItems.Where(r => !r.IsSuccess))
|
||||
{
|
||||
csv.WriteRecord(item.Item);
|
||||
csv.WriteField(item.ErrorMessage);
|
||||
csv.WriteField(item.Timestamp.ToString("o"));
|
||||
csv.NextRecord();
|
||||
}
|
||||
|
||||
return writer.ToString();
|
||||
}
|
||||
|
||||
public async Task WriteFailedItemsCsvAsync<T>(
|
||||
IReadOnlyList<BulkItemResult<T>> failedItems,
|
||||
string filePath,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var content = BuildFailedItemsCsv(failedItems);
|
||||
await System.IO.File.WriteAllTextAsync(filePath, content, new UTF8Encoding(true), ct);
|
||||
}
|
||||
}
|
||||
14
SharepointToolbox/Services/IBulkMemberService.cs
Normal file
14
SharepointToolbox/Services/IBulkMemberService.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using Microsoft.SharePoint.Client;
|
||||
using SharepointToolbox.Core.Models;
|
||||
|
||||
namespace SharepointToolbox.Services;
|
||||
|
||||
public interface IBulkMemberService
|
||||
{
|
||||
Task<BulkOperationSummary<BulkMemberRow>> AddMembersAsync(
|
||||
ClientContext ctx,
|
||||
string clientId,
|
||||
IReadOnlyList<BulkMemberRow> rows,
|
||||
IProgress<OperationProgress> progress,
|
||||
CancellationToken ct);
|
||||
}
|
||||
13
SharepointToolbox/Services/IBulkSiteService.cs
Normal file
13
SharepointToolbox/Services/IBulkSiteService.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using Microsoft.SharePoint.Client;
|
||||
using SharepointToolbox.Core.Models;
|
||||
|
||||
namespace SharepointToolbox.Services;
|
||||
|
||||
public interface IBulkSiteService
|
||||
{
|
||||
Task<BulkOperationSummary<BulkSiteRow>> CreateSitesAsync(
|
||||
ClientContext adminCtx,
|
||||
IReadOnlyList<BulkSiteRow> rows,
|
||||
IProgress<OperationProgress> progress,
|
||||
CancellationToken ct);
|
||||
}
|
||||
12
SharepointToolbox/Services/ICsvValidationService.cs
Normal file
12
SharepointToolbox/Services/ICsvValidationService.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using System.IO;
|
||||
using SharepointToolbox.Core.Models;
|
||||
|
||||
namespace SharepointToolbox.Services;
|
||||
|
||||
public interface ICsvValidationService
|
||||
{
|
||||
List<CsvValidationRow<T>> ParseAndValidate<T>(Stream csvStream) where T : class;
|
||||
List<CsvValidationRow<BulkMemberRow>> ParseAndValidateMembers(Stream csvStream);
|
||||
List<CsvValidationRow<BulkSiteRow>> ParseAndValidateSites(Stream csvStream);
|
||||
List<CsvValidationRow<FolderStructureRow>> ParseAndValidateFolders(Stream csvStream);
|
||||
}
|
||||
18
SharepointToolbox/Services/IFileTransferService.cs
Normal file
18
SharepointToolbox/Services/IFileTransferService.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using Microsoft.SharePoint.Client;
|
||||
using SharepointToolbox.Core.Models;
|
||||
|
||||
namespace SharepointToolbox.Services;
|
||||
|
||||
public interface IFileTransferService
|
||||
{
|
||||
/// <summary>
|
||||
/// Transfers files/folders from source to destination.
|
||||
/// Returns per-item results (file paths as string items).
|
||||
/// </summary>
|
||||
Task<BulkOperationSummary<string>> TransferAsync(
|
||||
ClientContext sourceCtx,
|
||||
ClientContext destCtx,
|
||||
TransferJob job,
|
||||
IProgress<OperationProgress> progress,
|
||||
CancellationToken ct);
|
||||
}
|
||||
14
SharepointToolbox/Services/IFolderStructureService.cs
Normal file
14
SharepointToolbox/Services/IFolderStructureService.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using Microsoft.SharePoint.Client;
|
||||
using SharepointToolbox.Core.Models;
|
||||
|
||||
namespace SharepointToolbox.Services;
|
||||
|
||||
public interface IFolderStructureService
|
||||
{
|
||||
Task<BulkOperationSummary<string>> CreateFoldersAsync(
|
||||
ClientContext ctx,
|
||||
string libraryTitle,
|
||||
IReadOnlyList<FolderStructureRow> rows,
|
||||
IProgress<OperationProgress> progress,
|
||||
CancellationToken ct);
|
||||
}
|
||||
22
SharepointToolbox/Services/ITemplateService.cs
Normal file
22
SharepointToolbox/Services/ITemplateService.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using Microsoft.SharePoint.Client;
|
||||
using SharepointToolbox.Core.Models;
|
||||
using ModelSiteTemplate = SharepointToolbox.Core.Models.SiteTemplate;
|
||||
|
||||
namespace SharepointToolbox.Services;
|
||||
|
||||
public interface ITemplateService
|
||||
{
|
||||
Task<ModelSiteTemplate> CaptureTemplateAsync(
|
||||
ClientContext ctx,
|
||||
SiteTemplateOptions options,
|
||||
IProgress<OperationProgress> progress,
|
||||
CancellationToken ct);
|
||||
|
||||
Task<string> ApplyTemplateAsync(
|
||||
ClientContext adminCtx,
|
||||
ModelSiteTemplate template,
|
||||
string newSiteTitle,
|
||||
string newSiteAlias,
|
||||
IProgress<OperationProgress> progress,
|
||||
CancellationToken ct);
|
||||
}
|
||||
@@ -27,7 +27,9 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.2" />
|
||||
<PackageReference Include="CsvHelper" Version="33.1.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.Graph" Version="5.74.0" />
|
||||
<PackageReference Include="Microsoft.Identity.Client" Version="4.83.3" />
|
||||
<PackageReference Include="Microsoft.Identity.Client.Broker" Version="4.82.1" />
|
||||
<PackageReference Include="Microsoft.Identity.Client.Extensions.Msal" Version="4.83.3" />
|
||||
|
||||
Reference in New Issue
Block a user