7

I have an in-memory assembly MyAssembly (class library) that is used in my main assembly MyApp.exe:

byte[] assemblyData = GetAssemblyDataFromSomewhere();

(For testing, the GetAssemblyDataFromSomewhere method can just do File.ReadAllBytes for an existing assembly file, but in my real app there is no file.)

MyAssembly has only .NET Framework references and has no dependencies to any other user code.

I can load this assembly into the current (default) AppDomain:

Assembly.Load(assemblyData);

// this works
var obj = Activator.CreateInstance("MyAssembly", "MyNamespace.MyType").Unwrap();

Now, I want to load this assembly into a different AppDomain and instantiate the class there. MyNamespace.MyType is derived from MarshalByRefObject, so I can share the instance across the app domains.

var newAppDomain = AppDomain.CreateDomain("DifferentAppDomain");

// this doesn't really work...
newAppDomain.Load(assemblyData);

// ...because this throws a FileNotFoundException
var obj = newAppDomain.CreateInstanceAndUnwrap("MyAssembly", "MyNamespace.MyType");

Yes, I know there is a note in the AppDomain.Load docs:

This method should be used only to load an assembly into the current application domain.

Yes, it should be used for that, but...

If the current AppDomain object represents application domain A, and the Load method is called from application domain B, the assembly is loaded into both application domains.

I can live with that. There's no problem for me if the assembly will be loaded into both app domains (because I actually load it into the default app domain anyway).

I can see that assembly loaded into the new app domain. Kind of.

var assemblies = newAppDomain.GetAssemblies().Select(a => a.GetName().Name);
Console.WriteLine(string.Join("\r\n", assemblies));

This gives me:

mscorlib
MyAssembly

But trying to instantiate the class always leads to a FileNotFoundException, because the CLR tries to load the assembly from file (despite it is already loaded, at least according to AppDomain.GetAssemblies).

I could do this in MyApp.exe:

newAppDomain.AssemblyResolve += CustomResolver;

private static Assembly CustomResolver(object sender, ResolveEventArgs e)
{
    byte[] assemblyData = GetAssemblyDataFromSomewhere();
    return Assembly.Load(assemblyData);
}

This works, but this causes the second app domain to load the calling assembly (MyApp.exe) from file. It happens because that app domain now needs the code (the CustomResolver method) form the calling assembly.

I could move the app domain creation logic and the event handler into a different assembly, e.g. MyAppServices.dll, so the new app domain will load that assembly instead of MyApp.exe.

However, I want to avoid the file system access to my app's directory at any cost: the new app domain must not load any user assemblies from files.

I also tried AppDomain.DefineDynamicAssembly, but that did't work either, because the return value's type System.Reflection.Emit.AssemblyBuilder is neither MarshalByRefObject nor marked with [Serializable].

Is there any way to load an assembly from byte array into a non-default AppDomain without loading the calling assembly from file into that app domain? Actually, without any file system access to my app's directory?

dymanoid
  • 14,771
  • 4
  • 36
  • 64
  • "_This works, but this causes the second app domain to load the calling assembly (MyApp.exe) from file. It happens because the app domain needs the code (the types) form that assembly_" ... "_I want to avoid this at any cost: the new app domain must not load any user assemblies from files._" Well, at some point your 2nd app domain needs to have MyApp.exe loaded in some way or other, no? You said so yourself, because some assembly code you loaded into the 2nd app domain seems to need types from MyApp.exe. I mean, what do you expect it to do if it needs types from some other assembly...? –  May 07 '19 at 13:40
  • @elgonzo, the `MyApp.exe` file is huge because of lots of embedded resources. It is first loaded at startup, yes, but I need to avoid any further loads. `MyAssembly` doesn't reference `MyApp.exe` and has no other references to my assemblies. It only happens when I hook up the `AssemblyResolve` event - so I can't do that. – dymanoid May 07 '19 at 13:46
  • Do i understand you correctly that the AssemblyResolve handler is a method (anonymous or not) declared in your MyApp.exe, thus "pulling" MyApp.exe into the 2nd app domain? –  May 07 '19 at 13:49
  • @elgonzo, correct. I slightly edited my question to make that more clear. – dymanoid May 07 '19 at 13:53
  • I guess you need then "lift" that handler out of your MyApp.exe. Either make it part of your assembly returned by _GetAssemblyDataFromSomewhere_ (if feasible), or just create (yet) another assembly (dynamically in-memory or as a separate DLL, depending on your requirements). There are several ways to trigger the event subscription of the AssemblyResolve handler in the 2nd app domain. Either set it up explicitly from the 1st app domain, or utilize a (static?) method or similar of a type in the 2nd app domain which will be invoked from the 1st app domain through reflection... –  May 07 '19 at 14:00
  • There is not much to show as an answer here, except that your AssemblyResolve handler, and perhaps the related event subscription, has to be declared in an assembly different from MyApp.exe. An assembly that is allowed to be loaded into the 2nd app domain and which does not contain references to the MyApp.exe assembly. If you are faced with the problem of the AssemblyResolve handler itself relying on other types in MyApp.exe, then this could in the worst case demand some refactoring... –  May 07 '19 at 14:09
  • It's most straightforward to execute all code in the new domain. Have a look at [this post](https://stackoverflow.com/questions/50127992/appdomain-assembly-not-found-when-loaded-from-byte-array) which uses a proxy. – Funk Jan 31 '20 at 17:19
  • Just out of curiosity, when you load you assembly from your misterious non-file source, what does it say in the assembly's `CodeBase` property? – Matheus Rocha Feb 06 '20 at 04:47
  • @MatheusRocha, that mysterious source is the (calling) assembly resource storage. The `CodeBase` returns a path to that (calling) assembly which holds the resources and loads the assemblies from the resources. The `Location` property is, however, empty for that loaded assembly (as expected). – dymanoid Feb 06 '20 at 10:50

