Files
Sharepoint-Toolbox/.planning/research/SUMMARY.md
Dev 4ad5f078c9 docs: synthesize v2.3 research summary
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 11:00:02 +02:00

40 KiB
Raw Blame History

Project Research Summary

Project: SharePoint Toolbox — C#/WPF SharePoint Online Administration Desktop Tool Domain: SharePoint Online administration, auditing, and provisioning (MSP / IT admin) Researched: 2026-04-02 (v1.0 original) | 2026-04-08 (v2.2 addendum) Confidence: HIGH


Note: This file contains two sections. The original v1.0 research summary is preserved below the v2.2 section. The roadmapper should consume v2.2 first for the current milestone.


v2.2 Research Summary — Report Branding & User Directory

Milestone: v2.2 — HTML Report Branding (MSP/client logos) + User Directory Browse Mode Synthesized: 2026-04-08 Sources: STACK.md (v2.2 addendum), FEATURES.md (v2.2), ARCHITECTURE.md (v2.2), PITFALLS.md (v2.2 addendum)


Executive Summary

v2.2 adds two independent, self-contained features to a mature WPF MVVM codebase: logo branding across all five HTML export services, and a full-directory browse mode as an alternative to the existing people-picker in the User Access Audit tab. Both features are well within the capabilities of the existing stack — no new NuGet packages are required. The implementation path is low risk because neither feature touches the audit execution engine; they are purely additive layers on top of proven infrastructure.

The branding feature follows a single clear pattern: store logos as base64 strings in existing JSON settings and profile files, pass them at export time via a new optional ReportBranding record, and inject <img data-URI> tags into a shared HTML header block. The architecture keeps the five export services independent (each receives an optional parameter) while avoiding code duplication through a shared header builder. The user directory browse feature adds a new IGraphUserDirectoryService alongside the existing search service, wires it to new ViewModel state in UserAccessAuditViewModel, and presents it as a toggle-panel in the View. The existing audit pipeline is completely untouched.

The primary risks are not technical complexity but execution discipline: logo size must be enforced at import time (512 KB limit) to prevent HTML report bloat, Graph pagination must use PageIterator to handle tenants with more than 999 users, and logo data must be stored as base64 strings (not file paths) to ensure portability across machines. All three of these are straightforward to implement once the storage strategy is decided and locked in at the beginning of each feature's implementation phase.


Key Findings

Stack Additions — None Required

The entire v2.2 scope is served by the existing stack:

Capability Provided By Notes
Logo encoding (file → base64) BCL Convert.ToBase64String + File.ReadAllBytesAsync Zero new packages
Logo preview in WPF settings UI BitmapImage (WPF PresentationCore, already a transitive dep) Standard WPF pattern
Logo file picker OpenFileDialog (WPF Microsoft.Win32, already used in codebase) Filter to PNG/JPG/GIF/BMP
User directory listing with pagination Microsoft.Graph 5.74.0 PageIterator<User, UserCollectionResponse> Already installed
Local directory filtering ICollectionView.Filter (WPF System.Windows.Data) Already used in PermissionsViewModel
Logo + profile JSON persistence System.Text.Json + existing Repository pattern Backward-compatible nullable fields

Do NOT add: HTML template engines (Razor/Scriban), image processing libraries (ImageSharp, Magick.NET), or PDF export libraries. All explicitly out of scope.


Feature Table Stakes vs. Differentiators

Feature 1: HTML Report Branding

Table stakes (must ship):

  • MSP global logo in every HTML report header
  • Client (per-tenant) logo in report header
  • Logo renders without external URL (data-URI embedding for self-contained HTML portability)
  • Graceful absence — no broken image icon when logo is not configured
  • Consistent placement across all five HTML export types

Differentiators (build after table stakes):

  • Auto-pull client logo from Microsoft Entra tenant branding (GET /organization/{id}/branding/localizations/default/bannerLogo) — zero-config path using the existing User.Read delegated scope
  • Report timestamp and tenant display name in header

Anti-features — do not build:

  • Per-tenant CSS color themes (design system complexity, disproportionate to MSP value)
  • PDF export with embedded logo (requires third-party binary dependency)
  • SVG logo support (XSS risk in data-URIs; PNG/JPG/GIF/BMP only)
  • Hotlinked logo URL field (breaks offline/archived reports)

Feature 2: User Directory Browse Mode

