top of page

PowerShell to .NET for Admins, A Practical Training Guide

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-Date

This is a .NET type:

[System.DateTime]

The cmdlet returns an object of that type:

(Get-Date).GetType().FullName

Expected output:

System.DateTime

So 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().FullName

Example:

$date = Get-Date
$date.GetType().FullName

That tells you exactly what you are dealing with.


4.2 Inspect an object

$obj | Get-Member

Example:

$date | Get-Member

This shows the objects:

  • methods

  • properties

  • type name


Show only methods

$date | Get-Member -MemberType Method

Show only properties

$date | Get-Member -MemberType Property

Inspect a .NET class directly

If you already know the class name:

[System.IO.File] | Get-Member -Static

That 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.StringBuilder

Cleaner 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().FullName

What 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-Member

Goal

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()
}
$cleanNames

Why .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
sql01

One 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')
$reportPath

This 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, WorkingSet64

Admin 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 -Static

Notice 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')
$list

Why 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 5

Exercise 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()
$report

Exercise 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].Name

Admin 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().FullName

Method 2, inspect classes you know about

[System.IO.File] | Get-Member -Static

[System.Net.Dns] | Get-Member -Static

Method 3, list loaded assemblies

[AppDomain]::CurrentDomain.GetAssemblies() |    
Select-Object FullName, Location

Method 4, list types from assemblies

[AppDomain]::CurrentDomain.GetAssemblies() |    
	ForEach-Object { $_.GetTypes() } |    
	Select-Object FullName

That can be noisy, so filter it:

[AppDomain]::CurrentDomain.GetAssemblies() |    
	ForEach-Object { $_.GetTypes() } |    
	Where-Object FullName -like 'System.Net*' |    
	Select-Object FullName

Method 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")
$file

Lab 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()
$report

Lab 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-Member

Confusing 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().FullName

What can it do?

$obj | Get-Member

What static members does this class have?

[System.IO.File] | Get-Member -Static

How 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

Rated 0 out of 5 stars.
No ratings yet

Add a rating
bottom of page