Summary: Learn how to correctly package .NET Framework classes into reusable Windows PowerShell advanced functions in this step-by-step solution.

 

Hey, Scripting Guy! QuestionHey, Scripting Guy! I have been exploring the Microsoft .NET Framework on MSDN and attempting to convert some examples I have found there into Windows PowerShell code. I will admit that it can be somewhat tedious and I find myself getting confused if the example is too complex. It seems that when I do get a sample working, I have a script that works but it is pretty much single purpose. If I want to be able to do something else I have to write another script. Is there a better approach that I should be taking?

 

-- BR

 

Hey, Scripting Guy! AnswerHello BR, Microsoft Scripting Guy Ed Wilson here. I am glad you have started exploring the .NET Framework classes. In my mind, the ability to use the .NET Framework from inside a Windows PowerShell script is one of the great strengths of Windows PowerShell. It means that we are not limited to the 236 cmdlets that are included with and are preloaded  in Windows PowerShell 2.0. In fact, with a bit of work (sometimes with lots of work) you can wrap these .NET Framework classes into advanced functions that behave just like the Windows PowerShell cmdlets that are included with Windows. These functions could then be placed in a module to facilitate deployment. I asked James Brundage to share his thoughts on how to correctly package .NET Framework classes to promote ease of reuse.

