Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Parameter type validation in functions - binary modules vs. script modules

Having built a number of PowerShell modules (both binary and script), there's still an inconsistency between the two models that I cannot quite get my head around. Perhaps one of you could shed some light on this matter.

  1. Imagine a function that accepts a single DateTime value. In a script function, I would define this as a [DateTime] parameter; in the C# function the parameter would be of type DateTime. So far, so good.

  2. Now imagine passing a DateTime to the function, to which an additional note property has been added using Add-Member. Despite being defined as a [DateTime], the script function parameter would gladly accept this value, since it's actually a PSObject wrapping the original DateTime (and potentially containing additional members) which is unwrapped upon use - I hope I'm using the correct terminology here. As expected, passing something other than a (wrapped) DateTime would fail, thus making the function more or less type-safe.

  3. The equivalent C# function defines the parameter as a DateTime, so AFAIK it has no way to access the additional parameter. After all, the only "interface" that the parameter provides comes from the DateTime type.

  4. Alternatively, I could define the C# function's parameter type as a PSObject, but then I would have to do my own type checking for the PSObject's BaseObject.

Is there a flaw in my logic? Or, more importantly, is there a way around this, so that I can still leave my binary module's type checking to PowerShell?

Many thanks in advance!

like image 945
Jan Hoek Avatar asked Jan 23 '18 13:01

Jan Hoek


People also ask

What is the difference between ps1 and psm1?

While the only difference between the two is the extension, it is far more cumbersome to develop directly with the psm1 files as they cannot be directly executed. A simple and convenient workaround is to develop with ps1 files but convert them to psm1 during the build process.

What is psd1 and psm1?

. psm1 files contain main source code for a powershell module and . psd1 manifest data. You have to install them.

What is PowerShell psd1?

A module manifest is a PowerShell data file ( . psd1 ) that describes the contents of a module and determines how a module is processed. The manifest file is a text file that contains a hash table of keys and values.


1 Answers

You're both right and wrong - it entirely depends on whether the target parameter is of a value type (System.DateTime is a struct for example) - in which case everything is lost on type coercion during parameter binding.

If, however, the parameter type is of a reference type you can "resurrect" the PSObject wrapper using PSObject.AsPSObject().

I came up with the following example in pure(-ish) PowerShell for ease of replicability, but I believe it adequately shows my point

Paste the following into a C# source file (say, TestCmdlets.cs):

using System;
using System.Management.Automation;

namespace TestPSObject
{
  // This will be our parameter type
  public class TestObject {}

  // This will be our reference type test cmdlet
  [Cmdlet(VerbsDiagnostic.Test, "PSObjectByRef")]
  public class TestPSObjectByRefCommand : Cmdlet
  {
    [Parameter(Mandatory=true)]
    public TestObject TestObject
    {
      get { return testObject; }
      set { testObject = value; }
    }
    private TestObject testObject;

    protected override void ProcessRecord()
    {
      // If this works, we should receive an object with
      // identical psextended properties
      WriteObject(PSObject.AsPSObject(this.TestObject));
    }
  }

  // This will be our value type test cmdlet
  [Cmdlet(VerbsDiagnostic.Test, "PSObjectByValue")]
  public class TestPSObjectByValueCommand : Cmdlet
  {
    [Parameter(Mandatory=true)]
    public DateTime DateTime
    {
      get { return dateTime; }
      set { dateTime = value; }
    }
    private DateTime dateTime;

    protected override void ProcessRecord()
    {
      // If this works, we should receive an object with
      // identical psextended properties (hint: we won't)
      WriteObject(PSObject.AsPSObject(this.DateTime));
    }
  }
}

Now, in your shell, compile and import our test module:

Add-Type -Path .\TestCmdlets.cs -OutputAssembly TestPSObject.dll -OutputType Library
Import-Module .\TestPSObject.dll

Next up we create our test subjects and add a note property to them:

$TestObject = New-Object TestPSObject.TestObject
$TestObject |Add-Member -MemberType NoteProperty -Name TestProperty -Value "Hi there!"
$DateTime = Get-Date
$DateTime |Add-Member -MemberType NoteProperty -Name TestProperty -Value "Hi there!"

They now both return the string value Hi there! when you dereference the TestProperty member.

Now for the actual test:

$TestObjectAfter = Test-PSObjectByRef -TestObject $TestObject
$DateTimeAfter   = Test-PSObjectByValue -DateTime $DateTime

This will still return Hi there!:

$TestObjectAfter.TestProperty

But this will not:

$DateTimeAfter.TestProperty
like image 148
Mathias R. Jessen Avatar answered Sep 24 '22 02:09

Mathias R. Jessen