2 Answers2

1

You first problem is the way you load the assembly into the second AppDomain.

You need some type loaded / shared between both AppDomains. You can't load assembly into the second AppDomain from the first AppDomain if the assembly is not already loaded into the first AppDomain (it also won't work if you load the assembly bytes into the first AppDomain uisng .Load(...)).
This should be a good starting point:
Lets say i have class library named Models with single class Person as follows:

namespace Models
{
    public class Person : MarshalByRefObject
    {
        public void SayHelloFromAppDomain()
        {
            Console.WriteLine($"Hello from {AppDomain.CurrentDomain.FriendlyName}");
        }
    }
}

and console application as follows (the Models class library is NOT references from the project)

namespace ConsoleApp
{
    internal class Program
    {
        [LoaderOptimizationAttribute(LoaderOptimization.MultiDomain)]
        public static void Main(String[] args)
        {
            CrossAppDomain();
        }

        private static Byte[] ReadAssemblyRaw()
        {
            // read Models class library raw bytes
        }

        private static void CrossAppDomain()
        {
            var bytes = ReadAssemblyRaw();
            var isolationDomain = AppDomain.CreateDomain("Isolation App Domain");

            var isolationDomainLoadContext = (AppDomainBridge)isolationDomain.CreateInstanceAndUnwrap(Assembly.GetExecutingAssembly().FullName, "ConsoleApp.AppDomainBridge");
            // person is MarshalByRefObject type for the current AppDomain
            var person = isolationDomainLoadContext.ExecuteFromAssembly(bytes);
        }
    }


    public class AppDomainBridge : MarshalByRefObject
    {
        public Object ExecuteFromAssembly(Byte[] raw)
        {
            var assembly = AppDomain.CurrentDomain.Load(rawAssembly: raw);
            dynamic person = assembly.CreateInstance("Models.Person");
            person.SayHelloFromAppDomain();
            return person;
        }
    }
}

The way it works is by creating instance of the AppDomainBridge from the ConsoleApp project which is loaded into both AppDomains. Now this instance is living into the second AppDomain. Then you can use the AppDomainBridge instance to actually load the assembly into the second AppDomain and skipping anything to do with the first AppDomain.
This is the output of the console when i execute the code (.NET Framework 4.7.2), so the Person instance is living in the second AppDomain:

enter image description here


Your second problem is sharing instances between AppDomains.

The main problem between AppDomains sharing the same code is the need to share the same JIT compiled code (method tables, type information ... etc).
From docs.microsoft:

JIT-compiled code cannot be shared for assemblies loaded into the load-from context, using the LoadFrom method of the Assembly class, or loaded from images using overloads of the Load method that specify byte arrays.

So you won't be able to fully share the type information when you load assmebly from bytes, which means your object at this point is just MarshalByRefObject for the first AppDomain. This means that you can execute and access methods / properties only from the MarshalByRefObject type (it does not matter if you try to use dynamic / reflection - the first AppDomain does not have the type information of the instance).

What you can do is not to return the object from ExecuteFromAssembly, but to extend the AppDomainBridge class to be simple wrapper around the created Person instance and use it to delegate any method execution from the first AppDomain to the second if you really need it for those purposes.

