Invoking Generic Methods on Non-Generic Classes in PowerShell

A question recently came in asking, “How do you invoke a generic method on a non-generic class in PowerShell?”

[Edit: this "just works" in PowerShell V3]

PS C:\Users\Lee> add-type -Path C:\temp\generic.cs
PS C:\Users\Lee> $r = New-Object NonGenericClass
PS C:\Users\Lee> $r.GenericMethod(“Hello”, “Hello”)
Hello Multi Generic World: True

In an earlier post (http://www.leeholmes.com/blog/CreatingGenericTypesInPowerShell.aspx), we talked about how to create generic types in PowerShell, but classes can contain generic methods even when the classes themselves don’t represent generic types.

 

It is possible to call the generic methods on this type of class, with the complexity of the solution depending on the complexity of the class.

 

Take, for example, the following class definition:

 

// csc /target:library GenericClass.cs
// [Reflection.Assembly]::LoadFile(“c:\temp\GenericClass.dll”)
using System;

public class NonGenericClass
{
public void SimpleGenericMethod<T>(T input)
{
Console.WriteLine(“Hello Simple World: “ + input);
}

public void GenericMethod<T>(T input)
{
Console.WriteLine(“Hello World: “ + input);
}

public void GenericMethod<T>(T input, int count)
{
for(int x = 0; x < count; x++)
Console.WriteLine(“Hello Multi Partially Generic World: “ + input);
}

public void GenericMethod<T,U>(T input, U secondInput) where T : IEquatable<T> where U : IEquatable<U>
{
Console.WriteLine(“Hello Multi Generic World: “ + input.Equals(secondInput));
}

public static void GenericStaticMethod<T>(T input)
{
Console.WriteLine(“Hello Static World: “ + input);
}
}

 

It gives an example of the breadth of the problem. We have a simple generic method, a generic method with overrides, and finally, a static generic method.

 

If you don’t need to worry about naming conflicts (as in the SimpleGenericMethod and GenericStaticMethod methods,) the solution is just a few lines of code:

 

$nonGenericClass = New-Object NonGenericClass
$method = [NonGenericClass].GetMethod("SimpleGenericMethod")
$closedMethod = $method.MakeGenericMethod([string])
$closedMethod.Invoke($nonGenericClass, "Welcome!")

 

If you do have to worry about multiple methods with the same name, then the GetMethod() method can no longer help you. In that case, we need to call the GetMethods() method, and then find which method shares the proper parameter types.

 

To abstract all of this logic, use the following Invoke-GenericMethod script:

 

## 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”

13 Responses to “Invoking Generic Methods on Non-Generic Classes in PowerShell”

  1. Keith Hill writes:

    Wow thanks for providing this functionality. However please consider enhancing PowerShell v.next to make using .NET generic types and methods easier!!! :-)

  2. Zach Blocker writes:

    Thank you for responding to my question!!

    I have one suggestion. It’s not working for me in the following situation:

    * The parameter is an interface type
    * The argument is an instantiable type that implements the interface

    In this case, your simple type equality test in ParameterTypesMatch will fail because the interface type is not equal to the instantiable type. How about using "IsAssignableFrom" instead?

    Thanks again!

  3. Richard writes:

    Some more issues with the ParameterTypesMatch function:

    If the generic parameters are not used in exactly the same order as the type parameters, or multiple parameters use the same generic type, they will be replaced with an incorrect type parameter. To resolve this, you need to replace the $typeReplacementIndex with $type.GenericParameterPosition, which will return the zero-based index of the generic type in the list.

    If the parameter is an array of the generic type, or a ref / out parameter of the generic type, the parameter type will not be replaced, as IsGenericParameter will return false. To resolve this, you need code similar to:

    if ($type.IsGenericParameter)
    {
    $type = $genericTypes[$type.GenericParameterPosition]
    }
    elseif ($type.IsArray)
    {
    $arrayType = $type.GetElementType()
    if ($arrayType.IsGenericParameter)
    {
    $arrayRank = $type.GetArrayRank()
    $arrayType = $genericTypes[$arrayType.GenericParameterPosition]
    if (1 -eq $arrayRank)
    {
    ## NB: MakeArrayType(1) returns "T[*]",
    ## whereas MakeArrayType() return "T[]":
    $type = $arrayType.MakeArrayType()
    }
    else
    {
    $type = $arrayType.MakeArrayType($arrayRank)
    }
    }
    }
    elseif ($type.IsByRef)
    {
    $valueType = $type.GetElementType()
    if ($valueType.IsGenericParameter)
    {
    $valueType = $genericTypes[$valueType.GenericParameterPosition]
    $type = $valueType.MakeByRefType()
    }
    }

  4. Justin Dearing writes:

    Hi,

    I’m having a bit of a problem with this script. So the executive summaryis as follows. Powershell (command line or ISE) just crashes upon $newMethod.Invoke($instance, $methodParameters) so I cannot debug my issue. Is there any way to debug it?

    Long version:

    I am trying to use your script, which I’ve slightly modified:
    http://pastebin.com/dRqZd0AA

    I am calling it in this script:
    http://pastebin.com/byTjYVjK which makes use of two DLLs:
    http://www.atlantis-interactive.co.uk/blog/post/2011/02/24/Free-SQL-Server-Schema-Synchronisation-Engine-announcing-the-release-of-the-AtlantisSchemaEngine-source-code.aspx
    https://github.com/mongodb/mongo-csharp-driver/downloads

    Summary of my modifications to your script:

    I shrunk the foreach loop that geos through all the members: (although its probably slorwer this way its a bit more readable:

    $methodCandidates = $type.GetMethods() | Where-Object {
    $_.Name -eq $methodName -and
    $_.IsPublic -and
    $_.IsGenericMethod
    };
    foreach($method in $methodCandidates)
    {

    I also dealt with base types in ParameterTypesMatch:
    if( $type -ne $set2[$currentTypeIndex] -and -not ($set2[$currentTypeIndex].IsSubclassOf($type)) )
    {
    return $false
    }

    That was necessary since I am dealing with a collection that contains a base type.

  5. Seriál Windows PowerShell: PowerShell z pohledu programátora (část 19.) - TechNet Blog CZ/SK - Site Home - TechNet Blogs writes:

    [...] Jak jsme si už řekli, volání generických metod v PowerShellu nemá přímou podporu. Přesto je ale možné, pokud využijeme prostředků .NET frameworku. Ukážeme si, jak použít již hotové řešení. [...]

  6. Andreas Zuckerhut writes:

    Thanks very much for that blogpost, safed me a lot of time

  7. Andreas Zuckerhut writes:

    Had a weird one here where the type in makegenericmethod casts into the generic list’s type.
    public void ApproveCredentialForDistribution(Microsoft.EnterpriseManagement.ISecuredData securedData, System.Collections.Generic.IList healthServiceList)

    Aside from that, it didn’t take an object[] and it complained about conversion errors. Since I played around with it a bit further I tried it with an object list instead of an array and that did the job.
    Therefore, in some occasions you need this:
    $ObjectList = New-Object ‘System.Collections.Generic.List[System.Object]‘

  8. Peter Halverson writes:

    Why not just call the extended overload of Type.GetMethod? No need to iterate over methods or match parameter lists (which is tricky)

    $parameterTypes = $methodParameters | % { $_.GetType() }
    $method = $type.GetMethod($methodName, “instance,public”, $null, $parameterTypes, $null)
    $newMethod = $method.MakeGenericMethod($typedParameters)
    $newMethod.Invoke($instance, $methodParameters)

  9. Lee Holmes writes:

    @Peter – Indeed, that’s a much better way. An even better way is to use PowerShell V3 :)

  10. Wayne Bradney writes:

    @Lee – I take it there’s no way (even in V3) to call, without Reflection, a generic method on a non-generic type where the generic type parameter can’t be inferred from the method parameters?

    Eg:

    public Something CreateMeOne() {}

    It seems I have to MakeGenericMethod in order to call this from PS (there doesn’t seem to be a way to explicitly declare T).

    [I know that Code Analysis Rule CA1004 frowns on this, but I have a particular instance where such a method makes sense]

  11. Lee Holmes writes:

    @Wayne – Correct.

  12. Some tips and tricks of using SharePoint Client Object Model in PowerShell | Yet Another SharePoint Blog writes:

    […] There are several options how to bypass this limitation but in this post I would like to concentrate only on one technique that was originally described in the post Invoking Generic Methods on Non-Generic Classes in PowerShell. […]

  13. Dave Wyatt writes:

    I looked into this over the last few days after someone posted a question related to the SCOM SDK (which has some Generic methods in the C# example code). I found this post after I had already started doing some of the same work, but I stuck with it after noticing a couple of limitations with Lee’s original version here (such as producing errors if any of the $methodParameters are set to $null).

    The result is up on the TechNet Gallery at http://gallery.technet.microsoft.com/Invoke-Generic-Methods-bf7675af , if anyone still needs this functionality and wants to use it. If you find a way to break the function, please let me know in comments there and I’ll try to sort it out.

Leave a Reply