Using C# to Create PowerShell Cmdlets: The Basics

Although PowerShell Cmdlets are usually written in PowerShell, there are occasions when the level of integration with existing C# or VB libraries is awkward to achieve with PowerShell. Yes, you can write Cmdlets in C# perfectly easily, but until now it has been tiresome to discover how. Now Michael Sorens shows you the simple route to writing effective C# Cmdlets.

Contents

So you really enjoy the power and flexibility you get from using PowerShell cmdlets. You have played around with writing some functions and perhaps some cmdlets in PowerShell. Now your organization wants to include a PowerShell front-end to the .NET libraries it produces, which are all developed in C#. Thus, it makes sense to write the cmdlets in C# as well, giving better integration with your build tools and unit tests… not to mention that the rest of your development team is not as versed in writing in PowerShell. This is not difficult to do; in fact, the hardest part is finding the information on how to do it… a task which you have just accomplished!

There are different ways to accomplish most significant development tasks. This one is no different.  This guide presents one way but it is certainly not the only way. In fact, you can take a short-cut around some of the things I will present: The ‘best practices’ I will describe herein are just that, but don’t discard them lightly.

The following is a complete recipe for creating a simple cmdlet, Get-NetworkAdapter, which reports on all the network adapters on your computer, with some filtering capabilities on different properties. Attached to this article is a complete, working solution with the implementation of the cmdlet. All the bits of code you see here come from that sample.

Step 1: Create a Visual Studio project

Within your Visual Studio solution, you will house your cmdlets in a project, just as you would any other component you are building. For PowerShell, create a Class Library project so that once you have built the project, you have a DLL that comprises your PowerShell cmdlets.

Step 2: Provide a reference to .NET PowerShell resources

Until recently (December, 2015) this step was tedious, vague, and wishy-washy. Now it is clean and streamlined-thanks, Microsoft, for “nuget-izing” PowerShell’s reference assemblies! Here the goal is to add a reference to System.Management.Automation-the core of PowerShell-but you won’t find it if you just browse .NET assemblies. You need to use the NuGet Package Manager in Visual Studio to install the PowerShell reference assemblies first. They are available for PowerShell versions 3, 4, or 5, so choose the appropriate one for your environment. You can easily find it in the package manager by searching for “PowerShell”, as shown.

2370-img39.jpg

Then just select your PowerShell project created in step 1 to attach it to. Once installed, it will not only make the reference assemblies available, but it will also add just the reference you need in the project.

2370-img3A.jpg

Step 3: Rename the default Class1 class to reflect your cmdlet

Rename the default Class1 class, or create a new class if you are adding another cmdlet to the project, so that its name conforms to this format:

<verb><noun>Cmdlet.cs

 For this walkthrough, the cmdlet will be Get-NetworkAdapter so rename Class1 to GetNetworkAdapterCmdlet.cs.

Step 4: Inherit from Cmdlet

Instantly make your class into a cmdlet by inheriting from System.Management.Automation.Cmdlet:

There are times that you may need to inherit from PSCmdlet instead of Cmdlet, but for this article Cmdlet will suffice.

Step 5: Decorate your class with CmdletAttribute

Sadly, step 4 did not really make it a cmdlet. You also need to use System.Management.Automation.CmdletAttribute with appropriate arguments, namely the same verb and noun used in the class name defined in step 3. Verbs, by the way, should always come from the list of approved verbs, and you should use the predefined value for it here, rather than a string constant. Nouns, on the other hand, are unconstrained, so you must supply your own string (or put it in a constant).

At this point, you have a cmdlet, though it does not yet do anything. To confirm that it is really a cmdlet, first build your project,  then load and examine what is in your module:

Note that the exposed name of the cmdlet comes from the Cmdlet, not from the class name!

Step 6: Create your output container

Inputs and outputs for your cmdlet need to be properly managed. You do not, for example, read from or write to the console as you would in a typical interactive program. Let’s consider outputs first. And let’s assume your cmdlet will have just one kind of output. (You can certainly have more: Get-ChildItem, for example, returns both FileInfo and DirectoryInfo objects.) For the sample cmdlet we are returning information about network adapters so we will simply call the output object a NetworkAdapter. Windows Management Instrumentation, or WMI, is the library to explore your system’s resources. It returns quite a lot of properties about a network adapter; for this exercise we will just use these select few:

Step 7: Decorate your class with OutputTypeAttribute

With your output object defined, you can now tell PowerShell what your cmdlet emits to the pipeline with the System.Management.Automation.OutputType attribute.  I have highlighted the new code in yellow here:

