using Microsoft.SharePoint.Client; using PnP.Framework.Sites; using Serilog; using SharepointToolbox.Web.Core.Helpers; using SharepointToolbox.Web.Core.Models; using SharepointToolbox.Web.Services.Audit; namespace SharepointToolbox.Web.Services; public class BulkSiteService : IBulkSiteService { private readonly IAuditService _audit; public BulkSiteService(IAuditService audit) { _audit = audit; } public async Task> CreateSitesAsync( ClientContext adminCtx, IReadOnlyList rows, IProgress progress, CancellationToken ct) { var createdUrls = new System.Collections.Concurrent.ConcurrentBag(); var result = await BulkOperationRunner.RunAsync(rows, async (row, idx, token) => { var siteUrl = await CreateSingleSiteAsync(adminCtx, row, progress, token); createdUrls.Add(siteUrl); Log.Information("Created site: {Name} ({Type}) at {Url}", row.Name, row.Type, siteUrl); }, progress, ct); var tenantHost = Uri.TryCreate(adminCtx.Url, UriKind.Absolute, out var u) ? u.Host : adminCtx.Url; await _audit.LogAsync("BulkCreateSites", tenantHost, createdUrls, $"{result.SuccessCount} created, {(result.TotalCount - result.SuccessCount)} failed"); return result; } private static async Task CreateSingleSiteAsync(ClientContext adminCtx, BulkSiteRow row, IProgress progress, CancellationToken ct) => row.Type.Equals("Team", StringComparison.OrdinalIgnoreCase) ? await CreateTeamSiteAsync(adminCtx, row, progress, ct) : row.Type.Equals("Communication", StringComparison.OrdinalIgnoreCase) ? await CreateCommunicationSiteAsync(adminCtx, row, progress, ct) : throw new InvalidOperationException($"Unknown site type: {row.Type}"); private static async Task CreateTeamSiteAsync(ClientContext adminCtx, BulkSiteRow row, IProgress progress, CancellationToken ct) { var owners = ParseEmails(row.Owners); if (owners.Count == 0) throw new InvalidOperationException($"Team site '{row.Name}' requires at least one owner."); 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; foreach (var memberEmail in ParseEmails(row.Members)) { ct.ThrowIfCancellationRequested(); try { var user = siteCtx.Web.EnsureUser(memberEmail); siteCtx.Load(user); await ExecuteQueryRetryHelper.ExecuteQueryRetryAsync(siteCtx, progress, ct); siteCtx.Web.AssociatedMemberGroup.Users.AddUser(user); await ExecuteQueryRetryHelper.ExecuteQueryRetryAsync(siteCtx, progress, ct); } catch (OperationCanceledException) { throw; } 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) { var alias = !string.IsNullOrWhiteSpace(row.Alias) ? row.Alias : SanitizeAlias(row.Name); var tenantUrl = new Uri(adminCtx.Url); var creationInfo = new CommunicationSiteCollectionCreationInformation { Title = row.Name, Url = $"https://{tenantUrl.Host}/sites/{alias}", 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; foreach (var ownerEmail in ParseEmails(row.Owners)) { ct.ThrowIfCancellationRequested(); try { var user = siteCtx.Web.EnsureUser(ownerEmail); siteCtx.Load(user); await ExecuteQueryRetryHelper.ExecuteQueryRetryAsync(siteCtx, progress, ct); siteCtx.Web.AssociatedOwnerGroup.Users.AddUser(user); await ExecuteQueryRetryHelper.ExecuteQueryRetryAsync(siteCtx, progress, ct); } catch (OperationCanceledException) { throw; } catch (Exception ex) { Log.Warning("Failed to add owner {Email}: {Error}", ownerEmail, ex.Message); } } return createdUrl; } private static List ParseEmails(string commaSeparated) => string.IsNullOrWhiteSpace(commaSeparated) ? new List() : commaSeparated.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).Where(e => !string.IsNullOrWhiteSpace(e)).ToList(); private static string SanitizeAlias(string name) => new string(name.Where(c => char.IsLetterOrDigit(c) || c == ' ' || c == '-').ToArray()).Replace(' ', '-').ToLowerInvariant(); }