26

How do you call a generic static method of a custom class in Powershell?

Given the following class:

public class Sample
{
    public static string MyMethod<T>( string anArgument )
    {
        return string.Format( "Generic type is {0} with argument {1}", typeof(T), anArgument );
    }
}

And this is compiled into an assembly 'Classes.dll' and loaded into PowerShell like this:

Add-Type -Path "Classes.dll"

What's the easiest way to call the MyMethod method?

Athari
  • 33,702
  • 16
  • 105
  • 146
David Gardiner
  • 16,892
  • 20
  • 80
  • 117

5 Answers5

20

The easiest way to call MyMethod is, as @Athari says, to use MakeGenericMethod. Since he doesn't actually show how to do that, here is a verified working code sample:

$obj = New-Object Sample

$obj.GetType().GetMethod("MyMethod").MakeGenericMethod([String]).Invoke($obj, "Test Message")
$obj.GetType().GetMethod("MyMethod").MakeGenericMethod([Double]).Invoke($obj, "Test Message")

with output

Generic type is System.String with argument Test Message
Generic type is System.Double with argument Test Message
JohnC
  • 1,797
  • 1
  • 18
  • 26
  • FYI: In [PS 7.3+](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_calling_generic_methods?view=powershell-7.3) you can do `[Sample]::MyMethod[string]("Test Message");`. – Granger May 05 '22 at 15:41
15

You can call generic methods, refer to the post Invoking Generic Methods on Non-Generic Classes in PowerShell.

This is not straightforward, you need to use MakeGenericMethod function. It is pretty simple if method doesn't have overrides, it gets harder if it does.

Just in case, copy-pasted code from there:

## Invoke-GenericMethod.ps1 
## Invoke a generic method on a non-generic type: 
## 
## Usage: 
## 
##   ## Load the DLL that contains our class
##   [Reflection.Assembly]::LoadFile("c:\temp\GenericClass.dll")
##
##   ## Invoke a generic method on a non-generic instance
##   $nonGenericClass = New-Object NonGenericClass
##   Invoke-GenericMethod $nonGenericClass GenericMethod String "How are you?"
##
##   ## Including one with multiple arguments
##   Invoke-GenericMethod $nonGenericClass GenericMethod String ("How are you?",5)
##
##   ## Ivoke a generic static method on a type
##   Invoke-GenericMethod ([NonGenericClass]) GenericStaticMethod String "How are you?"
## 

param(
    $instance = $(throw "Please provide an instance on which to invoke the generic method"),
    [string] $methodName = $(throw "Please provide a method name to invoke"),
    [string[]] $typeParameters = $(throw "Please specify the type parameters"),
    [object[]] $methodParameters = $(throw "Please specify the method parameters")
    ) 

## Determine if the types in $set1 match the types in $set2, replacing generic
## parameters in $set1 with the types in $genericTypes
function ParameterTypesMatch([type[]] $set1, [type[]] $set2, [type[]] $genericTypes)
{
    $typeReplacementIndex = 0
    $currentTypeIndex = 0

    ## Exit if the set lengths are different
    if($set1.Count -ne $set2.Count)
    {
        return $false
    }

    ## Go through each of the types in the first set
    foreach($type in $set1)
    {
        ## If it is a generic parameter, then replace it with a type from
        ## the $genericTypes list
        if($type.IsGenericParameter)
        {
            $type = $genericTypes[$typeReplacementIndex]
            $typeReplacementIndex++
        }

        ## Check that the current type (i.e.: the original type, or replacement
        ## generic type) matches the type from $set2
        if($type -ne $set2[$currentTypeIndex])
        {
            return $false
        }
        $currentTypeIndex++
    }

    return $true
}

## Convert the type parameters into actual types
[type[]] $typedParameters = $typeParameters

## Determine the type that we will call the generic method on. Initially, assume
## that it is actually a type itself.
$type = $instance

## If it is not, then it is a real object, and we can call its GetType() method
if($instance -isnot "Type")
{
    $type = $instance.GetType()
}

## Search for the method that:
##    - has the same name
##    - is public
##    - is a generic method
##    - has the same parameter types
foreach($method in $type.GetMethods())
{
    # Write-Host $method.Name
    if(($method.Name -eq $methodName) -and
    ($method.IsPublic) -and
    ($method.IsGenericMethod))
    {
        $parameterTypes = @($method.GetParameters() | % { $_.ParameterType })
        $methodParameterTypes = @($methodParameters | % { $_.GetType() })
        if(ParameterTypesMatch $parameterTypes $methodParameterTypes $typedParameters)
        {
            ## Create a closed representation of it
            $newMethod = $method.MakeGenericMethod($typedParameters)

            ## Invoke the method
            $newMethod.Invoke($instance, $methodParameters)

            return
        }
    }
}

## Return an error if we couldn't find that method
throw "Could not find method $methodName"
Athari
  • 33,702
  • 16
  • 105
  • 146
  • 2
    Sorry but I stand by my statement - `can't be done *directly* in PowerShell`. :-) Awesome work-around BTW ... but really, the PowerShell team needs to fix this hole. – Keith Hill Nov 22 '10 at 17:36
  • Agree with Keith that it would be nice to have built-in support for this, but as this is a solution (even if it isn't direct) this answer gets the tick. – David Gardiner Nov 24 '10 at 02:02
  • Lengthy code sample is not necessary to address OP, MakeGenericMethod is sufficient. – JohnC Mar 29 '14 at 19:08
6

This is a limitation of PowerShell and can't be done directly in PowerShell V1 or V2 AFAIK.

BTW your generic method isn't really generic. Shouldn't it be:

public static string MyMethod<T>(T anArgument)
{ 
   return string.Format( "Generic type is {0} with argument {1}", 
                         typeof(T), anArgument.ToString()); 
} 

If you own this code and want to use it from PowerShell, avoid generic methods or write a non-generic C# wrapper method.

Keith Hill
  • 194,368
  • 42
  • 353
  • 369
3

The good news is PowerShell v3 is much better at binding to generic methods (and reifying them?) and you often don't have to do anything special but call it as you would a normal method. I can't specify all of the criteria for which this now works, but in my experience certain situations with generic parameters still require workarounds even in PowerShell v4 (maybe its the existence or overloads or something similar).

Similarly I also sometimes have trouble passing a generic parameter to a method ... for instance passing a Func<T1, T2, TResult> parameter.

One work-around that to me is much simpler than MakeGenericMethod or other approaches is to just put a quick C# wrapper class directly in my script, and let C# sort out all the generic mapping ...

Here is an example of this approach that wraps the Enumerable.Zip method. In this example my c# class isn't generic at all but that isn't strictly speaking necessary.

Add-Type @'
using System.Linq;
public class Zipper
{
    public static object[] Zip(object[] first, object[] second)
    {
        return first.Zip(second, (f,s) => new { f , s}).ToArray();
    }
}
'@
$a = 1..4;
[string[]]$b = "a","b","c","d";
[Zipper]::Zip($a, $b);

This produces:

 f s
 - -
 1 a
 2 b
 3 c
 4 d

I'm sure there are better PowerShell ways to "Zip" two arrays but you get the idea. The real challenge that I dodged here was having a hard-coded (in the C# class) 3rd parameter to Zip so I didn't have to figure out how to pass in that Func<T1, T2, TResult> (Maybe there is a PowerShell way to do that as well?).

TCC
  • 2,546
  • 1
  • 24
  • 35
2

Fast way, if there are no name conflicts:

[Sample]::"MyMethod"("arg")
Zenexer
  • 18,788
  • 9
  • 71
  • 77