James Brundage is the founder of Start-Automating (http://www.start-automating.com), a company dedicated to saving people time and money by helping them automate. Start-Automating offers Windows PowerShell training and custom Windows PowerShell and .NET development.  James formerly worked on the Windows PowerShell team at Microsoft, and is the author of the PowerShellPack which is a collection of Windows PowerShell scripts to enable building user interfaces, interact with RSS feeds, retrieve system information, and more. You can follow James on Twitter. He is @JamesBru.

 

A Series on Splatting (and Stuff)

Part 4: Splatting and the .NET Framework

Welcome back to A Series on Splatting (and Stuff).  In previous posts, we’ve covered how to build commands that wrap other commands with splatting and building bridges between two commands with splatting.  Today, we’ll learn how to apply those skills to make some usable Windows PowerShell scripts to replace some hard-to-use .NET Framework classes.

Even though Windows PowerShell is built on top of the .NET Framework, I believe that using Windows PowerShell hides away its .NET Framework origins within the cmdlets. This makes things miles easier for everyone (yourself included) to understand than using the .NET Framework classes directly.  Unfortunately, there is a lot of stuff surfaced in the .NET Framework universe that is not put into really clean Windows PowerShell cmdlets (at least not yet). This means that if you write enough Windows PowerShell scripts, you will eventually come across something that can only be achieved by resorting to directly manipulating .NET Framework classes.

Because you’re reading this series and this blog, and have made it to this point, I’m going to assume that you are worth your salt.  I’m going to go out on a limb and guess that you still might prefer to think of things in Windows PowerShell’s verb-noun parlance (Get-Process) instead of C# coding conventions (System.Diagnostics.Processes.GetProcesses()).

Today, we’ll walk through turning .NET commands into easy to use Windows PowerShell functions, by using splatting and some other stuff we’ve talked about this week.

Splatting is especially great here because most interactions with .NET can be thought of as creating an object, setting lots of properties, and invoking methods.  You can put creating an object and setting its properties in a private function and you can put whatever things that you would need to do before and after you run the method inside a another function, and you can use splatting to call these two functions for many different operations.

We’ll learn how to do this by using System.Net.Webclient. The WebClient .NET Framework class lets you upload or download data to the web. It is the key to interacting with web sites, FTP sites, and REST web services from Windows PowerShell.

System.Net.Webclient is actually a great candidate, because it has lots of operations that are very similar and several properties that do something useful.

Let's start off by creating a new instance of the webclient class. To do this, use the New-Object Windows PowerShell cmdlet as shown here.

$webClient = New-Object Net.Webclient

Now let’s see what methods and properties might be interesting by piping the newly created object to the Get-Member Windows PowerShell cmdlet and the Out-GridView cmdlet as seen here.

$webClient |
    Get-Member |
    Out-GridView

There are a lot there, but I'd say the operations that are most interesting are the download/upload operations: DownloadData / DownloadString / DownloadFile and UploadData / UploadString / UploadFile / UploadValues.

In the properties, there are many more we will use with any of the operations.  There are the credentials to use, and a collection of parameters to use when a web request is made, and a collection of headers to use. All this information is documented on MSDN.

I won't lead you on. Making a good command out of this will be a bit of work but the techniques that you learn today will help you any time that you have to make similar commands.

The first step will be creating good Windows PowerShell parameters for each .NET property we could end up using in any scenario.  The properties we are interested in are:

  • BaseAddress
  • Credentials
  • Headers
  • QueryString
  • UseDefaultCredentials

Windows PowerShell 2.0 introduced a new parameter for the New-Object cmdlet property, that makes setting these up very easy.  The core function that creates the WebClient will take these parameters and convert them into a webclient object. This is seen here.

function New-WebClient(
[string]$BaseAddress,
[Net.ICredentials]$Credentials,
[Net.WebHeaderCollection]$Headers,
[Collections.Specialized.NameValueCollection]$QueryString,
[bool]$UseDefaultCredentials
) {
    New-Object Net.Webclient -Property $psBoundParameters
}

If you have done a decent amount of Windows PowerShell scripting, you might notice some problems with the script above.  There are several things ”wrong” with this kind of script which makes it both inconvenient and buggy to use. These problems are listed here.

  • There is no clear way to turn Get-Credential into [Net.ICredentials].
  • WebHeaderCollection and NameValueCollection might be convenient to put in as a hash table. But it won't work.
  • UseDefaultCredentials should be a switch parameter.
  • Credentials, Headers, and UseDefaultCredentials are plurals (good Windows PowerShell parameters are never plural).

We want a function that has “good” Windows PowerShell input.  Here’s what New-Webclient might resemble if it had the parameters we wanted.

function New-WebClient(
[string]$BaseAddress,
[Management.Automation.PSCredential]$Credential,
[Hashtable]$Header,
[Hashtable]$Query,
[Switch]$Anonymous
) {
    
}

The following things have changed.

  • $Credential is now [Management.Automation.PSCredential], and is no longer plural.
  • $Headers is no longer plural, and is now a hash table.
  • $QueryString was renamed to $Query (because we are not constructing a string).
  • $UseDefaultCredentials was turned into $Anonymous (this will be handy if most of the stuff you want to access requires impersonation).

We’ve created several wrapper commands in the past.  If you have a situation like this where you want to provide convenient input for users for a complex set of properties, make a second parameter set that has the complex properties. Creating multiple parameter sets is easier than it sounds, just add [Parameter(ParameterSetName=”ParameterSetName”)] in front of each parameter that is in each set.  If you want the parameter to appear in multiple parameter sets, use the attribute multiple times.  If you want the parameter to be in all parameter sets, leave the attribute out.

Because one parameter set is a lot more difficult to use in Windows PowerShell than the other, I’ve named the parameter sets “Hard” and “Easy”.  When the “Hard” parameter set is used, the values just get passed on in.  The “Easy” parameter set will translate them. Here’s a stub that shows performing this task.

function New-WebClient
{
    param(
    [string]$BaseAddress,
    [Parameter(ParameterSetName='Hard')]
    [Net.ICredentials]$Credentials,
    [Parameter(ParameterSetName='Easy')]
    [Management.Automation.PSCredential]$Credential,
    [Parameter(ParameterSetName='Hard')]
    [Net.WebHeaderCollection]$Headers,
    [Parameter(ParameterSetName='Easy')]
    [Hashtable]$Header,
    [Parameter(ParameterSetName='Hard')]
    [Collections.Specialized.NameValueCollection]$QueryString,
    [Parameter(ParameterSetName='Easy')]
    [Hashtable]$Query,
    [Parameter(ParameterSetName='Hard')]
    [bool]$UseDefaultCredentials,
    [Parameter(ParameterSetName='Easy')]
    [Switch]$Anonymous   
    )
   
    process {
        if ($psCmdlet.ParameterSetName -eq "Hard") {
            New-Object Net.Webclient -Property $psBoundParameters
        } else {
            # ...
        }
    }   
}

We’ll walk through filling out that stub one-by-one in the next bit, but let’s set up some test data first.  Here’s a fairly complex $psBoundParameters I might get from the “Easy” parameter set that I’ll want to translate into the Hard one.

$psBoundParameters = @{
    BaseAddress = "http://www.bing.com"
    Credential = $null
    Header = @{
        "User-Agent" = "WindowsPowerShell /2.0"
    }
    Query = @{
        "Q" = "PowerShell"
    }
    Anonymous = $true   
}

Everything else goes inside the “…” in the New-WebClient function.

To turn this into New-WebClient's Hard parameters, we'll have to correct the parameters one by one. To do this, we'll first create a copy of $psBoundParameters by adding it to a blank hash table. This is seen here.

$newWebClientParameters = @{} + $psBoundParameters

Now we have to turn each item into the correct one.  Because several were renamed we'll have to see whether the convenient value is there, create a new entry with the raw value, and remove the convenient value.  We do not have to worry about BaseAddress because this will be the same in both the easy and hard parameter sets.

Anonymous is the easiest.  Because -Anonymous is a switch, it may or may not have been in psBoundParameters (remember, $psBoundParameters contains supplied values, not defaults).  We simply force $newWebClientParameters to not contain a value for UseDefaultCredentials and remove the value for Anonymous. This is illustrated here.

$newWebClientParameters.UseDefaultCredentials = -not $Anonymous
$null = $newWebClientParameters.Remove("Anonymous")

$Credential is fairly easy.  ICredentials are used throughout .NET. Therefore, Windows PowerShell credentials have a fairly well paved road to them. This is seen here.

if ($newWebClientParameters.Credential) {
    $newWebClientParameters.Credentials = $newWebClientParameters.Credential.GetNetworkCredential()
    $null = $newWebClientParameters.Remove("Credential")
}

$Header and $Query are almost the same.  We have to walk through the hash table and convert it into the collection that is needed. This is illustrated here.

if ($newWebClientParameters.Header) {
    $newWebClientParameters.Headers = New-Object Net.WebHeadercollection
    foreach ($headerPair in $newWebClientParameters.Header.GetEnumerator()) {
        $null = $newWebClientParameters.Headers.Add($headerPair.Key, $headerPair.Value)
    }
    $null = $newWebClientParameters.Remove("Header")
}
if ($newWebClientParameters.Query) {
    $newWebClientParameters.QueryString = New-Object Collections.Specialized.NameValueCollection
    foreach ($QueryPair in $newWebClientParameters.Query.GetEnumerator()) {
        $null = $newWebClientParameters.QueryString.Add($QueryPair.Key, $QueryPair.Value)
    }
    $null = $newWebClientParameters.Remove("Query")
}

Now here’s our finished New-WebClient, all in one function:

function New-WebClient
{
    [CmdletBinding(DefaultParameterSetName="Easy")]
    param(
    [string]$BaseAddress,
    [Parameter(ParameterSetName='Hard')]
    [Net.ICredentials]$Credentials,
    [Parameter(ParameterSetName='Easy')]
    [Management.Automation.PSCredential]$Credential,
    [Parameter(ParameterSetName='Hard')]
    [Net.WebHeaderCollection]$Headers,
    [Parameter(ParameterSetName='Easy')]
    [Hashtable]$Header,
    [Parameter(ParameterSetName='Hard')]
    [Collections.Specialized.NameValueCollection]$QueryString,
    [Parameter(ParameterSetName='Easy')]
    [Hashtable]$Query,
    [Parameter(ParameterSetName='Hard')]
    [bool]$UseDefaultCredentials,
    [Parameter(ParameterSetName='Easy')]
    [Switch]$Anonymous   
    )
   
    process {
        if ($psCmdlet.ParameterSetName -eq "Hard") {
            New-Object Net.Webclient -Property $psBoundParameters
        } else {
            $newWebClientParameters = @{} + $psBoundParameters

            $newWebClientParameters.UseDefaultCredentials = -not $Anonymous
            $null = $newWebClientParameters.Remove("Anonymous")

            if ($newWebClientParameters.Credential) {
                $newWebClientParameters.Credentials = $newWebClientParameters.Credential.GetNetworkCredential()
                $null = $newWebClientParameters.Remove("Credential")
            }

            if ($newWebClientParameters.Header) {
                $newWebClientParameters.Headers = New-Object Net.WebHeadercollection
                foreach ($headerPair in $newWebClientParameters.Header.GetEnumerator()) {
                    $null = $newWebClientParameters.Headers.Add($headerPair.Key, $headerPair.Value)
                }
                $null = $newWebClientParameters.Remove("Header")
            }

            if ($newWebClientParameters.Query) {
                $newWebClientParameters.QueryString = New-Object Collections.Specialized.NameValueCollection
                foreach ($QueryPair in $newWebClientParameters.Query.GetEnumerator()) {
                    $null = $newWebClientParameters.QueryString.Add($QueryPair.Key, $QueryPair.Value)
                }
                $null = $newWebClientParameters.Remove("Query")
            }
            New-WebClient @newWebclientParameters
        }
    }   
}

Let’s take it for a spin. To do this, all that is required is to supply a few parameters when calling the New-WebClient function and store the returned object in the $webClient variable. The DownloadString method is then called and outputs the webpage to a htm file. The Invoke-Item cmdlet the opens the saved Webpage. This code is shown here.

$webClient = New-WebClient -BaseAddress "http://www.start-automating.com/" -Header @{"User-Agent" = "WindowsPowerShell /2.0"}
$webClient.DownloadString("") > test.htm
Invoke-Item .\Test.htm

The output from this command is seen in the figure below.

 

BR, that is all there is to using Windows PowerShell splatting to wrap messy .NET Framework commands into a nice, easy to use Windows PowerShell function. Guest blogger week will continue tomorrow when James will wrap up his series on splatting and stuff in Windows PowerShell 2.0.

We invite you to follow us on Twitter and Facebook. If you have any questions, send email to us at scripter@microsoft.com, or post your questions on the Official Scripting Guys Forum. See you tomorrow. Until then, peace.

 

Ed Wilson and Craig Liebendorfer, Scripting Guys