10

I'm experimenting with loading an assembly using just byte arrays, but I can't figure out how to get it to work properly. Here is the setup:

public static void Main() 
{
    PermissionSet permissions = new PermissionSet(PermissionState.None);
    AppDomainSetup setup = new AppDomainSetup { ApplicationBase = Environment.CurrentDirectory };
    AppDomain friendlyDomain = AppDomain.CreateDomain("Friendly", null, setup, permissions);

    Byte[] primary = File.ReadAllBytes("Primary.dll_");
    Byte[] dependency = File.ReadAllBytes("Dependency.dll_");

    // Crashes here saying it can't find the file.
    friendlyDomain.Load(dependency);

    AppDomain.Unload(friendlyDomain);

    Console.WriteLine("Stand successful");
    Console.ReadLine();
}

I created two mock dlls, and renamed their extension to '.dll_' intentionally so the system wouldn't be able to find the physical files. Both primary and dependency fill correctly, but when I try to call the AppDomain.Load method with the binary data, it comes back with:

Could not load file or assembly 'Dependency, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null' or one of its dependencies. The system cannot find the file specified.

Why would it be searching the system for a file?

UPDATE

This on the other hand seems to work:

public class Program {
    public static void Main() {
        PermissionSet permissions = new PermissionSet(PermissionState.Unrestricted);
        AppDomainSetup setup = new AppDomainSetup { ApplicationBase = Environment.CurrentDirectory };
        AppDomain friendlyDomain = AppDomain.CreateDomain("Friendly", null, setup, permissions);

        Byte[] primary = File.ReadAllBytes("Primary.dll_");
        Byte[] dependency = File.ReadAllBytes("Dependency.dll_");

        // Crashes here saying it can't find the file.
        // friendlyDomain.Load(primary);

        Stage stage = (Stage)friendlyDomain.CreateInstanceAndUnwrap(typeof(Stage).Assembly.FullName, typeof(Stage).FullName);
        stage.LoadAssembly(dependency);

        Console.WriteLine("Stand successful");
        Console.ReadLine();
    }

}

public class Stage : MarshalByRefObject {
    public void LoadAssembly(Byte[] data) {
        Assembly.Load(data);
    }
}

So it appears there is a difference between AppDomain.Load and Assembly.Load.

sircodesalot
  • 11,231
  • 8
  • 50
  • 83
  • Does the Dependency DLL have any dependencies that haven't been copied, perhaps? – Chris Mantle Jul 24 '13 at 14:01
  • Primary relies on Dependency. Dependency has no (non-CLR) dependencies though. Seems like the runtime shouldn't be searching for the file to begin with though. – sircodesalot Jul 24 '13 at 14:13

4 Answers4

11

This is normal, the CLR doesn't consider the "dependency" you loaded to be a suitable assembly when it searches for the assembly that "primary" needs. A problem associated with "loading context", there isn't one for assemblies loaded like this. This is intentional, the CLR cannot ensure that DLL Hell won't be an issue since it has no idea where the assembly came from. Since you opened the door to DLL Hell, you also have to avoid hell yourself.

You'll need to implement the AppDomain.AssemblyResolve event. It will fire when the CLR cannot find "dependency", you can return the assembly you get from Assembly.Load(byte[]). You will however have to do so consistently when it fires more than once for the same assembly, in other words return the exact same Assembly, or you'll have more problems induced by .NET type identity. Producing hard to understand casting exceptions, "can't cast Foo to Foo" style.

There are other problems, it is rather inefficient. The virtual memory for the assembly cannot be backed by a file on disk so it is backed by the paging file. Which increases the commit size for your process.

It is certainly better to not do this.

Hans Passant
  • 922,412
  • 146
  • 1,693
  • 2,536
  • Why does `Assembly.Load` seem to allow this, while `AppDomain.Load` does not? Also, good point on the commit size. – sircodesalot Jul 24 '13 at 14:36
  • 1
    It doesn't have anything to do with Assembly.Load. When you use AssemblyResolve, the CLR say "I need this" and you say "here it is". So the CLR knows what it got. Your original approach was "Here is something" and the CLR says "no idea what that is". – Hans Passant Jul 24 '13 at 14:43
  • I follow what you're saying, but just by switching methods it seems to work. I never had to use `AssemblyResolve` because the CLR accepted it when it came from `Assembly.Load`. – sircodesalot Jul 24 '13 at 14:48
  • Noting your advice about the paging file, I decided to create a temporary folder/copy of the assemblies on disk, and then use `friendlyDomain.Load` (passing in the full assembly name) and have it load from disk itself. Thanks for the suggestion. – sircodesalot Jul 25 '13 at 15:04
  • Actually I've discovered that assembly from memory (byte[]) is loaded into isolated AppDomain (add handler for isolatedDomain.AssemblyLoad). But Manifest fails to load due missing file. If I offer same file name but different assembly file, I got manifest mismatch error. I guess MS implementation doesn't handle case of memory assembly very well. Only working scenario I know is to write assembly byte[] to same filename and then call load from byte[]. But this is ridiculous the point of loading from memory is to load from memory. – SoLaR Jan 24 '21 at 10:16