Table stakes (must ship):

  • Full directory listing (all enabled member users) with pagination
  • In-memory text filter on DisplayName/UPN/Mail without server round-trips
  • Sortable columns (Name, UPN)
  • Select user from list to trigger existing audit pipeline
  • Loading indicator with user count feedback ("Loaded X users...")
  • Toggle between Browse mode and Search (people-picker) mode

Differentiators (add after core browse is stable):

  • Filter by account type (member vs. guest toggle)
  • Department / Job Title columns
  • Session-scoped directory cache (invalidated on tenant switch)

Anti-features — do not build:

  • Eager load on tab open (large tenants block UI and risk throttling)
  • Delta query / incremental sync (wrong pattern for single-session audit)
  • Multi-user bulk simultaneous audit (different results model, out of scope)
  • Export user directory to CSV (identity reporting, not access audit)

Recommended MVP build order:

  1. MSP logo in all HTML reports — highest visible impact, lowest complexity
  2. Client logo in HTML reports (import from file) — completes co-branding
  3. User directory browse core (load, select, filter, pipe into audit)
  4. Auto-pull client logo from Entra branding — add after file import path is proven
  5. Directory guest filter + department/jobTitle columns — low-effort polish

Architecture Integration Points and Build Order

New files to create (7):

Component Layer Purpose
Core/Models/BrandingSettings.cs Core/Models MSP logo base64 + MIME type; global, persisted in branding.json
Core/Models/ReportBranding.cs Core/Models Lightweight record assembled at export time; NOT persisted
Core/Models/PagedUserResult.cs Core/Models Page of GraphUserResult items + next-page cursor token
Infrastructure/Persistence/BrandingRepository.cs Infrastructure Atomic JSON write (mirrors SettingsRepository pattern exactly)
Services/BrandingService.cs Services Orchestrates file read → MIME detect → base64 → save
Services/IGraphUserDirectoryService.cs Services Contract for paginated tenant user enumeration
Services/GraphUserDirectoryService.cs Services Graph API user listing with PageIterator cursor pagination

Existing files to modify (17), by risk level:

Medium risk (left-panel restructure or new async command):

  • ViewModels/Tabs/UserAccessAuditViewModel.cs — add IGraphUserDirectoryService injection + browse mode state/commands
  • Views/Tabs/UserAccessAuditView.xaml — add mode toggle + browse panel in left column

