Files
Sharepoint-Toolbox/SharepointToolbox/Services/SiteListService.cs
Dev 78b3d4f759 feat(02-03): implement ISiteListService and SiteListService with admin URL derivation
- SiteInfo record added to Core/Models
- ISiteListService interface with GetSitesAsync signature
- SiteListService derives admin URL via Regex, connects via SessionManager
- Filters to Active sites only, excludes OneDrive personal (-my.sharepoint.com)
- Access denied ServerException wrapped as InvalidOperationException with actionable message
- DeriveAdminUrl marked internal static for unit testability
- InternalsVisibleTo added to AssemblyInfo.cs to expose internal to test project
- 2 DeriveAdminUrl tests pass; full suite: 53 pass, 4 skip, 0 fail
2026-04-02 13:50:35 +02:00

70 lines
2.2 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Online.SharePoint.TenantAdministration;
using Microsoft.SharePoint.Client;
using SharepointToolbox.Core.Models;
namespace SharepointToolbox.Services;
public class SiteListService : ISiteListService
{
private readonly SessionManager _sessionManager;
public SiteListService(SessionManager sessionManager)
{
_sessionManager = sessionManager;
}
public async Task<IReadOnlyList<SiteInfo>> GetSitesAsync(
TenantProfile profile,
IProgress<OperationProgress> progress,
CancellationToken ct)
{
ct.ThrowIfCancellationRequested();
progress.Report(OperationProgress.Indeterminate("Loading sites..."));
var adminUrl = DeriveAdminUrl(profile.TenantUrl);
var adminProfile = new TenantProfile
{
Name = profile.Name,
TenantUrl = adminUrl,
ClientId = profile.ClientId
};
ClientContext adminCtx;
try
{
adminCtx = await _sessionManager.GetOrCreateContextAsync(adminProfile, ct);
}
catch (ServerException ex) when (ex.Message.Contains("Access denied", StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException(
"Site listing requires SharePoint administrator permissions. Connect with an admin account.", ex);
}
var tenant = new Tenant(adminCtx);
var siteProps = tenant.GetSitePropertiesFromSharePoint("", true);
adminCtx.Load(siteProps);
await adminCtx.ExecuteQueryAsync();
ct.ThrowIfCancellationRequested();
return siteProps
.Where(s => s.Status == "Active"
&& !s.Url.Contains("-my.sharepoint.com", StringComparison.OrdinalIgnoreCase))
.Select(s => new SiteInfo(s.Url, s.Title))
.OrderBy(s => s.Url)
.ToList();
}
internal static string DeriveAdminUrl(string tenantUrl)
=> Regex.Replace(tenantUrl.TrimEnd('/'),
@"(https://[^.]+)(\.sharepoint\.com)",
"$1-admin$2",
RegexOptions.IgnoreCase);
}