5

There is no difference between these two methods (you can check the official source code if you want).

In the MSDN page for AppDomain.Load Method (Byte[]) it is remarked that this method is loading the assembly in the current application domain:

This method should be used only to load an assembly into the current application domain. This method is provided as a convenience for interoperability callers who cannot call the static Assembly.Load method. To load assemblies into other application domains, use a method such as CreateInstanceAndUnwrap.

the line:

friendlyDomain.Load(dependency);

behaves exactly the same with:

Assembly.Load(dependency);

The reason it works in your updated sample code, is because the Stage object is actually calling Assembly.Load inside the child AppDomain.

Note: This answer complements the answers by Hans Passant and colinsmith.

Panos Rontogiannis
  • 4,154
  • 1
  • 24
  • 29
  • I ultimately decided to go with saving the assemblies to a temporary folder on disk and then set the `AppDomain` base to that folder. I might be crazy, but I swear it seems like `AppDomain.Load` will still check the disk for the physical presence of an assembly, whereas `Assembly.Load` does not. In fact, in my current implementation, I simply pass the full name of the assembly into `friendlyDomain.Load` and it retrieves it from the `ApplicationBase` (as expected). But why it wouldn't take the byte array without verification is beyond me. – sircodesalot Jul 25 '13 at 15:02
  • This is spot on. I checked out the source. – Jonathan Mitchell Oct 15 '18 at 12:08
0

If you use FusionLogViewer you can see more details about the particular problem the CLR is having in loading an assembly .... it can show you which locations it's trying to probe to give you a clue, etc.

You could also handle the AssemblyLoad / AssemblyResolve / ResourceResolve events on your AppDomain in your code, to trace through the sequence.

This is a handy example that uses a custom MSBuild step to embed any of your project dependent assemblies as Resources into your EXE program, and then use AssemblyResolve to load them from a ResourceStream (doing Assembly.Load() on the byte[] array).

Colin Smith
  • 12,375
  • 4
  • 39
  • 47
  • `It can show you which locations it's trying to probe`. That's just it, it shouldn't be probing it seems like. I'm already specifying the binary, so there is no reason for it to go out and load it yet again from the file system. – sircodesalot Jul 24 '13 at 14:28
  • `Primary` depends on `Dependency`, though. Loading `Dependency` shouldn't cause any problems, and in fact I found out above that using `Assembly.Load` seems to work fine, while `AppDomain.Load` does not. – sircodesalot Jul 24 '13 at 14:53
0

So I was doing a bunch of research I came a across a few answers on stackoverflow and the solution I put together is as follows:

using System;
using System.Windows.Forms;
using System.Reflection;

public partial class MainForm : Form
{
    private AppDomain = AppDomain.CreateDomain("asmDomain");

    public MainForm()
    {
        InitializeComponent();
    }

    /// <summary>
    /// Loads a Byte array as raw assmebly then loads and creates defined object from 
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    [LoaderOptimizationAttribute(LoaderOptimization.MultiDomain)]
    private void loadAsmObject(object sender, TileItemEventArgs e)
    {
        Byte[] rawAssembly = getFileAsm(); // Load the bytes however you wish.

        try
        {
            AppDomain.Unload(appDomain);

            appDomain = AppDomain.CreateDomain("asmDomain");

            AppDomainBridge isolationDomainLoadContext = (AppDomainBridge)appDomain.CreateInstanceAndUnwrap(Assembly.GetExecutingAssembly().FullName, typeof (AppDomainBridge).ToString());

            // Form is MarshalByRefObject type for the current AppDomain
            MyObject obj = isolationDomainLoadContext.ExecuteFromAssembly(rawAssembly, "MyNamespace.MyObject"/*, new object[] { "Arg1", "Arg2" } Optional args*/) as MyObject;

            obj.callMethod();
        }

        catch (Exception Ex)
        {
            MessageBox.Show("Failed to load Object!", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
        }
    }


    /// <summary>
    /// Acts as a shared app domain so we can use AppDomain.CurrentDomain.Load without errors.
    /// </summary>
    private class AppDomainBridge : MarshalByRefObject
    {
        public Object ExecuteFromAssembly(Byte[] rawAsm, string typeName, params object[] args)
        {
            Assembly assembly = AppDomain.CurrentDomain.Load(rawAssembly: rawAsm);

            return Activator.CreateInstance(assembly.GetType(typeName), args);
        }
    }
}
DharmanBot
  • 1,066
  • 2
  • 6
  • 10
Trent
  • 66
  • 4