Skip to content

Architecture & Design

A$PECT UTILS is a Windows setup toolkit with two front ends: a PowerShell app with a WPF (Windows Presentation Foundation) GUI, and a native .NET desktop app. Both read the same JSON configuration files. This document explains the PowerShell architecture, code structure, and how the components work together; the desktop app is covered in Desktop app.

The desktop app in desktop/ is a .NET 8 WPF application with no NuGet dependencies. It targets net8.0-windows, requests administrator rights through app.manifest, and copies config/*.json plus its helper scripts into the build output.

  • Services/ConfigService.cs loads the shared JSON catalogs and tolerates control characters inside upstream script strings.
  • Services/TweakEngine.cs applies tweaks and records registry snapshots in %LocalAppData%\AspectUtils\state\applied-tweaks.json so changes can be undone.
  • Services/PackageManagerService.cs drives WinGet and Chocolatey for install, upgrade, and uninstall.
  • Services/FeatureService.cs and the scripts under desktop/Scripts/ cover Windows features, fixes, and update profiles.
  • Views/ contains the seven pages: Dashboard, Install apps, Tweaks, Fixes, Features, Updates, and Activity log.
  • AppX package management and Win11 Creator remain PowerShell-only in the current version.
┌─────────────────────────────────────────────────────┐
│ A$PECT UTILS GUI │
│ (WPF XAML Interface) │
└──────────────────┬──────────────────────────────────┘
│
┌─────────┴─────────┐
│ │
┌────────▼──────┐ ┌───────▼────────┐
│ Public APIs │ │ Private APIs │
│ (User-facing)│ │ (Internal) │
└───────┬───────┘ └───────┬────────┘
│ │
└────────┬──────────┘
│
┌────────────▼────────────┐
│ Configuration Files │
│ (JSON definitions) │
└────────────┬────────────┘
│
┌────────────▼────────────┐
│ External Tools │
│ (WinGet, Chocolatey) │
└─────────────────────────┘
AspectUtils/
├── Compile.ps1 # Build script that combines all files
├── AspectUtils.ps1 # Compiled output (generated)
├── scripts/
│ ├── main.ps1 # Entry point and GUI initialization
│ └── start.ps1 # Startup logic
├── functions/
│ ├── private/ # Internal helper functions
│ │ ├── Get-WinUtilVariables.ps1
│ │ ├── Install-WinUtilWinget.ps1
│ │ └── ...
│ ├── public/ # User-facing functions
│ │ ├── Initialize-WPFUI.ps1
│ │ └── ...
├── config/ # Shared JSON configuration files
│ ├── applications.json # Application definitions
│ ├── tweaks.json # Tweak definitions
│ ├── feature.json # Windows feature definitions
│ └── preset.json # Preset configurations
├── xaml/
│ └── inputXML.xaml # GUI layout definition
├── desktop/ # Native WPF desktop app (.NET 8)
│ ├── AspectUtils.Desktop.csproj
│ ├── Services/ # Config, tweaks, packages, undo
│ ├── Views/ # Install, Tweaks, Fixes, Features, Updates, Log
│ └── Scripts/ # Fix and update helper scripts
└── docs/ # Documentation

Purpose: Combines all separate script files into a single AspectUtils.ps1 for distribution.

Process:

  1. Reads all function files from /functions/
  2. Includes configuration JSON files
  3. Embeds XAML GUI definition
  4. Combines into a single script
  5. Outputs AspectUtils.ps1

Why: Makes distribution easier (single file) and improves load time.

Purpose: Entry point that manages the run.

Responsibilities:

  • Load configurations
  • Run the headless -Preset and -Config paths
  • Start the interface on a dedicated STA runspace and wait for it
  • Report anything the interface thread failed with, then clean up

The interface itself lives in Start-WinUtilUserInterface, not here. See Threading Model.

Purpose: User-facing functions that implement main features.

Key Functions:

  • Initialize-WPFUI.ps1: Sets up the GUI
  • Invoke-WPFTweak*: Applies system tweaks
  • Invoke-WPFFeature*: Enables Windows features
  • Install-WinUtilProgram*: Installs applications

Naming Convention: Functions start with WPF or Winutil to be loaded into the runspace.

Purpose: Internal helper functions not directly called by users.

Key Functions:

  • Get-WinUtilVariables.ps1: Retrieves UI element references
  • Install-WinUtilWinget.ps1: Ensures WinGet is installed
  • Get-WinUtilCheckBoxes.ps1: Gets checkbox states
  • Invoke-WinUtilCurrentSystem.ps1: Gets system information

Purpose: Define available applications, tweaks, and features declaratively.

Files:

  • applications.json: Application definitions with WinGet/Choco IDs
  • tweaks.json: Registry tweaks and their undo actions
  • feature.json: Windows features that can be enabled/disabled
  • preset.json: Predefined tweak combinations
  • dns.json: DNS provider configurations

Purpose: WPF GUI layout and design.

Structure:

  • Buttons with event handlers
  • TextBoxes for input
  • CheckBoxes for options
  • ListBoxes for selections

The Win11 Creator is a specialized subsystem within Winutil that creates customized Windows 11 ISOs. It operates independently of the main package installation and tweak system.

Core Functions (functions/private/):

  • Invoke-WinUtilISO.ps1: Main orchestrator containing ISO workflow and helper functions

    • Invoke-WinUtilISOBrowse: ISO file selection dialog
    • Invoke-WinUtilISOMountAndVerify: Validates and mounts ISO, verifies it is an official Windows 11 ISO
    • Invoke-WinUtilISOModify: Launches modification in background runspace
    • Invoke-WinUtilISOExport: Builds bootable ISO via oscdimg.exe
    • Invoke-WinUtilISOCheckExistingWork: Recovers incomplete work sessions
    • Invoke-WinUtilISOCleanAndReset: Cleans up temp directories and resets UI
    • Write-WinUtilISOLog: Posts log messages to UI status box and session log
    • Set-WinUtilISOStep: Controls wizard navigation state
    • Get-WinUtilEditionIdFromName: Maps edition display names to setup edition IDs
    • Invoke-WinUtilRobocopy: Copies files with exit code verification
    • Find-WinUtilOscdimg / Get-WinUtilOscdimgPath: Locates or installs oscdimg.exe
  • Invoke-WinUtilISOUSB.ps1: USB drive detection and formatting

    • Invoke-WinUtilISORefreshUSBDrives: Enumerates USB drives
    • Get-WinUtilFreeDriveLetter: Finds available drive letters
    • Invoke-WinUtilISOWriteUSB: Formats USB drive as GPT/FAT32, splits WIM if needed, and copies files
  • Invoke-WinUtilISOScript.ps1: Prepares setup media and customizations

    • Stages AppX removal (19 bloatware packages) into autounattend.xml for first logon
    • Applies 50+ registry tweaks during Windows Setup and first logon; removes scheduled tasks at first logon
    • Triggers OneDrive uninstall during first logon
    • Stages setup script fallbacks to sources\$OEM$\$$\Setup\Scripts\
    • Pins selected edition in autounattend.xml and writes sources\ei.cfg
    • Injects eligible current system drivers into WIM images if enabled (WIM mount is only used for driver servicing)
User selects official Windows 11 ISO
↓
Invoke-WinUtilISOBrowse → OpenFileDialog, validates file size
↓
Invoke-WinUtilISOMountAndVerify
├─ Mount ISO via Mount-DiskImage
├─ Verify install.wim or install.esd exists
├─ Check for "Windows 11" in image metadata
├─ Extract available editions (Home, Pro, Enterprise, etc.)
└─ Store ISO path, drive letter, WIM path, image info in $sync
↓
User optionally enables the Driver Injection checkbox
↓
Invoke-WinUtilISOModify (runs in background runspace)
├─ Create work directory: ~WinUtil_Win11ISO_[timestamp]
├─ Copy ISO contents to disk via robocopy (~5-6 GB)
├─ Invoke-WinUtilISOScript:
│ ├─ Generate autounattend.xml with Windows PE and specialize safeguards
│ ├─ Add first-logon script (19 AppX removals, registry tweaks, OneDrive uninstall)
│ ├─ Pin selected edition index in autounattend.xml (/IMAGE/INDEX)
│ ├─ Stage setup script fallbacks to sources\$OEM$\$$\Setup\Scripts\
│ ├─ Write sources\ei.cfg and remove stale sources\PID.txt
│ └─ If driver injection enabled:
│ ├─ Require install.wim; install.esd cannot accept driver injection
│ ├─ Export current system drivers via DISM
│ ├─ Exclude stale duplicate packages
│ ├─ Inject storage drivers into boot.wim index 2 and eligible drivers into install.wim
│ └─ Validate WIM metadata before and after injection
├─ Dismount source ISO
└─ Report completion, enable export options
↓
Invoke-WinUtilISOExport (user chooses output)
├─ Option 1: Save as ISO
│ ├─ Build bootable ISO via oscdimg.exe (BIOS/UEFI dual-boot)
│ └─ Output: Win11_Modified_[date].iso (close to the source ISO size)
│
└─ Option 2: Write to USB
├─ Format USB as GPT
├─ Create single FAT32 partition (capped at 32 GB)
├─ Split install.wim into .swm files if > 3.8 GB
├─ Reject install.esd files of 4 GB or more
├─ Copy files via robocopy
└─ Output: Bootable USB (minimum 8 GB)
↓
Invoke-WinUtilISOCleanAndReset (optional)
├─ Dismount any open WIM mounts with discard
├─ Delete temp working directory (~10-15 GB)
└─ Reset UI to initial state

ISO Validation:

  • Only accepts official Microsoft Windows 11 ISOs
  • Validates presence of install.wim or install.esd
  • Checks image metadata for “Windows 11” string
  • Rejects custom, modified, or non-Windows 11 ISOs

WIM Metadata Validation:

  • During driver injection, Assert-WinUtilISOWimMetadata validates critical fields (Languages, Installation, Edition, ProductSuite, ProductType) before and after WIM servicing
  • Driver injection stops before export if the required metadata is missing or changes
  • Without driver injection, WinUtil preserves the original installation image and skips WIM metadata validation

Edition Pinning & Setup Fallback:

  • Writes sources\ei.cfg and removes sources\PID.txt so setup does not use mismatched OEM product keys
  • Pins the selected edition index in autounattend.xml (/IMAGE/INDEX)
  • Stages fallback setup scripts under sources\$OEM$\$$\Setup\Scripts\ with UseConfigurationSet enabled

Work Session Recovery:

  • Auto-detects incomplete work from previous sessions
  • Allows resuming the export step without re-running selection and modification
  • Prevents redundant modifications

Modification Safety:

  • Windows PE and specialize set hardware bypass and setup safeguards; WinUtil-PostInstall.ps1 applies additional registry changes at first logon
  • Original ISO never modified; only working copy
  • Logged to the WinUtil session log and the live UI status panel
  • DISM handles image dismount with automatic cleanup and discard on error

The Invoke-WinUtilISOScript function applies 50+ registry tweaks during setup and first logon:

Hardware Bypass:

  • TPM 2.0 check bypass
  • Secure Boot requirement bypass
  • CPU compatibility bypass
  • RAM requirement bypass
  • Storage check bypass

Privacy & Telemetry:

  • Disable advertising ID
  • Disable tailored experiences
  • Disable input personalization
  • Disable speech online privacy
  • Disable cloud content suggestions
  • Disable app suggestion subscriptions
  • Remove CEIP, Appraiser, WaaSMedic, etc.

OOBE & Setup:

  • Enable local account setup
  • Skip Microsoft account requirement
  • Dark mode by default
  • Empty taskbar and Start Menu

Post-Setup Installations:

  • Prevent DevHome auto-installation
  • Prevent new Outlook Mail app installation
  • Prevent Teams auto-installation

System Features:

  • Disable BitLocker and device encryption
  • Disable Chat icon from the Taskbar
  • Disable OneDrive folder backup
  • Disable Copilot
  • Disable Windows Update during OOBE (re-enabled at first login)

Optional Enhancement: When enabled, exports all drivers from the running system and injects them into:

  • install.wim (main OS image), excluding stale duplicates
  • boot.wim index 2 (Windows Setup PE environment), storage controllers only (SCSIAdapter / HDC)

Use Case: Enables offline installation on systems with missing drivers.

  • Temporary working directory: ~10-15 GB
  • Original ISO: 4-6 GB
  • Modified ISO: close to the source ISO size
  • Total needed: ~25 GB for safe operation
User clicks "Install"
↓
Get-WinUtilCheckBoxes → Retrieves selected apps
↓
For each selected app:
↓
Check if WinGet/Choco is installed
↓
Install-WinUtilWinget/Choco (if needed)
↓
Install-WinUtilProgramWinget/Choco → Install app
↓
Update UI with progress
↓
Display completion message
User selects tweaks and clicks "Run Tweaks"
↓
Get-WinUtilCheckBoxes → Get selected tweaks
↓
For each selected tweak:
↓
Load tweak definition from tweaks.json
↓
Invoke-WPFTweak → Apply registry/service changes
↓
Log changes
↓
Store original values (for undo)
↓
Update UI
↓
Display completion
User selects tweaks and clicks "Undo"
↓
Get-WinUtilCheckBoxes → Get selected tweaks
↓
For each tweak:
↓
Retrieve "OriginalState" from tweak definition
↓
Invoke-WPFUndoTweak → Restore original values
↓
Remove from the applied tweaks log
↓
Update UI
config/applications.json
{
"WPFInstall<AppName>": {
"category": "Browsers",
"choco": "googlechrome",
"content": "Google Chrome",
"description": "Google Chrome browser",
"link": "https://chrome.google.com",
"winget": "Google.Chrome"
}
}

Fields:

  • category: Which section in the Install tab
  • content: Display name in GUI
  • description: Tooltip/description text
  • winget: WinGet package ID
  • choco: Chocolatey package name
  • link: Official website
config/tweaks.json
{
"WPFTweaksTelemetry": {
"Content": "Disable Telemetry",
"Description": "Disables Microsoft Telemetry",
"category": "Essential Tweaks",
"panel": "1",
"registry": [
{
"Path": "HKLM:\\SOFTWARE\\Policies\\Microsoft\\Windows\\DataCollection",
"Name": "AllowTelemetry",
"Type": "DWord",
"Value": "0",
"OriginalValue": "1"
}
]
}
}

Fields:

  • Content: Display name
  • Description: What it does
  • category: Essential/Advanced/Customize
  • registry: Registry changes to make
  • registry[].Values: Per-state values for a registry-backed combobox
  • registry[].DefaultValue: Effective value when the registry entry is absent
  • service: Services to change
  • OriginalValue/State: For undo functionality

WinUtil runs on three kinds of thread, and each has one job:

Thread Runspace Responsibility
Main The one the script started in Start the interface, wait for it, surface its errors, clean up
Interface $sync.UIRunspace, a dedicated STA runspace Own the window. Paint and dispatch, nothing else
Workers $sync.runspace, a shared pool Run everything long: installs, tweaks, features, AppX, Win11 Creator

All three are created from the same starting point, New-WinUtilSessionState, which carries $sync, the compiled script’s globals, and every WinUtil function. That is what lets any thread call any helper without the caller injecting function definitions.

Terminal window
# main.ps1 - the interface gets its own thread
$sync.UIRunspace = [runspacefactory]::CreateRunspace($Host, (New-WinUtilSessionState))
$sync.UIRunspace.ApartmentState = "STA"
$sync.UIRunspace.Open()
$uiShell = [powershell]::Create()
$uiShell.Runspace = $sync.UIRunspace
[void]$uiShell.AddScript({ Start-WinUtilUserInterface })
$uiHandle = $uiShell.BeginInvoke()
$uiHandle.AsyncWaitHandle.WaitOne()

Why: the window never blocks on work, and a failure on the interface thread is reported instead of disappearing.

Every long action goes through Start-WinUtilJob, which owns everything a running operation needs: refusing to start while another job runs, the busy flag ($sync.ActiveJob), the progress bar and taskbar item, the boxed console banner, a start/finish/failure line in the log, and restoring the interface in a finally whatever happens.

Invoke-WPFButton decides what counts as a long action. Anything that changes the system gets a job; anything that only changes what the interface is showing runs on the interface thread. That single classification is why no workflow arranges its own progress, banner or busy state.

Terminal window
Start-WinUtilJob -Name "Features" -Description "Installing Windows Features" -Parameters @{
Features = @($sync.selectedFeatures)
} -ScriptBlock {
param($Features)
$total = @($Features).Count
$completed = 0
foreach ($feature in $Features) {
$completed++
Step-WinUtilJob -Status "Installing $feature ($completed/$total)" -Percent ([int](($completed / $total) * 100))
Invoke-WinUtilFeatureInstall $feature
}
}

The body only has to do the work and call Step-WinUtilJob. Anything it throws is caught, logged, and shown on the taskbar as a failure.

Values, not closures: the body is rebuilt inside the worker from its text, so it receives what it needs through -Parameters rather than capturing the caller’s variables.

Events are wired up via XAML element names:

Terminal window
# Get all named elements
$sync.keys | ForEach-Object {
if($sync.$_.GetType().Name -eq "Button") {
$sync.$_.Add_Click({
$button = $sync.$($args[0].Name)
& "Invoke-$($args[0].Name)"
})
}
}

Convention: Button named WPFInstallButton calls function Invoke-WPFInstallButton.

Terminal window
# Check if installed
if (!(Get-Command winget -ErrorAction SilentlyContinue)) {
Install-WinUtilWinget
}
# Install package
winget install --id $app.winget --silent --accept-source-agreements
Terminal window
# Check if installed
if (!(Get-Command choco -ErrorAction SilentlyContinue)) {
Install-WinUtilChoco
}
# Install package
choco install $app.choco -y

A job body does not need its own error handling. Start-WinUtilJob catches whatever the body throws, logs it, and marks the run as failed on the taskbar, so a failure can never leave the interface stuck busy. Only catch inside a body when you have something specific to do first, such as cleaning up a mounted image, and then rethrow.

Terminal window
Write-WinUtilLog -Level "ERROR" -Component "Install" -Message "winget install failed: $($_.Exception.Message)"

Entries and console diagnostics go to the same %LocalAppData%\AspectUtils\logs\aspectutils_<timestamp>.log session file. While Start-Transcript owns that file, Write-WinUtilLog writes through the host so the transcript captures the entry without a competing direct file write.

At startup, Winutil loads all configurations:

Terminal window
# Load JSON configs
$sync.configs = @{}
$sync.configs.applications = Get-Content "config/applications.json" | ConvertFrom-Json
$sync.configs.tweaks = Get-Content "config/tweaks.json" | ConvertFrom-Json
$sync.configs.features = Get-Content "config/feature.json" | ConvertFrom-Json

Sync Hash: $sync hashtable shares state across runspaces.

Controls may only be touched from the thread that owns the window, so background work reaches them through Invoke-WPFUIThread:

Terminal window
Invoke-WPFUIThread -Parameters @{ Count = $installed.Count } -ScriptBlock {
param($Count)
$sync.WPFselectedAppsButton.Content = "Selected Apps: $Count"
}

Add -Async to post the update instead of waiting for it. Step-WinUtilJob and the Win11 Creator status log use that so a per-item update never stalls the worker.

Values, not closures: the body is rebuilt inside the interface runspace, so it takes what it needs through -Parameters. Handing over a scriptblock from a worker instead would keep that worker’s session state, which loses the caller’s variables on an async post and costs roughly twenty times as much per command - enough to turn a checkbox refresh into a visible freeze.

  1. Edit config/applications.json:
config/applications.json
{
"WPFInstallNewApp": {
"category": "Utilities",
"content": "New App",
"description": "Description of new app",
"winget": "Publisher.AppName",
"choco": "appname"
}
}
  1. Recompile: .\Compile.ps1
  2. The app appears automatically in the Install tab
  1. Edit config/tweaks.json:
config/tweaks.json
{
"WPFTweaksNewTweak": {
"Content": "New Tweak",
"Description": "What it does",
"category": "Essential Tweaks",
"registry": [
{
"Path": "HKLM:\\Path\\To\\Key",
"Name": "ValueName",
"Type": "DWord",
"Value": "1",
"OriginalValue": "0"
}
]
}
}
  1. Recompile: .\Compile.ps1
  2. Tweak appears in the Tweaks tab
  1. Create file in functions/public/ or functions/private/:
functions/public/Invoke-WPFNewFeature.ps1
function Invoke-WPFNewFeature {
<#
.SYNOPSIS
Does something new
#>
# Implementation
}
  1. File naming must include “WPF” or “Winutil” to load
  2. Recompile: .\Compile.ps1
Terminal window
# Compile and run with -run flag
.\Compile.ps1 -run

Tests are in /pester/:

  • configs.Tests.ps1: Validates JSON configurations
  • functions.Tests.ps1: Tests PowerShell functions

Run tests:

Terminal window
Install-Module -Name Pester -RequiredVersion 5.8.0 -Scope CurrentUser -Force -SkipPublisherCheck
Import-Module Pester -RequiredVersion 5.8.0 -Force
Invoke-Pester -Path 'pester/*.Tests.ps1' -Output Detailed -CI
Terminal window
.\Compile.ps1

Outputs AspectUtils.ps1 in the root directory.

  1. Tag release in Git
  2. GitHub Actions builds and uploads AspectUtils.ps1
  3. Release appears on GitHub Releases
  4. Users download the release asset or run it from the repository

Required:

  • PowerShell 5.1+
  • .NET Framework 4.5+
  • Windows 11

Optional (auto-installed):

  • WinGet (Windows Package Manager)
  • Chocolatey

Optimization Strategies:

  • Lazy-load configurations (only when needed)
  • Use runspaces for long operations
  • Cache expensive lookups
  • Minimize registry reads/writes
  • Batch operations when possible

Safety Measures:

  • All operations logged
  • Registry backups for undo
  • No credential storage
  • Open source (auditable)
  • Digitally signed (future)

Code Standards:

  • Use proper PowerShell cmdlet naming (Verb-Noun)
  • Include comment-based help
  • Follow existing code style
  • Test thoroughly before PR
  • Document significant changes

File Naming:

  • Public functions: Invoke-WPF*.ps1 or Invoke-Winutil*.ps1
  • Private functions: Get-WinUtil*.ps1 or verb-WinUtil*.ps1`
  • Must include “WPF” or “Winutil” to load

Roadmap Considerations:

  • Plugin system for community extensions
  • Config import/export
  • Cloud sync for configurations
  • Enhanced logging dashboard
  • Modular compilation (choose features)

Last Updated: January 2026 Maintainers: A$PECT UTILS contributors