Using .NET With PowerShell
Posted on April 21, 2024
- and tagged as
- dotnet,
- 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?
- Determining what version of .NET CLR PowerShell is using
- String validation
- IP Address validation using [IPAddress]
- MAC Address Validation and Normalisation using [PhysicalAddress]::Parse()
- Fast TCP port testing with [System.Net.Sockets.TcpClient]
- Generating passwords with [System.Web.Security]
- Using List<T> instead of PowerShell Arrays
- .NET Syntax in PowerShell
- Using LINQ in PowerShell
- Listing Available Methods and Properties of .NET Classes
- Using LINQ with other .NET collection types
- Casting PowerShell arrays to an IEnumerable<T> object
- .NET HashSet<T> in PowerShell
Why use .NET classes in PowerShell?
There are two main reasons:.
- Extended Functionality: Access features in .NET not available natively in PowerShell.
- 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()
5.1.19041.3996
PS C:\> [Environment]::Version.ToString()
4.0.30319.42000
PowerShell 7.4.1 uses .NET Core 8.0.1
PS C:\> ($PSVersionTable).PSVersion.ToString()
7.4.1
PS C:\> [Environment]::Version.ToString()
8.0.1
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"
[String]::IsNullOrEmpty($NullVar)
[String]::IsNullOrEmpty($Empty)
[String]::IsNullOrEmpty($Whitespace)
[String]::IsNullOrEmpty($Valid)
Write-Host "IsNullOrWhiteSpace"
[String]::IsNullOrWhiteSpace($NullVar)
[String]::IsNullOrWhiteSpace($Empty)
[String]::IsNullOrWhiteSpace($Whitespace)
[String]::IsNullOrWhiteSpace($Valid)
Result:
IsNullOrEmpty
True
True
False
False
IsNullOrWhiteSpace
True
True
True
False
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]"10.1.1.1"
Address : 16843018
AddressFamily : InterNetwork
ScopeId :
IsIPv6Multicast : False
IsIPv6LinkLocal : False
IsIPv6SiteLocal : False
IsIPv6Teredo : False
IsIPv4MappedToIPv6 : False
IPAddressToString : 10.1.1.1
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 : 10.0.0.1
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.1.1", "10.1", "0xA51F2C44", "134744072", "192.168.1.1.1", "300.1.1.1", "abcd") | % {$_ -eq ([IPAddress]$_).IPAddressToString}
True
False
False
False
InvalidArgument: Cannot convert value "192.168.1.1.1" 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")
34ED1BAABBCC
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")
34ED1BAABBCC
PS C:\> [PhysicalAddress]::Parse("34:ED:1B:AA:BB:CC")
34ED1BAABBCC
PS C:\> [PhysicalAddress]::Parse("34ED.1BAA.BBCC")
34ED1BAABBCC
These changes are documented in the Microsoft documentation.
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 = "xkln.net"
PS C:\> $Port = 443
PS C:\> $TimeoutMS = 100
PS C:\> $Client = [System.Net.Sockets.TcpClient]::New()
PS C:\> $Client.ConnectAsync($Site,$Port).Wait($TimeoutMS)
True
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)
7!=vvIOh45ib3p+]d{U.&AGSvHy42+
PS C:\> [System.Web.Security.Membership]::GeneratePassword(20,10)
g}(2H|t@A)M-pv&iz+d$hsZmwxk[NU
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.
@(10,20,"thirty")
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}
5
10
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()
$Numbers.Add(10)
$Numbers.Add(20)
PS C:\> $Numbers
10
20
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()
$Numbers.Add("ten")
$Numbers.Add("twenty")
$Numbers.Add(30)
Here the integer 30
was automatically cast to the string "30"
.
PS C:\> $Numbers
ten
twenty
30
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();
Numbers.Add("ten");
Numbers.Add("twenty");
Numbers.Add(30);
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
0
PS C:\> $List = [List[object]]::New()
PS C:\> $List.Add($null)
PS C:\> $List.Count
1
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.
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()
.
Type Accelerators
To use List<T>
, we specify using namespace System.Collections.Generic
. This wasn’t necessary for [IPAddress]
or [PhysicalAddress]
because PowerShell makes many classes available for us by default using type accelerators.
The documentation puts it succinctly:
Type accelerators are aliases for .NET framework classes. They allow you to access specific .NET framework classes without having to explicitly type the entire class name.
As the documentation tells us, they need to be wrapped in [SquareBrackets]
. We can see a list of all type accelerators using the following command:
PS C:\> [PSObject].Assembly.GetType("System.Management.Automation.TypeAccelerators")::Get
Key Value
--- -----
Alias System.Management.Automation.AliasAttribute
AllowEmptyCollection System.Management.Automation.AllowEmptyCollectionAttribute
AllowEmptyString System.Management.Automation.AllowEmptyStringAttribute
AllowNull System.Management.Automation.AllowNullAttribute
ArgumentCompleter System.Management.Automation.ArgumentCompleterAttribute
array System.Array
bool System.Boolean
byte System.Byte
char System.Char
CmdletBinding System.Management.Automation.CmdletBindingAttribute
datetime System.DateTime
decimal System.Decimal
double System.Double
DscResource System.Management.Automation.DscResourceAttribute
float System.Single
# Truncated
In short, some classes will be available via type accelerators, others you’ll need to import manually. Microsoft’s documentation is an excellent resource for mapping classes to namespaces and assemblies.
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:
$List1.Sort()
$List2.Sort()
$List3.Sort()
[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)
32
PS C:\> [Linq.Enumerable]::Max($List)
983
PS C:\> [Linq.Enumerable]::Average($List)
513.95
PS C:\> [Linq.Enumerable]::Sum($List)
51395
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
get_Capacity
set_Capacity
get_Count
get_Item
set_Item
Add
AddRange
AsReadOnly
BinarySearch
Clear
Contains
ConvertAll
CopyTo
EnsureCapacity
Exists
Find
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
Empty
Aggregate
Any
All
Append
Prepend
Average
OfType
Cast
Chunk
Concat
Contains
Count
TryGetNonEnumeratedCount
LongCount
DefaultIfEmpty
Distinct
DistinctBy
ElementAt
ElementAtOrDefault
AsEnumerable
Except
ExceptBy
First
# 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.
GetInterfaces
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
.
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)
True
Let’s check PowerShell arrays.
PS C:\> $Type = [Array]
PS C:\> $Interface = [IEnumerable[string]]
PS C:\> $Interface.IsAssignableFrom($Type)
False
We can also check the class documentation.
Documentation
Here is a screenshot showing what interfaces List<T>
implements.
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)
3
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.
- It is unordered (though there is also a
SortedSet<T>
) - All items in the HashSet must be unique (no duplicates)
- 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)
True
# Checking if the set contains an item
PS C:\> $HashSet.Contains(1)
True
# Duplicates are not permitted
PS C:\> $HashSet.Add(1)
False
# Removing an item
PS C:\> $HashSet.Remove(1)
True
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.
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
10.250.1.100
10.250.1.101
# accesslog.txt
192.168.1.50
10.250.1.120
10.250.1.100
21.57.145.95
10.250.1.100
10.250.1.100
10.250.1.100
125.28.60.35
125.28.60.35
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
192.168.1.50
10.250.1.120
10.250.1.100
21.57.145.95
125.28.60.35
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
192.168.1.50
10.250.1.120
21.57.145.95
125.28.60.35
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.