4

I am developing on software that uses the Matrox Imaging Library (MIL).
This software used the version 9 of the MIL in the past, now we moved to v10. Due to backwards compatibility we must continue supporting v9.

There are some difficulties when working with MIL and its DLLs:

  1. MIL 9 and MIL 10 cannot be installed at the same time. It wouldn't make any sense either.
  2. The C# DLLs of MIL 9 and MIL 10 are both named Matrox.MatroxImagingLibrary.dll.
  3. The namespaces in both DLLs are identical.

While this is quite useful for exchangeability (despite the fact that some functions have changed), it is a big issue for parallel use.
I could not reference both DLLs in the same assembly due to the identical file name and namespace, so I created one assembly for each,
imaging-system_mil9 and
imaging-system_mil10.
This is necessary, but quite useless so far. What I needed was a base class in a common assembly so that I could use inheritance, therefore I created the assembly
imaging-system.
Inside this assembly I added my own wrappers for the MIL commands. This seemed to be a pretty nice solution and worked really well when I initially developed and tested on my development computer where MIL 9 is installed. When I moved to another computer and developed and tested with MIL 10, I found that some commands in my wrapper need to be adapted because they had changed in the MIL 10 C# DLL. So far so good.

Today I moved back to my MIL 9 computer and wanted to test more things, but my test program failed to start, saying MissingMethodException. After some search I found that I completely forgot to find a solution for one point: The identical file names:
My test program references imaging-system, imaging-system_mil9, and imaging-system_mil10. The last two both reference a file Matrox.MatroxImagingLibrary.dll, thus the output in the bin folder looked like:

test.exe
imaging-system.dll
imaging-system_mil9.dll
imaging-system_mil10.dll
Matrox.MatroxImagingLibrary.dll (the one from MIL 9)
Matrox.MatroxImagingLibrary.dll (the one from MIL 10)

As you can see, the last two files have the same name, so it basically is like lottery which one is overwritten by the other one.

The first idea that I had to solve this problem was renaming the files into
Matrox.MatroxImagingLibrary9.dll and
Matrox.MatroxImagingLibrary10.dll.
This works on the first compilation level when the
imaging-system_mil9.dll and
imaging-system_mil10.dll
are compiled because they directly reference the respective files. The output in one of the bin folders:

imaging-system_mil10.dll
Matrox.MatroxImagingLibrary10.dll

But it fails on the next level when the assembly is compiled that does not reference the Matrox DLLs directly. The compiler just skips the renamed files, most likely because the assembly name does not match the file name any more. The bin folder here:

test.exe
imaging-system.dll
imaging-system_mil9.dll
imaging-system_mil10.dll
missing: Matrox.MatroxImagingLibrary9.dll, Matrox.MatroxImagingLibrary10.dll

Furthermore, copying the renamed files manually into the EXE's output folder does not help either, because the EXE does not "see" them. This makes sense: imagine that there are 1000 DLLs and none is named like the assembly that the program is looking for. How should it find it? It can't load all 1000 DLLs... Thus the file name must match the assembly name.

The next idea that I have is setting CopyLocal = false for the Matrox DLLs and copying them separately by a post-build event into a dll\mil9 resp. dll\mil10 subfolder.
Every assembly will run a pre-build or post-build PowerShell script that copies all content from all dll subfolders of all referenced DLLs.
Every EXE will get an adapted app.config file as described in How to save DLLs in a different folder when compiling in Visual Studio?.

Problem: I have not done this before because there was no need to. Thus I am currently facing several questions:
1) Will the EXE find the correct Matrox DLL because it sees both of them when searching the subfolders? The DLLs have same name, same culture and same publicKeyToken, but different version numbers, so they can be distinguished from each other.
2) How can I get a list of referenced DLL paths during build to feed into my PowerShell script that looks for dll subfolders and copies files? The only way that comes to my mind is reading the csproj file.


What I have tested so far on #1:
I have already done several tests with a test solution containing a console EXE and 2 DLLs, which replicate the situation. I used "CopyLocal=false" and "SpecificVersion=true", I tried <probing> and codebase> in the app.config file, but it only works with one of the DLLs:

Test folder structure:

test.exe
dll\testDLL9.DLL
dll\testDLL10.DLL
mil9-x64\mil.net\Matrox.MatroxImagingLibrary.dll
mil10-x64\mil.net\Matrox.MatroxImagingLibrary.dll

The test EXE:

private static void Main ()
{
  Mil10 ();  // when stepping into this, dll\testDLL10.dll is loaded
  Mil9 ();   // when stepping into this, dll\testDLL9.dll is loaded
}

