2

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.

alx9r
  • 3,675
  • 4
  • 26
  • 55
  • It seems to work if you define them all at the same time. Is that an option for you? – arco444 Apr 13 '15 at 17:02
  • @arco444 I noticed that. It would require a bit of refactoring in my case. – alx9r Apr 13 '15 at 17:05
  • Have you tried getting the assembly that gets dynamically created and seeing if you can manually import/reference that? (This may be nonsense I'm a bit out of my depth here.) – Etan Reisner Apr 13 '15 at 17:13
  • @arco444 After refactoring, combining the types into a single `Add-Type` call actually works out rather well. I wonder if there are cases where that doesn't work... – alx9r Apr 13 '15 at 17:13
  • @EtanReisner I have not. I'm not even sure where to look for such an assembly since it's created dynamically at runtime. – alx9r Apr 13 '15 at 17:15
  • I found a dll name and assembly information by poking at `$obj1.gettype() | fl` but got stuck at that point as I wasn't sure where to go next. But doing it all at once seems like it is probably a better solution. – Etan Reisner Apr 13 '15 at 17:30
  • @EtanReisner I see that now. I haven't figured out how you'd reference that assembly from the C# block. It'd be great if `MyType1` ended up in a namespace you could reference from the second call to `Add-Type`. But `$obj1.GetType().NameSpace` is null. – alx9r Apr 13 '15 at 17:41
  • If you define namespace within your type definition `@' namespace Mynamespace { public class MyType1...` then `$obj1.GetType().NameSpace` will show the namespace, but I'm stuck at how to pass that namespace back to c#. – Jan Chrbolka Apr 14 '15 at 02:12

2 Answers2

2

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
alx9r
  • 3,675
  • 4
  • 26
  • 55
1

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
Community
  • 1
  • 1
Jan Chrbolka
  • 4,184
  • 2
  • 29
  • 38
  • I've seen uglier things than this. It would be nice to avoid the file system, but this might be the least worst option in some cases. – alx9r Apr 14 '15 at 03:49
  • @alx9r here is a similar question asked in C# context [In C#, how do you reference types from one in-memory assembly inside another?](http://stackoverflow.com/questions/454561/in-c-how-do-you-reference-types-from-one-in-memory-assembly-inside-another) According to the answer apparently it is not possible. – Jan Chrbolka Apr 14 '15 at 22:42
  • I have really wanted to sort this one out, because it's like a tip of an iceberg. Many other problems, I have had in the past, run up against similar issues (like having proper "code behind" in powershell XAML/WPF app). Ah well.... – Jan Chrbolka Apr 14 '15 at 22:48
  • Nice find. It looks like their solution was also writing out the assembly too disk. Thanks for the help. – alx9r Apr 14 '15 at 23:42
  • @alx9r I have added another piece of code to the answer. It eliminates the need to manually specify temporary DLL. On the flip side, you need a custom function to call the compiler. – Jan Chrbolka Apr 15 '15 at 01:47