Importing Modules using -AsCustomObject

I recently got thinking about the -AsCustomObject switch for the Import-Module cmdlet. I have seen it several times in discussions of implementing “classes” in PowerShell. Here’s a typical (i.e. trivial) example:

#module adder.psm1
function add-numbers($x,$y){
   return $x+$y
}

With that module, we can do the standard module stuff:

PS> import-module adder
PS> add-numbers 1 2
3

Ok, that was way too basic. Here’s something a lot closer to the topic at hand:

PS> $adder=import-module adder -ascustomobject
PS> $adder | gm

   TypeName: System.Management.Automation.PSCustomObject

Name        MemberType   Definition                    
----        ----------   ----------                    
Equals      Method       bool Equals(System.Object obj)
GetHashCode Method       int GetHashCode()             
GetType     Method       type GetType()                
ToString    Method       string ToString()             
add-numbers ScriptMethod System.Object add-numbers(); 

PS> $adder.add-numbers(1,2)
Unexpected token '-numbers' in expression or statement.
At line:1 char:11
PS>  $adder."add-numbers"( 1, 2)
3

There are a several interesting things to notice about this example. First of all, note that the add-numbers function has become a scriptmethod on the $adder object. As the help topic for import-module states, the members of the custom object are the (exported) members of the module. When we try to call the add-numbers method, we find that our decision to use the noun-verb naming convention has bitten us. To use the method, we need to enclose the offending method name in quotes (both single and double work fine). Note that since this is a method we need to use commas to separate the arguments to the function.

A second thing to note is since this is a method, not a function, we can’t skip arguments.

PS> $adder.add-numbers(,2)

Note that we could definitely do

add-numbers -y 2

if we had used a normal import-module. Granted that in this case there would be no need to.

What if we try to fix the quotation issue by including an alias (say, AddNumbers) to the module and exporting it?

function add-numbers($x,$y){
   return $x+$y
}
new-alias addNumbers add-numbers
export-modulemember -Function * -Variable * -alias *

Here’s what we find:

PS> $adder=import-module adder -ascustomobject -force
PS> $adder | gm

   TypeName: System.Management.Automation.PSCustomObject

Name        MemberType   Definition                    
----        ----------   ----------                    
Equals      Method       bool Equals(System.Object obj)
GetHashCode Method       int GetHashCode()             
GetType     Method       type GetType()                
ToString    Method       string ToString()             
add-numbers ScriptMethod System.Object add-numbers(); 

PS> get-alias AddNumbers

Capability      Name                             ModuleName                                                 
----------      ----                             ----------                                                       
Script          addNumbers -> add-numbers        adder

Hey! Our alias is missing. Unfortunately, it got imported into the global scope (possibly hiding another function). Note that I used the -force switch to make sure that we re-import it if it was already loaded.

When I first read about the -asCustomObject switch, I could see myself using this to import modules that had conflicting function names, and using the custom objects to call the methods in question. However, consider a function with a large number of switches or parameters. With an “-ascustomobject” object, you would need to specify all of the switches or parameters. Again, what about a function which used parametersets? As it turns out, scriptmethods don’t seem to use parametersets. Here’s function to demonstrate:

function test-psets{
param([Parameter(ParameterSetName="Set1")]$x,
      [Parameter(ParameterSetName="Set2")]$y)
      switch ($PsCmdlet.ParameterSetName){
         "Set1" {write-host "we're using Set1"}
         "Set2"  {write-host "we're using Set2"}
         default {write-host "don't know what parameter set we're in"}
      }
      Write-host "we had better be using $($PsCmdlet.ParameterSetName)"
}

Calling that function on a custom object looks like this:

PS> $adder=import-module adder -ascustomobject -force
PS> $adder."test-psets"(1)   #should use pset 1, since we're only using the first parameter
don't know what parameter set we're in
we had better be using 
PS> $adder."test-psets"(1,2)  #shouldn't be valid, since they're different parametersets
don't know what parameter set we're in
we had better be using
PS> #Sanity check to make sure the function works 
PS> import-module adder 
PS> test-psets -x 1
we're using Set1
we had better be using Set1

PS> test-psets -y 1
we're using Set2
we had better be using Set2

PS> test-psets -x 1 -y 2
test-psets : Parameter set cannot be resolved using the specified named parameters.
At line:1 char:1
+ test-psets -x 1 -y 2
+ ~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidArgument: (:) [test-psets], ParameterBindingException
    + FullyQualifiedErrorId : AmbiguousParameterSet,test-psets


So it seems that functions that utilize parametersets are going to be a lot less useful with -AsCustomObject imports.

As I mentioned, there are several examples floating around concerning creating new objects (or classes, depending on your perspective) using modules and this option. Given the drawbacks I’ve noted in this article, I think I’m going to stay away from that particular use case.

What do you think? Did I miss something important? Please let me know what your opinion is.

-Mike

2 Comments

  1. Your comments are closed on http://www.powershellstation.com/2010/04/09/checking-a-field-for-null-in-powershell/

    I have a question that I think is a rather simple one-off of that post:

    What if your SQL query returns an empty result set? How do you check for that?

    I tried:

    $DataAdapter = New-Object System.Data.SqlClient.SqlConnection($ConnectionString)
    $DataSet = New-Object System.Data.DataSet
    $DataAdapter.Fill($DataSet)

    function is-null($value){
    return [System.DBNull]::Value.Equals($value)
    }

    if (is-null $DataSet) { “DataSet is Null” }
    if (is-null $DataSet.Tables) { “Tables are Null” }
    if (is-null $DataSet.Tables[0]) { “Tables[0] is Null” }

    But the results indicates that although I purposefully crafted an SQL statement to produce an empty list, these objects are not null. Any clue how I can determine if the resultset returned is empty or null?

    Any help is appreciated.

    • This is a common misunderstanding. NULL in database terms is an indicator that a value (in a column on a row) is either missing or somehow not applicable. How As such, the is-null function I defined would only apply in situations where at least one row was returned. Usually you can just check if you got a result back like this (pseudocode, but it should be close):

      $rows=invoke-query "select * from table" -conn $connection
      if ($rows){
      write-host "something was returned"
      } else {
      write-host "Nothing was returned"
      }

      If you’re actually getting a dataset object back, you should be able to look at the tables collection and see if there are any returned, and if so, each will have a rows collection you can look at.

      Here’s a concrete example (using SQPSX…not sure if it’s the published download or just a recent patch):

      $ds=Invoke-SQLQuery "select * from sys.databases where name='blah';select * from sys.databases" -server "." -AsResult DataSet

      if ($ds.tables){
      write-host "there is at least 1 result set"
      if ($ds.tables[0].rows.count -gt 0){
      write-host "there were rows in the first result set"
      } else {
      write-host "there were no rows in the first result set"
      }
      } else {
      write-host "there were no result sets returned"
      }

      One more thing…I usually don’t worry about datasets, and just have the function return the rows. I only explicitly use datasets when I know that a query is going to return multiple result sets.

      Hope this helps.

Comments are closed.