Dependency Injection with PowerShell
Is it possible to use Dependency Injection (DI) with Windows PowerShell?
My intitial experiments suggest that it isn't. If I attempt to use Constructor Injection in a CmdLet it doesn't even register itself. In other words, this is not possible:
[Cmdlet(VerbsDiagnostic.Test, "Ploeh")]
public class PloehCmdlet : Cmdlet
{
public PloehCmdlet(IFoo foo)
{
if (foo == null)
{
throw new ArgumentNullException("foo");
}
// save foo for later use
}
protected override void ProcessRecord()
{
thi开发者_如何学JAVAs.WriteObject("Ploeh");
}
}
If I add a default constructor, the CmdLet can be registered and used, but without the default constructor, it's simply not available.
I know I could use a Service Locator to retrieve my dependencies, but I consider that an anti-pattern so don't want to go that way.
I would have hoped that the PowerShell API had some kind of 'Factory' hook similar to WCF's ServiceHostFactory, but if there is, I can't find it.
An instance of the cmdlet class is created from an empty constructor every time the cmdlet is used in PowerShell. You have no control over which constructor PowerShell will pick, so you cannot do what you are proposing in a straightforward way (and I really have a hard time imagining why you would want to). So the simple answer to this question is NO.
To acheive a similar affect, you can make an interface that looks like a cmdlet (has BeginProcessing/EndProcessing/ProcessRecord/StopProcessing) and use to populate a bunch of cmdlets that are thin wrappers over the real code. IMHO this would be an overcomplicated approach.
I really don't see why you are trying to do this. Could you explain a little more about the scenario?
Using PSCmdlet as a base class requires a RunSpace to execute and only lets you specify the command to execute as a string. See this link for an example.
I switched back to Cmdlet as a base class and used Property injection to set the dependencies. Not the cleanest solution, but it worked for me. The nice thing about Cmdlet as a base class is that you can invoke it directly from the unit test like this:
var cmdlet = new MyCmdlet {
Dependency = myMockDependencyObject
};
var result = cmdlet.Invoke().GetEnumerator();
Assert.IsTrue(result.MoveNext());
To Expand on Start-Automating answer:
Essentially you will have your IView interface that would define the operations for the PSCmdlet (WriteObject, WriteError, WriteProgress, and so on) you will have your implementation of that View which would be the actual Commandlet.
Also you will have a Controller, which is the Actual functionallity. On the constructor the Controller receives a IProvider (Which is the one that you want to mock) and an IView. the provider performs the call to the provider and writes the results ot the IView, which will reflect on the IView (Powershell Commandlet).
During the initialization of the View you will create a Controller, pass along itself (the IView) and a Provider, and then it will perform the operation against the controller.
With this approach your Cmdlet is a thin layer that doesn't perform any business logic, and everything is on your controller, which is a component that is testable.
Though you can't use constructor injection for this, you could use the cmdlet itself. Have a first call to it on one parameter set to initialise, store the relevant information in the current session state, and have subsequent calls pull the stored value back from the session state.
Here I've used a single string, message
to represent the stored value; but obviously you can have as many parameters / of whatever types you like.
NB: The below C# is wrapped in PowerShell, so you can test the whole thing directly in PS.
$cs = @'
using System.Management.Automation;
[Cmdlet(VerbsDiagnostic.Test, "Ploeh", DefaultParameterSetName = "None")]
public class PloehCmdlet : PSCmdlet
{
const string InitialiseParameterSetName = "Initialise";
const string MessageVariable = "Test_Ploeh_Message_39fbe50c_25fc_48b1_8348_d155cad99e93"; //since this is held as a variable in the session state, make sure the name will not clash with any existing variables
[Parameter(Mandatory=true, ParameterSetName = InitialiseParameterSetName)]
public string InitialiseMessage
{
set { SaveMessageToSessionState(value); }
}
protected override void ProcessRecord()
{
if (this.ParameterSetName != InitialiseParameterSetName) //do not run the cmdlet if we're just initialising it
{
this.WriteObject(GetMessageFromSessionState());
base.ProcessRecord();
}
}
void SaveMessageToSessionState(string message)
{
this.SessionState.PSVariable.Set(MessageVariable, message);
}
string GetMessageFromSessionState()
{
return (string)this.SessionState.PSVariable.GetValue(MessageVariable);
}
}
'@
#Trick courtesy of: http://community.idera.com/powershell/powertips/b/tips/posts/compiling-binary-cmdlets
$DLLPath = Join-Path $env:temp ('CSharpPSCmdLet{0:yyyyMMddHHmmssffff}.dll' -f (Get-Date))
Add-Type -OutputAssembly $DLLPath -Language 'CSharp' -ReferencedAssemblies 'System.Management.Automation.dll' -TypeDefinition $cs
Import-Module -Name $DLLPath -Force -Verbose
#demo commands
Test-Ploeh -InitialiseMessage 'this is a test'
Test-Ploeh
Test-Ploeh
Test-Ploeh -InitialiseMessage 'change value'
Test-Ploeh
Test-Ploeh
"NB: Beware, your value can be accessed / amended outside of your cmdlet: $Test_Ploeh_Message_39fbe50c_25fc_48b1_8348_d155cad99e93"
$Test_Ploeh_Message_39fbe50c_25fc_48b1_8348_d155cad99e93 = "I've changed my mind"
Test-Ploeh
Example Output
VERBOSE: Loading module from path 'C:\Users\UserNa~1\AppData\Local\Temp\CSharpPSCmdLet201711132257130536.dll'.
VERBOSE: Importing cmdlet 'Test-Ploeh'.
this is a test
this is a test
change value
change value
NB: Beware, your value can be accessed / amended outside of your cmdlet: change value
I've changed my mind
精彩评论