8

I am writing a simple plugin and stumbled upon contractType.IsAssignableFrom(pluginType) returning different results depending on the order of loading.

Calling IsAssignableFromon the Plugin returns True as expected.
But if I load the Contract assembly before loading the Plugin, IsAssignableFrom on the Plugin returns False.

I am running Win10 and dotnet4.7 but I doubt that has any relevancy.


Code

[TestMethod]
public void SimplyLoadingPlugin_Succeeds()
{
    var plugin = Assembly.LoadFrom(PluginPathFilename);
    var res = typeof(Contract).IsAssignableFrom(plugin.GetExportedTypes().Single());

    Assert.IsTrue(res); // Succeeds.
}

[TestMethod]
public void LoadingContractAndThenPlugin_Fails()
{
    var contract = Assembly.LoadFrom(ContractPathFilename);
    var plugin = Assembly.LoadFrom(PluginPathFilename);
    var res = typeof(Contract).IsAssignableFrom(plugin.GetExportedTypes().Single());

    Assert.IsTrue(res); // Fails.
}

To make it harder to test:
If i run the LoadingContractAndThenPlugin_Fails test by itself is fails. But If i run the tests together it is dependent on order. Running SimplyLoadingPlugin_Succeeds first and LoadingContractAndThenPlugin_Fails last, makes both tests green but running them in the reverse order makes both fail.
So somehow the very loading of Contract before Plugin messes up something for me.
I can se nothing related in the GAC.


Below are all files needed. The paths in the probably have to be updated.
4 project with one file in each. 1 solution.

Contract.cs (a library project)

public abstract class Contract
{
    public abstract int Version { get; set; }
}

Plugin.cs (a library project)

public class Plugin : Contract
{
    public override int Version { get; set; }
}

Tests.cs (a test project)

[TestClass]
public class Tests
{
    private const string PluginPath = @"C:\DATA\Projekt\LoadFromOrder\Plugin\bin\Debug";
    private string PluginPathFilename = Path.Combine(PluginPath, "Plugin.dll");
    private string ContractPathFilename = Path.Combine(PluginPath, "Contract.dll");

    [TestMethod]
    public void SimplyLoadingPlugin_Succeeds()
    {
        var plugin = Assembly.LoadFrom(PluginPathFilename);
        var res = typeof(Contract).IsAssignableFrom(plugin.GetExportedTypes().Single());

        Assert.IsTrue(res); // Succeeds.
    }

    [TestMethod]
    public void LoadingContractAndThenPlugin_Fails()
    {
        var contract = Assembly.LoadFrom(ContractPathFilename);
        var plugin = Assembly.LoadFrom(PluginPathFilename);
        var res = typeof(Contract).IsAssignableFrom(plugin.GetExportedTypes().Single());

        Assert.IsTrue(res); // Fails.
    }

    // BEGIN ---- Update. ----
    [TestMethod]
    public void LoadingPluginFromTestProject_Succeeds()
    {
        var contract = Assembly.LoadFrom(
            @"C:\DATA\Projekt\LoadFromOrder\TestProject\bin\Debug\Contract.dll");
        var plugin = Assembly.LoadFrom(PluginPathFilename);
        var res = typeof(Contract.Contract).IsAssignableFrom(plugin.GetExportedTypes().Single());

        Assert.IsTrue(res); // Succeeds.
    }
    // END ---- Update. ----

}

Program.cs (a console project)

class Program
{
    static void Main(string[] args)
    {
        var tests = new Tests();
        try
        {
            System.Console.WriteLine("Press A for Success and B for Fail.");
            switch (System.Console.ReadKey(true).Key)
            {
                case ConsoleKey.A:
                    tests.SimplyLoadingPlugin_Succeeds();
                    break;
                case ConsoleKey.B:
                    tests.LoadingContractAndThenPlugin_Fails();
                    break;
            }
            System.Console.WriteLine("SUCCESS");
        }
        catch (Exception exc)
        {
            System.Console.WriteLine($"FAIL: {exc.Message}");
        }
    }
}
Community
  • 1
  • 1
