3

I'm working in Windows 10 x64, PowerShell version 5.1.

I've got multiple versions of a .NET Framework 4.0 assembly (.dll) written in C# (by myself). Assemblies are not signed. Their versions are set in AssemblyInfo.cs via [assembly: AssemblyVersion("X.X.X.X")]. The [assembly: AssemblyFileVersion("X.X.X.X")] tag does not exist in my AssemblyInfo.cs! These assemblies are used in a regular .NET Framework application, they're not specialized PowerShell modules nor they have manifest files.

I use this assembly in some PowerShell scripts to create objects and call methods from it.

I've got two versions of this assembly, let's say 1.1.1.100 and 1.2.1.100. When I import let's say 1.1.1.100 version in PowerShell via Import-Module "D:\path\to_v1.1\MyAssembly.dll", it works just fine. When I call Get-Module I see this imported assembly in the list:

ModuleType Version    Name
---------- -------    ----
Binary     1.1.1.100  MyAssembly

Now the things start to get interesting. I import the other version of the assembly Import-Module "D:\another\path\to_v1.2\MyAssembly.dll". It again works just fine. So here're my questions:

Q1. Why are no errors shown? I expect to get an error: I'm trying to load an assembly with the same name but different version. I thought you could not load two different versions of the same assembly in one context. How are these situations handled, which loading contexts are used? Maybe the second version is not loaded at all?

Q2. Get-Module now shows two modules with the same name and version, like this:

ModuleType Version    Name
---------- -------    ----
Binary     1.1.1.100  MyAssembly
Binary     1.1.1.100  MyAssembly

It doesn't matter which version I import first. On the second Import-Module the earliest imported version is duplicated in the Get-Module list. I can only use the types/methods from the earliest imported version of the assembly as well.

Q3. My assembly has some dependencies on my other assemblies (in the same folder). These dependencies are automatically resolved and implicitly "imported" to the current session (I can use types from them no problem). Each version of the main MyAssembly.dll depends on different versions of these secondary assemblies. When I import another version of MyAssembly, I get no error about conflicting versions either. Once again, I can only use the types from the earliest imported secondary assemblies. I've read about "Dependency hell" and this should be impossible -- so how is that possible?

I've performed the same experiment with NuGet package Microsoft.CodeAnalysis.CSharp.dll, versions 3.4.0 and 3.11.0. The results are the same: versions and their dependencies are imported no problem, but only the earliest imported version is available.

Summary

When I import different versions of the same assemblies, I get no errors, but only the earliest imported versions are available. I want to understand why I'm able to load multiple versions of an assembly in one session without errors, how PowerShell handles these situations in general and why it shows two modules with the same versions, why I don't get dependency hell.

I want to understand what's going on, not just "make it work".

What am I missing here?

