PowerShell 7 has long since emancipated itself from its image as a pure “scripting tool”. Thanks to open source, the leap to .NET Core and now .NET 9 (since version 7.5), you now get a cross-platform automation, scripting and even development environment in one package. For us system integrators, this opens up two highly exciting playgrounds:
- Build reusable modules that can be cleanly versioned and reliably distributed in a team.
- Build rich desktop tools with WPF front-end without having to use Visual Studio C#.
The article will guide you through all the major innovations, explain best practices from customer projects and show stumbling blocks so that you can set up your next PowerShell projects robustly, maintainably and future-proof.
1 – What’s new in PowerShell
7.5 GA – Stability, Fine-Tuning, Performance
- .NET 9: Current foundation + 18 months of support – PowerShell 7.5 relies on .NET 9.0.1 and receives 18 months of maintenance; 7.4 remains as LTS until Nov 2026
- New cmdlets –
ConvertTo‑CliXml/ConvertFrom‑CliXmlallow you to serialize or restore objects from a pure XML string – handy for quick IPC scenarios. - PSResourceGet 1.1.0 replaces PowerShellGet as the default and supports authenticated repos and container feeds, among other things.
- PSReadLine 2.3.6 brings better predictive IntelliSense and more stable history search.
- Error with proposed solution – the Concise error view now
RecommendedActionshows , so that a possible fix is displayed to you immediately. - Tab-Completion 2.0 – Tilde expansion(
~) now also works under Windows, parameters such as‑Verbor‑Scopeprovide context suggestions,New‑ItemPropertyknows its‑PropertyType. - Performance kick: The infamous one on arrays has been massively optimized and even
List<T>.Add()beats in many scenarios+=in 7.5.
In addition, there are dozens of detail improvements:
– Remove‑Item shows progress bar, Start‑Process ‑PassThru returns the ExitCode correctly, Test‑Json optionally ignores comments, ConvertFrom‑Json gets ‑DateKind, Get‑Process ‑IncludeUserName works without admin tokens and much more.
7.6 Preview – A look into the future
- ThreadJob becomes Microsoft.PowerShell.ThreadJob – new package, old alias remains as a proxy for backwards compatible scripts.
- Multi-Path-Join-Path –
‑ChildPathaccepts arrays; watch out for existing scripts. - Further tab completion improvements, bug fixes and minor breaking changes will be added on an ongoing basis; productive 7.5 remains the recommendation for the time being.
2 – Why modules?
A loose hodgepodge of individual .ps1 files only scales as long as you work on it alone – larger teams quickly lose track of this. A module (
Manifest & SemVer – trusted
In the module manifest (MyTools.psd1) you not only maintain author, copyright and minimum PSEdition, but above all a semantic version number (ModuleVersion = '1.4.2'). Strictly follow [SemVer 2.0.0]; a major jump signals breaking changes, minor stands for backwards compatible features and patch fixes bugs. CI pipelines can thus check exactly whether an upgrade is permissible without breaking down productive scripts.
Project Structure & Naming Conventions
- Folder layout:
Public\for exported cmdlets,Private\for helpers,Classes\for DSC or OOP code; Autoload is done viaExport‑ModuleMember. - cmdlet names follow the verb-noun scheme of the official verb list; check it by
Get‑Verb. - Alias trap: aliases only for typing laziness in the terminal; in the module code itself. PSScriptAnalyzer supports these conventions with rules such as
PSUseApprovedVerbs. Microsoft LearnMicrosoft Learn
Autoloading & Dependency Management
As soon as a script Get‑MyDisk is called that does not exist in the current scope, PowerShell automatically $env:PSModulePathunloads the corresponding module. In the manifest, declare RequiredModules – including minimal version – and ExternalModuleDependencies for optional plug-ins. When packing , PSResourceGet checks this list; missing packages are reinstalled or a publish process fails. Microsoft LearnGitHub
Distribution: Public, Private, Hybrid
- Public:
Publish‑PSResource -Repository PSGallery -ApiKey $keyUnloads your package into the PowerShell Gallery. Don’t forget to set tags (Cloud,M365,WPF, ) and compatible platforms. - Internal: A NuGet feed (Artifactory, ProGet, Azure DevOps Artifacts) or even a simpler FileShare repository is integrated via .
Register‑PSResourceRepositoryRole-based authentication keeps proprietary code under lock and key. - Hybrid: Multiple repos can be prioritized (
–Priority); this allows you to obtain community packages from the gallery, while enterprise modules are only searched for internally.
Security: Signing, Scopes & ExecutionPolicy
Set the company CA as the root certificate and sign releases via Set‑AuthenticodeSignature. In combination with Set‑ExecutionPolicy -Scope CurrentUser -ExecutionPolicy AllSigned prevent unchecked scripts from running.
The Gallery has also been accepting signatures since 2024 and refuses unsigned updates for modules with PowerShellGetPrereleasea flag.
Quality | PSScriptAnalyzer & Pester
- Static Code Analysis:
Invoke‑PSScriptAnalyzerintegrates with GitHub Actions or Azure Pipelines; Rules such asPSAvoidUsingWriteHostreduce maintenance costs. - Tests: Drop under
tests/Pester files. ATest‑Driven‑Moduledoes not return a deployment artifact for each commit until all tests are green.
This turns “works on my laptop” into a reproducible release.
CI/CD Pipelines & Release Automation
A typical GitHub workflow:
- Checkout + Cache Dependencies
pwsh -c Invoke‑Pesterpwsh -c Publish‑PSResource -Path . -Repository Internal(only on main-branch)dotnet publishfor Self-Contained Tools
This means that rollbacks are only one command away thanks to versioning.
Compatibility layer for Windows PowerShell modules
If you are dependent on the old ActiveDirectorymodule or AzureADmodule, it is often sufficient:
Import‑Module AzureAD -UseWindowsPowerShellPowerShell 7 opens a 5.1 session (WinPSCompatSession) in the background and proxy-t the cmdlets. However, be aware of the overhead – critical jobs should be migrated to native 7.x modules as soon as possible.
3 – Best practices for module development
Scaffolding – faster to a clean module
The first step is and remains New‑ModuleManifest, the manifest specifies the name, version and root module. Then a three-part folder structure is recommended:
MyTools\
├─ Public\ # exportierte Cmdlets
├─ Private\ # Helper‑Funktionen
└─ Classes\ # DSC‑ oder OOP‑Klassen
Each function resides in its own .ps1 file; this keeps git diffs small and allows selective loading. If you don’t want to start from scratch, you can use Catesta (Invoke‑ModuleScaffold -ModuleName MyTools) or Plaster:
powershellCopyEditInvoke‑Plaster -TemplatePath (Get‑PlasterTemplate -IncludeInstalledModules |
Where‑Object Title -eq 'Module').TemplatePath -Destination ./MyTools
Both tools create tests, CI pipelines and README at the same time and save an hour of manual click work. GitHubGitHub
Code Quality – Pester & GitHub Actions
Each public function gets a . *.Tests.ps1 With Pester 5, you rely on a modular, object-based test framework; the tests are versioned like source code. In CI, a minimal workflow is recommended:
name: Run unit tests
uses: PSModule/Invoke-Pester@v2
- name: Upload test results
uses: actions/upload-artifact@v4
with:
name: pester-results
path: TestResults.xml
This means that pull requests are automatically aborted in the event of failed tests, and an artifact permanently documents the result.
Signatures & Security – Single-File Builds
For the release branch, “flattening” is recommended: The individual source files are merged into a compact MyTools.psm1 (e.g. via Build‑Module.ps1). Then sign this file:
$cert = Get‑ChildItem Cert:\CurrentUser\My –CodeSigningCert
Set‑AuthenticodeSignature -FilePath .\dist\MyTools.psm1 -Certificate $cert
A signed single file loads faster, meets audit requirements, and harmonizes with an AllSigned ExecutionPolicy. Microsoft Learn
Documentation – Help as Code
Comment-based Help(<# .SYNOPSIS … #>) belongs directly above each function. For external help, use PlatyPS :
New‑MarkdownHelp -ModulePath .\MyTools -OutputFolder .\docs
Update‑MarkdownHelp -Path .\docs -RefreshModulePageThis creates Markdown files that live in the repo like any other source; Git diffs show immediately when a parameter has been changed without a doc update. Later, it can ConvertTo‑Maml even generate an Updateable Help ZIP. Microsoft LearnGitHub
4 – WPF GUI – why not just WinForms?
WinForms is still the “quick-and-dirty champion” for fast admin tools, but the technology renders every control absolutely – DPI scaling, high-contrast themes or rounding from Windows 11 are only incompletely adopted. The result therefore quickly looks like an app from the XP era and tends to break apart on 4K displays.
WPF relies on XAML as a declarative description language and thus clearly separates layout from code-behind (PowerShell or C#). Thanks to Data Binding , a single property assignment is enough to automatically update entire tables or TreeViews; Styles & Templates allow a theme-ready design without additional CSS frameworks. Larger surfaces in particular become manageable because UI elements can be logically grouped and reused.
Officially, WPF under .NET Core/.NET 9 runs exclusively on Windows – the runtime still contains Win32 dependencies. However, if you really want to develop cross-platform, you can either use Avalonia (a WPF-like XAML framework for Windows, macOS and Linux) or integrate PowerShell scripts directly via PSAvalonia . Alternatively, .NET MAUI comes with a XAML syntax that can be distributed to desktop and mobile OS via the community toolkit – but controls and namespaces differ somewhat from classic WPF.
5 – Design tools
- Visual Studio 2022 Community with the WPF designer is still the most convenient option in my opinion.
- VS Code + PowerShell Pro Tools: provides a lightweight XAML designer and compiles scripts to an EXE if needed.
- PoshGUI or web designer for quick mock-ups – completely sufficient for small tools. Reddit
6 – Practical example: Module + WPF frontend
Suppose we want to build a GUI for quickly checking free disk quotas on file servers:
# DiskTools.psm1 (Public/Disk.ps1)
function Get-DiskQuota {
[CmdletBinding()]
param(
[Parameter(Mandatory)][string]$ComputerName
)
Get-CimInstance -ComputerName $ComputerName -ClassName Win32_LogicalDisk |
Select-Object DeviceID, @{N='FreeGB';E={[math]::Round($_.FreeSpace/1GB,1)}}
}
Export-ModuleMember -Function Get-DiskQuota
# MainWindow.xaml (Auszug)
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
Title="Disk Quota Checker" Height="240" Width="460">
<Grid Margin="10">
<TextBox x:Name="txtServer" Width="240" Height="24"/>
<Button Content="Prüfen" Width="80" Height="24" Margin="260,0,0,0"
Click="Check_Click"/>
<DataGrid x:Name="dgResult" Margin="0,40,0,0"/>
</Grid>
</Window>
# Launch.ps1
Import-Module .\DiskTools.psd1
Add-Type -AssemblyName PresentationFramework
[xml]$xaml = Get-Content .\MainWindow.xaml
$reader = New-Object System.Xml.XmlNodeReader $xaml
$window = [Windows.Markup.XamlReader]::Load($reader)
$window.FindName('Check_Click').Add_Click({
$srv = $window.FindName('txtServer').Text
$data = Get-DiskQuota -ComputerName $srv
$window.FindName('dgResult').ItemsSource = $data
})
$window.ShowDialog()The UI remains lean, the business logic is neatly encapsulated in the module. With data binding, no loop has to manually fill the grid rows; the WPF framework updates the view automatically.
7 – MVVM light for PowerShell
For larger projects, a light MVVM pattern is worthwhile: Define view model classes in Powershell, INotifyPropertyChanged add them via [ComponentModel.INotifyPropertyChanged()]attribute and call the module functions asynchronously with lazy loading . The example template on Reddit shows how to encapsulate ProgressBars and status bars without blocking the UI. Reddit
8 – Deployment & Updates
- Self-Contained: PowerShell
dotnet publish -p:PublishSingleFile=true7 and its dependent DLLs can be placed in a directory – ideal for clients without admin rights. - MSIX / WinGet: A signed MSIX package with
pwsh.exe, module and XAML can be rolled out via Intune and updates itself automatically via PSResourceGet. - Cross-platform: On macOS or Linux, simply publish the module – the GUI part can run there via Avalonia bindings or be deactivated via device fallback.
9 – Tuning & Troubleshooting
- Performance: Never execute long tasks on the GUI thread;
Start‑Job,ThreadJobor[System.Threading.Tasks.Task]+asyncawaitfrom the template. - Compatibility: If legacy cmdlets (for example
Get‑ADUser, ) are missing, use Windows compatibility mode (Import‑Module -UseWindowsPowerShell). - Security: Sign with
Set‑AuthenticodeSignaturemodules and set ExecutionPolicy for Userscope to AllSigned . In mixed environments, a KeyVault-secured root cert helps.
Result
With PowerShell 7.5, cleanly structured modules and a lean WPF frontend, you can now build tools that used to be clear C# terrain. If you follow the best practices outlined here – clear folder structures, automated tests, SemVer versioning, asynchronous calls and modern packaging paths – you will get maintainable, high-performance solutions that shine in everyday admin life as well as in the end user. Now it’s time to clone the module template, design XAML and get started. Have fun coding!