private static void Mil10 ()  // when arriving here, dll\testDLL10.dll has been loaded
{
  testDLL10.CDLL10.Work ();   // when stepping into this, mil10-x64\Mil.net\Matrox.MatroxImagingLibrary.dll is loaded
}

private static void Mil9 ()  // when arriving here, dll\testDLL9.dll has been loaded
{
  testDLL9.CDLL9.Work ();   // when stepping into this, MissingMethodException is thrown, which is correct, because the EXE uses the already loaded DLL, which is the wrong one.
}

Now, when Mil9() is called first, it also loads the
mil10-x64\Mil.net\Matrox.MatroxImagingLibrary.dll
when testDLL9.CDLL9.Work() is called, which obviously is completely wrong. Why does this happen?
It only works when I remove the reference to testDLL10 and comment out the related functions.

app.config:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <startup>
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6" />
  </startup>
  <runtime>
    <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
      <probing privatePath="dll" />

      <dependentAssembly>
        <assemblyIdentity name="Matrox.MatroxImagingLibrary"
                          publicKeyToken="5a83d419d44a9d98"
                          culture="neutral" />
        <codeBase version="9.2.1109.1" href="mil9-x64\Mil.net\Matrox.MatroxImagingLibrary.dll" />
      </dependentAssembly>

      <dependentAssembly>
        <assemblyIdentity name="Matrox.MatroxImagingLibrary"
                          publicKeyToken="5a83d419d44a9d98"
                          culture="neutral" />
        <codeBase version="10.30.595.0" href="mil10-x64\Mil.net\Matrox.MatroxImagingLibrary.dll" />
      </dependentAssembly>
    </assemblyBinding>
  </runtime>
</configuration>

Final notes:

  • The DLLs don't have to be loaded at the same time, as this is just impossible, because MIL 9 and MIL 10 cannot be installed in parallel, see above.
  • I have read Referencing DLL's with same name, but up to now I don't want to load the DLLs manually as suggested in step 3 of the answer. I would prefer if the CLR loaded the correct DLL for me.
  • I have read How to reference different assemblies with the same name?. I cannot use the GAC because I need to be able to switch between different versions of the software just by changing folders. As little connection to the operating system as possible.
Tobias Knauss
  • 3,361
  • 1
  • 21
  • 45
  • How about creating 2 identical wrapper classes, both based on the same interface, where v9 has some functions that throw NotImplementedException, and v10 has all functions implemented. Then you instantiate the correct one at runtime (which will load the correct underlying dll). – Neil Feb 21 '19 at 14:58
  • @Neil: That's just what I did, maybe I didn't describe detailled enough. My issue is just loading the correct DLL. – Tobias Knauss Feb 21 '19 at 15:01

1 Answers1

2

Will the EXE find the correct Matrox DLL [...]?

YES

No, if <probing> is used, because this only adds certain folders to the list of folders that are checked when the referenced assemblies are searched. If a file with the requested name is found, this file is used. No version check is performed. If this is the right file, it works. If it's the wrong file, it fails. And if a file with the requested name has been loaded, it will be reused later, no matter if the version matches the requested version.
Yes, if <codebase> is used, because this includes a version check.

After a lot of tests and further reading on the internet, I found the cause of the problem that the wrong file was loaded:
MS Doc "How the Runtime Locates Assemblies" states in chapter "Step 1: Examining the Configuration Files":

First, the common language runtime checks the application configuration file for information that overrides the version information stored in the calling assembly's manifest.

So I thought "well, let's have a look at the manifest to see, whether it contains some useful data." I opened it and found... nothing. So I checked the other files in the output folder, and the testAPP.exe.config caught my attention. Up to that time I thought that it was a plain copy of the app.config that I created, but surprisingly besides my content it contained another very relevant block, which immediately caught my attention:

<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
  <dependentAssembly>
    <assemblyIdentity name="Matrox.MatroxImagingLibrary" publicKeyToken="5a83d419d44a9d98" culture="neutral" />
    <bindingRedirect oldVersion="0.0.0.0-10.30.595.0" newVersion="10.30.595.0" />
  </dependentAssembly>
</assemblyBinding>

This was the reason why my test program always tried to load the v10 library. My next question was: How the hell did this get in there?
Therefore I searched for "c# compiler adds assembly version redirect" and found How to: Enable and Disable Automatic Binding Redirection, which says:

