Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / PowerShell

Double layered application with PowerShell in the middle

4.82/5 (5 votes)
8 Nov 2013CPOL5 min read 17.4K  
How to support PowerShell in .NET application

Introduction

There are several reasons why use PowerShell. The most significant advantage is ability to use script instead of user interface. PowerShell is for administrator almost the same environment as a console application for developer. When the core of your application is highly independent so it is extreme easy build a console application based on it consider using PowerShell layer.

Why should I use a PowerShell interface in my application instead of direct referencing library? When developer wants to explore a library the first step is usually console application. It is developer’s sandbox with quick graphical output because every object has a ToString method. No everyone knows how to write a console application. Administrators like PowerShell where write-compile-run loop is much faster. Developers like code where a breakpoint can be set on every command. So again – why PowerShell interface? The answer is not surprising – because it is very useful for a specific kind of applications.

Background

Why should I use a PowerShell interface in my application instead of direct referencing library? When developer wants to explore a library the first step is usually console application. It is developer’s sandbox with quick graphical output because every object has a ToString method. No everyone knows how to write a console application. Administrators like PowerShell where write-compile-run loop is much faster. Developers like code where a breakpoint can be set on every command. So again – why PowerShell interface? The answer is not surprising – because it is very useful for a specific kind of applications.

When the code configures something there is clear that implementing PowerShell support is a great investment. In my case I used a PowerShell interface for a cryptanalysis library. There are many algorithms, but I didn’t want to hardcode steps how to decrypt an encrypted message. I wanted give ability to user use algorithms and decide based on heuristics and own judgment which ones put together. It is similar approach like workflow defined in XAML. What PowerShell can do best is a fast user interaction and, of course, IntelliSense is not lost because PowerShell has autocomplete capability.

Application layers Application layers

Architecture is simple. Library implements PowerShell interface. User interface runs a PowerShell host, generating commands to it and receiving objects from it to fill ViewModels with. Implementation of this pattern is not trivial for many reasons. First of all, there are basically two versions of .NET – 2.0 and 4.0 (1.0 is obsolete, 3.0 and 3.5 are extensions of 2.0). PowerShell has 2 versions, third is currently in a beta stage. Another complication is dual environment – 32 bit and 64 bit. I spent many hours solving issues causing these aspects. When I was finally done I felt duty to write an article about it.

Let’s talk about library point of view first. Library has to reference System.Management.Automation library which is part of PowerShell SDK. There are two important classes – PSCmdlet and CustomPSSnapIn. PSCmdlet is a command which PowerShell can execute and optionally return an object or collection. CustomPSSnapIn is a set of PowerShell commands with vendor information in one pack. Your classes must derivate from them. Cmdlet classes must be decorated by Cmdlet attribute and the SnapIn class muse be decorated by RunInstaller attribute. It is required for reflection because that’s how PowerShell finds cmdlets in your library. Library is compiled to MSIL, so when you keep Any CPU configuration, it works both in 32 bit and 64 bit environment. You should choose .NET Framework 3.5 against PowerShell 2 and .NET Framework 4 against PowerShell 3. SnapIn assembly compiled as .NET 3.5 should works fine in PowerShell 3, but not vice versa.

Using the code

The library containing cmdlets must be registered. There is an InstallUtil.exe doing this job for you. The problem is this utility is platform dependent. Avoiding guessing which framework version run or whenever use 32 bit or 64 bit variant requires knowledge that this utility is just a wrapper of AssemblyInstaller class. This means that during application launch the library can be easily registered so hosting PowerShell knows about its existence. During application exit the library is unregistered so its location can be changed in future without causing any errors. This registration approach doesn’t fit all scenarios at all. Library registration should be made by installer rather that application itself, but who likes installers? Users who use PowerShell have often User Account Protection turned off because they know really well what they are doing.

/* LIBRARY IMPLEMENTING POWERSHELL CMDLETS SAMPLE */

[Cmdlet(VerbsCommunications.Send, "MyCmdlet")]
public class GetMyCmdletCommand : PSCmdlet {

    [Parameter(Mandatory = true, HelpMessage = "This is a sample parameter.")]
    public string Param { get; set; }

    protected override void ProcessRecord() {
        var obj = new ObjectToReturn(Param);
        WriteObject(obj);
    }
}

[RunInstaller(true)]
public class Cryptanalysis : CustomPSSnapIn {
        
    public Cryptanalysis() {
        cmdlets = new Collection<CmdletConfigurationEntry>();
        cmdlets.Add(new CmdletConfigurationEntry("Get-MyCmdlet", typeof(GetMyCmdletCommand), null));
    }

    public override string Description {
        get { return "Demo"; }
    }

    public override string Name {
        get { return "SnapInName"; }
    }

    public override string Vendor {
        get { return "Václav Dajbych"; }
    }

    private Collection<CmdletConfigurationEntry> cmdlets;
    public override Collection<CmdletConfigurationEntry> Cmdlets {
        get {
            return cmdlets;
        }
    }
}

User interface point of view is more difficult. First of all it is worth to know that even 64 bit application runs 32 bit PowerShell instance. Application .NET Framework version is not important. What is important is ability to reference System.Management.Automation library. This library is located in v1.0 directory but it doesn’t mean that PowerShell 1 is used. Latest available version is always used.

When user interface launches a PowerShell host it must load the SnapIn first using AddPSSnapIn class in RunspaceConfiguration. Then application can call PowerShell.Create method and use PowerShell commands defined in the library. Advantage of PowerShell 3 is that is built on the top of Dynamic Language Runtime. This means you can type PSObject as dynamic and shorten your code.

/* USER INTERFACE INTERACTION SAMPLE */
  
// PowerShell host
Runspace runSpace;
  
private Init() {
  
  // load PowerShell
  var rsConfig = RunspaceConfiguration.Create();
  runSpace = RunspaceFactory.CreateRunspace(rsConfig);
  runSpace.Open();

  // register snapin
  using (var ps = PowerShell.Create()) {
      ps.Runspace = runSpace;
      ps.AddCommand("Get-PSSnapin");
      ps.AddParameter("Registered");
      ps.AddParameter("Name", "SnapInName");
      var result = ps.Invoke();
      if (result.Count == 0) Register(false);
  }

  // load snapin
  PSSnapInException ex;
  runSpace.RunspaceConfiguration.AddPSSnapIn("SnapInName", out ex);
}

void Register(bool undo) {
    var core = Path.Combine(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location), "MySnapInLib.dll");
    using (var install = new AssemblyInstaller(core, null)) {
      IDictionary state = new Hashtable(); 
      install.UseNewContext = true;
      try {
          if (undo) {
              install.Uninstall(state);
          } else {
              install.Install(state);
              install.Commit(state);
          }
      } catch {
          install.Rollback(state);
      }
  }
}

dynamic DoJob(string parameter) {
    using (var ps = PowerShell.Create()) {
        ps.Runspace = runSpace;
        ps.AddCommand("Get-MyCmdlet");
        ps.AddParameter("Param", parameter);
        dynamic result = ps.Invoke().Single();
        return result.ReturnedObjectProperty;
    }
}

Conclusion

Hope this helps building friendly configured environments. When you know how to do that you will find it’s really easy. Just follow the naming conventions.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)