Reputation: 634
I'm creating a Powershell module that acts as a wrapper for interfacing with a RESTful API.
In order to simplify things for the scripter, I provide this Cmdlet to "establish a connection" to the API (essentially create the Worker class object and specify the authentication token):
public class GetAPIConnection : Cmdlet
{
[Parameter(Position=0, Mandatory = true, ValueFromPipeline = true)]
[Alias("T","Password","Pwd","APIKey")]
public string Key { get; set; }
//This is the object I intend to send to the pipeline
private APIWorker API;
protected override void BeginProcessing()
{
base.BeginProcessing();
BuildOutputObject();
}
protected override void ProcessRecord()
{
base.ProcessRecord();
WriteObject(API);
}
private BuildOutputObject()
{
API = new APIWorker(APIKey);
}
The problem I'm having is that I want other cmdlets to be able to accept the object that is generated by Get-APIConnection from the pipeline. Here's an example of how I tried to implement that in another Cmdlet:
[Cmdlet(VerbsCommon.Get, @"SecurityGroups")]
[OutputType(typeof(string))]
public class GetSecurityGroups : Cmdlet
{
private APIWorker connection;
[Parameter(Position = 0,
Mandatory = true,
ValueFromPipeline = true)]
public APIWorker Connection
{
get { return connection; }
set
{
//Confirmation that set is actually being called
Console.WriteLine(@"Got here!");
connection = value;
}
}
[Parameter(Mandatory = true)]
public string Identity { get; set; }
private IEnumerable<string> Result;
protected override void BeginProcessing()
{
base.BeginProcessing();
BuildOutputObject();
}
protected override void ProcessRecord()
{
base.ProcessRecord();
WriteObject(Result);
}
private BuildOutputObject()
{
Result = Connection.GetUserGroups(Identity);
}
So, after I import the module, I am able to create the PoshAPI object just fine using the Get-APIConnection cmdlet:
>$API = Get-APIConnection -Key '[My Token]'
...And Get-SecurityGroups works if I pass $Connection as a named parameter:
>Get-SecurityGroups -Connection $API -Identity 'abcdefg'
Got here!
PowershellUsers
DotNetCOE
CompanyCarOwners
...
Notice that "Got here!" is sent to the screen indicating that set is indeed being called on the "Connection" parameter
...But, even though I specified the ValueFromPipeline attribute for the "Connection" parameter in "Get-SecurityGroups", I can't pass $API from the pipeline for some reason:
$API | Get-SecurityGroups -Identity 'abcdefg'
Get-SecurityGroups : Object reference not set to an instance of an object.
At line:1 char:7
+ $API | Get-SecurityGroups -Identity 'abcdefg'
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [Get-SecurityGroups], NullReferenceException
+ FullyQualifiedErrorId : System.NullReferenceException,APIHelper.GetSecurityGroups
The exception details indicate that the null reference exception is being thrown in BuildOutputObject():
> $error[0] | select *
PSMessageDetails :
Exception : System.NullReferenceException: Object reference not set to an instance of an object.
at APIHelper.GetSecurityGroups.BuildOutputObject()
at System.Management.Automation.Cmdlet.DoBeginProcessing()
at System.Management.Automation.CommandProcessorBase.DoBegin()
TargetObject :
CategoryInfo : NotSpecified: (:) [Get-SecurityGroups], NullReferenceException
FullyQualifiedErrorId : System.NullReferenceException,APIHelper.GetSecurityGroups
ErrorDetails :
InvocationInfo : System.Management.Automation.InvocationInfo
ScriptStackTrace : at <ScriptBlock>, <No file>: line 1
PipelineIterationInfo : {}
It’s interesting to me that I was able to reach BuildOutputObject() at all given that Connection is a mandatory parameter, and set wasn’t called on it (hence no “Got here!”). Wouldn’t proper behavior have been to prompt me to define that parameter??
Any help would be greatly appreciated!
Upvotes: 3
Views: 540
Reputation: 634
In simple terms, my issue was that I needed to move my BuildOutputObject() method into ProcessRecord(). The Pipeline hasn't been read yet when BeginProcessing() is executing. So, set the value of the output object in the ProcessRecord() method
[Cmdlet(VerbsCommon.Get, @"SecurityGroups")]
[OutputType(typeof(string))]
public class GetSecurityGroups : Cmdlet
{
private APIWorker connection;
[Parameter(Position = 0,
Mandatory = true,
ValueFromPipeline = true)]
public APIWorker Connection
{
get { return connection; }
set
{
//Confirmation that set is actually being called
Console.WriteLine(@"Got here!");
connection = value;
}
}
[Parameter(Mandatory = true)]
public string Identity { get; set; }
private IEnumerable<string> Result;
protected override void BeginProcessing()
{
base.BeginProcessing();
}
protected override void ProcessRecord()
{
base.ProcessRecord();
BuildOutputObject();
WriteObject(Result);
}
private BuildOutputObject()
{
Result = Connection.GetUserGroups(Identity);
}
Upvotes: 1
Reputation: 1445
In PowerShell pipeline, argument binding is done in process block, not in the begin block. I presume the same rules will apply if you are building a PowerShell commandlet.
Take a look at this example, which is similar to your GetSecurityGroups
function Test-Pipeline {
[CmdletBinding()]
param (
[Parameter(Mandatory,ValueFromPipeline)]
[string]$a
)
BEGIN {Write-Verbose "begin $a"}
PROCESS {Write-Verbose "process $a"}
END {Write-Verbose 'end'}
}
Calling it without pipeline, gives the output as you expect:
PS C:\> Test-Pipeline 'abc' -Verbose
VERBOSE: begin abc
VERBOSE: process abc
VERBOSE: end
But, calling it with pipeline argument has small difference. Do you see it?
PS C:\> 'abc' | Test-Pipeline -Verbose
VERBOSE: begin
VERBOSE: process abc
VERBOSE: end
Yes, in begin block there is no value for $a! In this case, $a is simple type, so there is no error. In your case, you call method of null
object so you are getting the error.
Reason for this is simple. It is visible in this example.
PS C:\> 'abc','def' | Test-Pipeline -Verbose
VERBOSE: begin
VERBOSE: process abc
VERBOSE: process def
VERBOSE: end
Pipeline execution is starting even before first values are processed by execution begin block in each PowerShell function in the pipeline.
P.S. I presume you would also get unexpected result if you try this:
'[My Token]' | Get-APIConnection
Upvotes: 5