Low risk (optional param or uniform inject-and-call pattern, batchable):

  • All 5 Services/Export/*HtmlExportService.cs — add ReportBranding? branding = null optional parameter
  • PermissionsViewModel, StorageViewModel, SearchViewModel, DuplicatesViewModel — add BrandingService injection + use in ExportHtmlAsync
  • SettingsViewModel.cs — add MSP logo browse/preview/clear commands
  • ProfileManagementViewModel.cs — add client logo browse/preview/clear commands
  • SettingsView.xaml, ProfileManagementDialog.xaml — add logo UI sections
  • App.xaml.cs — register 3 new services

Dependency-aware build phases:

Phase Scope Risk Gate
A — Models BrandingSettings, ReportBranding, PagedUserResult, TenantProfile logo fields None POCOs; no dependencies
B — Services BrandingRepository, BrandingService, IGraphUserDirectoryService, GraphUserDirectoryService Low Unit-testable with mocks; Phase A required
C — Export services Add optional ReportBranding? to all 5 HTML export services Low Phase A required; regression tests: null branding produces identical HTML
D — Branding ViewModels SettingsVM, ProfileManagementVM, 4 export VMs, App.xaml.cs registration Low Phase B+C required; steps are identical pattern, batch them
E — Directory ViewModel UserAccessAuditViewModel browse mode state + commands Medium Phase B required; do after branding ViewModel pattern is proven
F — Branding Views SettingsView.xaml, ProfileManagementDialog.xaml, base64→BitmapSource converter Low Phase D required; write converter once, reuse in both views
G — Directory View UserAccessAuditView.xaml + code-behind SelectionChanged handler Medium Phase E required; do last, after ViewModel unit tests pass

Key architectural constraints (must not violate):

  • Client logo on TenantProfile, NOT in BrandingSettings. Client logos are per-tenant; mixing them with global MSP settings makes per-profile deletion and serialization awkward.
  • Logos stored as base64 strings in JSON, not as file paths. File paths become stale when the tool is redistributed to another machine. Decided once at Phase A; all downstream phases depend on it.
  • Export services use optional ReportBranding? parameter, not required. All existing call sites compile unchanged; branding is injected only where desired.
  • No IHtmlExportService interface for this change. The existing 5-concrete-classes pattern needs no interface for an optional parameter addition.
  • GraphUserDirectoryService is a new service, separate from GraphUserSearchService. Different call patterns (no startsWith filter, different pagination), different cancellation needs.
  • Do NOT load the directory automatically on tab open. Require explicit "Load Directory" button click to avoid blocking UI on large tenants.

Top Pitfalls and Prevention Strategies

v2.2-1 (Critical): Base64 logo bloat in every report Large source images (300-600 KB originals) become 400-800 KB of base64 inlined in every exported HTML file, re-allocated on every export call. Prevention: Enforce 512 KB max at import time in the settings UI. Store pre-encoded base64 in JSON (computed once on import, never re-encoded). Inject the cached string directly into the <img> tag.

v2.2-2 (Critical): Graph directory listing silently truncates at 999 users GET /users returns at most 999 per page. A 5,000-user tenant appears to have 999 users with no error and no indication of truncation. Prevention: Use PageIterator<User, UserCollectionResponse> for all full directory fetches. Never call .GetAsync() on the users collection without following @odata.nextLink until null.

v2.2-3 (Critical): Directory browse exposes guests, service accounts, and disabled accounts by default Raw GET /users returns all object types. An MSP tenant with 50+ guest collaborators and service accounts produces a noisy, confusing directory. Prevention: Default filter accountEnabled eq true and userType eq 'Member'. Expose an "Include guest accounts" checkbox for explicit opt-in. Apply this filter at the service level, not the ViewModel, so the ViewModel is not aware of Graph filter syntax.

v2.2-4 (Critical): Directory load hangs UI without progress feedback 3,000-user tenant takes 3-8 seconds. Without count feedback, the user assumes the feature is broken and may double-click the button (triggering concurrent Graph requests). Prevention: DirectoryLoadStatus observable property updated via IProgress<int> in the PageIterator callback ("Loading... X users"). Guard AsyncRelayCommand.CanExecute during loading. Add cancellation button wired to the same CancellationToken passed to PageIterator.IterateAsync.

v2.2-5 (Critical): Logo file format validation skipped — broken images in reports OpenFileDialog filter is not sufficient. Renamed non-image files, corrupted JPEGs, and SVG files pass the filter but produce broken <img> tags in generated reports. Prevention: Validate by loading as BitmapImage in a try/catch before persisting. Check PixelWidth and PixelHeight are non-zero. Use BitmapCacheOption.OnLoad + retry with IgnoreColorProfile for EXIF-corrupt JPEGs. Reject SVG explicitly.

v2.2-6 (Critical): Logo file path stored in JSON becomes stale across machines Storing C:\Users\admin\logos\msp-logo.png works on the import machine only. After redistribution or reinstall, the path is missing and logos silently disappear from new reports. Prevention: Store base64 string directly in AppSettings and TenantProfile JSON. The original file path is discarded after import. The settings file becomes fully portable.

Moderate pitfalls:

  • v2.2-7: Logo breaks HTML report print layout — apply max-height: 60px; max-width: 200px CSS and add @media print rules in the report <style> block.
  • v2.2-8: Logo cleared on profile overwrite — verify ClientLogoBase64 and ClientLogoMimeType survive the profile save/reload cycle before shipping.
  • v2.2-9: DirectoryPageTokenNotFoundException on Graph page iteration retry — use PageIterator (which handles retry token correctly) rather than a manual @odata.nextLink loop.

Implications for Roadmap

Suggested Phase Structure

The two features are architecturally independent. A single developer should follow phases A-G in order. Two developers can run branding (A→D→F) and directory browse (A→B→E→G) in parallel after Phase A completes.

Phase 1 — Data foundation (models + repositories + services) Rationale: Unblocks both features simultaneously. Establishes the logo storage strategy (base64-in- JSON) before any export service or ViewModel is written. Establishing this here prevents a full re-architecture later if file-path storage is chosen first. Delivers: Phases A + B from the build order above. Key decision to make before starting: confirm base64-in-JSON as the storage strategy (not file paths). Document the decision explicitly. Pitfalls to avoid: v2.2-6 (file path portability). The wrong storage decision here propagates to all downstream phases.

Phase 2 — HTML export service extensions + branding ViewModel integration Rationale: Modifying the 5 export services with an optional parameter is low-risk and unblocks all ViewModel callers. The 4 export ViewModel changes are an identical inject-and-call pattern — batch them. The SettingsViewModel and ProfileManagementViewModel changes complete the logo management UX. Delivers: Phases C + D from the build order above. All HTML reports support optional logo headers. MSP logo manageable from Settings. Client logo manageable from ProfileManagementDialog. Pitfalls to avoid: v2.2-1 (size limit at import), v2.2-5 (file format validation), v2.2-7 (print layout CSS). All three must be implemented in this phase, not deferred.

Phase 3 — Branding UI views Rationale: Views built after ViewModel behavior is unit-tested. Requires the base64→BitmapSource converter, written once and reused in both views. Delivers: Phase F from the build order. Settings branding section + ProfileManagementDialog logo fields, both with live preview.

Phase 4 — User directory browse ViewModel Rationale: UserAccessAuditViewModel is the highest-risk change. New async command with progress, cancellation, and tenant-switch reset. Implement and unit-test before touching the View. Delivers: Phase E from the build order. Full browse mode behavior is testable via unit tests before any XAML is written. Pitfalls to avoid: v2.2-2 (pagination), v2.2-3 (default filter), v2.2-4 (progress feedback), v2.2-9 (PageIterator vs. manual nextLink loop).

Phase 5 — Directory browse UI view Rationale: Left panel restructure in UserAccessAuditView.xaml is the highest-risk XAML change. Done last, after all ViewModel behavior is proven by tests. Delivers: Phase G from the build order. Complete browse mode UX.

Phase 6 — Differentiators (after core features proven) Rationale: Auto-pull Entra branding, directory guest filter toggle, department/jobTitle columns, session-scoped directory cache. These are enhancements, not blockers for the milestone. Delivers: Zero-config client logo path, richer directory filtering, faster repeat access. Pitfalls to avoid: Auto-pull Entra logo must handle empty-body response gracefully (not all tenants have branding configured). Fall back silently to no logo rather than showing an error.

Research Flags

Phases 1-5 are standard patterns verified by direct codebase inspection. No additional research needed. The architecture file provides exact file locations, class signatures, and data flows.

Phase 6 (auto-pull Entra branding): MEDIUM confidence. Test the bannerLogo stream endpoint against a real tenant with and without branding configured before committing to the implementation. The Graph API documentation states the response is an empty stream (not a 404) when no logo is set — verify this behavior live before building the error handling path around it.


Open Questions for Product Decisions

These are not technical blockers but should be resolved before the phase that implements them:

  1. SVG logo support: anti-feature or bring-your-own-library feature? Current recommendation: reject SVG (XSS risk in data-URIs, requires SharpVectors for rasterization). If SVG support is needed, SharpVectors adds a dependency. Decide before Phase 2.

  2. Client logo source priority when both auto-pull and manual import are configured? Recommendation: manual import wins; auto-pull is the fallback when no manual logo is set. Implement as ClientLogoSource enum: None | Imported | AutoPulled. Decide before Phase 6.

  3. Session-scoped directory cache: ViewModel lifetime or shared service? ViewModel-scoped = cache lost on tab navigation (ViewModel is transient). Service-scoped = cache survives tab switches. Recommendation: start with no cache (Refresh button), add service-level caching in Phase 6 only if user feedback indicates it is needed. Defers scope decision.

  4. Report header layout: logos side-by-side or MSP left + client right? Visual design decision only; does not affect services or ViewModels. Current spec uses display: flex; gap: 16px (left-to-right). Can be changed at any time.

  5. "Load Directory" button placement: inside browse panel or tab-level toolbar? Recommendation: inside the browse panel, visible only in Browse mode. Avoids confusion when in Search mode. Does not affect architecture.


Confidence Assessment (v2.2)

Area Confidence Basis
Stack (no new packages needed) HIGH Direct codebase inspection + BCL and Graph SDK documentation
Feature scope (table stakes vs. differentiators) HIGH Official Graph API docs + direct codebase inspection + MSP tool competitive research
Architecture (integration points, build order) HIGH Direct inspection of all affected files; exact class names and property signatures verified
Branding pitfalls (base64, file validation, portability) HIGH BCL behavior verified; file path portability pitfall is a well-known pattern
Graph pagination pitfalls HIGH Microsoft Learn PageIterator docs (updated 2025-08-06); DirectoryPageTokenNotFoundException documented
Directory filter behavior (accountEnabled, userType) MEDIUM-HIGH Graph docs confirm filter syntax; recommend verifying against a real tenant before shipping
Auto-pull Entra banner logo (Phase 6) MEDIUM API documented but empty-body behavior (no logo configured) needs live tenant verification
Print CSS behavior for logo header MEDIUM MDN/W3C verified; browser rendering varies; requires cross-browser manual test

Overall confidence: HIGH for Phases 1-5. MEDIUM for Phase 6 (Entra auto-pull live behavior).

Gaps to address during planning:

  • Confirm $filter=accountEnabled eq true and userType eq 'Member' works without ConsistencyLevel: eventual on the v1.0 /users endpoint. If eventual consistency is required, the GraphUserDirectoryService adds the ConsistencyLevel header and $count=true to this call path.
  • Verify the Entra bannerLogo stream endpoint returns an empty response body (not HTTP 404) when tenant branding is not configured. This determines the error handling branch in the auto-pull code path.

Sources (v2.2)

Source Confidence Used In
Microsoft Learn — List users (Graph v1.0), updated 2025-07-23 HIGH STACK, FEATURES, PITFALLS
Microsoft Learn — Page through a collection (Graph SDKs), updated 2025-08-06 HIGH STACK, PITFALLS
Microsoft Learn — Get organizationalBranding (Graph v1.0), updated 2025-11-08 HIGH STACK, FEATURES
.NET BCL docs — Convert.ToBase64String, File.ReadAllBytesAsync HIGH STACK
Microsoft Learn — Graph throttling guidance HIGH PITFALLS
Direct codebase inspection (GraphClientFactory, HtmlExportService, TenantProfile, AppSettings, UserAccessAuditViewModel, SettingsViewModel, UserAccessAuditView.xaml, App.xaml.cs) HIGH ARCHITECTURE, STACK
Existing codebase CONCERNS.md audit (2026-04-02) HIGH PITFALLS


v1.0 Research Summary (Original — Preserved for Reference)

Researched: 2026-04-02 Confidence: HIGH

Executive Summary

This project is a full rewrite of a PowerShell-based SharePoint Online administration toolbox into a standalone C#/WPF desktop application targeting MSP administrators who manage 1030 client tenants simultaneously. The research confirms that the correct technical path is .NET 10 LTS with WPF, PnP.Framework (not PnP.Core SDK) as the SharePoint library, and CommunityToolkit.Mvvm for the MVVM layer. The key architectural constraint is that multi-tenant session caching — holding MSAL token caches per tenant with MsalCacheHelper — must be the very first infrastructure component built, because every single feature gates on it. The recommended architecture is a strict four-layer MVVM pattern (View → ViewModel → Service → Infrastructure) with no WPF types below the ViewModel layer, constructor-injected interfaces throughout, and AsyncRelayCommand for every SharePoint operation.

The feature scope is well-defined: parity with the existing PowerShell tool is the v1 MVP (permissions reports, storage metrics, file search, bulk operations, site templates, duplicate detection, error reporting, EN/FR localization). Three new features are justified for a v1.x release once core parity is validated — user access export across sites, simplified plain-language permissions view, and storage charts by file type. These represent genuine competitive differentiation against SaaS tools like ShareGate and ManageEngine, which are cloud-based, subscription-priced, and do not offer local offline operation or MSP-grade multi-tenant context switching.

The most dangerous risk is not technical complexity but porting discipline: the existing codebase has 38 silent catch blocks and no async discipline. The single highest-priority constraint for the entire project is that async patterns (AsyncRelayCommand, IProgress<T>, CancellationToken, ExecuteQueryRetryAsync) must be established in the foundation phase and enforced through code review before any feature work begins. Retrofitting these patterns after-the-fact is among the most expensive refactors possible in a WPF codebase. Similarly, the write-then-replace JSON persistence pattern and SharePoint pagination helpers must be built once in the foundation and reused everywhere — building these per-feature guarantees divergence and bugs.

Key Findings

The stack is fully resolved with high confidence. All package versions are confirmed on NuGet as of 2026-04-02. The runtime is .NET 10 LTS (EOL November 2028); .NET 8 was explicitly rejected because it reaches EOL in November 2026 — too soon for a new project. PnP.Framework 1.18.0 is the correct SharePoint library choice because this is a CSOM-heavy migration from PnP.PowerShell patterns and the PnP Provisioning Engine (required for site templates) lives only in PnP.Framework, not in PnP.Core SDK. Do not use PublishTrimmed=true — PnP.Framework and MSAL use reflection and are not trim-safe; the self-contained EXE will be approximately 150200 MB, which is acceptable per project constraints.

Core technologies:

  • .NET 10 LTS + WPF: Windows-only per constraint; richer MVVM binding than WinForms (the existing framework)
  • PnP.Framework 1.18.0: CSOM operations, PnP Provisioning Engine, site templates — the direct C# equivalent of PnP.PowerShell
  • Microsoft.Graph 5.103.0: Teams, groups, user enumeration across tenants — Graph-native operations only
  • MSAL.NET 4.83.1 + Extensions.Msal 4.83.3 + Desktop 4.82.1: Multi-tenant token cache per tenant, Windows broker (WAM) support
  • CommunityToolkit.Mvvm 8.4.2: Source-generated [ObservableProperty], [RelayCommand], AsyncRelayCommand — eliminates MVVM boilerplate
  • Microsoft.Extensions.Hosting 10.x: DI container (IServiceCollection), app lifetime, IConfiguration
  • Serilog 4.3.1 + file sink: Structured logging to rolling files in %AppData%\SharepointToolbox\logs\ — essential for diagnosing the silent failures in the existing app
  • ScottPlot.WPF 5.1.57: Pie and bar charts for storage metrics — stable MIT-licensed library (LiveCharts2 WPF is still RC as of April 2026)
  • System.Text.Json (built-in): JSON profiles, settings, templates — no Newtonsoft.Json dependency
  • CsvHelper: CSV export — replaces manual string concatenation
  • .resx localization: EN/FR compile-time-safe resource files

Expected Features

The feature scope is well-researched. Competitive analysis against ShareGate, ManageEngine SharePoint Manager Plus, and AdminDroid confirms that local offline operation, instant multi-tenant switching, plain-language permissions, and folder structure provisioning are genuine differentiators that no competitor SaaS tool offers.

Must have (table stakes — v1 parity):

  • Tenant profile registry + multi-tenant session caching — everything gates on this
  • Permissions report (site-level) with CSV + HTML export
  • Storage metrics per site
  • File search across sites
  • Bulk operations (member add, site creation, transfer) with progress and cancellation
  • Site template management + folder structure provisioning
  • Duplicate file detection
  • Error reporting (replace 38 silent catch blocks with visible failures)
  • Localization (EN/FR) — existing users depend on this

Should have (competitive differentiators — v1.x):

  • User access export across selected sites — "everything User X can access across 15 sites" — no native M365 equivalent
  • Simplified permissions view (plain language) — "can edit files" instead of "Contribute"
  • Storage graph by file type (pie + bar toggle) via ScottPlot.WPF

Defer (v2+):

  • Scheduled scan runs via Windows Task Scheduler (requires stable CLI/headless mode first)
  • Permission comparison/diff between two time points (requires snapshot storage)
  • XLSX export (CSV opens in Excel adequately for v1)

Anti-features to reject outright: real-time permission change alerts (requires persistent Azure service), automated remediation (liability risk), cloud sync, AI governance recommendations (Microsoft's own roadmap).

Architecture Approach

The recommended architecture is a strict four-layer MVVM pattern hosted in Microsoft.Extensions.Hosting. The application is organized as: Views (XAML only, zero code-behind) → ViewModels (CommunityToolkit.Mvvm, one per feature tab) → Services (domain logic, stateless, constructor-injected via interfaces) → Infrastructure (PnP.Framework, Microsoft.Graph, local JSON files). Cross-ViewModel communication uses WeakReferenceMessenger (e.g., tenant-switched event resets all feature VM state). A singleton SessionManager is the only class that holds ClientContext objects — services request a context per operation and never store it. The Core/ folder contains pure C# models and interfaces with no WPF references, making all services independently testable.

Major components:

  1. AuthService / SessionManager — multi-tenant MSAL token cache, TenantSession per tenant, active profile state; singleton; every feature gates on this
  2. Feature Services (6) — PermissionsService, StorageService, SearchService, TemplateService, DuplicateService, BulkOpsService — stateless, cancellable, progress-reporting; registered as transient
  3. ReportExportService + CsvExportService — self-contained HTML reports (embedded JS/CSS) and CSV generation; called after operation completes
  4. SettingsService — JSON profiles, templates, settings with write-then-replace pattern and SemaphoreSlim concurrency guard; singleton
  5. MainWindowViewModel — shell navigation, tenant selector, log panel; delegates all feature logic to feature ViewModels via DI
  6. Feature ViewModels (7) — one per tab (Permissions, Storage, Search, Templates, Duplicates, BulkOps, Settings); own CancellationTokenSource and ObservableCollection<T> per operation

Critical Pitfalls

10 pitfalls were identified. All 10 are addressed in Phase 1 (Foundation) — none can be deferred to feature phases.

  1. Sync calls on the UI thread — Never use .Result/.Wait() on the UI thread; every PnP call must use await with the async overload or Task.Run; use AsyncRelayCommand for all commands. Establish this pattern before any feature work begins or retrofitting costs will be severe.

  2. Porting silent error suppression — The existing app has 38 empty catch blocks. Every catch in the C# rewrite must do one of three things: log-and-recover, log-and-rethrow, or log-and-surface to the user. Treat empty catch as a build defect from day one.

  3. SharePoint 5,000-item list view threshold — All CSOM list enumeration must use CamlQuery with RowLimit ≤ 2,000 and ListItemCollectionPosition pagination. Build a shared pagination helper in Phase 1 and mandate its use in every feature that enumerates list items.

  4. Multi-tenant token cache race conditions — Use MsalCacheHelper (Microsoft.Identity.Client.Extensions.Msal) for file-based per-tenant token cache serialization. Scope IPublicClientApplication per ClientId, not per tenant URL. Provide a "Clear cached sessions" UI action.

  5. JSON settings file corruption on concurrent writes — Use write-then-replace (filename.tmp → validate → File.Move) plus SemaphoreSlim(1) per file. Implement before any feature persists data. Known bug in the existing app per CONCERNS.md.

  6. WPF ObservableCollection updates from background threads — Collect results into List<T> on background thread, then assign new ObservableCollection<T>(list) atomically via Dispatcher.InvokeAsync. Use IProgress<T> for streaming. Never modify ObservableCollection from Task.Run.

  7. async void command handlers — Use AsyncRelayCommand exclusively for async operations. async void swallows exceptions post-await. Wire Application.DispatcherUnhandledException and TaskScheduler.UnobservedTaskException as last-resort handlers.

  8. API throttling (429/503) — Always use ExecuteQueryRetryAsync (never ExecuteQuery). For Graph SDK, the default retry handler respects Retry-After automatically. Surface retry events to the user as progress messages.

  9. ClientContext resource disposal gaps — Always obtain ClientContext inside using or await using. Verify Dispose() is called on cancellation via unit tests.

  10. WPF trimming breaks self-contained EXE — Never set PublishTrimmed=true. Accept the ~150200 MB EXE size. Use PublishReadyToRun=true for startup performance instead.

Implications for Roadmap

Based on the combined research, the dependency graph from ARCHITECTURE.md and FEATURES.md, and the pitfall-to-phase mapping from PITFALLS.md, the following phase structure is strongly recommended:

Phase 1: Foundation and Infrastructure

Rationale: All 10 critical pitfalls must be resolved before feature work begins. The dependency graph in FEATURES.md shows that every feature requires the tenant profile registry and session caching layer. Establishing async patterns, error handling, DI container, logging, and JSON persistence now prevents the most expensive retrofits. Delivers: Runnable WPF shell with tenant selector, multi-tenant session caching (MSAL + MsalCacheHelper), DI container wiring, Serilog logging, SettingsService with write-then-replace persistence, ResX localization scaffolding, shared pagination helper, shared AsyncRelayCommand pattern, global exception handlers. Research flag: Standard patterns — no additional research needed.

Phase 2: Permissions and Audit Core

Rationale: Permissions reporting is the highest-value daily-use feature and validates the auth layer and pagination helper under real conditions. Delivers: Site-level permissions report with recursive scan, CSV export, self-contained HTML export, progress feedback, error surface for failed scans. Research flag: Standard PnP Framework patterns — HIGH confidence.

Phase 3: Storage Metrics and File Operations

Rationale: Storage metrics and file search reuse the auth session and export infrastructure from Phases 12. Duplicate detection depends on file enumeration built here. Delivers: Storage metrics, file search (KQL), duplicate detection, storage data export. Research flag: Duplicate detection at scale under Graph throttling may need targeted research.

Phase 4: Bulk Operations and Provisioning

Rationale: Highest-complexity features (bulk writes to client tenants) benefit from stable async/cancel/progress infrastructure from Phase 1. Delivers: Bulk member add/remove, bulk site creation, ownership transfer, site template capture and apply, folder structure provisioning. Research flag: PnP Provisioning Engine for Teams-connected sites — edge cases need validation.

Phase 5: New Differentiating Features (v1.x)

Rationale: New capabilities (not existing-tool parity) that depend on stable v1 infrastructure. Group here to avoid blocking the v1 release. Delivers: User access export across sites, simplified plain-language permissions view, storage graph by file type. Research flag: User access export — Graph API approach for enumerating all permissions for user X across N sites needs targeted research.

Phase 6: Distribution and Hardening

Rationale: Packaging, end-to-end validation on clean machines, FR locale completeness, "looks done but isn't" checklist. Delivers: Single self-contained EXE, validated on a machine with no .NET runtime, all checklist items verified. Research flag: Standard dotnet publish configuration — no additional research needed.

Confidence Assessment

Area Confidence Notes
Stack HIGH All package versions verified on NuGet; .NET lifecycle dates confirmed; PnP.Framework vs PnP.Core SDK choice verified
Features MEDIUM Microsoft docs HIGH; competitor feature analysis from marketing pages MEDIUM; no direct API testing
Architecture HIGH MVVM patterns from Microsoft Learn; PnP Framework auth patterns from official PnP docs; MsalCacheHelper from official MSAL.NET docs
Pitfalls HIGH Critical pitfalls verified via official docs, PnP GitHub issues, and direct audit of existing codebase (CONCERNS.md)

Overall confidence: HIGH

Gaps to address:

  • PnP Provisioning Engine for Teams-connected sites: behavior not fully documented; validate during Phase 4 planning.
  • User cross-site access enumeration via Graph API: multiple possible approaches with different throttling profiles; validate during Phase 5 planning.
  • Graph API volume for duplicate detection at large scale: practical concurrency limits need validation.
  • ScottPlot.WPF XAML integration: WpfPlot binding patterns less documented than WinForms equivalent; validate during Phase 5 planning.

Sources

Primary (HIGH confidence)

Secondary (MEDIUM confidence)


v2.3 Tenant Management & Report Enhancements

Researched: 2026-04-09 Confidence: HIGH

Stack Additions

None. All five features are delivered using existing dependencies:

  • Microsoft.Graph 5.74.0 — app registration, service principal, admin consent, group member resolution
  • PnP.Framework 1.18.0 — Tenant.SetSiteAdmin for auto-ownership
  • BCL .NET 10 — LINQ consolidation, HTML5 <details>/<summary>

Do NOT add Azure.Identity — conflicts with existing MSAL PCA + MsalCacheHelper pattern.

Feature Table Stakes

Feature Table Stakes Differentiators
App Registration Create app + SP + grant roles; guided fallback mandatory Auto-detect admin permissions, single-click register
App Removal Delete app + SP, revoke consent Clear MSAL cache for removed app
Auto-Ownership Tenant.SetSiteAdmin on access denied; global toggle OFF by default Persistent cleanup list, startup warning for pending removals
Group Expansion Resolve members at scan time; HTML5 details/summary transitiveMembers for nested groups; pagination for large groups
Report Consolidation Toggle per-export; merge same-user same-access rows New ConsolidatedUserAccessEntry type (never modify existing)

Critical Pitfalls

  1. App registration requires Application.ReadWrite.All + AppRoleAssignment.ReadWrite.All — MSP app likely doesn't have these consented. Guided fallback is first-class, not a degraded mode.
  2. POST /applications does NOT create service principal — Must be 3-step atomic: create app → create SP → grant roles, with rollback on failure.
  3. Auto-ownership cleanuptry/finally insufficient for hard termination. Need persistent JSON cleanup-pending list + startup warning.
  4. $expand=members caps at ~20 silently — Must use GET /groups/{id}/transitiveMembers?$top=999 with pagination.
  5. Consolidation is a schema change — Must be off by default, opt-in per export.

Suggested Build Order (5 phases, starting at 15)

  1. Phase 15 — Model extensions + PermissionConsolidator (zero API calls, data shapes)
  2. Phase 16 — Report consolidation toggle (first user-visible, pure LINQ)
  3. Phase 17 — Group expansion in HTML reports (Graph at export time, HTML5 details/summary)
  4. Phase 18 — Auto-take ownership (PnP Tenant.SetSiteAdmin, retry once, default OFF)
  5. Phase 19 — App registration + removal (highest blast radius, Entra changes, guided fallback default)

Research Flags

  • Phase 19: Admin consent grant appRole GUIDs need validation against real tenant
  • Phase 17: Confirm GroupMember.Read.All scope availability on MSP app registration

v1.0 research completed: 2026-04-02 v2.2 research synthesized: 2026-04-08 v2.3 research synthesized: 2026-04-09 Ready for roadmap: yes