This is important for two reasons. First, it contributes to your cmdlet’s auto-generated help text. Here is what Get-Help provides before and after applying the OutputType attribute (note the use of the -Full switch). Everything is the same except for the OUTPUTS section, highlighted below:

Second, and perhaps more importantly, using the OutputType provides tab completion! Before you add the OutputType attribute, if you type this (the last part is “Select-Object“, then a space, then a tab)…

… nothing happens. But after you apply the OutputType attribute, here is what happens. (As this is not a video, you need to imagine the time-lapse nature of this figure.) When you type the first line of each pair, PowerShell turns it into the second line. I have added the highlighting to draw your attention to the iteration through the list of NetworkAdapter‘s properties as defined above.

The first <tab> activated the tab-completion, filling in the remaining portion of the first available choice lexicographically (from the set of NetworkAdapter‘s properties), i.e.  Description.

By immediately pressing <tab> again, you will cycle through to the next choice, DeviceId. If you keep pressing <tab>  it will eventually circle back to the start of the list, as shown. Note that I said the “remaining portion” just above. So if you had typed, say, “Select-Object De<tab>” then it will just fill in the first available choice beginning with “De”, namely, Description and cycle between Description and DeviceId.

Step 8: Create your inputs

Now let’s turn our attention to the input side of things. Inputs to your cmdlet come from command-line parameters or from the pipeline or both. To provide parameters, you start with standard C# public properties. For this exercise, we will use three parameters whose names correspond to properties of the NetworkAdapter object defined earlier. These will be used as filters by the cmdlet. That is, if for example the Name parameter is supplied, the cmdlet will only return NetworkAdapter objects whose name matches that Name parameter. We will also define a fourth parameter, MaxEntries, which lets us throttle the number of output records we receive from the cmdlet.

Step 9: Decorate your parameters with the ParameterAttribute

For PowerShell to know that you want certain properties to be exposed as parameters of your cmdlet, you must add the System.Management.Automation.ParameterAttribute:

Each property decorated with the Parameter attribute becomes able to accept input as a command-line parameter. Examples:

But this does not support obtaining input from the pipeline. You can accept pipeline input in two different ways, covered in the next two steps.

Step 10: Setup a single pipeline input

Say you want to be able to list all the NetworkAdapter objects for one or more specific manufacturers by feeding them from the pipeline:

You want to direct that pipeline input into the appropriate parameter-in this case, the Manufacturer parameter. To do this, you add the ValueFromPipeline argument to the Parameter attribute for that parameter:

There’s nothing technically stopping you from applying the ValueFromPipeline argument to more than one parameter (say, for example, if you wanted to see whether the manufacturer or the name of the network adapter contained Intel or Microsoft). But in practice, ValueFromPipeline is typically attached to only one parameter because there is really no need to have two parameters receiving identical values. When you do want to get multiple different inputs from the pipeline, set up multiple properties as described next.

Step 11: Setup multiple pipeline-able properties

Say you have a NetworkAdapter object at hand; let’s call it $adapter1. You want to feed $adapter1 to Get-NetworkAdapter as a template, so that Get-NetworkAdapter only returns objects that match the Name, Manufacturer, and PhysicalAdapter values of $adapter1; we will choose to ignore the other properties of $adapter1 for filtering purposes.

Each property that we want to treat as significant must use the ValueFromPipelineByPropertyName argument to the Parameter attribute:

Notice that Manufacturer now has two arguments on its Parameter attribute so you can still feed it an array of strings, as was done in the previous step, or you can feed an array of objects containing appropriate properties.

Let’s check our work so far. Recompile, re-import and examine the help. I’ve reduced the output to just the relevant parts here:

Note that each parameter shows up in the SYNTAX section, indicating its name and type. Then in the PARAMETERS section, you again see the name and type of each parameter along with additional properties of the parameters-notably whether it accepts pipeline input. Finally, the INPUTS section is there; not to be redundant, but to show-along with the OUTPUTS section you have already seen-what data comes and goes in the pipeline. It seems to be telling you that it accepts one string and one Boolean from the pipeline as inputs, but it really means that this cmdlet will accept one or more strings and one or more Booleans from the pipeline as input. It does not reveal, as with this particular cmdlet, that, two strings and one Boolean may come from the pipeline.

Note: This is an appropriate time to mention that you have about reached the limit of the documentation PowerShell can generate from your code alone, i.e. without you having decorated your code with documentation-comments. While this documentation is useful, it is but a mere skeleton of what your documentation should be. For a practical guide to easily instrumenting your code with doc-comments, see my recent article, Documenting Your PowerShell Binary Cmdlets.