Automatic binding redirects are enabled by default for Windows desktop apps that target the .NET Framework 4.5.1 and later versions. The binding redirects are added to the output configuration (app.config) file when the app is compiled and override the assembly unification that might otherwise take place. The source app.config file is not modified.

In VS 2015 and below, the csproj file must be edited manually:
Set <AutoGenerateBindingRedirects> to false.
You may also delete the whole entry, but setting to false should ensure that it's not automatically re-added with true later.
After this edit, the output config file was 100% identical to my source file (including any line breaks and blank lines). And finally, my test EXE loaded exactly those DLLs that it needed, and in the right order at the right time:

'testAPP.vshost.exe' (CLR v4.0.30319: testAPP.vshost.exe): Loaded 'D:\Visual Studio Projects\testTKS_vs2015\testAPP\bin\x64\Debug\dll\testDLL9.dll'. Symbols loaded.
'testAPP.vshost.exe' (CLR v4.0.30319: testAPP.vshost.exe): Loaded 'D:\Visual Studio Projects\testTKS_vs2015\testAPP\bin\x64\Debug\mil9-x64\Mil.net\Matrox.MatroxImagingLibrary.dll'. Skipped loading symbols. Module is optimized and the debugger option 'Just My Code' is enabled.
'testAPP.vshost.exe' (CLR v4.0.30319: testAPP.vshost.exe): Loaded 'D:\Visual Studio Projects\testTKS_vs2015\testAPP\bin\x64\Debug\dll\testDLL10.dll'. Symbols loaded.
'testAPP.vshost.exe' (CLR v4.0.30319: testAPP.vshost.exe): Loaded 'D:\Visual Studio Projects\testTKS_vs2015\testAPP\bin\x64\Debug\mil10-x64\Mil.net\Matrox.MatroxImagingLibrary.dll'. Skipped loading symbols. Module is optimized and the debugger option 'Just My Code' is enabled.

YEAH!! :-)

The only issue that's left is a compiler warning:

1>------ Build started: Project: testAPP, Configuration: Debug x64 ------
1>  No way to resolve conflict between "Matrox.MatroxImagingLibrary, Version=10.30.595.0, Culture=neutral, PublicKeyToken=5a83d419d44a9d98" and "Matrox.MatroxImagingLibrary, Version=9.2.1109.1, Culture=neutral, PublicKeyToken=5a83d419d44a9d98". Choosing "Matrox.MatroxImagingLibrary, Version=10.30.595.0, Culture=neutral, PublicKeyToken=5a83d419d44a9d98" arbitrarily.
1>  Consider app.config remapping of assembly "Matrox.MatroxImagingLibrary, Culture=neutral, PublicKeyToken=5a83d419d44a9d98" from Version "9.2.1109.1" [] to Version "10.30.595.0" [] to solve conflict and get rid of warning.
1>C:\Program Files (x86)\MSBuild\14.0\bin\Microsoft.Common.CurrentVersion.targets(1820,5): warning MSB3276: Found conflicts between different versions of the same dependent assembly. Please set the "AutoGenerateBindingRedirects" property to true in the project file. For more information, see http://go.microsoft.com/fwlink/?LinkId=294190.
1>  testAPP -> D:\Visual Studio Projects\testTKS_vs2015\testAPP\bin\x64\Debug\testAPP.exe
========== Build: 1 succeeded, 0 failed, 2 up-to-date, 0 skipped ==========

Maybe I can disable it somehow.


How can I get a list of referenced DLL paths during build to feed into my PowerShell script that looks for dll subfolders and copies files?

Most likely I can't.

I am going to write a small C# programm that will do the job: It will take the name of the project file, read it and search all project references and file based references. Then it will look for a dll subfolder in the folders of these referenced projects and files, and copy the content to the local dll subfolder.


Final notes:

  • During my tests I successfully referenced my weak assemblies using <codebase>:

The assembly does not have a publicKeyToken, so I just left it away:

  <!-- probing privatePath="dll" /-->
  <dependentAssembly>
    <assemblyIdentity name="testDLL9" culture="neutral" />
    <codeBase version="1.0.0.0" href="dll\testDLL9.dll" />
  </dependentAssembly>
  <dependentAssembly>
    <assemblyIdentity name="testDLL10" culture="neutral" />
    <codeBase version="1.0.0.0" href="dll\testDLL10.dll" />
  </dependentAssembly>
Tobias Knauss
  • 3,361
  • 1
  • 21
  • 45
  • Thanks man! I was still struggeling with this issue (I'm the author from your linked question), and managed to solve it with the code you provided! – Roger Far May 07 '19 at 17:16