using Microsoft.SharePoint.Client; using PnP.Framework.Sites; using Serilog; using SharepointToolbox.Core.Helpers; using SharepointToolbox.Core.Models; namespace SharepointToolbox.Services; public class BulkSiteService : IBulkSiteService { public async Task> CreateSitesAsync( ClientContext adminCtx, IReadOnlyList rows, IProgress progress, CancellationToken ct) { return await BulkOperationRunner.RunAsync( rows, async (row, idx, token) => { var siteUrl = await CreateSingleSiteAsync(adminCtx, row, progress, token); Log.Information("Created site: {Name} ({Type}) at {Url}", row.Name, row.Type, siteUrl); }, progress, ct); } private static async Task CreateSingleSiteAsync( ClientContext adminCtx, BulkSiteRow row, IProgress progress, CancellationToken ct) { if (row.Type.Equals("Team", StringComparison.OrdinalIgnoreCase)) { return await CreateTeamSiteAsync(adminCtx, row, progress, ct); } else if (row.Type.Equals("Communication", StringComparison.OrdinalIgnoreCase)) { return await CreateCommunicationSiteAsync(adminCtx, row, progress, ct); } else { throw new InvalidOperationException($"Unknown site type: {row.Type}. Expected 'Team' or 'Communication'."); } } private static async Task CreateTeamSiteAsync( ClientContext adminCtx, BulkSiteRow row, IProgress progress, CancellationToken ct) { var owners = ParseEmails(row.Owners); var members = ParseEmails(row.Members); var creationInfo = new TeamSiteCollectionCreationInformation { DisplayName = row.Name, Alias = row.Alias, Description = string.Empty, IsPublic = false, Owners = owners.ToArray(), }; progress.Report(new OperationProgress(0, 0, $"Creating Team site: {row.Name}...")); using var siteCtx = await adminCtx.CreateSiteAsync(creationInfo); siteCtx.Load(siteCtx.Web, w => w.Url); await ExecuteQueryRetryHelper.ExecuteQueryRetryAsync(siteCtx, progress, ct); var siteUrl = siteCtx.Web.Url; // Add additional members if specified if (members.Count > 0) { foreach (var memberEmail in members) { ct.ThrowIfCancellationRequested(); try { var user = siteCtx.Web.EnsureUser(memberEmail); siteCtx.Load(user); await ExecuteQueryRetryHelper.ExecuteQueryRetryAsync(siteCtx, progress, ct); // Add to Members group var membersGroup = siteCtx.Web.AssociatedMemberGroup; membersGroup.Users.AddUser(user); await ExecuteQueryRetryHelper.ExecuteQueryRetryAsync(siteCtx, progress, ct); } catch (Exception ex) { Log.Warning("Failed to add member {Email} to {Site}: {Error}", memberEmail, row.Name, ex.Message); } } } return siteUrl; } private static async Task CreateCommunicationSiteAsync( ClientContext adminCtx, BulkSiteRow row, IProgress progress, CancellationToken ct) { // Build the site URL from alias or sanitized name var alias = !string.IsNullOrWhiteSpace(row.Alias) ? row.Alias : SanitizeAlias(row.Name); var tenantUrl = new Uri(adminCtx.Url); var siteUrl = $"https://{tenantUrl.Host}/sites/{alias}"; var creationInfo = new CommunicationSiteCollectionCreationInformation { Title = row.Name, Url = siteUrl, Description = string.Empty, }; progress.Report(new OperationProgress(0, 0, $"Creating Communication site: {row.Name}...")); using var siteCtx = await adminCtx.CreateSiteAsync(creationInfo); siteCtx.Load(siteCtx.Web, w => w.Url); await ExecuteQueryRetryHelper.ExecuteQueryRetryAsync(siteCtx, progress, ct); var createdUrl = siteCtx.Web.Url; // Add owners and members if specified var owners = ParseEmails(row.Owners); var members = ParseEmails(row.Members); foreach (var ownerEmail in owners) { ct.ThrowIfCancellationRequested(); try { var user = siteCtx.Web.EnsureUser(ownerEmail); siteCtx.Load(user); await ExecuteQueryRetryHelper.ExecuteQueryRetryAsync(siteCtx, progress, ct); var ownersGroup = siteCtx.Web.AssociatedOwnerGroup; ownersGroup.Users.AddUser(user); await ExecuteQueryRetryHelper.ExecuteQueryRetryAsync(siteCtx, progress, ct); } catch (Exception ex) { Log.Warning("Failed to add owner {Email} to {Site}: {Error}", ownerEmail, row.Name, ex.Message); } } foreach (var memberEmail in members) { ct.ThrowIfCancellationRequested(); try { var user = siteCtx.Web.EnsureUser(memberEmail); siteCtx.Load(user); await ExecuteQueryRetryHelper.ExecuteQueryRetryAsync(siteCtx, progress, ct); var membersGroup = siteCtx.Web.AssociatedMemberGroup; membersGroup.Users.AddUser(user); await ExecuteQueryRetryHelper.ExecuteQueryRetryAsync(siteCtx, progress, ct); } catch (Exception ex) { Log.Warning("Failed to add member {Email} to {Site}: {Error}", memberEmail, row.Name, ex.Message); } } return createdUrl; } private static List ParseEmails(string commaSeparated) { if (string.IsNullOrWhiteSpace(commaSeparated)) return new List(); return commaSeparated .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) .Where(e => !string.IsNullOrWhiteSpace(e)) .ToList(); } private static string SanitizeAlias(string name) { // Remove special characters, spaces -> dashes, lowercase var sanitized = new string(name .Where(c => char.IsLetterOrDigit(c) || c == ' ' || c == '-') .ToArray()); return sanitized.Replace(' ', '-').ToLowerInvariant(); } }