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 theLoad
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?
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
:
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With