Monday, October 14, 2013

Add-Type vs. [reflection.assembly] in PowerShell

Well PowerShell did it to me again.  There is always something new to learn.  Or relearn.  Or sometimes unlearn.

I had a perfectly good article planned wherein I was going to tell you to use Add-Type instead of [reflection.assembly]::LoadWithPartialName.  But then as I started to research some of the finer details, so that I could sound like I actually knew what I was talking about, I found out I didn’t know what I was talking about.

And then I did some more research, and I am back to recommending Add-Type, but with some caveats.  And this article has morphed from a useful tip into a long rant about the poor decisions Microsoft made when implementing Add-Type.

In PowerShell, we have all of .Net at our disposal for making Windows do cool things.  But .Net has a zillion moving parts, and almost everything you install on your computer adds more .Net components.  PowerShell only loads the most commonly need parts of .Net—or .Net assemblies—into memory when it loads.  Additional .Net assemblies might be loaded by imported modules.  (And in PowerShell 3.0 and up, PowerShell can import some referenced modules without being explicitly told to do so.)

But if you want to make use of a .Net component that isn’t there by default, you have to tell PowerShell to load it.  For example, if you want to put a nice GUI interface on your script, you need to load the Windows.Forms assembly, before you can create a [form] object and load it full of [buttons] and [textbox]es and things.

In PowerShell v1.0, we would use the methods built-in to the static .Net class [system.reflection.assembly] to load assemblies.  (Unless there is an ambiguity, you can leave off “system.” any time you see it.  So we can just use [reflection.assembly])

If you know the full name of the assembly you need, you can use the .Load() method.  But what is considered the “full name” of an assembly is a little crazy.

[reflection.assembly]::Load(“System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089”)

Or, if you know the full path to the file, you can use .LoadFrom() instead, but that’s also a little crazy.

[reflection.assembly]::LoadFrom("C:/Windows/Microsoft.Net/assembly/GAC_MSIL/System.Windows.Forms/v4.0_4.0.0.0__b77a5c561934e089/System.Windows.Forms.dll")

Fortunately, there is a third useful method, .LoadWithPartialName(), which allows you to use just that part of the “full name” that most of us would consider the name.

[reflection.assembly]::LoadWithPartialName("Windows.Forms")

And that worked fine.  It’s kind of ugly, and if you aren’t from a programming background, it isn’t obvious exactly what it is doing or why.  You copied it over with some code you found from the internet, and the code worked with that line, but not without it.  So you didn’t ask questions, your script worked and you moved on.

Then PowerShell 2.0 came along and gave us something better.  Well, at least they gave us something prettier.

Add-Type –AssemblyName Windows.Forms

does the same thing as

[reflection.assembly]::LoadWithPartialName("Windows.Forms")

(most of the time).

It’s slightly less opaque in meaning, and it looks nicer and more PowerShelly.

So since then, I have used Add-Type in my scripts instead of [reflection.assembly].

Add-Type works great.  Most of the time.  Sometimes it’s a little quirky, and some of my script required extra research, troubleshooting, and the one line became several ugly lines to compensate.

After researching for this article, I know why.

If you use the full name, Add-Type works every time.  But as we said, the "full name" is crazy, and we don't want to have to use it.  We want to use the more normal name.

