PolyMon

Monitor * Measure * Analyze * Alert

Integrating PowerShell Scripting into .NET Applications

PolyMon integrates PowerShell as a scripting language to give users the ability to create custom monitors as well as script post-monitoring event actions that can be run based on the results of the last monitoring data received.

This integration is actually quite easy to do and I am surprised that using PowerShell as a scripting engine for applications has not become more common.

I will attempt to explain here how this integration can be achieved in .NET applications.

Very simplistically you follow these steps:
  • Create an instance of PowerShell.
  • Pass any objects you want to that PowerShell instance.
  • Assign a script (text) to the PowerShell Instance. That script can use any of the objects you passed in to it, including method calls, properties, etc.
  • Run the script.
  • After script has completed any changes made to the objects you passed in before running your script are now available.

Basically, this allows PowerShell scripts to interact with any of the objects your application uses (that you care to make available to it).

The first step is to add a reference to PowerShell in your Visual Studio project. Now the PowerShell assembly is located in the GAC so you cannot just use the Add Reference option in your VS project.

[A good discussion on the GAC in general is located here: www.codeproject.com/KB/dotnet/demystifygac.aspx
Another good resource for referencing GAC assemblies from your VS project can be found here: support.microsoft.com/default.aspx?scid=kb;en-us;306149]

Now, you can certainly follow the advice given in the KB article referenced above, but I have simply been editing my VS project file by hand and entering the reference there directly.

To do so, first close your VS project. Then open your VS project file in your favorite text editor.

In the project file, find an ItemGroup section that contains references such as:
<Reference Include="System" />

In that section you will need to add the following code (please remove line breaks):

<Reference Include="System.Management.Automation,
Version=1.0.0.0, Culture=neutral,
PublicKeyToken=31bf3856ad364e35,
processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\..\..\..\Program Files\Reference Assemblies\
Microsoft\WindowsPowerShell\v1.0\
System.Management.Automation.dll</HintPath>
</Reference>


Here I am using v1.0 of PowerShell. You will have to modify this entry based on where your instance of PowerShell is installed and based on the version you are using.

Save this file and re-open your project. You should now see a reference to System.Management.Automation in your project. This is the reference you will need in order to use PowerShell within your application.

The namespace we will use is the System.Management.Automation namespace and can be imported in your code file thus:
Imports System.Management.Automation.RunSpaces

You create an instance of PowerShell by using the RunspaceFactory.CreateRunSpace method:
Dim PSRunSpace As Runspace = RunspaceFactory.CreateRunspace()
PSRunSpace.Open()

You can now make any object available to that instance of PowerShell by using the SessionStateProxy.SetVariable method using a Name/Value pair:
PSRunSpace.SessionStateProxy.SetVariable("myObject", myPSObj)

Note that the name you specify here is the name of the variable as it will be available within your PS script, in this case $myObject.

You now create a PS pipeline object with your script using the CreatePipeline method:
Dim PSPipeline As Pipeline = PSRunSpace.CreatePipeline(PSScript)

Finally, the PowerShell script is executed using the Invoke method:
PSPipeline.Invoke()

Since this is a synchronous call, immediately after the script execution completes, control returns to your code and you can now access the objects you passed in to the PS script, including any properties you may have set within your PS script.

You can release the PS objects you created by closing your runspace and disposing it:
PSRunSpace.Close()
PSRunSpace.Dispose()

In practice you may want to cache your PS run space to avoid the overhead of creating/destroying it, but that will very much depend on your application needs.


Here is the above code put together:

Private Sub RunPSScript(ByVal PSScript As String)
Dim PSRunSpace As Runspace = Nothing
Try
'Create PS Instance
PSRunSpace = RunspaceFactory.CreateRunspace()
PSRunSpace.Open()

'Create an instance of an object we want to pass to PS
Dim myPSObj As New myObj()

'Pass this object to PS
PSRunSpace.SessionStateProxy.SetVariable("myObject", myPSObj)

'Create PS pipeline with the script we want to run
Dim PSPipeline As Pipeline = PSRunSpace.CreatePipeline(PSScript)

'And run the script
PSPipeline.Invoke()

'Script is complete
'In this example we simply output the values of each property in the object
'to a text box.
txtResults.Text = Nothing
Dim sb As New System.Text.StringBuilder
sb.AppendFormat("Property1= {0}{1}{1}", myPSObj.Property1, vbCrLf)
sb.AppendFormat("Property2 = {0}{1}{1}", myPSObj.Property2, vbCrLf)
sb.AppendFormat("Property3 = {0}{1}{1}", myPSObj.Property3, vbCrLf)
txtResults.Text = sb.ToString()

Catch ex As Exception
txtResults.Text = ex.ToString()

Finally
'Release PS instance.
'Note: In practice you may want to cache your PS instance for re-use,
'not creating/destroying it at every call to avoid the overhead.
If PSRunSpace IsNot Nothing Then
If PSRunSpace.RunspaceStateInfo.State <> RunspaceState.Closed Then PSRunSpace.Close()
PSRunSpace.Dispose()
End If
End Try
End Sub



You can also download this as a .NET project here which has the rest of the code for the custom object used in this example and a simple front-end.

3 comments:

Parrotlover77 said...

Excellent post!

One remark: I'd recommend putting the clean-up (If .. IsNot Nothing .. PSRunSpace.Dispose() and so-on) in the Finally clause of the Try..Catch. If you don't, you might leak PS sessions if the PS session crashes and dumps out of the Try..Catch prematurely.

You might not leak, just due to garbage collection, but I'm not sure because I don't know if the Close() or Dispose() is mandatory for the PowerShell objects to clean up after themselves.

Parrotlover77 said...

Nevermind, you did use Finally. Somehow my brain glossed over that like the first three times I read the code. :-D

Chromebuster said...

Great article, except for one thing. Visual Studio has it's own editor which can take care of opening those files. I see no reason why somebody should have to leave the IDE to edit a .vbproj file.

Label Cloud

About Me

My photo
You can contact me via email here: Google Profile
Using the StudioPress WordPress Theme, Bloggerized by Girly Blogger for BTemplates.