Thanks!

  • 2
    I think it has to do with not following the guidelines specified in [Installing Multiple Versions of a Module](https://learn.microsoft.com/en-us/powershell/scripting/developer/module/installing-a-powershell-module?view=powershell-5.1#installing-multiple-versions-of-a-module). At least you didn't mention manifest files, so I suppose you don't have some. You've also not specified the `MinimumVersion` or `RequiredVersion` parameters of the Import-Module cmdlet. Maybe you get some hints by adding `-Verbose` and/or `-Force` parameter to `Import-Module`. – Steeeve Aug 29 '21 at 15:44
  • 1
    @Steeeve Yes, you are correct: I'm not using modules manifests. I'll update the question in a minute to clarify that and the fact that assemblies are not designed to be PowerShell modules, they're just convenient to use both in my C# project and in PowerShell. The `-Verbose` parameter does not help, it only logs "Loading module from path: ...". The `-Force` parameter does not change anything. I'm really curious about what's going on here. Thanks for the link though. – TheNightdrivingAvenger Aug 29 '21 at 17:13
  • Can you not add them using `Add-Type -AssemblyName` ? – Scepticalist Aug 30 '21 at 09:15
  • @Scepticalist `Add-Type` behaves exactly the same as `Import-Module` in my case (except for the `Get-Module` part since the loaded assemblies are not shown in that list when imported via `Add-Type`). I've even asked a question about them to understand it better, and it seems like they're not that different: https://stackoverflow.com/questions/68958466/powershell-import-module-or-add-type-for-net-assemblies – TheNightdrivingAvenger Aug 30 '21 at 16:28

1 Answers1

4

In your scenario, your modules are stand-alone .NET assemblies that implement PowerShell cmdlets.

In both Windows PowerShell and PowerShell (Core) 7+, all assemblies are loaded into the same:

  • application domain in Windows PowerShell, which is based on .NET Framework.

  • ALC (assembly-load context) in PowerShell (Core) 7+, which is based on .NET Core / .NET 5+.

By default, loading multiple version of the same assembly is not supported, and the first one that is loaded into the session wins.

Subsequent attempts to load a different version of the assembly:

  • are quietly ignored in Windows PowerShell - irrespective of whether you use Import-Module, Add-Type -LiteralPath or its underlying .NET API method, [System.Reflection.Assembly]::LoadFrom() (be sure to pass a full file path to the latter, because PowerShell's working dir. usually differs from .NET's; the method outputs an object representing the assembly ostensibly just loaded, but it it actually represents the originally loaded version).

  • cause a statement-terminating error in Powershell (Core) 7+

    Assembly with same name is already loaded.
    

Note: You can load an assembly with a different version, namely via the [System.Reflection.Assembly]::LoadFile() method (note the File instead of From), but the only way you can use its types is via reflection.


From what I can tell:

  • There is no solution for cmdlet-implementing assemblies, as in your case.

    • By cmdlet-implementing I mean that the module's cmdlets are themselves implemented as .NET classes stored in an assembly, as opposed to cmdlets implemented in PowerShell code, in *.psm1 script-module files.

    • Whichever version of the cmdlet-implementing assembly you happen to import / load first in your session is the only one available in the remainder of the session; assemblies, once loaded, cannot be unloaded.

    • Another way of putting it: while you can have side-by-side versions of such modules on disk, you cannot load them simultaneously into a session, from what I can tell - this only works with script-based modules (see next point).

  • There are - nontrivial - solutions for loading multiple versions of helper assemblies into the same session; these are detailed in Resolving PowerShell module assembly dependency conflicts, which also provided extensive background information.

    • That is, if your cmdlets are implemented in PowerShell code and that code only uses auxiliary .NET assemblies, the linked documentation offers solutions for loading versions of these assemblies that would by default clash with different versions loaded by other modules or different versions of the same module.
mklement0
  • 382,024
  • 64
  • 607
  • 775
  • Thanks for your answer, it explains some things. However I still have some questions. **1.** Why do you call my assemblies "cmdlet-implementing"? I've never seen this term before. My assemblies are "very normal" .NET Framework assemblies that just happen to be loaded into PowerShell :) **2.** Does the rule of "silently ignoring another version loading" apply to the helper assemblies (dependencies) in PS 5.1 as well? If so, this explains why I get no errors at all. **3.** Could you please provide some link to where the "silently ignore" strategy is mentioned/described? – TheNightdrivingAvenger Aug 31 '21 at 14:17
  • 1
    @TheNightdrivingAvenger Re 1: please see my update. Re 2. Yes, the silent ignoring applies to all assemblies that themselves conflict with an already loaded, different version of the same assembly. However, you may see errors if a given assembly's _dependencies_ cause a conflict - see the linked documentation. Re 3. Please see my update, which added links to the `[System.Reflection.Assembly]::LoadFrom()` and `[System.Reflection.Assembly]::LoadFile()` methods, along with a hint as to how they differ (see also the next comment). My own knowledge doesn't go very deep here. – mklement0 Aug 31 '21 at 19:54
  • 1
    @TheNightdrivingAvenger: From the `LoadFile` help topic: "`LoadFrom` cannot be used to load assemblies that have the same identities but different paths; it will load only the first such assembly." - Curiously, the `LoadFrom` help topic doesn't mention the _error_ that occurs in this case, as of at least .NET 5 / PS Core 7.1 (as opposed to .NET Framework / WinPS). – mklement0 Aug 31 '21 at 19:57
  • Thanks for the links. The thing is that I don't get errors about conflicting dependencies. I expected errors, but (as I stated in my question) the dependencies are loaded by the same principle: only the _first_ loaded dependency assembly is used. If I import the other version of the main assembly which references different version of dependency assembly, no errors are shown. And from what I can tell, the _first_ loaded dependency assembly is used. I appreciate your efforts. If there'll be no more answers in some time, I'll mark yours as the answer. – TheNightdrivingAvenger Aug 31 '21 at 20:48
  • Thanks, @TheNightdrivingAvenger. My _guess_ is that you're not seeing errors if the version of the dependency loaded first is _higher_ than the one the second attempt tries to load. In the inverse scenario I would expect to see errors. – mklement0 Aug 31 '21 at 20:52
  • Nope, the order of the versions import is not important. It just keeps being silent. I've no idea why that happens :) P.S. For some reason my "@mklement0" at the beginning is automatically removed from the comment when I post it. – TheNightdrivingAvenger Aug 31 '21 at 21:18
  • @TheNightdrivingAvenger, is it also silent if you load via `[System.Reflection.Assembly]::LoadFrom()` / `Add-Type -LiteralPath` rather than`Import-Module`? – mklement0 Aug 31 '21 at 21:20
  • 1
    @TheNightdrivingAvenger, as for the @-reference getting removed: This indicates that the reference is superfluous, because as the author of the answer I'm always notified. – mklement0 Aug 31 '21 at 21:22
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/236627/discussion-between-thenightdrivingavenger-and-mklement0). – TheNightdrivingAvenger Aug 31 '21 at 21:37