alx9r
alx9r

Reputation: 4194

How do you nest custom .NET types in Powershell?

Powershell permits creating custom .NET types using some C# as a parameter for the Add-Type Cmdlet. Here is an example:

Add-Type @'
    public class MyType1
    {
        public string a { get; set; }
        public string b { get; set; }
    }
'@

$obj1 = New-Object MyType1
$obj1.a = 'my a'
$obj1.b = 'my b'

PS C:\> $obj1 | fl *
a : my a
b : my b

Suppose I now want to create another type that itself has a property of type MyType1. The most obvious method would be to create another custom type using Add-Type:

Add-Type @'
    public class MyType2
    {
        public string  c       { get; set; }
        public MyType1 subObj  { get; set; }
    }
'@

This, however, results in the following error:

The type or namespace name 'MyType1' could not be found (are you 
missing a using directive or an assembly reference?)

How do you create nested custom .NET types in Powershell for use in Powershell?


Note: I am aware that you can create nested objects with New-Object PSObject and Add-Member. Those Cmdlets use Powershell's Extended Type System and produce objects of type PSObjects. I am working with .NET APIs so I need to create particular bona fide .NET objects.

Upvotes: 2

Views: 2548

Answers (2)

Jan Chrbolka
Jan Chrbolka

Reputation: 4454

Based on the comments, I have managed to get this working using a temporary DLL assembly. It's ugly and I'm sure somebody with a better understanding of what goes on "under the hood", can set me straight and improve the answer. Here it is:

$TypeDef1= @'
    namespace Mynamespace
    {     
        public class MyType1
        {
            public string a { get; set; }
            public string b { get; set; }
        }
    }
'@ 

$type1 = Add-Type $TypeDef1 -PassThru -OutputAssembly "c:\temp\my.dll"

$obj1 = New-Object Mynamespace.MyType1
$obj1.a = 'my a'
$obj1.b = 'my b'

$TypeDef2 = @'
    namespace Mynamespace
    {     
        public class MyType2
        {
            public string  c       { get; set; }
            public Mynamespace.MyType1 subObj  { get; set; }
        }
    }   
'@

Add-Type -TypeDefinition $TypeDef2 -ReferencedAssemblies "c:\temp\my.dll"

$obj2 = New-Object Mynamespace.MyType2
$obj2.subObj = $obj1

Basically, result of the first compilation (add-type) is saved in a DLL and this DLL is passed as a referenced assembly to the second add type statement.

I understand that there is already a temp DLL created by add-type statement and can be seen in $type1.Module, but I could not find a way to reference that in the second type-add command.


EDIT:

While trying to figure out how to make this less "ugly", I have come across other people trying to accomplish similar task in C# natively.

In C#, how do you reference types from one in-memory assembly inside another?

C# - Referencing a type in a dynamically generated assembly

The second link points out a method which may be just a little bit more .NETish.

By default PowerShell Add-Type command executes .NET compiler with GenerateInMemory option set to $true.

This compiles code and loads resulting Types into memory, not leaving actual copy of the compiled assembly. A copy of the assembly is requited to compile additional Types which reference the original one.

One way to get around this is to write our own New-Type function. This is a simplified version of the Add-Type cmdlet, which executes the compiler with GenerateInMemory = $false and returns reference to the compiled assembly. This reference can then be used to compile subsequent Types.

A temporary file is still generated on disk, but at least the process and location are obfuscated by the compiler.

Here is the code:

function New-Type {
   param([string]$TypeDefinition,[string[]]$ReferencedAssemblies)


   $CodeProvider = New-Object Microsoft.CSharp.CSharpCodeProvider
   # Location for System.Management.Automation DLL
   $dllName = [PsObject].Assembly.Location
   $Parameters = New-Object System.CodeDom.Compiler.CompilerParameters
   $RefAssemblies = @("System.dll", $dllName)
   $Parameters.ReferencedAssemblies.AddRange($RefAssemblies)
   if($ReferencedAssemblies) { 
      $Parameters.ReferencedAssemblies.AddRange($ReferencedAssemblies) 
   }
   $Parameters.IncludeDebugInformation = $true
   $Parameters.GenerateInMemory = $false # Do not compile in memory (generates a temp DLL file)

   $Results = $CodeProvider.CompileAssemblyFromSource($Parameters, $TypeDefinition) #compile
   if($Results.Errors.Count -gt 0) {
     $Results.Errors | % { Write-Error ("{0}:`t{1}" -f $_.Line,$_.ErrorText) }
   }
   return $Results.CompiledAssembly # return info for the assembly
}


$TypeDef1= @'
    public class MyType1
    {
        public string a { get; set; }
        public string b { get; set; }
    }
'@ 

$Asembly1 = New-Type $TypeDef1

$obj1 = New-Object MyType1
$obj1.a = 'my a'
$obj1.b = 'my b'

$TypeDef2 = @'
    public class MyType2
    {
        public string  c       { get; set; }
        public MyType1 subObj  { get; set; }
    } 
'@

$Asembly2 = New-Type -TypeDefinition $TypeDef2 -ReferencedAssemblies $Asembly1.Location

$obj2 = New-Object MyType2
$obj2.subObj = $obj1

Upvotes: 1

alx9r
alx9r

Reputation: 4194

As @arco444 pointed out, creating custom nested .NET types work when you define both the parent and child in a single call to Add-Type. Here's what that looks like:

Add-Type @'
    public class MyType3a
        {
            public string a { get; set; }
            public string b { get; set; }
        }

    public class MyType3
    {
        public string  c       { get; set; }
        public MyType3a subObj  { get; set; }
    }
'@

$obj3a = New-Object MyType3a
$obj3a.a = 'my a'
$obj3a.b = 'my b'

$obj3 = New-Object MyType3
$obj3.subObj = $obj3a

PS C:\> $obj3.subObj | fl 

a : my a
b : my b

Upvotes: 2

Related Questions