vasil oreshenski
  • 2,788
  • 1
  • 14
  • 21
  • @dymanoid My mistake. I was experimenting with different load options for the AppDomains and this code has left behind. I've made an edit - it should be CrossAppDomain(). – vasil oreshenski Feb 02 '20 at 20:44
  • Thanks for the answer. However, `AppDomain.CurrentDomain.GetAssemblies()` in the "Isolation App Domain" shows me that the `ConsoleApp.exe` assembly is loaded into that app domain from a file - and that is what I'm trying to avoid. – dymanoid Feb 02 '20 at 21:01
  • @dymanoid The JITed code is shared between the App Domains - so it actually executed only once and then shared. I mean it has no difference if you have single AppDomain or several at this point. – vasil oreshenski Feb 02 '20 at 21:04
  • @dymanoid I've added LoaderOptimization attribute annotation to the main method. This should ensure maximum resource sharing between AppDomains. I think this is the default behavior but i could be wrong. Being explicit is better. – vasil oreshenski Feb 02 '20 at 21:10
  • 1
    No, this doesn't seem to help. If I delete the `ConsoleApp.exe` right after startup, the `isolationDomain.CreateInstanceAndUnwrap` call throws `FileNotFoundException` for "ConsoleApp.exe". – dymanoid Feb 02 '20 at 21:30
  • @dymanoid Not sure why it still tries to load the assembly from the hard drive. As alternative you can define the AppDomainBridge class in different assembly from the executable and reference it. This should do the trick. :) In the morning i will try to investigate how exactly MultiDomain is working. – vasil oreshenski Feb 02 '20 at 21:51
  • @dymanoid I've attached event to the AssemblyLoad of the second AppDomain and the ConsoleApp.exe is never loaded into the second AppDomain, so the JIT code must be shared as expected. How did you even delete the ConsleApp.exe ? - it is the executing assembly it should be locked by the OS. Can you attach handler to the same event and confirm that the executing assembly is requested in the second AppDomain. Thanks. – vasil oreshenski Feb 06 '20 at 17:18
  • The exe is not locked. I "delete" it via `File.Move` in the `Main` method just before creating the second app domain. We can continue in a chat if you wish. – dymanoid Feb 06 '20 at 19:30
  • @dymanoid Yeah, chat is OK, but i am not sure how to start one. – vasil oreshenski Feb 06 '20 at 19:33
0

I'm not quite sure what are you trying to achieve, but I would try the following.

In general, your approach seems OK. You have to make sure your probing paths (especially, the appbase path) for the secondary appdomain are set correctly. Otherwise, .NET Fusion will probe these locations for dependencies, and you'll have those unwanted file system access attempts, you're trying to avoid. (Well, at least make sure that these paths are configured to some temp folders with no real permissions set up).

PROPOSED SOLUTION

In any case, you can try adding to your dynamic (is this how I should call it?) assembly an entry point (ex. Main method in some Bootstrap class), and try calling AppDomain.ExecuteAssemblyByName, after loading the assembly into the secondary AppDomain.

I would add to the Bootstrap class your CustomResolver method, and in the Main method, I would subscribe to AssemblyResolve.

This way, when the Main method is called (and hopefully it works as expected), the subscription to AppDomain's AssemblyResolve won't trigger fusion.

I didn't test this solution, and it could be a long shot, but worse trying.

P.S.: I do see that documentation on this method does state, that the runtime will try to load the assembly first (by probably using regular probing logic), but it doesn't say anything about situation in which the assembly was pre-loaded into the AppDomain before the call was made.

Remarks

The ExecuteAssemblyByName method provides similar functionality to the ExecuteAssembly method, but specifies the assembly by display name or AssemblyName rather than by file location. Therefore, ExecuteAssemblyByName loads assemblies with the Load method rather than with the LoadFile method.

The assembly begins executing at the entry point specified in the .NET Framework header.

This method does not create a new process or application domain, and it does not execute the entry point method on a new thread.

Load method documentation doesn't provide a clear answer either.

P.P.S: Calling the Unwrap method, may trigger the fusion in your main AppDomain, since a proxy is created for your class. I would think, that at this point your main AppDomain would try to locate that dynamically loaded assembly. Are you sure it's the secondary AppDomain that throws the exception?

Community
  • 1
  • 1
Aryéh Radlé
  • 1,310
  • 14
  • 31
  • 1
    There is no way to define an entry point for a class library which I'm trying to load. I edited my question to make this more obvious. – dymanoid Feb 02 '20 at 20:40