Step 12: Setup multiple inputs without the pipeline

You have seen that you can feed multiple inputs to the cmdlet like this:

That works even though the receiving parameter, Manufacturer, is just a simple string, not a list or array!

It works because of the nature of the pipeline: the pipeline itself handles feeding multiple values one at a time, so Manufacturer ever only gets asked to accept a single string at a time. But it would seem very reasonable to be able to specify multiple values to a parameter via direct parameters as well (i.e. not through the pipeline):

To do that, though, you must specify that Manufacturer is an array-the only change here is using string[] in place of string:

Thus, while the commands typed by the user seem very similar, just inverted in a sense…

…internally they are handled quite differently. With the pipeline inputs, they are processed individually; the cardinality of the Manufacturer array is always one. With direct inputs, the cardinality of Manufacturer is the length of the array that you pass in.

Step 13: Setup inputs without accompanying parameter names

Revisiting the last examples, we can make the parallel between pipeline and direct inputs even closer by not forcing the user to specify the name of the parameter in the latter case (i.e. “-Manufacturer” has been omitted):

To accomplish this, you need to map your cmdlet parameters to positions on the command line, so that PowerShell will know-by position alone-which parameters you are filling with values. In this case, because we assume Manufacturer will be wanted the most, we give that the first position, position zero. It is good practice to give all your parameters position values, though, to allow the user to decide whether they want to use names at all.

With these positions specified, as I’ve just demonstrated,  any of the commands below achieve precisely the same result. If you use parameter names, you can put them in any order you please, but if you want to omit names you must honor the order based on the Position argument to each Parameter attribute:

Step 14: Provide parameter aliases

PowerShell automatically provides prefix-parameter recognition, which is a great convenience feature: you may type the entire name of a given parameter, but you are only required to type enough to make it unambiguous. For example, for the Manufacturer parameter all of these are equivalent:

That’s fine, you say, but everyone in your shop abbreviates ‘manufacturer’ as ‘mfg’. This is actually short for manufacturing, but let’s run with it! Of course, ‘mfg’ is not a proper prefix of ‘manufacturer’ so that will not work by default. However, you can easily define an alias to accept an alternative name or several alternative names.  This example shows two aliases being defined.

And PowerShell provides prefix-parameter recognition on the aliases as well! So all of these are equivalent:

Step 15: Write some production code

To wrap up this short and simple (!) 15-step process to create PowerShell cmdlets in C#, let’s add just one more little detail-actual production code to do the work! To do this in your cmdlet skeleton, there are four principal methods you need to override:

  • BeginProcessing (to do initialization),
  • ProcessRecord (to process each item in the pipeline),
  • EndProcessing (to do finalization), and
  • StopProcessing (to handle abnormal termination).

Without further ado, here is the complete code for the Get-NetworkAdapter cmdlet. Note that this uses a bit of syntactic C# 6.0 (e.g. the amazingly useful nameof operator) so if you are using a Visual Studio edition prior to 2015 you will just need to substitute constant strings for those expressions. In the interests of brevity-and the fact that you are reading this article to understand how to build cmdlets, not how to report network adapter statistics!-I present the code without commentary. Take a look at the sample project attached to this article to load it up in Visual Studio and experiment with it!

Conclusion

The process of writing a PowerShell cmdlet is actually quite straightforward, though it may seem daunting the first time you go through it. Jump right in to the sample project and play with it. Use it as a model as you start on your own cmdlets. By carefully following the recipe provided in this article, you should be able to achieve what you were likely looking to do: writing a cmdlet that works in a way that a user would reasonably expect a cmdlet to work.

Update – April 2016
Since this article, I’ve created a wallchart putting both XmlDoc2Cmdlet and DocTreeGenerator in context, showing you how to do a complete documentation solution for your PowerShell work in both C# and PowerShell. Click here for more details. 2407-1-5728ddc3-433a-4b82-a6dc-84f5f450b

How you log in to Simple Talk has changed

We now use Redgate ID (RGID). If you already have an RGID, we’ll try to match it to your account. If not, we’ll create one for you and connect it.

This won’t sign you up to anything or add you to any mailing lists. You can see our full privacy policy here.

Continue

Simple Talk now uses Redgate ID

If you already have a Redgate ID (RGID), sign in using your existing RGID credentials. If not, you can create one on the next screen.

This won’t sign you up to anything or add you to any mailing lists. You can see our full privacy policy here.

Continue