LosManos
  • 7,195
  • 6
  • 56
  • 107
  • 3
    Since both of your tests directly reference the `Contract` type, the assembly containing that type has to have been loaded in order for those methods to JIT *compile*. As such, I don't think you're controlling ordering as much as you think. – Damien_The_Unbeliever Aug 09 '17 at 10:35
  • Agree. [`Assembly.LoadFrom` vs `Assembly.LoadFile`](https://stackoverflow.com/questions/1477843/difference-between-loadfile-and-loadfrom-with-net-assemblies) **Control** is out of my hands. But somehow Loading Contract´ before Loading Plugin which in turn loads Contract´´ makes a difference. It is like I have loaded Contract´ into my assembly and then it stops Plugin from finding Contract´´. Did I just answer my own question? Still no remedy though. – LosManos Aug 09 '17 at 10:47
  • 2
    I tried re-creating your problem in a new solution, with 1 console application and 2 class library projects, but I was not able to get the behaviour that you're describing... Can you please try to create a [Minimal, Complete, and Verifiable example](https://stackoverflow.com/help/mcve) of your problem, so we can re-create the problem? – bassfader Aug 09 '17 at 12:06
  • Are you sure your testproject doesnt contain any project references that might cause a rebuild on one of the referenced dlls? – CSharpie Aug 10 '17 at 07:03
  • @bassfader Thank you Very much for investing your time in my problem. I have added the the class for the tests so that copy/paste and update of path constants should be enough to reproduce the problem. I did create a brand new solution on a rebooted system and reproduced the problem; running all tests they succeed. Running the `..._Fails` test it failed. It is that very text that is copied into my question. – LosManos Aug 10 '17 at 07:04
  • @CSharpie Good point. I am quite sure there is no recompiling issue as: I have 1 totally fresh solution with 3 files. When I run the tests in one order all are green and in the reversed order (MSTests seems to work from top to bottom) one test is red. VS cannot recompile in the middle of a test run because that would mess up all variables. – LosManos Aug 10 '17 at 07:10
  • @bassfader I did the brunt of the work for reducing the problem with a console project so test vs console is not the problem either. – LosManos Aug 10 '17 at 07:12
  • I updated with yet a test `LoadingPluginFromTestProject_Succeeds`. It loads `Contract` from the same project as the `Test`. That worked as expected: `Plugin` is `IsAssignableFrom` from `Contract`. So my educated guess is that when the test project loads `Plugin`, dotnet checks if it has loaded a `Contract` and if it has it, is used, otherwise it is loaded per `Plugin`'s wish. So when I try `IsAssignableFrom` everything works unless I have earlier forced dotnet to load `Contract` from elsewhere. What fools me is that the `Plugin` folder is "elsewhere". – LosManos Aug 22 '17 at 13:34

2 Answers2

10

By loading the Contract using Assembly.LoadFrom you are creating a reference ambiguity. That library is already loaded that is why we can do typeof(Contract) no need to load it again...

My recommendation: use reflection to determine what references you have and only load those that are not already there, here is a sample code snippet:

        var dllFiles = Directory.GetFiles(DIR, "*.DLL", SearchOption.AllDirectories);
        var plugins = new HashSet<Assembly>();

        var references = typeof(Program).Assembly.GetReferencedAssemblies();
        foreach (var dllPath in dllFiles)
        {
            string name = Path.GetFileNameWithoutExtension(dllPath);
            if (!references.Any(x => x.Name == name) && !plugins.Any(x => x.GetName().Name == name))
                plugins.Add(Assembly.LoadFrom(dllPath));
        }

On that sample we get every DLL for a given directory (subdirectories included) the DIR can be a relative path like ..\..\.. and only load those that are not already in the assembly references.


And here is the full solution with two plugin projects:
https://github.com/heldersepu/csharp-proj/tree/master/PluginSystem

Helder Sepulveda
  • 15,500
  • 4
  • 29
  • 56
  • And if you want to speed up the search (```Directory.GetFiles```) you should make all your plugins follow a common naming that way instead of ```*.DLL``` you can have something like ```Plugin*.DLL``` – Helder Sepulveda Aug 14 '17 at 19:05
  • The `Contract` is already loaded by the test program, otherwise it would not compile. It is then ok to load it again. But not a third time... – LosManos Aug 15 '17 at 06:38
  • Yes: The ```Contract``` is already loaded, I think my answer covered that... and it should not be OK to load anything twice, it is inefficient, and can cause reference ambiguities. – Helder Sepulveda Aug 15 '17 at 12:57
  • 2
    Interesting, I guess the Contract assembly in the test project is probably loaded from `Projekt\LoadFromOrder\SomeTestProject` and when you load the same dll from another path (i.e. `Projekt\LoadFromOrder\Plugin`) you create the ambiguity. Probably the problem would gone if your Contract dll is **always** referenced by the **same** path (i.e. `Projekt\LoadFromOrder\Plugin`) and withouth `local copying` at build. – John-Philip Aug 16 '17 at 19:29
  • @John-Philip That Dotnet reacts on where the assembly is loaded from sounds plausible. Unfortunately I cannot test it now as I got a hardware crash and hence suddenly got more urgent matters at hand. – LosManos Aug 21 '17 at 09:01
  • I cannot check this answer together with @John-Philip 's comment as answer as I presently don't have the possibility to test it properly. – LosManos Aug 21 '17 at 09:02
1

As @HelderSepu purported, loading the contract assembly a second time may be the problem.

I'd suggest that you test in a different but easier way. Instead of manually (re)loading the assemblies in the tests, just add references to the assemblies/projects in the test project, and directly reference typeof(Contract) and typeof(Plugin) and check if typeof(Contract).IsAssignableFrom(typeof(Plugin)). Nothing complicated, just add the references to the test project.

You do not need to test whether the assembly is loaded correctly, the CLR will handle that. You need to test whether the plugin assembly contains a Contract definition. Whatever use cases you might have for your architecture, it's not whether assembly loading works that you should be worried about; it is whether the plugin has been implemented correctly.

EvilTak
  • 7,091
  • 27
  • 36
  • The proposed solution cannot be used in my chosen architecture as referencing the plugin would defeat the very purpose of a plugin system. With that said; one might learn something so I tried referencing both `Contract` and `Plugin` from the test project but it didn't really change anything. I *guess* that the test project does not load `Plugin` just because it is referenced; and when it does load it, it is from the path it is asked to load from. – LosManos Aug 22 '17 at 13:10