54

Consider the following PowerShell snippet:

$csharpString = @"
using System;

public sealed class MyClass
{
    public MyClass() { }
    public override string ToString() {
        return "This is my class. There are many others " +
            "like it, but this one is mine.";
    }
}
"@
Add-Type -TypeDefinition $csharpString;
$myObject = New-Object MyClass
Write-Host $myObject.ToString();

If I run it more than once in the same AppDomain (e.g. run the script twice in powershell.exe or powershell_ise.exe) I get the following error:

Add-Type : Cannot add type. The type name 'MyClass' already exists.
At line:13 char:1
+ Add-Type -TypeDefinition $csharpString;
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidOperation: (MyClass:String) [Add-Type],
 Exception
    + FullyQualifiedErrorId :
 TYPE_ALREADY_EXISTS,Microsoft.PowerShell.Commands.AddTypeCommand

How do I wrap the call to Add-Type -TypeDefinition so that its only called once?

x0n
  • 51,312
  • 7
  • 89
  • 111
Justin Dearing
  • 14,270
  • 22
  • 88
  • 161

5 Answers5

62

This technique works well for me:

if (-not ([System.Management.Automation.PSTypeName]'MyClass').Type)
{
    Add-Type -TypeDefinition 'public class MyClass { }'
}
  • The type name can be enclosed in quotes 'MyClass', square brackets [MyClass], or both '[MyClass]' (v3+ only).
  • The type name lookup is not case-sensitive.
  • You must use the full name of the type, unless it is part of the System namespace (e.g. [System.DateTime] can be looked up via 'DateTime', but [System.Reflection.Assembly] cannot be looked up via 'Assembly').
  • I've only tested this in Win8.1; PowerShell v2, v3, v4.

Internally, the PSTypeName class calls the LanguagePrimitives.ConvertStringToType() method which handles the heavy lifting. It caches the lookup string when successful, so additional lookups are faster.

I have not confirmed whether or not any exceptions are thrown internally as mentioned by x0n and Justin D.

skataben
  • 2,023
  • 1
  • 22
  • 25
28

Actually, none of this is required. Add-Type maintains a cache of any code that you submit to it, along with the resulting type. If you call Add-Type twice with the same code, then it won't bother compiling the code and will just return the type from last time.

You can verify this by just running an Add-Type call twice in a row.

The reason you got the error message in the example above is that you changed the code between calls to Add-Type. While the solution above makes that error go away in this situation, it also means that you're working with an older definition of the type that probably isn't acting the way you think it is.

LeeHolmes
  • 2,177
  • 16
  • 5
  • When I write the question I was editing the C# code I was compiling more than the PowerShell that was calling it. I'll edit the question to reflect this. This helps explain the behavior though. Thanks for pointing that out. – Justin Dearing Mar 04 '14 at 02:06
  • How were you validating that your edits to the C# were doing the right thing? If you use this manual "if not defined..." thing, Add-Type will never see your code. – LeeHolmes Mar 04 '14 at 02:16
  • You're right. I don't remember exactly. Glad I understand the correct behavior now. – Justin Dearing Mar 04 '14 at 14:28
  • 1
    p.s. there's no `Remove-Type`; see this answer for more on how to best work around this limitation: http://stackoverflow.com/questions/3369662/can-you-remove-an-add-ed-type-in-powershell-again – JohnLBevan Jul 30 '15 at 16:06
22

There's a nicer way to do this without incurring the cost of exceptions:

if (-not ("MyClass" -as [type])) {
    add-type @"
        public class MyClass { }
"@
}

update: well, apparently powershell signals internally with an exception anyway. It has a bad habit of doing this. The interpreter uses SEH to signal with the break and continue keywords, for example.

x0n
  • 51,312
  • 7
  • 89
  • 111
  • 2
    This actually incurs an exception. I hooked up powershell.exe to WinDbg and when I run `"MyCladdss" -as [type]` I get `(1e04.2424): CLR exception - code e0434352 (first chance)`. So, I'm not explicitly doing a try/catch in my code, but the CLR is. – Justin Dearing May 14 '13 at 21:16
  • What is the exception? Did you look closer? – x0n May 14 '13 at 21:18
  • It was a `Automation.PSInvalidCastException` Message: *Cannot convert the "MyCladdss" value of type "System.String" to type "System.Type"*. There is no InnerException. Perhaps I am ignorant of some detail of Windows Internals, but wouldn't any CLR exception invoke the Structured Exception Handler (SEH) and invoke a CPU exception, and therefore have similar performance issues? Does something I'm unaware of make the particular of the exception matter? – Justin Dearing May 14 '13 at 22:21
  • 1
    Nope, I was just asking if you had looked any closer. I didn't know that the "as" operator would throw internally, clearly. Unfortunate PowerShell has a bad habit of signalling with SEH - the break and continue keywords for example. Good sleuthing. – x0n May 15 '13 at 00:21
4

The simplest way to do this is a try/catch block. You have two options for doing this:

  • try { [MyClass] | Out-Null } catch { Add-Type -TypeDefinition $csharpString; }
  • try { Add-Type -TypeDefinition $csharpString; } catch {}
Justin Dearing
  • 14,270
  • 22
  • 88
  • 161
4

This way no exception is thrown, it's just a little slow base on number of assemblies loaded:

[bool]([appdomain]::CurrentDomain.GetAssemblies() | ? { $_.gettypes() -match 'myclass' })
CB.
  • 58,865
  • 9
  • 159
  • 159
  • You can probably speed this up if you check for an empty "Location" on the assembly. – Droj Feb 12 '16 at 22:00