Using .NET With PowerShell

This goal of this post is to explore some .NET classes that may be useful to sysadmins and PowerShell devs without .NET (C#, etc.) experience, with a bit of a deeper dive into some concepts when warranted.

By the end, you’ll be familiar with several .NET classes and understand how to discover and use others that might better meet your specific needs.

Why use .NET classes in PowerShell?

There are two main reasons:.

  1. Extended Functionality: Access features in .NET not available natively in PowerShell.
  2. Enhanced Performance: Many .NET classes perform faster than their PowerShell equivalents.

Exploring .NET also helps deepen your understanding of the foundation upon which PowerShell is built.

Determining what version of .NET CLR PowerShell is using

Different .NET/Core versions support different classes and behaviors. Knowing the .NET CLR (Common Language Runtime) version PowerShell is using helps you refer to the correct documentation and troubleshoot unexpected behaviors.

We can determine the version using the [Environment]::Version command.

PowerShell 5.1 uses .NET Framework 4.0.30319.42000.

PS C:\> ($PSVersionTable).PSVersion.ToString()

PS C:\> [Environment]::Version.ToString()

PowerShell 7.4.1 uses .NET Core 8.0.1

PS C:\> ($PSVersionTable).PSVersion.ToString()

PS C:\> [Environment]::Version.ToString()

We can already see a big difference here, v5.1 uses the older .NET Framework while 7.x uses the more modern .NET Core. This is why PowerShell 7.x can be cross platform, and why it can’t ship with Windows as Microsoft does not want to bundle the .NET Core runtime due to support lifecycles of the products being different (at least that is my understanding).

There will be an example below where the .NET CLR version is important.

Let’s begin with something simple, validating whether a string is null, empty, or whitespace.

String validation

.NET has very two very convenient string validation methods built into the String class, IsNullOrEmpty, and IsNullOrWhiteSpace. While not difficult, performing this kind of validation in PowerShell 5.1 requires a bit more work than just calling a method.

Here are a few examples:

$NullVar = $null
$Empty = ""
$Whitespace = " "
$Valid = "abc"

Write-Host "IsNullOrEmpty"

Write-Host "IsNullOrWhiteSpace"




String validation natively in PowerShell

PowerShell 5.1 has ValidateNotNullOrEmpty built in, while PowerShell 7 contains both validators in the form of attributes. Here is an example:

[ValidateNotNullOrEmpty()]$Str = ""       # Works in PowerShell 5.1+
[ValidateNotNullOrWhiteSpace()]$Str = "  "  # Works in PowerShell 7+

Using the .NET method however works across all versions.

Moving on, let’s tackle IP address validation

IP Address validation using [IPAddress]

Validating IP addresses with regex is common, but [IPAddress] offers a simpler and often more convenient method.

PS C:\> [IPAddress]""

Address            : 16843018
AddressFamily      : InterNetwork
ScopeId            :
IsIPv6Multicast    : False
IsIPv6LinkLocal    : False
IsIPv6SiteLocal    : False
IsIPv6Teredo       : False
IsIPv4MappedToIPv6 : False
IPAddressToString  :

However, it has limitations; for example, 10.1 is considered a valid value:

PS C:\> [IPAddress]"10.1"

Address            : 16777226
AddressFamily      : InterNetwork
ScopeId            :
IsIPv6Multicast    : False
IsIPv6LinkLocal    : False
IsIPv6SiteLocal    : False
IsIPv6Teredo       : False
IsIPv4MappedToIPv6 : False
IPAddressToString  :

However, we can get around this by comparing the input against the IPAddressToString output:

$IP -eq ([IPAddress]$IP).IPAddressToString

Here are some examples:

PS C:\> @("", "10.1", "0xA51F2C44", "134744072", "", "300.1.1.1", "abcd") | % {$_ -eq ([IPAddress]$_).IPAddressToString}

InvalidArgument: Cannot convert value "" to type "System.Net.IPAddress". Error: "An invalid IP address was specified."
InvalidArgument: Cannot convert value "300.1.1.1" to type "System.Net.IPAddress". Error: "An invalid IP address was specified."
InvalidArgument: Cannot convert value "abcd" to type "System.Net.IPAddress". Error: "An invalid IP address was specified."

The second value of 10.1 while accepted as valid input for the class failed the equality check, and the same goes for the third hex value and the fourth decimal value. Both are ‘valid’ input for the class as they’re just different ways a valid IP can be represented, but if we’re expecting the usual dotted-decimal IP representation in our input they fail the string equality check.

The last three are invalid IP addresses and return errors.

While we’re on the topic of networking, let’s look at MAC Addresses.

MAC Address Validation and Normalisation using [PhysicalAddress]::Parse()

As with IP addresses we can use regex and some string manipulation here, but .NET again has a convenient method to make our code a little cleaner and easier to read.

Valid MAC address example:

PS C:\> [PhysicalAddress]::Parse("34-ED-1B-AA-BB-CC")


Invalid MAC address example:

PS C:\> [PhysicalAddress]::Parse("xyz")

Exception calling "Parse" with "1" argument(s): "An invalid physical address was specified."
At line:1 char:1
+ [PhysicalAddress]::Parse("xyz")
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [], MethodInvocationException
    + FullyQualifiedErrorId : FormatException

Compatibility Across .NET Versions

If you recall earlier the underlying version of .NET CLR can make a difference, this the is one class where Microsoft has made improvements between .NET Framework (used by PowerShell 5.1) and .NET Core.

Unfortunately the above format (all uppercase, - delimited) is the only format valid under .NET Framework. All of these valid and commonly formatted MAC address values fail:

PS C:\> [PhysicalAddress]::Parse("34-ed-1b-aa-bb-cc")
Exception calling "Parse" with "1" argument(s): "An invalid physical address was specified."

PS C:\> [PhysicalAddress]::Parse("34:ED:1B:AA:BB:CC")
Exception calling "Parse" with "1" argument(s): "An invalid physical address was specified."

PS C:\> [PhysicalAddress]::Parse("34ED.1BAA.BBCC")
Exception calling "Parse" with "1" argument(s): "An invalid physical address was specified."

However, they all work under PowerShell 7.x which is built on top of .NET Core.

PS C:\> [PhysicalAddress]::Parse("34-ed-1b-aa-bb-cc")

PS C:\> [PhysicalAddress]::Parse("34:ED:1B:AA:BB:CC")

PS C:\> [PhysicalAddress]::Parse("34ED.1BAA.BBCC")

These changes are documented in the Microsoft documentation.

parse remarks

Sticking with the networking theme, let’s test TCP ports

Fast TCP port testing with [System.Net.Sockets.TcpClient]

This is one of my favourites simply because it’s so much faster than Test-NetConnection thanks to the configurable timeout.

Here is an example:

PS C:\> $Site = ""
PS C:\> $Port = 443
PS C:\> $TimeoutMS = 100
PS C:\> $Client = [System.Net.Sockets.TcpClient]::New()
PS C:\> $Client.ConnectAsync($Site,$Port).Wait($TimeoutMS)


PS C:\> $Client.Dispose()

It returns $true if the connection was successfully established, and $false if not.

Last one before things get a little more complicated, generating passwords with [System.Web.Security].

Generating passwords with [System.Web.Security]

I’m sure at some stage we’ve all needed to programmatically generate passwords. The System.Web.Security namespace has a nice method for doing this. The downside is that it did not make it across to .NET Core, so it only works on PowerShell 5.1.

The syntax is GeneratePassword (int length, int minimumNumberOfNonAlphanumericCharacters)

Here are a couple of examples:

PS C:\> [System.Web.Security.Membership]::GeneratePassword(20,1)

PS C:\> [System.Web.Security.Membership]::GeneratePassword(20,10)

Let’s move onto some .NET classes that have a wider use cases.

Using List<T> instead of PowerShell Arrays

This is probably one of the most common .NET types I’ve seen recommended for PowerShell users, but it’s worth covering again. The issue with PowerShell arrays (@()) is they’re fixed size, they cannot be expanded to add more items. When we add an item to an array using += syntax a new array is created and the original destroyed. If you’re doing this inside a loop it is being re-created every iteration and likely having an impact on performance.

Before we go further let’s quickly cover using statements.

‘Using’ statements

‘Using’ statements allow us to use types without having to reference the entire namespace each time we want to interact with the class. For example, if we wanted to create a List<T> (a .NET class we will cover below) we can do so in two ways.

The first is to specify the full namespace path:

$List = [System.Collections.Generic.List[string]]::New()

The second is to place a using statement at the top of the script, and then reference the type only by its name.

using namespace System.Collections.Generic

$List = [List[string]]::New()

We already have a similar concept to this in PowerShell. We can run Get-Process by itself, or we can reference the module-qualified cmdlet with Microsoft.PowerShell.Management\Get-Process. If we had two commands that mapped to Get-Process we could remove ambiguity by writing out the full path.

Back to using List<T>.

List<T> in PowerShell

A better option to PowerShell arrays is the .NET List<T> type. The <T> allows us to specify the data type that will be contained in the list: string, int, object, etc. This is referred to as a Generic class. While our PowerShell arrays can hold any data type it is generally not good practice. For example, the following is perfectly valid in PowerShell.


However, when we attempt to perform some task against each item in the collection (as is common) we’re likely to encounter errors.

PS C:\> @(10,20,"thirty") | % {$_ / 2}
InvalidArgument: Cannot convert value "thirty" to type "System.Int32". Error: "The input string 'thirty' was not in a correct format."

While this is a very contrived example, we can imagine the array items coming from some user input or an API and the data we get back not being what we expect.

With generics we specify what type will be contained inside the collection. Let’s begin with creating a List<T> to hold integers.

using namespace System.Collections.Generic

$Numbers = [List[int]]::New()
PS C:\> $Numbers

Now when we add “thirty” we will receive an error.

PS C:\> $Numbers.Add("thirty")

MethodException: Cannot convert argument "item", with value: "thirty", for "Add" to type "System.Int32": "Cannot convert value "thirty" to type "System.Int32". Error: "The input string 'thirty' was not in a correct format.

List Caveats

It’s worth noting that when we add items to a List<T>, a type conversion will be attempted. So the following can happen:

$Numbers = [List[string]]::New()

Here the integer 30 was automatically cast to the string "30".

PS C:\> $Numbers

PS C:\> $Numbers[2].GetType()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     String                                   System.Object

This appears to be a PowerShell specific behaviour as the equivalent C# code will throw an exception.

using System.Collections.Generic;

List<string> Numbers = new();
Error: (6,13): error CS1503: Argument 1: cannot convert from 'int' to 'string'

Another caveat is that List<T> accepts null values regardless of the type specified.

PS C:\> $List = [List[int]]::New()
PS C:\> $List.Add($null)
PS C:\> $List

PS C:\> $List = [List[object]]::New()
PS C:\> $List.Add($null)
PS C:\> $List.Count

Setting aside tangents and caveats, List<T> vastly outperforms PowerShell arrays, offering significant performance improvements.

.NET Syntax in PowerShell

We have explored several .NET classes, which might raise questions about their syntax in PowerShell.

Understanding [ClassName]::MethodName() Syntax

We’ve seen this used when creating a new list ([List[int]]::New()) and when parsing MAC addresses ([PhysicalAddress]::Parse()). This syntax is used when we’re executing static methods of classes. Static in this context means the class does not need to be instantiated into an object first for us to use the method.

Here it is shown in the Microsoft documentation for the PhysicalAddress class.

Microsoft documentation showing Parse as a static method

When we use the usual . notation to invoke a method (as we did with $List.Add()) this the same as invoking a method on a PowerShell object, for example $SomeString.ToUpper().

Default imports

To use List<T>, we specify using namespace System.Collections.Generic. This wasn’t necessary for [IPAddress] or [PhysicalAddress] because PowerShell imports many common namespaces by default, simplifying type resolution.

There’s no way that I’m aware of to list all default imported namespaces. There is [AppDomain]::CurrentDomain.GetAssemblies(), which shows loaded assemblies, but having the assembly loaded does not guarantee all nested namespaces in that assembly will be imported. Here we can see the System.Collections.dll assembly is loaded, but we still need to fully qualify List<T>.

PS C:\> [AppDomain]::CurrentDomain.GetAssemblies() | ? Location -match "System.Collections"

GAC    Version        Location
---    -------        --------
False  v4.0.30319     C:\Program Files\PowerShell\7\System.Collections.dll
False  v4.0.30319     C:\Program Files\PowerShell\7\System.Collections.Concurrent.dll
False  v4.0.30319     C:\Program Files\PowerShell\7\System.Collections.Specialized.dll
False  v4.0.30319     C:\Program Files\PowerShell\7\System.Collections.NonGeneric.dll
False  v4.0.30319     C:\Program Files\PowerShell\7\System.Collections.Immutable.dll

PS C:\> [List[string]]::New()
InvalidOperation: Unable to find type [List].

In short, some namespaces will be imported by default, others you’ll need to specify even if the assembly is loaded.

Microsoft’s documentation is an excellent resource for mapping classes to namespaces and assemblies.

List class documentation

And here is a list of all classes in the System.Collections.Generic namespace. I will briefly cover HashSets, but I’d recommend exploring others as a learning exercise.

Loading custom or third party assemblies

Sometimes the classes we want to work with are in third party assemblies. We can load these using Add-Type. As this is slightly out of scope for this post and I’ve shown examples of it before, you can check it out here.

Using LINQ in PowerShell

LINQ (Language Integrated Query) is a .NET component that allows for data querying and filtering. Think of a combination PowerShell’s Where, Measure-Object, and Select cmdlets, but usually a lot faster than native PowerShell cmdlets, especially when dealing with large collections.

Let’s look at a couple of simple examples.

List comparison with LINQ in PowerShell

We’re going to create a few lists and compare their contents with LINQ, including the order of the items in the lists.

$List1 = [List[string]]("one", "two", "three")
$List2 = [List[string]]("one", "two", "three")
$List3 = [List[string]]("three", "two", "one") # Same items, different order

[Linq.Enumerable]::SequenceEqual($List1, $List2);
# True

[Linq.Enumerable]::SequenceEqual($List1, $List3);
# False due to ordering being different

There is a Sort method we can use if we have lists where the items may have different ordering:


[Linq.Enumerable]::SequenceEqual($List1, $List2);
# True

[Linq.Enumerable]::SequenceEqual($List1, $List3);
# True

Getting minimum, maximum, average values from a collection

$List = [List[int]]::new()
1..100 | % {$List.Add((Get-Random -Minimum 1 -Maximum 1000))}

PS C:\> [Linq.Enumerable]::Min($List)

PS C:\> [Linq.Enumerable]::Max($List)

PS C:\> [Linq.Enumerable]::Average($List)

PS C:\> [Linq.Enumerable]::Sum($List)

As a very quick performance comparison let’s see how LINQ compares with Measure-Object for calculating the sum of one million numbers.

$List = [List[int]]::new()
1..1000000 | % {$List.Add((Get-Random -Minimum 1 -Maximum 100))}

# Measure-Object
PS C:\> 1..10 | % {Measure-Command {$List | Measure-Object -Sum}  | select -ExpandProperty TotalMilliseconds} | Measure-Object -Average

Average           : 857.56657

# Linq.Enumerable
PS C:\> 1..10 | % {Measure-Command {[Linq.Enumerable]::Sum($List)}  | select -ExpandProperty TotalMilliseconds} | Measure-Object -Average

Average           : 0.45215

In this example LINQ is ~1897 times faster than piping to Measure-Object

There are too many LINQ methods to cover here, and I’ve only scratched the surface to show the most basic syntax. Michael Sorens has written a fantastic article on using LINQ with PowerShell titled High Performance PowerShell with LINQ that I recommend you check out.

Listing Available Methods and Properties of .NET Classes

If you’re wondering what other static methods LINQ has, you may have tried to pipe the class to Get-Member. You may have also tried to do the same for a List<T> object and found unexpected results.

What you’ll find with collections like List<T> is Get-Member returns data of the type of the first item inside the collection.

PS C:\> $List = [List[int]]::new()

PS C:\> $List | Get-Member
Get-Member: You must specify an object for the Get-Member cmdlet.

PS C:\> $List.Add(1)

PS C:\> $List | Get-Member

   TypeName: System.Int32 # <------------

Name                 MemberType Definition
----                 ---------- ----------
CompareTo            Method     int CompareTo(System.Object value), int CompareTo(int value), int IComparable.CompareTo(System.Object obj), int IComparable[int].CompareTo(int other)
Equals               Method     bool Equals(System.Object obj), bool Equals(int obj), bool IEquatable[int].Equals(int other)
GetByteCount         Method     int IBinaryInteger[int].GetByteCount()
GetHashCode          Method     int GetHashCode()
GetShortestBitLength Method     int IBinaryInteger[int].GetShortestBitLength()
# truncated

We can use GetMembers() to get the list of methods and properties. When calling it on an already instantiated object we first need to bubble up the class using GetType(), and then call GetMembers().

PS C:\> PS C:\> $List.GetType().GetMembers() | ? MemberType -eq "Method" | Select -ExpandProperty Name -Unique

If calling it on a type, we can omit the GetType() call. Here are some of the LINQ methods available:

PS C:\> [Linq.Enumerable].GetMembers() | ? MemberType -eq "Method" | ? IsStatic | Select -ExpandProperty Name -Unique
# truncated

Using LINQ with other .NET collection types

LINQ works with any collection class that implements IEnumerable<T>, not just lists. So what is IEnumerable<T> and how do we find out what “implements” it?

.NET Interfaces

Interfaces are a contractual blueprint for classes that define what methods and properties implementing classes must provide. The classes that implement that interface then define how those members are implemented.

For example, we could have an IAnimal interface (the convention for interface naming is to begin with a capital I) which states that any classes implementing it must have a MakeNoise method that returns a string (e.g., “Woof”).

When we then create our various animal classes we make them implement the IAnimal interface, and in each animal class we will need to write the MakeNoise method (or we will get IDE / compilation errors) and have it return the noise of that specific animal.

Using interfaces means we can have confidence that regardless of the animal, there will be a MakeNoise method that returns a string. There are other advantages to interfaces such as decoupling which helps facilitate changes but none of that is important for our needs here.

Back to LINQ - it works with classes that implement the IEnumerable<T> interface. How do we find what those are?

There are two ways in PowerShell.


We can use the GetInterfaces method:

PS C:\> [System.Collections.Generic.List[int]].GetInterfaces()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     False    IList`1
True     False    ICollection`1
True     False    IEnumerable`1True     False    IEnumerable
True     False    IList
True     False    ICollection
True     False    IReadOnlyList`1
True     False    IReadOnlyCollection`1

We can see List<T> list implements both IEnumerable and IEnumerable'1, so what’s the difference? The the backtick and digit indicates this is a generic type (List<T> instead of just List) and refers to the number of type parameters it accepts.

For example, List<T> only accepts one, but a Dictionary<T key,T value> accepts two (one for the key and one for the value), as seen below:

PS C:\> [System.Collections.Generic.Dictionary[int,string]].GetInterfaces()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     False    IDictionary`2True     False    ICollection`1
True     False    IEnumerable`1
True     False    IEnumerable
True     False    IDictionary
True     False    ICollection
True     False    IReadOnlyDictionary`2
True     False    IReadOnlyCollection`1
True     False    ISerializable
True     False    IDeserializationCallback

PowerShell native arrays only implement IEnumerable, not IEnumerable<T>, so they don’t work with LINQ without casting (more on this below).

PS C:\> [Array].GetInterfaces()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     False    ICloneable
True     False    IList
True     False    ICollection
True     False    IEnumerableTrue     False    IStructuralComparable
True     False    IStructuralEquatable

Let’s try it out:

PS C:\> $Arr = @(1,2,3,4,5,6,7,8,9)
PS C:\> [Linq.Enumerable]::Sum($Arr)

MethodException: Cannot find an overload for "Sum" and the argument count: "1".

The second method to determine whether something implements an interface is to use IsAssignableFrom.


First we need to define our type, then the interface, and then call the IsAssignableFrom method.

PS C:\> $Type = [List[string]]
PS C:\> $Interface = [IEnumerable[string]]
PS C:\> $Interface.IsAssignableFrom($Type)


Let’s check PowerShell arrays.

PS C:\> $Type = [Array]
PS C:\> $Interface = [IEnumerable[string]]
PS C:\> $Interface.IsAssignableFrom($Type)


We can also check the class documentation.


Here is a screenshot showing what interfaces List<T> implements.

List<T> interfaces

Let’s cover casting PowerShell arrays to an object that enables us to use LINQ.

Casting PowerShell arrays to an IEnumerable<T> object

We may already have an existing array that we would like to use with LINQ, and in these cases LINQ provides a casting method that returns a usable objects which implements IEnumerable<T>.

$Numbers = @(1, 2, 3, 4, 5)
$GenericNumbers = [Linq.Enumerable]::Cast[int]($Numbers)

# We can now use LINQ
PS C:\> [Linq.Enumerable]::Average($GenericNumbers)

PS C:\> $GenericNumbers.GetType()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
False    False    <CastIterator>d__68`1                    System.Object

Let’s move onto the final .NET class for this post: HashSets

.NET HashSet<T> in PowerShell

A HashSet<T> is another generic .NET collection class similar to a List<T> with a few notable differences.

  1. It is unordered (though there is also a SortedSet<T>)
  2. All items in the HashSet must be unique (no duplicates)
  3. It cannot be indexed into using $HashSet[<int>]

As we’re now familiar with most of the syntax, let’s jump straight into the examples:

Creating the HashSet<T> object:

using namespace System.Collections.Generic

$HashSet = [HashSet[int]]::New()

Adding, searching for, and removing items:

# Adding an item
PS C:\> $HashSet.Add(1)

# Checking if the set contains an item
PS C:\> $HashSet.Contains(1)

# Duplicates are not permitted
PS C:\> $HashSet.Add(1)

# Removing an item
PS C:\> $HashSet.Remove(1)

As HashSet<T> implements IEnumerable<T> we can use LINQ without any casting. Let’s look at slightly more complex example than before. We’re going to create two HashSets, the first will be a set of IP addresses we “trust”. The second will be a list of IP addresses perhaps pulled from some access logs. What we want to do is return a collection of unique IPs in the second set, but exclude the “trusted” IPs from the first set.

HashSet Venn diagram showing overlap between trusted IP set and Access Log IP set

using namespace System.Collections.Generic

$TrustedIPs = [HashSet[IPAddress]]::New([IPAddress[]] (gc .\trusted.txt))
$LoggedIPs = [HashSet[IPAddress]]::New([IPAddress[]] (gc .\accesslog.txt))

Here are the contents of those two files:

# trusted.txt

# accesslog.txt

We can see our accesslog file has several duplicates, but once we add it to our set the duplicate values are no longer present.

PS C:\> $LoggedIPs | select -ExpandProperty IPAddressToString

Now let’s use LINQ to extract the logged IPs but exclude our trusted addresses.

PS C:\> $SuspiciousIPs = [Linq.Enumerable]::Except($LoggedIPs, $TrustedIPs)
PS C:\> $SuspiciousIPs | Select -Expand IPAddressToString

That wraps it up. Hopefully you’ve not only discovered some useful .NET classes but also have a bit of insight into how to find and use other types that haven’t been covered here.

If you enjoyed this post consider sharing it on , , , or , and .