The challenge, for the architects, is how do you have your command find the correct assembly based only on a "partial" name?  There many different things they could have done, not the least of which is to use the .LoadWithPartialName() method behind the scenes.  They could have used a layered approach with different methods to fall back on if preferred methods failed.  (For comparison, the PowerShell algorithm for converting object types will try no less than 10 different methods, if necessary.  See Understanding PowerShell's Type Conversion Magic)

Unfortunately, .LoadWithPartialName() has been deprecated, so they can't use that.  (Actually, several version of .Net later, it's still there, but it might not be there in version 5.  Of course, version 5 will also come with a new version of PowerShell, but logic doesn't seem to have been a large part of this decision.)

The PowerShell team, despite the numerous examples of where they made it so much more flexible and useful than more rigid programming languages, decided in his case to sacrifice useful for…I'm still not sure what.

It seems they decided that it was more important for Add-Type to work consistently than for it to work consistently.  They would prefer that your script fail on all computers, rather than work better on some than on others.

Rather than make any attempt to parse your request in the context of your system, it looks at a static, internal table to translate the "partial name" to a "full name".

If your "partial name" doesn't appear in their table, your script will fail.

If you have multiple versions of the assembly installed on your computer, there is no intelligent algorithm to choose between them.  You are going to get whichever one appears in their table, probably the older, outdated one.

If the versions you have installed are all newer than the obsolete one in the table, your script will fail.

Add-Type has no intelligent parser of "partial names" like .LoadWithPartialNames.

I can almost understand the logic behind deprecating .LoadWithPartialNames, forcing software developers to demand specific versions of assemblies.  But extending that decision into PowerShell makes no sense.  If I am writing a formal script in PowerShell that needs to run reliably on production servers for years to come, I have a responsibility to tighten it up and be specific about ensuring the right dependencies are loaded.

But PowerShell is not just about formal scripts.  It's about ad hoc scripts and command-line work.

When I am tossing together a quick couple of lines to run a SQL query and use the results in some Active Directory work, for example, I can accomplish that most efficiently if I can write the whole thing off the top of my head.

I can remember

Add-Type -Assembly Microsoft.SqlServer.Smo

I can't remember

Add-Type -AssemblyName "Microsoft.SqlServer.SMO, Version=11.0.0.0, Culture=neutral, PublicKeyToken=89845dcd8080cc91"

and I sure don't want to have to type it.

And worse, if I don't know what version of SQL is installed, I don't want to have to enter

Add-Type -AssemblyName "Microsoft.SqlServer.SMO, Version=11.0.0.0, Culture=neutral, PublicKeyToken=89845dcd8080cc91"
If ( -not $? ) { Add-Type -AssemblyName "Microsoft.SqlServer.SMO, Version=10.0.0.0, Culture=neutral, PublicKeyToken=89845dcd8080cc91" }


when I'm just fooling around at a PowerShell prompt.

PowerShell is a scripting language, Microsoft, not a programming language.  It is wonderful that you have made it so that we can use it as formally and powerfully as a programming language.  It is wonderful that in so many other respects you simultaneously give it the flexibility to be used informally as an ad hoc scripting and command-line tool.  But Add-Type is a failure in this regard.

End of rant

Conclusion

So.

If your assembly name works with Add-Type, and you are happy with the version it loads, use:

Add-Type -AssemblyName Windows.Forms

If your short assembly name doesn't work with Add-Type because it isn't in the internal table, or the table lists a version that isn't available on this computer, and you are happy with any version that might be loaded, use:

[reflection.assembly]::LoadWithPartialName( "Microsoft.SqlServer.Smo" )

If you need a specific version of your assembly, or if this is the future and neither of the two methods above work anymore, use:

Add-Type -AssemblyName "Microsoft.SqlServer.SMO, Version=11.0.0.0, Culture=neutral, PublicKeyToken=89845dcd8080cc91"

11 comments:

  1. Hi Tim,
    a nice piece of work explaining a lot!
    Thanks, Andreas

    ReplyDelete
  2. I like your viewpoint. One note - "It seems they decided that it was more important for Add-Type to work consistently than for it to work consistently." ???
    One of those "consistently"s needs to be changed.

    ReplyDelete
    Replies
    1. I have edited the article to put back the italics that had gotten lost somewhere along the way. It's all in the intonation. So it's admittedly not the best wording for a written article with a multilingual audience, but I try to use a conversational style in my writing, and those things creep in from time to time.

      Delete
  3. Excellent post. Thank you. Microsoft.SQLServer.Smo was actually the assembly I was having trouble loading.

    ReplyDelete
  4. Hi Tim,

    A really great post and this is one area that I wanted to explore in detail myself but didn't really ever get around to it ... I have also found that when trying to import/load 3rd party .net libraries into PowerShell, that it can be frustrating, especially when 3rd party .dll's they have their own dependencies and I have to get the order and namespaces right. However, I'm really glad that the add-type functionality exists in PowerShell, otherwise I probably wouldn't use it to be honest. And maybe what you're saying is correct: that to make it work *consistently* will require being extremely specific with those long names, despite them being next to impossible to remember :/ Thanks a lot for your article!

    Kind Regards,
    Steve Rathbone

    ReplyDelete
  5. Really good post. Certainly helped with my confusion around why PowerShell was failing to load "Microsoft.SqlServer.SMO" assembly.

    ReplyDelete
  6. Hi Tim,

    Great rant. I was encountering the same behaviour and had no idea why PowerShell wouldn't load 'Microsoft.SqlServer.SMO' assembly when using Add-Type.

    Thanks for the enlightenment.

    ReplyDelete
  7. You can also specify a full path to the assembly with Add-Type e.g.:

    Add-Type -Path 'C:\Program Files\Microsoft SQL Server\110\SDK\Assemblies\Microsoft.SqlServer.Smo.dll'

    ReplyDelete
  8. Reflection.Assembly wins. Add-Type is fine if all the DLL's are actually installed to the SDK\Assemblies directory. For me I reverted my code to use Relection.Assembly because installing the SDK didn't install all the DLL's. Nor did installing SharedManagementObjects.msi. My example is for SQL Server 2014. I tried to force using Add-Type I really did... Thanks for this post I finally broke... lol

    ReplyDelete
  9. Thanks a lot. You saved me~~~ it's so hard to force it to user certain version of SMO

    ReplyDelete