# 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*
