Research covers stack (NET10/WPF/PnP.Framework), features (v1 parity + v1.x differentiators), architecture (MVVM four-layer pattern), and pitfalls (10 critical pitfalls all addressed in foundation phase). SUMMARY.md synthesizes findings with phase-structured roadmap implications. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
29 KiB
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<T>, 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.
DataContextset 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:
public partial class PermissionsViewModel : ObservableObject
{
private readonly IPermissionsService _permissionsService;
[ObservableProperty]
private bool _isRunning;
[ObservableProperty]
private string _statusMessage = string.Empty;
[ObservableProperty]
private ObservableCollection<PermissionEntry> _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<string>(msg => StatusMessage = msg));
Results = new ObservableCollection<PermissionEntry>(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:
public class SessionManager
{
private readonly ConcurrentDictionary<string, TenantSession> _sessions = new();
public async Task<TenantSession> 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<T> + CancellationToken for All Long Operations
What: Every service method that calls SharePoint accepts IProgress<OperationProgress> and CancellationToken. The ViewModel creates Progress<T> (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<T> captures SynchronizationContext on creation — must be created on the UI thread (i.e., inside the ViewModel, not inside the service).
Example:
// In ViewModel (UI thread context):
var cts = new CancellationTokenSource();
CancelCommand = new RelayCommand(() => cts.Cancel());
var progress = new Progress<OperationProgress>(p => StatusMessage = p.Message);
// In Service (any thread):
public async Task<IList<PermissionEntry>> ScanAsync(
string siteUrl,
CancellationToken ct,
IProgress<OperationProgress> 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:
// App.xaml.cs
protected override void OnStartup(StartupEventArgs e)
{
_host = Host.CreateDefaultBuilder()
.ConfigureServices(services =>
{
// Core services (singletons)
services.AddSingleton<ISettingsService, SettingsService>();
services.AddSingleton<ILocalizationService, LocalizationService>();
services.AddSingleton<SessionManager>();
services.AddSingleton<IAuthService, AuthService>();
services.AddSingleton<IDialogService, DialogService>();
// Feature services (transient — no shared state)
services.AddTransient<IPermissionsService, PermissionsService>();
services.AddTransient<IStorageService, StorageService>();
services.AddTransient<ISearchService, SearchService>();
// ViewModels
services.AddTransient<MainWindowViewModel>();
services.AddTransient<PermissionsViewModel>();
services.AddTransient<StorageViewModel>();
// Views
services.AddSingleton<MainWindow>();
})
.Build();
_host.Start();
var mainWindow = _host.Services.GetRequiredService<MainWindow>();
mainWindow.DataContext = _host.Services.GetRequiredService<MainWindowViewModel>();
mainWindow.Show();
}
Data Flow
SharePoint Operation Request Flow
User clicks "Run" button
↓
View command binding triggers AsyncRelayCommand.ExecuteAsync()
↓
ViewModel validates inputs → creates CancellationTokenSource + Progress<T>
↓
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<T>.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<TModel> 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<T> → 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<T> |
| 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.*) — useIProgress<T>for UI feedback - ViewModels must not instantiate
ClientContextorAuthenticationManagerdirectly — only viaIAuthService - SessionManager is the only class that holds
ClientContextobjects — 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<T> parameter. Progress<T> 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 — HIGH confidence
- AsyncRelayCommand - CommunityToolkit — HIGH confidence
- PnP Framework AuthenticationManager API — HIGH confidence
- PnP Framework Getting Started — HIGH confidence
- Acquire and cache tokens with MSAL - Microsoft Learn — HIGH confidence
- WPF Development Best Practices 2024 - MESCIUS — MEDIUM confidence
- Modern WPF Development: MVVM and Prism - Einfochips — MEDIUM confidence
- Async Programming Patterns for MVVM - Microsoft Learn — HIGH confidence
Architecture research for: C#/WPF SharePoint Online administration desktop tool Researched: 2026-04-02