PowerShell to .NET for Admins, A Practical Training Guide
- Tenaka

- 2 days ago
- 11 min read
This guide is about making the jump into .NET as an IT admin, namely me. I’ve reached the limits of what I can realistically achieve with just PowerShell and need to push further.
To be clear, I’m not a developer, my developer friends will attest to this by reminding me that PowerShell isn't a proper programming language and I should stop messing around.
For my sins, I’m a Microsoft professional wishing he had gone the Linux route, who hates repetitive manual work. So this blog presents a good opportunity to combine learning with sharing the knowledge as I go. Sit back and enjoy.
Cmdlets are clean, readable, and designed for common admin work. The problem is that sooner or later you hit the edge of what cmdlets expose. That is where .NET starts to matter.
PowerShell is built on .NET. That means the objects you work with, the methods you call, the data types you manipulate, and a lot of the platform features underneath are all .NET based.
This blog is aimed at someone who already knows the basics of PowerShell, variables, loops, functions, pipelines, and object properties, and wants to move into using .NET classes directly from the shell.
The goal is not to turn you into a C# developer, I wouldn't know where to start. The goal is to make you far more effective in PowerShell. I know this is my goal, and for too long I've limited myself by what Cmdlets are available.
Why .NET matters in PowerShell
PowerShell works with objects, and those objects are usually .NET objects. When you run a cmdlet, the result is often an instance of a .NET class. When you manipulate a string, date, file, certificate, IP address, event log entry, XML document, or web response, you are usually handling a .NET type.
Cmdlets vs .NET
The first mental change is this:
A cmdlet is a PowerShell command.
A .NET class is a type.
That is a cmdlet:
Get-DateThis is a .NET type:
[System.DateTime]The cmdlet returns an object of that type:
(Get-Date).GetType().FullNameExpected output:
System.DateTimeSo when you run Get-Date, you are not getting some vague shell output. You are getting a real .NET object.
What you actually need to learn
You do not need to learn all of .NET. That would be daft. What you need to learn is how to:
identify the type of an object
inspect its properties and methods
understand static vs instance members
create new .NET objects
find available classes and methods
use those classes safely in real admin work
Find the real Type of an object
$obj.GetType().FullNameExample:
$date = Get-Date
$date.GetType().FullNameThat tells you exactly what you are dealing with.
4.2 Inspect an object
$obj | Get-MemberExample:
$date | Get-MemberThis shows the objects:
methods
properties
type name
Show only methods
$date | Get-Member -MemberType MethodShow only properties
$date | Get-Member -MemberType PropertyInspect a .NET class directly
If you already know the class name:
[System.IO.File] | Get-Member -StaticThat shows the static methods and properties on the class.
Show constructors
[System.Text.StringBuilder].GetConstructors()Show method overloads
[System.IO.File].GetMethods() | Where-Object Name -eq 'ReadAllText'That helps when the same method has multiple forms.
Static members vs instance members
This is one of the biggest early stumbling blocks.
Static member
Belongs to the class itself. You call it with :
Example:
[System.DateTime]::Now
[System.IO.File]::Exists('C:\Temp\Test.txt')You do not need to create an object first.
Instance member
Belongs to an existing object. You call it with :
Example:
$date = Get-Date
$date.AddDays(7)
$date.ToString('yyyy-MM-dd')Quick rule:
[TypeName]::Something means class level, static
$object.Something means object level, instance
How to create .NET objects
There are two common ways:
Old style
$sb = New-Object System.Text.StringBuilderCleaner modern style
$sb = [System.Text.StringBuilder]::new()That second form is usually better, example:
$sb = [System.Text.StringBuilder]::new()
[void]$sb.Append('Server ')
[void]$sb.Append('Report')
$sb.ToString()The most useful .NET areas for admins
You can go far just by learning a few namespaces:
System.IO, Files, directories, paths, streams.
System.DateTime, Dates, times, formatting, calculations.
System.Net, DNS, IP addresses, sockets, HTTP related types.
System.Text, Encoding, string handling, string builders.
System.Security.Cryptography, Hashing, encryption, certificates, randomness.
System.Xml, XML document parsing and manipulation.
System.Diagnostics, Processes, event logs, performance details.
System.Collections, Lists, dictionaries, queues, stacks, custom collections.
Training module 1, understanding objects properly
Before touching anything more advanced, make sure this part is solid.
Exercise 1.1, inspect common objects
Run each of these and check the type:
(Get-Date).GetType().FullName
('hello').GetType().FullName
(1).GetType().FullName
(Get-Process | Select-Object -First 1).GetType().FullName
(Get-Service | Select-Object -First 1).GetType().FullNameWhat to look for
Notice that strings, integers, dates, process objects, and service objects are all real types. They are not just plain text.
Exercise 1.2, inspect members
Get-Date | Get-Member
'hello world' | Get-Member
Get-Service | Select-Object -First 1 | Get-MemberGoal
Learn to spot:
property names
method names
type name at the top
Exercise 1.3, property vs method
Try these:
$date = Get-Date
$date.Year
$date.ToString('yyyy-MM-dd')Now deliberately try the wrong thing:
$date.Year()You should get an error because Year is a property, not a method.
Training module 2, using strings and text
Strings are everywhere in admin work, logs, file names, event messages, command output, config content.
PowerShell strings are .NET strings.
Useful methods
$text = 'Server01.contoso.local'
$text.ToUpper()
$text.ToLower()
$text.Contains('contoso')
$text.Replace('Server01','DC01')
$text.Split('.')
$text.StartsWith('Server')
$text.EndsWith('local')Real admin example, normalise computer names
Imagine you are reading a list of hostnames from a file and want to clean them up.
$names = @(
'srv-app-01',
'SRV-DB-02',
'Srv-Web-03 ')
$cleanNames = foreach ($name in $names){
$name.Trim().ToUpper()
}
$cleanNamesWhy .NET matters here
Methods like .Trim(), .ToUpper(), .Replace(), and .Split() are .NET string methods. You use them constantly in real scripts.
Exercise 2.1
Take this array:
$servers = @(
' dc01.corp.local ',
' FS01.corp.local',
'sql01.CORP.LOCAL '
)Produce output where each entry is:
trimmed
converted to lower case
split so that only the short hostname remains
Expected result:
dc01
FS01
sql01One way:
foreach ($server in $servers){
$server.Trim().ToLower().Split('.')[0]
}Training module 3, working with DateTime
Dates are a constant source of bad scripts. .NET helps make them predictable.
Common examples
[datetime]::Now
[datetime]::UtcNow
(Get-Date).AddDays(30)
(Get-Date).AddHours(-4)
(Get-Date).ToString('yyyy-MM-dd HH:mm:ss')Admin example, password expiry style reporting
$today = [datetime]::Now
$expiry = $today.AddDays(90)
[pscustomobject]@{
Today = $today.ToString('yyyy-MM-dd')
ExpiryDate = $expiry.ToString('yyyy-MM-dd')
DaysRemaining = ($expiry - $today).Days
}Admin example, file age check
$path = 'C:\Temp\backup.zip'
if (Test-Path $path) {
$file = Get-Item $path
$age = ([datetime]::file.LastWriteTime).TotalDays
[pscustomobject]@{
Path = $file.FullName
LastWriteTime = $file.LastWriteTime
AgeDays = [math]::Round($age,2)
}
}Exercise 3.1
Write a small block that:
gets the current time
adds 14 days
formats both dates as dd/MM/yyyy
shows how many days are between them
Training module 4, files, folders, and paths with System.IO
This is one of the best entry points into .NET for admin work.
Common file methods
[System.IO.File]::Exists('C:\Temp\demo.txt')
[System.IO.File]::ReadAllText('C:\Temp\demo.txt')
[System.IO.File]::WriteAllText('C:\Temp\demo.txt','hello')
[System.IO.File]::AppendAllText('C:\Temp\demo.txt',"`r`nnew line")
[System.IO.File]::Delete('C:\Temp\demo.txt')Common directory methods
[System.IO.Directory]::Exists('C:\Temp')
[System.IO.Directory]::CreateDirectory('C:\Temp\Logs')
[System.IO.Directory]::GetFiles('C:\Temp')
[System.IO.Directory]::GetDirectories('C:\Temp')Common path methods
[System.IO.Path]::GetFileName('C:\Temp\demo.txt')
[System.IO.Path]::GetExtension('C:\Temp\demo.txt')
[System.IO.Path]::GetDirectoryName('C:\Temp\demo.txt')
[System.IO.Path]::Combine('C:\Temp','Reports','report.txt')Real admin example, safe report path creation
$basePath = 'C:\AdminReports'
$dateFolder = (Get-Date).ToString('yyyyMMdd')
$targetFolder = [System.IO.Path]::Combine($basePath, $dateFolder)
if (-not [System.IO.Directory]::Exists($targetFolder)) {
[void][System.IO.Directory]::CreateDirectory($targetFolder)
}
$reportPath = [System.IO.Path]::Combine($targetFolder, 'services.txt')
$reportPathThis avoids crude string concatenation like:
"C:\AdminReports\" + $dateFolder + "\services.txt"That works until it does not.
Admin example, read a config file in one go
$configPath = 'C:\Scripts\appsettings.json'
if ([System.IO.File]::Exists($configPath)) {
$json = [System.IO.File]::ReadAllText($configPath)
$config = $json | ConvertFrom-Json
$config
}Exercise 4.1
Create a script block that:
creates C:\Temp\AdminLab if it does not exist
creates a file called serverlist.txt
writes three server names to it
reads the file back and displays the content
Exercise 4.2
Take the path:
'C:\Logs\IIS\W3SVC1\u_ex260328.log'Use System.IO. Path methods to extract:
file name
extension
parent folder
Training module 5, DNS and networking with
System.Net
There are PowerShell cmdlets for some network tasks, but .NET often gives you more direct access.
Examples
[System.Net.Dns]::GetHostName()
[System.Net.Dns]::GetHostAddresses('localhost')
[System.Net.Dns]::GetHostEntry('www.bbc.co.uk')Admin example, resolve multiple hosts
$hosts = 'localhost','dc01','fileserver'
foreach ($host in $hosts) {
try {
$addresses = [System.Net.Dns]::GetHostAddresses($host)
foreach ($address in $addresses)
{
[pscustomobject]@{
Hostname = $host
Address = $address.IPAddressToString
Family = $address.AddressFamily
}
}
}
catch {
[pscustomobject]@{
Hostname = $host
Address = 'Resolution failed'
Family = $null
}
}
}Admin example, IP address validation
$testIPs = '192.168.1.10','10.10.10.999','fe80::1','badvalue'
foreach ($ip in $testIPs) {
$parsed = $null
$valid = [System.Net.IPAddress]::TryParse($ip, [ref]$parsed)
[pscustomobject]@{
Input = $ip
IsValid = $valid
ParsedIP = if ($valid) { $parsed.IPAddressToString } else { $null}
}
}Exercise 5.1
Build a small resolver that takes an array of names and returns:
hostname
IP address
whether the address is IPv4 or IPv6
Training module 6, working with event logs and diagnostics
There are cmdlets for event logs, but understanding the .NET side is useful too.
One simple example is using System.Diagnostics.
Process example
[System.Diagnostics.Process]::GetProcesses() |
Select-Object -First 5 ProcessName, Id, WorkingSet64Admin example, show high memory processes
[System.Diagnostics.Process]::GetProcesses() |
Sort-Object WorkingSet64 -Descending |
Select-Object -First 10 @{
Name = 'ProcessName'
Expression = { $_.ProcessName }
}, @{
Name = 'PID'
Expression = { $_.Id }
}, @{
Name = 'MemoryMB'
Expression = { [math]::Round($_.WorkingSet64 / 1MB, 2) }
}Why it matters
Even when PowerShell has a cmdlet like Get-Process, the underlying objects and properties are still .NET. Knowing that helps when exploring what is available.
Exercise 6.1
Use Get-Process | Select-Object -First 1 | Get-Member and compare it to:
[System.Diagnostics.Process] | Get-Member -StaticNotice the difference between a process object and the class itself.
Training module 7, collections and lists
Arrays in PowerShell are fine for many tasks, but .NET collections are often better when you are building larger datasets or adding lots of items.
Example - List
$list = [System.Collections.Generic.List[string]]::new()
$list.Add('DC01')
$list.Add('FS01')
$list.Add('SQL01')
$listWhy use it
A PowerShell array gets recreated each time you add with +=, which is clumsy and less efficient for large loops.
Admin example, collecting results in a scan
$results = [System.Collections.Generic.List[object]]::new()
Get-Service | ForEach-Object {
$results.Add([pscustomobject]@{
Name = $_.Name
Status = $_.Status
})
}
$results | Select-Object -First 5Exercise 7.1
Rewrite a loop that uses $array += ... so that it uses List[object] instead.
Training module 8, StringBuilder for report generation
Admins often generate large text blobs, log files, config files, HTML fragments, CSV content, or report text.
Using repeated string concatenation works, but it gets ugly and can be slow in bigger loops.
System.Text.StringBuilder is built for that.
Example
$sb = [System.Text.StringBuilder]::new()
[void]$sb.AppendLine('Service Report')
[void]$sb.AppendLine('==============')
Get-Service | Select-Object -First 5 |
ForEach-Object {
[void]$sb.AppendLine("Name: $($_.Name), Status: $($_.Status)")
}
$sb.ToString()Admin example, build a plain text server health summary
$sb = [System.Text.StringBuilder]::new()
[void]$sb.AppendLine("Health Check Report")
[void]$sb.AppendLine("Generated: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')")
[void]$sb.AppendLine("Server: $env:COMPUTERNAME")
[void]$sb.AppendLine('')
$os = Get-CimInstance Win32_OperatingSystem
[void]$sb.AppendLine("OS: $($os.Caption)")
[void]$sb.AppendLine("Version: $($os.Version)")
[void]$sb.AppendLine("Last Boot: $($os.LastBootUpTime)")
[void]$sb.AppendLine('')
Get-Service | Where-Object Status -ne 'Running' | Select-Object -First 10 |
ForEach-Object {
[void]$sb.AppendLine("Service issue:
$($_.Name), Status:
$($_.Status)")}
$report = $sb.ToString()
$reportExercise 8.1
Use StringBuilder to create a report that contains:
current hostname
current date
top 5 stopped services
current PowerShell version
Training module 9, hashing and cryptography basics
You do not need to become a crypto engineer, but hashes are common in admin and security work.
Admin example, compute a SHA256 hash of a file
$path = 'C:\Temp\installer.msi'
if ([System.IO.File]::Exists($path)) {
$stream = [System.IO.File]::OpenRead($path)
try {
$sha256 = [System.Security.Cryptography.SHA256]::Create()
try {
$hashBytes = $sha256.ComputeHash($stream)
$hashString = [System.BitConverter]::ToString($hashBytes).Replace('-','')
$hashString
}
finally {
$sha256.Dispose()
}
}
finally {
$stream.Dispose()
}
}Yes, PowerShell has Get-FileHash, and for most cases that is easier. The point here is to show that you can use the underlying .NET classes directly if needed.
Exercise 9.1
Take a short text string, convert it to bytes with System.Text.Encoding, then hash it with SHA256.
Hint:
[System.Text.Encoding]::UTF8.GetBytes('hello world')Training module 10, XML and structured configuration
Windows admin work is full of XML, event data, config files, policy exports, scheduled tasks, web.config, and more.
PowerShell has the [xml] accelerator, which is tied to .NET XML classes.
Example
[xml]$xml = @"
<Servers>
<Server Name="DC01" Role="DomainController" />
<Server Name="FS01" Role="FileServer" />
</Servers>"@
$xml.Servers.Server
$xml.Servers.Server[0].NameAdmin example, parse a simple config
[xml]$config = @"
<Settings>
<App Name="AuditTool" Enabled="true" />
<Path Value="C:\Logs" />
</Settings>
"@
[pscustomobject]@{
AppName = $config.Settings.App.Name
Enabled = $config.Settings.App.Enabled
Path = $config.Settings.Path.Value
}Exercise 10.1
Create a small XML sample with three servers and extract:
all names
the first server
any server with a chosen role
Training module 11, finding available .NET classes
This is the part many people mean when they ask, “How do I find the .NET commands?”
Really, they mean classes, methods, constructors, and namespaces.
Method 1, inspect objects you already have
This is the best place to start.
Get-Service | Select-Object -First 1 | Get-Member
(Get-Date).GetType().FullNameMethod 2, inspect classes you know about
[System.IO.File] | Get-Member -Static
[System.Net.Dns] | Get-Member -StaticMethod 3, list loaded assemblies
[AppDomain]::CurrentDomain.GetAssemblies() |
Select-Object FullName, LocationMethod 4, list types from assemblies
[AppDomain]::CurrentDomain.GetAssemblies() |
ForEach-Object { $_.GetTypes() } |
Select-Object FullNameThat can be noisy, so filter it:
[AppDomain]::CurrentDomain.GetAssemblies() |
ForEach-Object { $_.GetTypes() } |
Where-Object FullName -like 'System.Net*' |
Select-Object FullNameMethod 5, use tab completion
Type part of a namespace and press Tab.
[System.IO.or
[System.Security.Cryptography.Exercise 11.1
Find and inspect:
System.IO.Path
System.Net.IPAddress
System.Text.Encoding
System.Environment
For each one, identify at least two useful static members.
Training module 12, method overloads and how to work them out
A lot of .NET methods come in multiple forms.
For example, ReadAllText might have overloads for different encodings.
To see that:
[System.IO.File].GetMethods() | Where-Object Name -eq 'ReadAllText'You can also inspect constructors this way:
[System.IO.FileInfo].GetConstructors()Admin example, choosing the right overload
Suppose you want to read a file using UTF8 explicitly.
You would inspect the available overloads, then pick the one that accepts both a path and an encoding object.
$content = [System.IO.File]::ReadAllText(
'C:\Temp\data.txt',
[System.Text.Encoding]::UTF8)Exercise 12.1
Inspect WriteAllText overloads and write a file using UTF8 explicitly.
Admin mini labs
These are realistic tasks where .NET makes sense.
Lab A, validate and normalise IP addresses
Goal:
take a mixed list of strings
validate whether each is an IP address
return clean output
$values = @(
'192.168.1.20',
' 10.0.0.5 ',
'bad-ip',
'fe80::1'
)
foreach ($value in $values) {
$ipObj = $null
$trimmed = $value.Trim()
$valid = [System.Net.IPAddress]::TryParse($trimmed, [ref]$ipObj)
[pscustomobject]@{
Input = $value
Normalised = if ($valid) { $ipObj.IPAddressToString } else { $null }
IsValid = $valid
Family = if ($valid) { $ipObj.AddressFamily } else { $null }
}
}Lab B, build a timestamped report path
Goal:
create a dated folder structure
generate a report path safely
$root = 'C:\Reports'
$today = (Get-Date).ToString('yyyyMMdd')
$server = $env:COMPUTERNAME
$folder = [System.IO.Path]::Combine($root, $today)
if (-not [System.IO.Directory]::Exists($folder)) {
[void][System.IO.Directory]::CreateDirectory($folder)
}
$file = [System.IO.Path]::Combine($folder, "${server}_services.txt")
[System.IO.File]::WriteAllText($file, "Service report for $server")
$fileLab C, build a text report with StringBuilder
Goal:
avoid horrible string concatenation
produce a clean report
$sb = [System.Text.StringBuilder]::new()
[void]$sb.AppendLine("Service Status Report")
[void]$sb.AppendLine("Generated: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')")
[void]$sb.AppendLine("Host: $env:COMPUTERNAME")
[void]$sb.AppendLine('')
Get-Service | Select-Object -First 10 |
ForEach-Object {
[void]$sb.AppendLine("$($_.Name), $($_.Status)")}
$report = $sb.ToString()
$reportLab D, hash a file and record the result
Goal:
compute a hash
write it to a log file
$filePath = 'C:\reports\file to hash.txt'
$logPath = 'C:\reports\hashlog.txt'
$logDir = Split-Path $logPath -Parent
if (-not [System.IO.Directory]::Exists($logDir)) {
[void][System.IO.Directory]::CreateDirectory($logDir)
}
if ([System.IO.File]::Exists($filePath)) {
$stream = [System.IO.File]::OpenRead($filePath)
try {
$sha256 = [System.Security.Cryptography.SHA256]::Create()
try {
$hashBytes = $sha256.ComputeHash($stream)
$hashString = [System.BitConverter]::ToString($hashBytes).Replace('-', '')
[System.IO.File]::AppendAllText($logPath, "$filePath,$hashString`r`n")
Write-Host "Hash written to $logPath" }
finally {
$sha256.Dispose()
}
}
finally {
$stream.Dispose()
}
}
else {
Write-Host "Source file not found: $filePath"
}Common admin patterns where .NET is genuinely useful
This is where .NET tends to be worth using instead of just sticking to cmdlets.
Safer path handling
Use:
[System.IO.Path]::Combine()Instead of stitching file paths together badly.
Better text generation
Use:
[System.Text.StringBuilder]For larger reports or file output.
Validation of structured values
Use:
[System.Net.IPAddress]::TryParse()[datetime]::TryParse()[guid]::TryParse()Instead of flaky regex
Precise file handling
Use:
[System.IO.File][System.IO.Directory]When you need exact behaviour.
Lower-level feature access
Sometimes there is no good cmdlet, or the cmdlet hides too much. That is where the raw class becomes useful.
Common mistakes
Treating everything as text
Bad scripts convert useful objects into strings too early, then try to parse them back later. Work with the object as long as possible.
Not checking the type first
Always verify:
$obj.GetType().FullName
$obj | Get-MemberConfusing class methods and object methods
Wrong:
$date = [System.DateTime]
$date.AddDays(1)Right:
[System.DateTime]::Now.AddDays(1)or
$date = Get-Date
$date.AddDays(1)Ignoring overloads
If a method is not behaving the way you expect, inspect the overloads. There may be several versions.
Using .NET where a cmdlet is clearer
Do not become one of those people who rewrites obvious PowerShell into ugly pseudo C# for no reason.
If Get-FileHash, Get-Content, Import-Csv, or Resolve-DnsName does the job cleanly, use it.
The point is to know when .NET helps, not to show off.
A sensible learning path
If you want to build this skill properly, do it in this order.
Stage 1, object fundamentals
Practice:
.GetType().FullName
Get-Member
property vs method
Stage 2, useful types
Practice with:
strings
DateTime
System.IO.File
System.IO.Path
System.Net.Dns
Stage 3, object creation
Practice with:
::new()
constructors
StringBuilder
generic lists
Stage 4, discovery
Practice with:
.GetMethods()
.GetConstructors()
loaded assemblies
filtered type discovery
Stage 5, real scripts
Use .NET deliberately in:
path building
file validation
date calculations
report generation
value parsing and validation
Quick reference cheat sheet
What type is this?
$obj.GetType().FullNameWhat can it do?
$obj | Get-MemberWhat static members does this class have?
[System.IO.File] | Get-Member -StaticHow do I create a new object?
$obj = [System.Text.StringBuilder]::new()How do I inspect constructors?
[System.Text.StringBuilder].GetConstructors()How do I inspect method overloads?
[System.IO.File].GetMethods() | Where-Object Name -eq 'ReadAllText'How do I safely combine file paths?
[System.IO.Path]::Combine('C:\Temp','Reports','output.txt')How do I validate an IP address?
$ip = $null[System.Net.IPAddress]::TryParse('192.168.1.1',[ref]$ip)How do I get the current time in .NET?
[datetime]::Now



Comments