Reputation: 4194
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
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
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