Freigeben über


PowerShell scripting in MEF-based applications

In my last post, I gave an overview of an approach to making MEF-based applications scriptable. While there are many .NET compatible scripting languages available these days, I used Iron Python in my last post since hosting it is so simple. This post builds upon the last (so read that one first), but uses PowerShell as the scripting language.

Hosting PowerShell is a bit more complex than hosting Iron Python, primarily due to PowerShell’s lack of support for .NET generics and MEF’s lack of non-generic methods on the CompositionContainer class (particularly GetExportedValue). We can overcome this by taking advantage of PowerShell’s Extended Type System (ETS). The first thing needed though is some extension methods for CompositionContainer that provide non-generic versions of GetExportedValue and GetExportedValues:

public static object GetExportedValue(this CompositionContainer compositionContainer, Type type = null, string contractName = null){ return compositionContainer.GetExports(CompositionContainerExtensions.GetImportDefinition(type, contractName, ImportCardinality.ExactlyOne)).Single().Value;}public static IEnumerable<object> GetExportedValues(this CompositionContainer compositionContainer, Type type = null, string contractName = null){ return compositionContainer.GetExports(CompositionContainerExtensions.GetImportDefinition(type, contractName, ImportCardinality.ZeroOrMore)).Select(export => export.Value);}private static ImportDefinition GetImportDefinition(Type type, string contractName, ImportCardinality cardinality){ if (string.IsNullOrEmpty(contractName)) { contractName = AttributedModelServices.GetContractName(type); } string typeIdentity = null; if (type != null) { typeIdentity = AttributedModelServices.GetTypeIdentity(type); } return new ContractBasedImportDefinition(contractName, typeIdentity, null, cardinality, false, true, CreationPolicy.Any);}

 

Next, we’ll need an extension method to make it easier to get the underlying .NET object wrapped by PowerShell’s PSObjects:

public static T Cast<T>(this PSObject psObject){ if (psObject == null) { return default(T); } if (psObject.BaseObject is T) { return (T)psObject.BaseObject; } throw new InvalidOperationException(string.Format("Cannot convert {0} to {1}.", psObject, typeof(T).FullName));}

 

The PowerShell scripting service takes advantage of the above extension methods and the PowerShell ETS:

[Export(typeof(IScriptingService))]internal class PowerShellScriptingService : IScriptingService{ [Import] private CompositionContainer container; public object RunScript(string script) { using (Runspace runspace = RunspaceFactory.CreateRunspace()) { var psContainer = new PSObject(this.container); psContainer.Members.Add(new PSCodeMethod("GetExportedValue", this.GetType().GetMethod("GetExportedValue"))); psContainer.Members.Add(new PSCodeMethod("GetExportedValues", this.GetType().GetMethod("GetExportedValues"))); runspace.ThreadOptions = PSThreadOptions.UseCurrentThread; runspace.Open(); runspace.SessionStateProxy.SetVariable("container", psContainer); using (Pipeline pipeline = runspace.CreatePipeline()) { pipeline.Commands.AddScript(script); PSObject result = pipeline.Invoke().LastOrDefault(); return result == null ? null : result.BaseObject; } } } public static object GetExportedValue(PSObject psContainer, PSObject psType, PSObject psContractName = null) { var container = psContainer.Cast<CompositionContainer>(); var type = psType.Cast<Type>(); var contractName = psContractName.Cast<string>(); return container.GetExportedValue(type, contractName); } public static object[] GetExportedValues(PSObject psContainer, PSObject psType, PSObject psContractName = null) { var container = psContainer.Cast<CompositionContainer>(); var type = psType.Cast<Type>(); var contractName = psContractName.Cast<string>(); return container.GetExportedValues(type, contractName).ToArray(); }}

 

Notice that the CompositionContainer is wrapped in a PSObject. This allows us to use the PowerShell ETS to add non-generic versions of the GetExportedValue and GetExportedValues methods to the CompositionContainer, which are significantly easier to invoke from a PowerShell script. The non-generic GetExportedValue and GetExportedValues implementations use the extension methods we previously defined.

Also notice that the Runspace’s ThreadOptions is explicitly set to use the current thread. This is a crucial step. Without this, PowerShell will use a separate thread for the RunSpace (not the UI thread) and therefore scripts will not be able to interact with the UI.

Finally, notice that we capture only the last PSObject from the Pipeline’s Invoke method. Invoke returns all objects that are output to the host. Capturing only the last one is analogous to the way the last value output from PowerShell functions (implicitly, or explicitly by using a return statement) is the value returned from the function. Also note that only the underlying .NET object of the PSObject is returned, which is probably desirable to the caller of the RunScript method.

I’ve made some sample code available that demonstrates this approach. The focus is definitely on the PowerShell scripting, so the rest of the application is very simple and rather poorly designed. Using PowerShell's ETS to add non-generic CompositionContainer methods was the cleanest approach I could find. If anyone has an alternative approach, I'd love to hear about it.