docs: complete project research for SharePoint Toolbox rewrite
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>
This commit is contained in:
581
.planning/research/ARCHITECTURE.md
Normal file
581
.planning/research/ARCHITECTURE.md
Normal file
@@ -0,0 +1,581 @@
|
||||
# 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. `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<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:**
|
||||
```csharp
|
||||
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:**
|
||||
```csharp
|
||||
// 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:**
|
||||
```csharp
|
||||
// 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.*`) — use `IProgress<T>` 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<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](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*
|
||||
Reference in New Issue
Block a user