Problem
CSharpCodeProvider can be used to compile source .cs files into an assembly.
However, the assembly is automatically loaded into the AppDomain.CurrentDomain by default. In my case, this is a problem because I need to be able to re-compile the assembly again during runtime, and since it's already loaded in the CurrentDomain, I can't unload that, so I'm stuck.
I have looked through the docs and there seems to be no way to set the target app domain. I have also tried searching it on Google and only found answers where Assembly.Load was used, which I don't think I can use because I need to compile from raw source code, not a .dll
How would one go about doing this? Are there any alternatives or workarounds?
Main program
using (var provider = new CSharpCodeProvider())
{
param.OutputAssembly = "myCompiledMod"
var classFileNames = new DirectoryInfo("C:/sourceCode").GetFiles("*.cs", SearchOption.AllDirectories).Select(fi => fi.FullName).ToArray();
CompilerResults result = provider.CompileAssemblyFromFile(param, classFileNames);
Assembly newAssembly = result.CompiledAssembly // The assembly is already in AppDomain.CurrentDomain!
// If you try compile again, you'll get an error; that class Test already exists
}
C:/sourceCode/test.cs
public class Test {}
What I tried already
I already tried creating a new AppDomain and loading it in there. What happens is the assembly ends up being loaded in both domains.
// <snip>compile code</snip>
Evidence ev = AppDomain.CurrentDomain.Evidence;
AppDomain domain = AppDomain.CreateDomain("NewDomain", ev);
domain.Load(newAssembly);
The answer was to use CSharpCodeProvider().CreateCompiler() instead of just CSharpCodeProvider, and to set param.GenerateInMemory to false. Now I'm able to see line numbers and no visible assembly .dll files are being created, and especially not being locked. This allows for keeping an assembly in memory and reloading it when needed.
Related
I've inherited some code that I think is a little odd, and want to make sure if it's a potential issue or not.
First, it gets the name of a loaded assembly based on a type it contains:
private readonly string _assemblyName = Assembly.GetAssembly(typeof(IEntityBase)).FullName;
Then, on each request to process a named entity, it loads the assembly by name, even though the assembly is already loaded:
var assembly = Assembly.Load(_assemblyName);
var type = assembly.GetType(entityFullName);
var instance = Activator.CreateInstance(type);
I'm wondering if the Load calls are potentially causing issues - in this case, since the assembly is already loaded, is the call idempotent and returns the already-loaded assembly, or does it create a new assembly each time?
The following experimental codes/projects are using netcore 2.0 and netstandard 2.0 in VS2017. Let's say I have two versions of a third party dll v1.0.0.0 and v2.0.0.0, which contains only one class Constants.cs.
//ThirdPartyDependency.dll v1.0.0.0
public class Constants
{
public static readonly string TestValue = "test value v1.0.0.0";
}
//ThirdPartyDependency.dll v2.0.0.0
public class Constants
{
public static readonly string TestValue = "test value v2.0.0.0";
}
Then I created my own solution named AssemblyLoadTest, which contains:
Wrapper.Abstraction: class library with no project references
namespace Wrapper.Abstraction
{
public interface IValueLoader
{
string GetValue();
}
public class ValueLoaderFactory
{
public static IValueLoader Create(string wrapperAssemblyPath)
{
var assembly = Assembly.LoadFrom(wrapperAssemblyPath);
return (IValueLoader)assembly.CreateInstance("Wrapper.Implementation.ValueLoader");
}
}
}
Wrapper.V1: class library with project reference Wrapper.Abstractions and dll reference ThirdPartyDependency v1.0.0.0
namespace Wrapper.Implementation
{
public class ValueLoader : IValueLoader
{
public string GetValue()
{
return Constants.TestValue;
}
}
}
Wrapper.V2: class library with project reference Wrapper.Abstractions and dll reference ThirdPartyDependency v2.0.0.0
namespace Wrapper.Implementation
{
public class ValueLoader : IValueLoader
{
public string GetValue()
{
return Constants.TestValue;
}
}
}
AssemblyLoadTest: console application with project reference Wrapper.Abstraction
class Program
{
static void Main(string[] args)
{
AppDomain.CurrentDomain.AssemblyResolve += (s, e) =>
{
Console.WriteLine($"AssemblyResolve: {e.Name}");
if (e.Name.StartsWith("ThirdPartyDependency, Version=1.0.0.0"))
{
return Assembly.LoadFrom(#"v1\ThirdPartyDependency.dll");
}
else if (e.Name.StartsWith("ThirdPartyDependency, Version=2.0.0.0"))
{
//return Assembly.LoadFrom(#"v2\ThirdPartyDependency.dll");//FlagA
return Assembly.LoadFile(#"C:\FULL-PATH-TO\v2\ThirdPartyDependency.dll");//FlagB
}
throw new Exception();
};
var v1 = ValueLoaderFactory.Create(#"v1\Wrapper.V1.dll");
var v2 = ValueLoaderFactory.Create(#"v2\Wrapper.V2.dll");
Console.WriteLine(v1.GetValue());
Console.WriteLine(v2.GetValue());
Console.Read();
}
}
STEPS
Build AssemblyLoadTest in DEBUG
Build Wrapper.V1 project in DEBUG, copy files in Wrapper.V1\bin\Debug\netstandard2.0\ to AssemblyLoadTest\bin\Debug\netcoreapp2.0\v1\
Build Wrapper.V2 project in DEBUG, copy files in Wrapper.V2\bin\Debug\netstandard2.0\ to AssemblyLoadTest\bin\Debug\netcoreapp2.0\v2\
Replace FULL-PATH-TO in AssemblyLoadTest.Program.Main with the correct absolute v2 path that you copied in step 3
Run AssemblyLoadTest - Test1
Comment FlagB line and uncomment FlagA line, run AssemblyLoadTest - Test2
Comment AppDomain.CurrentDomain.AssemblyResolve, run AssemblyLoadTest - Test3
My results and questions:
Test1 succeeds and prints v1.0.0.0 and v2.0.0.0 as expected
Test2 throws exception at v2.GetValue()
System.IO.FileLoadException: 'Could not load file or assembly
'ThirdPartyDependency, Version=2.0.0.0, Culture=neutral,
PublicKeyToken=null'. Could not find or load a specific file.
(Exception from HRESULT: 0x80131621)'
Question1: Why LoadFile with absolute path works as expected, while LoadFrom with relative path not working, meanwhile LoadFrom with relative path works for v1.0.0.0 in the first if statement?
Test3 fails with the same exception above at the same place, here my understanding is CLR locates the assemblies with the following priority rule:
Rule1: Check if AppDomain.AssemblyResolve is registered (highest priority)
Rule2: Otherwise check if the assembly is loaded.
Rule3: Otherwise search the assembly in folders(can be configured in probing and codeBase.
Here in Test3 where AssemblyResolve is not registered, v1.GetValue works because Rule1 and Rule2 is N/A, AssemblyLoadTest\bin\Debug\netcoreapp2.1\v1 is in Rule3 scan candidates. When executing v2.GetValue, Rule1 is still N/A, however Rule2 is applied here (if Rule3 is applied, why exceptions?)
Question2: Why the version is ignored even Wrapper.V2 reference ThirdPartyDependency.dll using
<Reference Include="ThirdPartyDependency, Version=2.0.0.0">
<HintPath>..\lib\ThirdPartyDependency\2.0.0.0\ThirdPartyDependency.dll</HintPath>
</Reference>
Great answer from Vitek Karas, original link here.
Kind of unfortunately all of the behavior you describe is currently as designed. That doesn't mean it's intuitive (which it's totally not). Let me try to explain.
Assembly binding happens based on AssemblyLoadContext (ALC). Each ALC can have only one version of any given assembly loaded (so only one assembly of a given simple name, ignoring versions, culture, keys and so on). You can create a new ALC which then can have any assemblies loaded again, with same or different versions. So ALCs provide binding isolation.
Your .exe and related assemblies are loaded into a Default ALC - one which is created at the start of the runtime.
Assembly.LoadFrom will try to load the specified file into the Default ALC - always. Let me stress the "try" word here. If the Default ALC already loaded assembly with the same name, and the already loaded assembly is equal or higher version, then the LoadFrom will succeed, but it will use the already loaded assembly (effectively ignoring the path you specified). If on the other hand the already loaded assembly is of a lower version then the one you're trying to load - this will fail (we can't load the same assembly for the second time into the same ALC).
Assembly.LoadFile will load the specified file into a new ALC - always creates a new ALC. So the load will effectively always succeed (there's no way this can collide with anything since it's in its own ALC).
So now to your scenarios:
Test1
This works because your ResolveAssembly event handler loads the two assemblies into separate ALCs (LoadFile will create a new one, so the first assembly goes to the default ALC, and the second one goes into its own).
Test2
This fails because LoadFrom tries to load the assembly into the Default ALC. The failure actually occurs in the AssemblyResolve handler when it calls the second LoadFrom. First time it loaded v1 into Default, the second time it tries to load v2 into Default - which fails because Default already has v1 loaded.
Test3
This fails the same way because it internally does basically exactly what Test2 does. Assembly.LoadFrom also registers event handler for AssemblyResolve and that makes sure that dependent assemblies can be loaded from the same folder. So in your case v1\Wrapper.V1.dll will resolve its dependency to v1\ThirdPartyDependency.dll because it's next to it on the disk. Then for v2 it will try to do the same, but v1 is already loaded, so it fails just like in Test2. Remember that LoadFrom loads everything into the Default ALC, so collisions are possible.
Your questions:
Question1
LoadFile works because it loads the assembly into its own ALC, which provides full isolation and thus there are never any conflicts. LoadFrom loads the assembly into the Default ALC, so if that already has assembly with the same name loaded, there might be conflicts.
Question2
The version is actually not ignored. The version is honored which is why Test2 and Test3 fail. But I might not understand this question correctly - it's not clear to me in which context you're asking it.
CLR binding order
The order of Rules you describe is different.
It's basically:
Rule2 - if it's already loaded - use it (including if higher version is already loaded, then use that)
Rule1 - if everything fails - as a last resort - call AppDomain.AssemblyResolve
Rule 3 actually doesn't exist. .NET Core doesn't have a notion of probing paths or code base. It sort of does for the assemblies which are statically referenced by the app, but for dynamically loaded assemblies no probing is performed (with the exception of LoadFrom loading dependent assemblies from the same folder as the parent as described above).
Solutions
To make this fully work, you would need to do either:
Use the LoadFile along with your AssemblyResolve handler. But the problem here is that if you LoadFile an assembly which itself has other dependencies, you will need to handle those in your handler as well (you lose the "nice" behavior of LoadFrom which loads dependencies from the same folder)
Implement your own ALC which handles all dependencies. This is technically the cleaner solution, but potentially more work. And it's similar in that regard that you still have to implement the loading from the same folder if needed.
We are actively working on making scenarios like this easy. Today they are doable, but pretty hard. The plan is to have something which solves this for .NET Core 3. We're also very aware of the lack of documentation/guidance in this area. And last but not least, we are working on improving the error messages, which are currently very confusing.
Apologies for the dodgy question - happy to rephrase if someone has a better suggestion.
I'm trying to create an object by dynamically invoking an assembly belonging to another application.
The following PowerShell code is working nicely for me:
[Reflection.Assembly]::LoadFrom("C:\Program Files\Vendor\Product\ProductAPI.dll")
$bobject = new-object ProductAPI.BasicObject
$bobject.AddName("Some Name")
I'm struggling to do the same thing in C#. Based on other posts on StackOverflow I currently have this:
System.Reflection.Assembly myDllAssembly =
System.Reflection.Assembly.LoadFile("C:\\Program Files\\Vendor\\Product\\ProductAPI.dll");
System.Type BasicObjectType = myDllAssembly.GetType("ProductAPI.BasicObject");
var basicObjectInstance = Activator.CreateInstance(BasicObjectType);
The final line results in a TargetInvocationException.
{"Could not load file or assembly 'AnotherObject, Version=1.2.345.0, Culture=neutral, PublicKeyToken=null' or one of its dependencies. The system cannot find the file specified."
It appears that the BasicObject constructor is trying to invoke AnotherObject (from AnotherObject.dll in the same folder) but can't find it.
Any tips on how to get around this?
If it can't find a dependent assembly in the usual places, you'll need to manually specify how to find them.
The two easiest ways I'm aware of for doing this:
manually load the dependent assemblies in advance with
Assembly.Load.
handle the AssemblyResolve event for the domain which is loading the
assembly with additional assembly dependencies.
Both essentially require you to know the dependencies for the assembly you're trying to load in advance but I don't think that's such a big ask.
If you go with the first option, it would also be worthwhile looking into the difference between a full Load and a reflection-only Load.
If you would rather go with 2 (which I'd recommend), you can try something like this which has the added benefit of working with nested dependency chains (eg MyLib.dll references LocalStorage.dll references Raven.Client.dll references NewtonSoft.Json.dll) and will additionally give you information about what dependencies it can't find:
AppDomain.CurrentDomain.AssemblyResolve += (sender,args) => {
// Change this to wherever the additional dependencies are located
var dllPath = #"C:\Program Files\Vendor\Product\lib";
var assemblyPath = Path.Combine(dllPath,args.Name.Split(',').First() + ".dll");
if(!File.Exists(assemblyPath))
throw new ReflectionTypeLoadException(new[] {args.GetType()},
new[] {new FileNotFoundException(assemblyPath) });
return Assembly.LoadFrom(assemblyPath);
};
This question already has answers here:
Closed 10 years ago.
Possible Duplicate:
Unloading the Assembly loaded with Assembly.LoadFrom()
I use custom AppDomain to load/unload assembly. But when assembly is unloaded I am able to see it under the AppDomain.CurrentDomain.
How it could be? Is this normal behavior or I am missing something?
Thank you for any clue!
string assemblyPath = #"C:\MyFile.dll";
var assemblyName = AssemblyName.GetAssemblyName(assemblyPath);
var ads = new AppDomainSetup
{
ApplicationBase = AppDomain.CurrentDomain.SetupInformation.ApplicationBase,
DisallowCodeDownload = true
};
AppDomain newDomainName = AppDomain.CreateDomain("newDomainName", null, ads);
try
{
Assembly testLibrary = newDomainName.Load(assemblyName);
var c1 = AppDomain.CurrentDomain.GetAssemblies();
var c2 = newDomainName.GetAssemblies();
}
finally
{
AppDomain.Unload(newDomainName);
var c3 = AppDomain.CurrentDomain.GetAssemblies();
// The assembly is still visible here!!!
}
You are calling the Load() method of an AppDomain, which according to the documentation: "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."
In other words, you're loading the assembly into the primary AppDomain because you're calling Load() from the primary AppDomain (even though you're using calling it on an instance of your secondary AppDomain), and this is why it is appearing even after you unload your secondary AppDomain.
As indicated in the extract from the documentation above, you probably want to use AppDomain.CreateInstanceAndUnwrap.
You can't remove a loaded assembly from an app domain.
http://blogs.msdn.com/b/jasonz/archive/2004/05/31/145105.aspx
http://msdn.microsoft.com/en-us/library/ms173101(v=vs.80).aspx
There is no way to unload an individual assembly without unloading all
of the application domains that contain it. Even if the assembly goes
out of scope, the actual assembly file will remain loaded until all
application domains that contain it are unloaded.
http://blogs.msdn.com/b/suzcook/archive/2003/07/08/unloading-an-assembly.aspx
There's no way to unload an individual assembly without unloading all
of the appdomains containing it.
At runtime, I'd like to be able to unload a DLL and reload a modified version of it. My first experiment went down in flames. Can anyone tell me why?
private static void Main()
{
const string fullPath = "C:\\Projects\\AppDomains\\distrib\\MyLibrary.dll";
// Starting out with a version of MyLibrary.dll which only has 1 method, named Foo()
AssemblyName assemblyName = AssemblyName.GetAssemblyName(fullPath);
AppDomain appDomain = AppDomain.CreateDomain("MyTemp");
appDomain.Load(assemblyName);
appDomain.DomainUnload += appDomain_DomainUnload;
AppDomain.Unload(appDomain);
// Breakpoint here; swap out different version of MyLibrary.dll which only has 1 method, named Goo()
AssemblyName assemblyName2 = AssemblyName.GetAssemblyName(fullPath);
AppDomain appDomain2 = AppDomain.CreateDomain("MyTemp2");
Assembly asm2 = appDomain2.Load(assemblyName2);
foreach (Type type in asm2.GetExportedTypes())
{
foreach (MemberInfo memberInfo in type.GetMembers())
{
string name = memberInfo.Name;
// Breakpoint here: Found Foo and but no Goo! I was expecting Goo and no Foo.
}
}
}
private static void appDomain_DomainUnload(object sender, EventArgs e)
{
// This gets called before the first breakpoint
}
Edit:
Okay, this is obviously my first time posting. Thanks Daniel for formatting my code (I now see the toolbar button to do that, and the preview pane, too!). I don't see a way to post a "comment" in reply to either those who asked for clarification to the original post or to the 1 answer, so I'll just post another "answer" to keep the conversation going. (Pointers appreciated on how comments are done would be appreciated).
Comments to the posting:
Mitch - Went down in flames 'cause my foreach loop should have been iterating over the types in the modified DLL, not the previously loaded/unloaded one.
Matthew - That might work, but I really need the case of the same file name to work.
Mike Two - Not strongly named.
Comments to the answer:
Blue & Mike Two - I'll noodle on your suggestions, but first I need to understand a critical aspect. I had read that you have to take care not to pull the assembly into the main app domain, and the code previously had a copy of the foreach loop before the unload. So, suspecting that accessing MethodInfos was sucking the assembly into the main app domain, I removed the loop. That's when I knew I needed to ask for help, 'cause the first DLL still wouldn't unload!
So my question is: What in the following code segment causes the main app domain to ever directly (or indirectly) access anything in the dll...why would it cause the assembly to also load in the main app domain:
appDomain.Load(assemblyName);
appDomain.DomainUnload += appDomain_DomainUnload;
AppDomain.Unload(appDomain);
Didn't give me much confidence I'd ever actually be able to use the DLL before unloading it.
Edit 2:
Mike Two - Thanks for your persistence...loading the assembly from within DoCallBack was the secret sauce. For anyone interested, here is some more info that might be useful down the road:
Sounds like no one could actually reproduce my conditions exactly. To demonstrate the original problem, I generated my dlls this way: 1. Added class library project to solution 2. Built version 1.0.0 with Foo; renamed resulting assembly as MyLibrary.dll.f. 3. Renamed Foo to Goo and built another version 1.0.0; renamed resulting assembly as MyLibrary.dll.g. 4. Removed project from solution. Before starting the run, I removed the .f and ran to breakpoint (first line after the unload). Then I tacked the .f back on and removed the .g from the other dll and ran to the next breakpoint. Windows didn't stop me from renaming. Note: Although it would be better practice, I didn't change the version number because I didn't want to assume my customers always would, since the default entry in AssemblyInfo isn't the wildcard version. It seemed like the uglier case to handle.
Also, I just now discovered something that would have clued me in sooner:
AssemblyName assemblyName = AssemblyName.GetAssemblyName(FullPath);
AppDomain appDomain = AppDomain.CreateDomain("MyTemp");
appDomain.Load(assemblyName);
Assembly[] tempAssemblies = appDomain.GetAssemblies();
// MyLibrary.dll has been loaded into the temp domain...good
Assembly[] mainAssemblies = AppDomain.CurrentDomain.GetAssemblies();
// MyLibrary.dll has been loaded into the main domain, too...bad!
So I'm not sure what the point of AppDomain.Load is, but it seems to have the bonus side effect of loading the assembly into the main app domain as well. Using this experiment, I could see Mike Two's solution cleanly loads it only into the temp domain:
AppDomain appDomain = AppDomain.CreateDomain("MyTemp");
appDomain.DoCallBack(CallBackDelegate); // Executes Assembly.LoadFrom
Assembly[] tempAssemblies = appDomain.GetAssemblies();
// MyLibrary.dll has been loaded into the temp domain...good
Assembly[] mainAssemblies = AppDomain.CurrentDomain.GetAssemblies();
// MyLibrary.dll has NOT been loaded into the main domain...great!
So, Mike Two, exactly how does this StackOverflow newbie mark your answer as "accepted"? Or can I not do that since I'm only a guest here?
Now I'm off to learn how to actually use MyLibrary without sucking the assembly into the main app domain. Thanks everyone for participating.
Edit 3:
BlueMonkMN - I went ahead and took out the event subscription and get the same result. The full program is now listed below:
const string fullPath = "C:\\Projects\\AppDomains\\distrib\\MyLibrary.dll";
// Starting out with a version of MyLibrary.dll which only has 1 method, named Foo()
AssemblyName assemblyName = AssemblyName.GetAssemblyName(fullPath);
AppDomain appDomain = AppDomain.CreateDomain("MyTemp");
appDomain.Load(assemblyName);
AppDomain.Unload(appDomain);
// Breakpoint here; swap out different version of MyLibrary.dll which only has 1 method, named Goo()
AssemblyName assemblyName2 = AssemblyName.GetAssemblyName(fullPath);
AppDomain appDomain2 = AppDomain.CreateDomain("MyTemp2");
Assembly asm2 = appDomain2.Load(assemblyName2);
foreach (Type type in asm2.GetExportedTypes())
{
foreach (MemberInfo memberInfo in type.GetMembers())
{
string name = memberInfo.Name;
// Breakpoint here: Found Foo and but no Goo! I was expecting Goo and no Foo.
}
}
Seems like there should be no way the assembly is getting pulled into the main app domain between these 2 lines:
appDomain.Load(assemblyName);
AppDomain.Unload(appDomain);
I tried to replicate this. In the dll to load (MyLibrary.dll) I built two versions. The first had one class with one method named Foo and had a version number of 1.0.0.0. The second had the same class but the method had been renamed Bar (I'm a traditionalist) and a version number of 2.0.0.0.
I put a breakpoint after the unload call. Then I tried to copy the second version on top of the first version. I assume that is what you are doing because the path never changes. Windows would not let me copy version 2 over version 1. The dll was locked.
I changed the code to load the dll using code executed inside the AppDomain by using DoCallback. That worked. I could swap the dll's and find the new method. Here is the code.
class Program
{
static void Main(string[] args)
{
AppDomain appDomain = AppDomain.CreateDomain("MyTemp");
appDomain.DoCallBack(loadAssembly);
appDomain.DomainUnload += appDomain_DomainUnload;
AppDomain.Unload(appDomain);
AppDomain appDomain2 = AppDomain.CreateDomain("MyTemp2");
appDomain2.DoCallBack(loadAssembly);
}
private static void loadAssembly()
{
string fullPath = "LoadMe1.dll";
var assembly = Assembly.LoadFrom(fullPath);
foreach (Type type in assembly.GetExportedTypes())
{
foreach (MemberInfo memberInfo in type.GetMembers())
{
string name = memberInfo.Name;
Console.Out.WriteLine("name = {0}", name);
}
}
}
private static void appDomain_DomainUnload(object sender, EventArgs e)
{
Console.Out.WriteLine("unloaded");
}
}
I did not strong name the assemblies. If you do you are likely to find the first one cached. You can tell by running gacutil /ldl (List download cache) from the command line. If you do find it cached run gacutil /cdl to clear the download cache.
I have found that if you ever directly access types in an assembly it gets loaded into your own domain. So what I have had to do is create a third assembly that implements interfaces common to both assemblies. That assembly gets loaded into both domains. Then just be careful to only use interfaces from that third assembly when interacting with the external assembly. That should allow you to unload the second assembly by unloading its domain.