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


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”



 

7 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]‘

Leave a Reply