# Architecture Research **Domain:** C#/WPF SharePoint Online Administration Desktop Tool **Researched:** 2026-04-02 **Confidence:** HIGH ## Standard Architecture ### System Overview ``` ┌─────────────────────────────────────────────────────────────────────┐ │ PRESENTATION LAYER │ │ ┌──────────────┐ ┌─────────────────────────────────────────────┐ │ │ │ MainWindow │ │ Feature Views (XAML) │ │ │ │ Shell.xaml │ │ Permissions │ Storage │ Search │ Templates │ │ │ │ │ │ Duplicates │ Bulk │ Reports │ Settings │ │ │ └──────┬───────┘ └──────────────────────┬────────────────────┘ │ │ │ DataContext binding │ DataContext binding │ ├─────────┴─────────────────────────────────┴────────────────────────┤ │ VIEWMODEL LAYER │ │ ┌─────────────┐ ┌──────────────────────────────────────────────┐ │ │ │ MainWindow │ │ Feature ViewModels │ │ │ │ ViewModel │ │ PermissionsVM │ StorageVM │ SearchVM │ │ │ │ (nav/shell)│ │ TemplatesVM │ BulkOpsVM │ DuplicatesVM │ │ │ └──────┬──────┘ └───────────────────────┬──────────────────────┘ │ │ │ ICommand, ObservableProperty │ AsyncRelayCommand │ ├─────────┴─────────────────────────────────┴────────────────────────┤ │ SERVICE LAYER │ │ ┌────────────────┐ ┌─────────────────┐ ┌──────────────────────┐ │ │ │ AuthService │ │ SharePoint │ │ Cross-Cutting │ │ │ │ SessionManager │ │ Feature Services │ │ Services │ │ │ │ TenantSession │ │ PermissionsService│ │ ReportExportService │ │ │ │ │ │ StorageService │ │ LocalizationService │ │ │ │ │ │ SearchService │ │ DialogService │ │ │ │ │ │ TemplateService │ │ SettingsService │ │ │ └────────┬───────┘ └────────┬────────┘ └──────────────────────┘ │ │ │ ClientContext │ IProgress, CancellationToken │ ├───────────┴────────────────────┴────────────────────────────────────┤ │ INFRASTRUCTURE / INTEGRATION LAYER │ │ ┌──────────────────┐ ┌───────────────────┐ ┌──────────────────┐ │ │ │ PnP Framework │ │ Microsoft Graph │ │ Local Storage │ │ │ │ AuthManager │ │ GraphServiceClient │ │ JSON Files │ │ │ │ ClientContext │ │ (Graph operations) │ │ Profiles │ │ │ │ (CSOM ops) │ │ │ │ Templates │ │ │ └──────────────────┘ └───────────────────┘ └──────────────────┘ │ └─────────────────────────────────────────────────────────────────────┘ ``` ### Component Responsibilities | Component | Responsibility | Typical Implementation | |-----------|----------------|------------------------| | MainWindow Shell | Tab navigation, tenant selector, app chrome, log panel | XAML with TabControl or navigation frame | | Feature Views | User input forms, result grids, progress indicators | UserControl XAML, zero code-behind | | Feature ViewModels | Commands, observable state, orchestrates services | ObservableObject subclass, AsyncRelayCommand | | AuthService / SessionManager | Multi-tenant session lifecycle, token cache, active tenant state | Singleton, MSAL token cache per tenant | | TenantSession | Per-tenant PnP ClientContext + auth token | Immutable record, created by AuthService | | SharePoint Feature Services | Domain logic that calls PnP Framework or Graph | Stateless class, injectable, cancellable | | ReportExportService | HTML/CSV generation from result models | Stateless, template-based string builder | | LocalizationService | Key-based EN/FR translation, dynamic language switch | Singleton, loads lang/*.json, INotifyPropertyChanged | | SettingsService | Read/write JSON settings, profiles, templates | Singleton, file I/O wrapped in async | | DialogService | Open files, show message boxes, pick folders | Interface + WPF implementation, testable | --- ## Recommended Project Structure ``` SharepointToolbox/ ├── App.xaml # Application entry, DI container bootstrap ├── App.xaml.cs # Host builder, service registration │ ├── Core/ # Domain models — no WPF dependencies │ ├── Models/ │ │ ├── PermissionEntry.cs │ │ ├── StorageMetrics.cs │ │ ├── SiteTemplate.cs │ │ ├── TenantProfile.cs │ │ └── SearchResult.cs │ ├── Interfaces/ │ │ ├── IAuthService.cs │ │ ├── IPermissionsService.cs │ │ ├── IStorageService.cs │ │ ├── ISearchService.cs │ │ ├── ITemplateService.cs │ │ ├── IBulkOpsService.cs │ │ ├── IDuplicateService.cs │ │ ├── IReportExportService.cs │ │ ├── ISettingsService.cs │ │ ├── ILocalizationService.cs │ │ └── IDialogService.cs │ └── Exceptions/ │ ├── SharePointConnectionException.cs │ └── AuthenticationException.cs │ ├── Services/ # Business logic + infrastructure │ ├── Auth/ │ │ ├── AuthService.cs # PnP AuthenticationManager wrapper │ │ ├── SessionManager.cs # Multi-tenant session store │ │ └── TenantSession.cs # Per-tenant PnP ClientContext holder │ ├── SharePoint/ │ │ ├── PermissionsService.cs # Recursive permission scanning │ │ ├── StorageService.cs # Storage metric traversal │ │ ├── SearchService.cs # KQL-based search via PnP/Graph │ │ ├── TemplateService.cs # Capture & apply site templates │ │ ├── DuplicateService.cs # File/folder duplicate detection │ │ └── BulkOpsService.cs # Transfer, site creation, member add │ ├── Reporting/ │ │ ├── HtmlReportService.cs # Self-contained HTML + JS reports │ │ └── CsvExportService.cs # CSV export │ ├── LocalizationService.cs # EN/FR key-value translations │ ├── SettingsService.cs # JSON profiles, templates, settings │ └── DialogService.cs # WPF dialog abstractions │ ├── ViewModels/ # WPF-aware but UI-framework-agnostic │ ├── MainWindowViewModel.cs # Shell nav, tenant switcher, log │ ├── Permissions/ │ │ └── PermissionsViewModel.cs │ ├── Storage/ │ │ └── StorageViewModel.cs │ ├── Search/ │ │ └── SearchViewModel.cs │ ├── Templates/ │ │ └── TemplatesViewModel.cs │ ├── Duplicates/ │ │ └── DuplicatesViewModel.cs │ ├── BulkOps/ │ │ └── BulkOpsViewModel.cs │ └── Settings/ │ └── SettingsViewModel.cs │ ├── Views/ # XAML — no business logic │ ├── MainWindow.xaml │ ├── Permissions/ │ │ └── PermissionsView.xaml │ ├── Storage/ │ │ └── StorageView.xaml │ ├── Search/ │ │ └── SearchView.xaml │ ├── Templates/ │ │ └── TemplatesView.xaml │ ├── Duplicates/ │ │ └── DuplicatesView.xaml │ ├── BulkOps/ │ │ └── BulkOpsView.xaml │ └── Settings/ │ └── SettingsView.xaml │ ├── Controls/ # Reusable WPF controls │ ├── TenantSelectorControl.xaml │ ├── LogPanelControl.xaml │ ├── ProgressOverlayControl.xaml │ └── StorageChartControl.xaml # LiveCharts2 wrapper │ ├── Converters/ # IValueConverter implementations │ ├── BytesToStringConverter.cs │ ├── BoolToVisibilityConverter.cs │ └── PermissionColorConverter.cs │ ├── Resources/ # Styles, brushes, theme │ ├── Styles.xaml │ └── Colors.xaml │ ├── Lang/ # Language files │ ├── en.json │ └── fr.json │ └── Infrastructure/ └── Behaviors/ # XAML attached behaviors (no code-behind workaround) └── ScrollToBottomBehavior.cs ``` ### Structure Rationale - **Core/**: Pure C# — no WPF references. Interfaces here make services testable. Models are plain data classes. - **Services/**: All domain logic and I/O. Injected via constructor DI. No static state. - **ViewModels/**: Mirror the feature structure. Depend on service interfaces, never on concrete implementations. - **Views/**: XAML-only. No logic. `DataContext` set by DI or ViewModelLocator pattern at startup. - **Controls/**: Reusable UI widgets that encapsulate chart, log, and progress concerns. --- ## Architectural Patterns ### Pattern 1: ObservableObject + AsyncRelayCommand (CommunityToolkit.Mvvm) **What:** Use `ObservableObject` as base class for all ViewModels. Use `[ObservableProperty]` source-gen attribute for bindable properties. Use `AsyncRelayCommand` (with `CancellationToken`) for all SharePoint operations. **When to use:** All ViewModels. This is the standard pattern for .NET 8 + WPF. **Trade-offs:** Source generators require C# 10+. Generated partial class syntax is unfamiliar at first but eliminates 80% of boilerplate. **Example:** ```csharp public partial class PermissionsViewModel : ObservableObject { private readonly IPermissionsService _permissionsService; [ObservableProperty] private bool _isRunning; [ObservableProperty] private string _statusMessage = string.Empty; [ObservableProperty] private ObservableCollection _results = new(); public IAsyncRelayCommand RunReportCommand { get; } public PermissionsViewModel(IPermissionsService permissionsService) { _permissionsService = permissionsService; RunReportCommand = new AsyncRelayCommand(RunReportAsync, allowConcurrentExecutions: false); } private async Task RunReportAsync(CancellationToken cancellationToken) { IsRunning = true; StatusMessage = "Scanning permissions..."; try { var results = await _permissionsService.ScanAsync( SiteUrl, cancellationToken, new Progress(msg => StatusMessage = msg)); Results = new ObservableCollection(results); } finally { IsRunning = false; } } } ``` ### Pattern 2: Multi-Tenant Session Manager **What:** A singleton `SessionManager` holds a dictionary of `TenantSession` objects keyed by tenant URL. When the user selects a tenant profile, the session is reused if still valid (MSAL token cache handles token refresh). No re-authentication unless the token is expired and silent refresh fails. **When to use:** Every SharePoint service operation resolves `IAuthService.GetSessionAsync(tenantUrl)` before calling PnP Framework. **Trade-offs:** MSAL token cache must be persisted across app restarts for seamless reconnect. For interactive login, MSAL `PublicClientApplicationBuilder` with `WithParentActivityOrWindow` is required on Windows to avoid a blank browser window. **Example:** ```csharp public class SessionManager { private readonly ConcurrentDictionary _sessions = new(); public async Task GetOrCreateSessionAsync( TenantProfile profile, CancellationToken ct) { if (_sessions.TryGetValue(profile.TenantUrl, out var session) && !session.IsExpired) return session; var authManager = new PnP.Framework.AuthenticationManager( profile.ClientId, openBrowserCallback: url => Process.Start(new ProcessStartInfo(url) { UseShellExecute = true })); var ctx = await authManager.GetContextAsync(profile.TenantUrl); var newSession = new TenantSession(profile, ctx, authManager); _sessions[profile.TenantUrl] = newSession; return newSession; } } ``` ### Pattern 3: IProgress\ + CancellationToken for All Long Operations **What:** Every service method that calls SharePoint accepts `IProgress` and `CancellationToken`. The ViewModel creates `Progress` (which marshals callbacks to the UI thread automatically) and `CancellationTokenSource`. **When to use:** All SharePoint service methods. This replaces the PowerShell runspace + timer polling pattern from the existing app. **Trade-offs:** `Progress` captures SynchronizationContext on creation — must be created on the UI thread (i.e., inside the ViewModel, not inside the service). **Example:** ```csharp // In ViewModel (UI thread context): var cts = new CancellationTokenSource(); CancelCommand = new RelayCommand(() => cts.Cancel()); var progress = new Progress(p => StatusMessage = p.Message); // In Service (any thread): public async Task> ScanAsync( string siteUrl, CancellationToken ct, IProgress progress) { progress.Report(new OperationProgress("Connecting...")); using var ctx = await _sessionManager.GetOrCreateSessionAsync(..., ct); // ... recursive scanning ... ct.ThrowIfCancellationRequested(); progress.Report(new OperationProgress($"Found {results.Count} entries")); return results; } ``` ### Pattern 4: Messenger for Cross-ViewModel Events **What:** Use `CommunityToolkit.Mvvm.Messaging.WeakReferenceMessenger` for decoupled communication between ViewModels (e.g., "tenant switched" notifies all feature VMs to reset state, "log entry added" updates the log panel ViewModel). **When to use:** When two ViewModels need to communicate without direct reference (shell ↔ feature VMs, service callbacks ↔ log panel). **Trade-offs:** Weak references mean recipients must be alive (held by DI container). Don't use for per-request data passing — use method return values for that. ### Pattern 5: Dependency Injection via Microsoft.Extensions.Hosting **What:** Bootstrap the app with `Host.CreateDefaultBuilder()` in `App.xaml.cs`. Register all services, ViewModels, and the main window in the DI container. Use constructor injection everywhere — no service locator anti-pattern. **Example:** ```csharp // App.xaml.cs protected override void OnStartup(StartupEventArgs e) { _host = Host.CreateDefaultBuilder() .ConfigureServices(services => { // Core services (singletons) services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); // Feature services (transient — no shared state) services.AddTransient(); services.AddTransient(); services.AddTransient(); // ViewModels services.AddTransient(); services.AddTransient(); services.AddTransient(); // Views services.AddSingleton(); }) .Build(); _host.Start(); var mainWindow = _host.Services.GetRequiredService(); mainWindow.DataContext = _host.Services.GetRequiredService(); mainWindow.Show(); } ``` --- ## Data Flow ### SharePoint Operation Request Flow ``` User clicks "Run" button ↓ View command binding triggers AsyncRelayCommand.ExecuteAsync() ↓ ViewModel validates inputs → creates CancellationTokenSource + Progress ↓ ViewModel calls IFeatureService.ScanAsync(params, ct, progress) ↓ Service calls SessionManager.GetOrCreateSessionAsync(profile, ct) ↓ SessionManager checks cache → reuses token or triggers interactive login ↓ Service executes PnP Framework / Graph SDK calls (async, awaited) ↓ Service reports incremental progress → Progress.Report() → UI thread ↓ Service returns result collection to ViewModel ↓ ViewModel updates ObservableCollection → WPF binding refreshes DataGrid ↓ ViewModel sets IsRunning = false → progress overlay hides ``` ### Authentication & Session Flow ``` User selects tenant profile from dropdown ↓ MainWindowViewModel calls SessionManager.SetActiveProfile(profile) ↓ SessionManager publishes TenantChangedMessage via WeakReferenceMessenger ↓ All feature ViewModels receive message → reset their state/results ↓ On first operation: SessionManager.GetOrCreateSessionAsync() ↓ [Cache hit: token valid] → return existing ClientContext immediately [Cache miss / expired] → PnP AuthManager.GetContextAsync() ↓ MSAL silent token refresh attempt ↓ [Silent fails] → open browser for interactive login ↓ User authenticates → token cached by MSAL ↓ ClientContext returned to caller ``` ### Report Export Flow ``` Service returns List to ViewModel ↓ User clicks "Export CSV" or "Export HTML" ↓ ViewModel calls IReportExportService.ExportAsync(results, format, outputPath) ↓ ReportExportService generates file (string building, no blocking I/O on UI thread) ↓ ViewModel calls IDialogService.OpenFile(outputPath) to auto-open result ``` ### State Management ``` AppState (DI-managed singletons): SessionManager → active profile, tenant sessions dict SettingsService → user prefs, data folder, profiles list LocalizationService → current language, translation dict Per-Operation State (ViewModel-local): ObservableCollection → bound to DataGrid CancellationTokenSource → cancel button binding IsRunning (bool) → progress overlay binding StatusMessage (string) → progress label binding ``` --- ## Component Boundaries ### What Communicates With What | Boundary | Communication Method | Direction | Notes | |----------|---------------------|-----------|-------| | View ↔ ViewModel | WPF data binding (two-way for inputs, one-way for results) | Both | No code-behind | | ViewModel ↔ Service | Constructor-injected interface, async method call | VM → Service | Services return Task\ | | ViewModel ↔ ViewModel | WeakReferenceMessenger messages | Broadcast | Tenant switch, log events | | Service ↔ SessionManager | `GetOrCreateSessionAsync()` | Service → SessionMgr | Every SharePoint call | | SessionManager ↔ PnP Framework | `AuthenticationManager.GetContextAsync()` | SessionMgr → PnP | On cache miss only | | Service ↔ Graph SDK | `GraphServiceClient` method calls | Service → Graph | For Graph-only operations | | SettingsService ↔ FileSystem | `System.Text.Json` + `File.ReadAllText/WriteAllText` | Both | Async I/O | | LocalizationService ↔ Views | XAML binding to translated string properties | Service → View | Via singleton binding | ### What Must NOT Cross Boundaries - Views must not call services directly — all via ViewModel commands - Services must not reference any WPF types (`System.Windows.*`) — use `IProgress` for UI feedback - ViewModels must not instantiate `ClientContext` or `AuthenticationManager` directly — only via `IAuthService` - SessionManager is the only class that holds `ClientContext` objects — services receive them per-operation --- ## Build Order (Dependency Graph) The following reflects the order components can be built because later items depend on earlier ones: ``` Phase 1: Foundation └── Core/Models/* (no dependencies) └── Core/Interfaces/* (no dependencies) └── Core/Exceptions/* (no dependencies) Phase 2: Infrastructure Services └── SettingsService (depends on Core models) └── LocalizationService (depends on lang files) └── DialogService (depends on WPF — implement last in phase) └── AuthService / SessionManager (depends on PnP Framework NuGet) Phase 3: Feature Services (depend on Auth + Core) └── PermissionsService └── StorageService └── SearchService └── TemplateService └── DuplicateService └── BulkOpsService Phase 4: Reporting (depends on Feature Services output models) └── HtmlReportService └── CsvExportService Phase 5: ViewModels (depend on service interfaces) └── MainWindowViewModel (shell, nav, tenant selector) └── Feature ViewModels (Permissions, Storage, Search, Templates, Duplicates, BulkOps) └── SettingsViewModel Phase 6: Views + App Bootstrap (depend on ViewModels + DI) └── XAML Views (bind to ViewModels) └── Controls (TenantSelector, LogPanel, Charts) └── App.xaml.cs DI container wiring ``` --- ## Scaling Considerations This is a local desktop tool with a single user. "Scaling" means handling larger SharePoint tenants, not more users. | Concern | Approach | |---------|----------| | Large site collections (1000+ sites) | Async streaming with early cancellation; paginated PnP calls; virtual DataGrid | | Deep permission hierarchies | Configurable scan depth; user can limit scope to top-level only | | Large file search results | Server-side KQL filtering first, client-side regex only as secondary pass | | Multiple simultaneous operations | Each ViewModel has its own CancellationTokenSource; operations are isolated | | Session token expiry during long scan | MSAL silent refresh + retry on 401; surface error to user if re-auth needed | --- ## Anti-Patterns ### Anti-Pattern 1: `Dispatcher.Invoke` in Services **What people do:** Call `Application.Current.Dispatcher.Invoke()` inside service classes to update UI state. **Why it's wrong:** Couples service layer to WPF, makes services untestable, causes deadlocks if called from wrong thread. **Do this instead:** Service accepts `IProgress` parameter. `Progress` marshals to UI thread automatically via the captured SynchronizationContext. ### Anti-Pattern 2: Giant "God ViewModel" **What people do:** Create one MainViewModel with all feature logic, mirroring the monolithic PowerShell script. **Why it's wrong:** Replicates the exact problem being solved. Hard to navigate, hard to test, merge conflicts on every change. **Do this instead:** One ViewModel per feature tab. MainWindowViewModel owns only shell navigation, active tenant, and log state. ### Anti-Pattern 3: Storing ClientContext as a Long-Lived Static **What people do:** Cache `ClientContext` in a static field for reuse. **Why it's wrong:** `ClientContext` is not thread-safe and has an auth token that expires. Static makes it impossible to manage per-tenant. **Do this instead:** `SessionManager` manages ClientContext lifetime. Services request a context per operation. PnP Framework handles token refresh. ### Anti-Pattern 4: Blocking Async on Sync Context **What people do:** Call `.Result` or `.Wait()` on Tasks inside WPF event handlers to avoid `async void`. **Why it's wrong:** Deadlocks the WPF SynchronizationContext. The UI freezes permanently. **Do this instead:** Use `async void` only for top-level event handlers (acceptable in WPF), or bind all user actions to `AsyncRelayCommand`. ### Anti-Pattern 5: Silent Catch Blocks (porting the existing bug) **What people do:** Wrap PnP calls in `catch {}` or `catch { /* ignore */ }` to prevent crashes. **Why it's wrong:** The existing PowerShell app has 38 such blocks — they produce silent failures, missing data, and phantom "success" states. **Do this instead:** Catch specific exceptions (`SharePointException`, `MicrosoftIdentityException`). Log with full stack trace via `ILogger`. Surface user-visible error message via ViewModel's `ErrorMessage` property. --- ## Integration Points ### External Services | Service | Integration Pattern | Library | Notes | |---------|---------------------|---------|-------| | SharePoint Online (CSOM) | PnP Framework `ClientContext` | `PnP.Framework` NuGet | Use for permissions, storage, templates, bulk ops | | SharePoint Search | PnP Framework `SearchRequest` | `PnP.Framework` NuGet | KQL queries; paginated | | Microsoft Graph | `GraphServiceClient` | `Microsoft.Graph` NuGet | Use for user/group lookups, Teams data | | Azure AD / MSAL | `PublicClientApplication` via PnP `AuthenticationManager` | Built into `PnP.Framework` | Interactive browser login; token cache callback | | WPF Charts | `LiveCharts2` or `OxyPlot.Wpf` | NuGet | Storage metrics visualization; LiveCharts2 preferred for richer WPF binding | ### Internal Boundaries | Boundary | Communication | Notes | |----------|---------------|-------| | SessionManager ↔ Feature Services | `TenantSession` passed per operation | Services do not store sessions | | LocalizationService ↔ XAML | Singleton bound via `StaticResource`; properties fire `INotifyPropertyChanged` on language switch | All UI text goes through this | | ReportExportService ↔ ViewModels | Called after operation completes; returns file path | Self-contained HTML with embedded JS/CSS | | SettingsService ↔ all singletons | Read at startup; written on change | JSON format must match existing `Sharepoint_Settings.json` schema for migration | --- ## Sources - [Introduction to MVVM Toolkit - Microsoft Learn](https://learn.microsoft.com/en-us/dotnet/communitytoolkit/mvvm/) — HIGH confidence - [AsyncRelayCommand - CommunityToolkit](https://learn.microsoft.com/en-us/dotnet/communitytoolkit/mvvm/asyncrelaycommand) — HIGH confidence - [PnP Framework AuthenticationManager API](https://pnp.github.io/pnpframework/api/PnP.Framework.AuthenticationManager.html) — HIGH confidence - [PnP Framework Getting Started](https://pnp.github.io/pnpframework/using-the-framework/readme.html) — HIGH confidence - [Acquire and cache tokens with MSAL - Microsoft Learn](https://learn.microsoft.com/en-us/entra/identity-platform/msal-acquire-cache-tokens) — HIGH confidence - [WPF Development Best Practices 2024 - MESCIUS](https://medium.com/mesciusinc/wpf-development-best-practices-for-2024-9e5062c71350) — MEDIUM confidence - [Modern WPF Development: MVVM and Prism - Einfochips](https://www.einfochips.com/blog/modern-wpf-development-leveraging-mvvm-and-prism-for-enterprise-app/) — MEDIUM confidence - [Async Programming Patterns for MVVM - Microsoft Learn](https://learn.microsoft.com/en-us/archive/msdn-magazine/2014/april/async-programming-patterns-for-asynchronous-mvvm-applications-commands) — HIGH confidence --- *Architecture research for: C#/WPF SharePoint Online administration desktop tool* *Researched: 2026-04-02*