Based on this Microsoft example, how can i Unload a loaded assembly by implementing it in the AssemblyLoadContext class? (Considering they are in a global List<Assembly>.
I have tried some ways, this example too but nothing seems to really release the dll and let me "delete it" or "overwrite it" with a new one without close the main app.
My goal would be to: load a dll (1.0) => execute the code => unload the dll => replace the *.dll file with a new version (v1.1) => load again the dll => execute the update code.
The actually loading code is this (in .Net 6):
static IEnumerable<ICommand> Plugs = Enumerable.Empty<ICommand>();
static readonly List<Assembly> PluginAssemblies = new();
static readonly List<string> PluginPath = new();
PluginPath.Add("C:\\...\\Plugin1.dll");
PluginPath.Add("C:\\...\\Plugin2.dll");
PluginPath.Add("C:\\...\\Plugin3.dll");
PluginPath.Add("C:\\...\\Plugin4.dll");
PluginPath.ForEach(P => { PluginAssemblies.Add(LoadPlugin(P)); });
Plugs = Plugs.Concat(PluginAssemblies.SelectMany(A => CreateCommands(A)));
And these are the functions from the example:
static Assembly LoadPlugin(string relativePath)
{
// Navigate up to the solution root
string root = Path.GetFullPath(Path.Combine(
Path.GetDirectoryName(
Path.GetDirectoryName(
Path.GetDirectoryName(
Path.GetDirectoryName(
Path.GetDirectoryName(typeof(Program).Assembly.Location)))))));
string pluginLocation = Path.GetFullPath(Path.Combine(root, relativePath.Replace('\\', Path.DirectorySeparatorChar)));
Console.WriteLine($"Loading commands from: {pluginLocation}");
PluginLoadContext loadContext = new PluginLoadContext(pluginLocation);
return loadContext.LoadFromAssemblyName(new AssemblyName(Path.GetFileNameWithoutExtension(pluginLocation)));
}
static IEnumerable<ICommand> CreateCommands(Assembly assembly)
{
int count = 0;
foreach (Type type in assembly.GetTypes())
{
if (typeof(ICommand).IsAssignableFrom(type))
{
ICommand result = Activator.CreateInstance(type) as ICommand;
if (result != null)
{
count++;
yield return result;
}
}
}
if (count == 0)
{
string availableTypes = string.Join(",", assembly.GetTypes().Select(t => t.FullName));
throw new ApplicationException(
$"Can't find any type which implements ICommand in {assembly} from {assembly.Location}.\n" +
$"Available types: {availableTypes}");
}
}
using System;
using System.Reflection;
using System.Runtime.Loader;
namespace AppWithPlugin
{
class PluginLoadContext : AssemblyLoadContext
{
private AssemblyDependencyResolver _resolver;
public PluginLoadContext(string pluginPath)
{
_resolver = new AssemblyDependencyResolver(pluginPath);
}
protected override Assembly Load(AssemblyName assemblyName)
{
string assemblyPath = _resolver.ResolveAssemblyToPath(assemblyName);
if (assemblyPath != null)
{
return LoadFromAssemblyPath(assemblyPath);
}
return null;
}
protected override IntPtr LoadUnmanagedDll(string unmanagedDllName)
{
string libraryPath = _resolver.ResolveUnmanagedDllToPath(unmanagedDllName);
if (libraryPath != null)
{
return LoadUnmanagedDllFromPath(libraryPath);
}
return IntPtr.Zero;
}
}
}
According to Microsoft, here, How to use and debug assembly unloadability in .NET
calling the AssemblyLoadContext.Unload method just initiates the
unloading
there are some conditions you need to meet to complete.
No threads have methods from the assemblies loaded into the AssemblyLoadContext on their call stacks.
None of the types from the assemblies loaded into the AssemblyLoadContext, instances of those types, and the assemblies themselves are referenced by:
References outside of the AssemblyLoadContext, except for weak references (WeakReference or WeakReference).
Strong garbage collector (GC) handles (GCHandleType.Normal or GCHandleType.Pinned) from both inside and outside of the AssemblyLoadContext.
In short, you need to be sure there are no references in memory to anything in the assemblies you loaded and wait for the GC to do its work.
It seems you're trying to load the assembly, keep the code and unload the assembly from memory keeping the 'command' assigned. That can not be done.
Besides, it doesn't seem you're removing the reference to the assembly from the PluginAssemblies list. You need to remove the 'command' from the plugs array too (or use weak references).
I can't see anywhere in your code where you're unloading the assembly. Instead of returning the Assembly object from the LoadPlugin method you can return your PluginLoadContext, so you can call it's Unload method, later when you're sure there are no references left to the assembly or inner classes (clear all assembly or command instances).
I have consulted code on the website http://codeproject.com with the title "Loading Assemblies from Anywhere into a New AppDomain" of Marius Bancila, but I tested the error as in the attached picture, currently, I don't know resolve, hope you help, thank you.
Link Code
https://www.codeproject.com/Articles/453778/Loading-Assemblies-from-Anywhere-into-a-New-AppDom#_articleTop
Test
public class Program
{
[STAThread]
public static void Main()
{
var project = #"D:\Github\BeyConsPlugin\BeyConsProject\bin\x64\Debug\BeyConsRevitProject.dll";//Path to assembly
var manager = new AssemblyReflectionManager();
var success = manager.LoadAssembly(project, Guid.NewGuid().ToString());
if (success)
{
var result = manager.Reflect(project, (a) =>
{
return a.GetTypes();
});
Console.WriteLine(string.Join("\n", result.Select(x => x.Name)));
}
Console.ReadKey();
manager.UnloadAssembly(project);
}
}
Error
Assembly Resolver not Set
Marius's code has a bug in AssemblyReflectionProxy with regards that the Assembly Resolver is not set if you call LoadAssembly unlike Reflect<> which does.
Depending on how a child app domain is created, when loading assemblies it might only have access to the folder as specified during creation. If you need to assembly probe elsewhere for the assembly or its dependencies you need a Assembly Resolver. When .NET is looking for an assembly for a domain, it will call your handler as specified in the assemblie's ReflectionOnlyAssemblyResolve event. If not specified or if the resolver fails to locate the assembly, it will bubble up and throw a load fail exception.
I suggest you change the code from:
public class AssemblyReflectionProxy : MarshalByRefObject
{
private string _assemblyPath;
public void LoadAssembly(String assemblyPath)
{
try
{
_assemblyPath = assemblyPath;
Assembly.ReflectionOnlyLoadFrom(assemblyPath);
}
catch (FileNotFoundException)
{
// Continue loading assemblies even if an assembly
// cannot be loaded in the new AppDomain.
}
}
...to:
public class AssemblyReflectionProxy : MarshalByRefObject
{
private string _assemblyPath;
public void LoadAssembly(String assemblyPath)
{
try
{
AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve // <---- add me
+= OnReflectionOnlyResolve;
_assemblyPath = assemblyPath;
Assembly.ReflectionOnlyLoadFrom(assemblyPath);
}
catch (FileNotFoundException)
{
// Continue loading assemblies even if an assembly
// cannot be loaded in the new AppDomain.
}
}
You can see this in Sacha's original code that on which Marius based his.
Add Provision for Resolve Paths
The other problem with the code is that both assume that when loading one assembly, any dependent assemblies are in the same folder something that may not always be the case.
Alter AssemblyReflectionProxy to include a list of paths in which to probe:
public List<string> ResolvePaths { get; set; }
Then modify OnReflectionOnlyResolve to resemble the following:
private Assembly OnReflectionOnlyResolve(ResolveEventArgs args, DirectoryInfo directory)
{
Assembly loadedAssembly =
AppDomain.CurrentDomain.ReflectionOnlyGetAssemblies()
.FirstOrDefault(
asm => string.Equals(asm.FullName, args.Name,
StringComparison.OrdinalIgnoreCase));
if (loadedAssembly != null)
{
return loadedAssembly;
}
foreach (var tryFolder in ResolvePaths)
{
var asmName = args.Name.Split(',');
var assemblyPath = Path.Combine(tryFolder, asmName[0] + ".dll");
if (!File.Exists(assemblyPath))
return null;
return Assembly.ReflectionOnlyLoadFrom(assemblyPath);
}
}
What's in a name?
Both articles neglected to point out the fine print when using ReflectionOnlyLoad. Though Sacha did at least mention his code was for a "code generator" I can't help but wonder that both articles with their "Loading Assemblies....into a New AppDomain" are perhaps somewhat subject to interpretation.
The aim of ReflectionOnlyLoad is just that - for reflection. If you load an assembly via this method you cannot execute any code in it. Additionally and somewhat surprising at first to most assembly reflector programmers including me is that calling GetCustomAttributes will also fail (because it attempts to "instantiate" types in the assembly).
If you are writng your own plug-in system in which each plug-in has its own App Domain, the Assembly reflection and load methods are useful for different stages of the plug-in system loading pipeline:
first pass - use ReflectionOnlyLoad as a way to inspect the plug-in to see if it is valid; perhaps you want to run some security checks safe in the knowledge that none of the plug-ins code can run during this phase
second pass - after verifying the plug-in, you can safely Load/LoadFrom the assembly and execute the code
Hi I have simple client to write value to UA server. I am using OpcLabs libraries in VS2017, Win10 NetFr 4.8. Libraries are loaded from other folder than executable. When dlls are in the same folder both methods below works, when resolver should be triggered = dlls are not in folder - it is not.
The problem is simple. Resolver:
private static Assembly OpcAssRes(object sender, ResolveEventArgs args)
{
var ProgramFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles);
string OpcAsmPath = Path.Combine(ProgramFiles, "OPC Labs QuickOPC 2020.2", "Assemblies", "net47");
var ReqAss = new AssemblyName(args.Name).Name;
foreach (string FAssName in Directory.GetFiles(OpcAsmPath, "*.dll"))
{
if (Path.GetFileNameWithoutExtension(FAssName) == ReqAss)
{
var DLL = Assembly.LoadFrom(FAssName);
return DLL;
}
}
return null;
}
And when I want to instantiate the client using the method from libraries:
Works:
static void Main(string[] args)
{
AppDomain.CurrentDomain.AssemblyResolve += OpcAssRes;
ClCon();
}
static void ClCon()
{
var cl = new EasyUAClient();
}
Does NOT work - Exception is immediately raised, that the library cannot be found. I tried even sleep it before resolver, but it doesnt - instant Exception- like the resolver isnt triggered. When breakpoint is added in resolver, it does not get to it = it is not started.:
static void Main(string[] args)
{
AppDomain.CurrentDomain.AssemblyResolve += OpcAssRes;
var cl = new EasyUAClient();
}
Is it normal? Thanks for your advices.
Types used in a method are loaded before the method is called. If you bind your assembly resolver inside a method that uses a type in an external assembly, the loader won't be bound yet.
I read this article, but it doesn't help me.
My goal is to find the class that has custom attributes.
since I don't need any instance or use codes, is there a way to load DLL file and search what I want in code, without solving dependencies problem? to lookup codes.
if you going to get all app domain assemblies types you can avoid that issue by catching the ReflectionTypeLoadException:
public static class AssemblyExtension
{
public static IEnumerable<Type> GetLoadableTypes(this Assembly assembly)
{
if (assembly == null) throw new ArgumentNullException(nameof(assembly));
try
{
return assembly.GetTypes();
}
catch (ReflectionTypeLoadException e)
{
return e.Types.Where(t => t != null);
}
}
}
you can use it like so:
var types = (from domainAssembly in AppDomain.CurrentDomain.GetAssemblies().Where(x => !x.IsDynamic)
from assemblyType in domainAssembly.GetLoadableTypes()
select assemblyType);
if you targeting an assembly you want to load it without loading its dependencies use one of overloads:
as documented here: How to: Load Assemblies into the Reflection-Only Context
Assembly.ReflectionOnlyLoad(/*your appropriate parameter assembly name or something else*/);
A) compiling C# EXE's and DLL's on the fly are relatively easy.
B) Executing an EXE means that a new application is run. Loading a DLL means that methods and functions can be used in cases that may be shared between applications or projects.
Now, the quickest and easiest way to compile your EXE (or with mild modifications, DLL) can be found from the MSDN or for your convenience:
private bool CompileCSharpCode(string script)
{
lvErrors.Items.Clear();
try
{
CSharpCodeProvider provider = new CSharpCodeProvider();
// Build the parameters for source compilation.
CompilerParameters cp = new CompilerParameters
{
GenerateInMemory = false,
GenerateExecutable = false, // True = EXE, False = DLL
IncludeDebugInformation = true,
OutputAssembly = "eventHandler.dll", // Compilation name
};
// Add in our included libs.
cp.ReferencedAssemblies.Add("System.dll");
cp.ReferencedAssemblies.Add("System.Windows.Forms.dll");
cp.ReferencedAssemblies.Add("Microsoft.VisualBasic.dll");
// Invoke compilation. This works from a string, but you can also load from a file using FromFile()
CompilerResults cr = provider.CompileAssemblyFromSource(cp, script);
if (cr.Errors.Count > 0)
{
// Display compilation errors.
foreach (CompilerError ce in cr.Errors)
{
//I have a listview to display errors.
lvErrors.Items.Add(ce.ToString());
}
return false;
}
else
{
lvErrors.Items.Add("Compiled Successfully.");
}
provider.Dispose();
}
catch (Exception e)
{
// never really reached, but better safe than sorry?
lvErrors.Items.Add("SEVERE! "+e.Message + e.StackTrace.ToString());
return false;
}
return true;
}
Now that you can compile on the fly, there are a few variances between how to load the DLL. Typically speaking, you would add it as a reference in Visual Studios to be compiled into the project. This is rather easy and you have probably done it many times over, but we want to use it in our current project, and we can't very well require the user to recompile the entire project every time they want to test out their new DLL. Therefor, I will simply discuss how one loads a library 'on the fly'. Another term here would by "programmatically". To do this, after a successful compile, we load up an Assembly as follows:
Assembly assembly = Assembly.LoadFrom("yourfilenamehere.dll");
If you have an AppDomain, you can try this:
Assembly assembly = domain.Load(AssemblyName.GetAssemblyName("yourfilenamehere.dll"));
Now that the lib is "referenced", we can open it up and use it. There are 2 ways to do this. One requires you to know if the method has parameters, another will check for you. I'll do the later, you can check the MSDN for the other.
// replace with your namespace.class
Type type = assembly.GetType("company.project");
if (type != null)
{
// replace with your function's name
MethodInfo method = type.GetMethod("method");
if (method != null)
{
object result = null;
ParameterInfo[] parameters = method.GetParameters();
object classInstance = Activator.CreateInstance(type, null);
if (parameters.Length == 0) // takes no parameters
{
// method A:
result = method.Invoke(classInstance, null);
// method B:
//result = type.InvokeMember("method", BindingFlags.InvokeMethod, null, classInstance, null);
}
else // takes 1+ parameters
{
object[] parametersArray = new object[] { }; // add parameters here
// method A:
result = method.Invoke(classInstance, parametersArray);
// method B:
//result = type.InvokeMember("method", BindingFlags.InvokeMethod, null, classInstance, parametersArray);
}
}
}
PROBLEM:
First compile works fine. First execution works fine. However, the recompile attempt will error, saying that your *.PDP (debugger database) is in use. I've heard some hints about marshaling, and AppDomains, but I haven't quite cleared up the problem. Recompile will only fail after the DLL has been loaded.
Current attempt at Marshaling && AppDomain:
class ProxyDomain : MarshalByRefObject
{
private object _instance;
public object Instance
{
get { return _instance; }
}
private AppDomain _domain;
public AppDomain Domain
{
get
{
return _domain;
}
}
public void CreateDomain(string friendlyName, System.Security.Policy.Evidence securityinfo)
{
_domain = AppDomain.CreateDomain(friendlyName, securityinfo);
}
public void UnloadDomain()
{
try
{
AppDomain.Unload(_domain);
}
catch (ArgumentNullException dne)
{
// ignore null exceptions
return;
}
}
private Assembly _assembly;
public Assembly Assembly
{
get
{
return _assembly;
}
}
private byte[] loadFile(string filename)
{
FileStream fs = new FileStream(filename, FileMode.Open);
byte[] buffer = new byte[(int)fs.Length];
fs.Read(buffer, 0, buffer.Length);
fs.Close();
return buffer;
}
public void LoadAssembly(string path, string typeName)
{
try
{
if (_domain == null)
throw new ArgumentNullException("_domain does not exist.");
byte[] Assembly_data = loadFile(path);
byte[] Symbol_data = loadFile(path.Replace(".dll", ".pdb"));
_assembly = _domain.Load(Assembly_data, Symbol_data);
//_assembly = _domain.Load(AssemblyName.GetAssemblyName(path));
_type = _assembly.GetType(typeName);
}
catch (Exception ex)
{
throw new InvalidOperationException(ex.ToString());
}
}
private Type _type;
public Type Type
{
get
{
return _type;
}
}
public void CreateInstanceAndUnwrap(string typeName)
{
_instance = _domain.CreateInstanceAndUnwrap(_assembly.FullName, typeName);
}
}
Errors on _instance = _domain.CreateInstanceAndUnwrap(_assembly.FullName, typeName); saying that my Assembly isn't serializable. Tried adding [Serializable] tag to my class with no luck. Still researching fixes.
Seems things can get a bit confusing when you can't see how they're being used, so here's making it easy?
private void pictureBox1_Click(object sender, EventArgs e)
{
pd.UnloadDomain();
if (CompileCSharpCode(header + tScript.Text + footer))
{
try
{
pd.CreateDomain("DLLDomain", null);
pd.LoadAssembly("eventHandler.dll", "Events.eventHandler");
pd.CreateInstanceAndUnwrap("Events.eventHandler"); // Assembly not Serializable error!
/*if (pd.type != null)
{
MethodInfo onConnect = pd.type.GetMethod("onConnect");
if (onConnect != null)
{
object result = null;
ParameterInfo[] parameters = onConnect.GetParameters();
object classInstance = Activator.CreateInstance(pd.type, null);
if (parameters.Length == 0)
{
result = pd.type.InvokeMember("onConnect", BindingFlags.InvokeMethod, null, classInstance, null);
//result = onConnect.Invoke(classInstance, null);
}
else
{
object[] parametersArray = new object[] { };
//result = onConnect.Invoke(classInstance, parametersArray);
//result = type.InvokeMember("onConnect", BindingFlags.InvokeMethod, null, classInstance, parametersArray);
}
}
}*/
//assembly = Assembly.LoadFrom(null);
}
catch (Exception er)
{
MessageBox.Show("There was an error executing the script.\n>" + er.Message + "\n - " + er.StackTrace.ToString());
}
finally
{
}
}
}
Once you have loaded a DLL into (the default appdomain of) a running process, the file on disk cannot be overwritten until the process is terminated. DLLs cannot be unloaded in managed code like they can be in unmanaged code.
You need to create a new appdomain in your host process and load the newly created DLL assembly into that appdomain. When you are ready to compile a new version of the DLL, you can dispose of the appdomain. This will unload the DLL from memory and release the lock on the DLL file, so that you can compile a new DLL to that same file. You can then construct a new appdomain to load the new DLL into.
The main hazard of using appdomains is that all calls across the appdomain boundary must be marshalled, much like an IPC or network RPC. Try to keep the interface of the objects you need to call across the appdomain boundary to a minimum.
You can also compile the assembly to memory, receiving a byte array or stream as the output, then load that assembly into the separate appdomain. This avoids creating debris on disk that will need to be deleted eventually.
Do not use compile to memory as a workaround for the file lock issue. The core issue is that assemblies cannot be removed from memory when they are loaded into the default appdomain of the process. You MUST create a new appdomain and load the DLL into that appdomain if you want to unload that assembly from memory later in the lifetime of the process.
Here's a rough outline of how to construct an object in the context of another appdomain:
var appdomain = AppDomain.CreateDomain("scratch");
byte[] assemblyBytes = // bytes of the compiled assembly
var assembly = appdomain.Load(assemblyBytes);
object obj = appdomain.CreateInstanceAndUnwrap(assembly.FullName, "mynamespace.myclass");
After this sequence, obj will contain a reference to a proxy that links to the actual object instance inside the appdomain. You can invoke methods on obj using reflection or typecast obj to a common interface type and call methods directly. Be prepared to make adjustments to support RPC marshalling of the method call parameters. (see .NET remoting)
When working with multiple appdomains, you have to be careful how you access types and assemblies because a lot of .NET functions default to operating in the current appdomain of the caller, which is usually not what you want when you have multiple appdomains. compilerResult.CompiledAssembly, for example, internally performs a Load of the generated assembly in the caller's appdomain. What you want is to load the assembly into your other appdomain. You have to do that explicitly.
Update:
In your recently added code snippet showing how you load your appdomain, this line is your problem:
_assembly = Assembly.LoadFrom(path);
That loads the DLL into the current appdomain (the caller's appdomain), not into the target appdomain (referenced by _domain in your example). You need to do use _domain.Load() to load the assembly into that appdomain.
if you have no need to debug, or don't mind to debug the "dynamic" code, with some missing information.
you can generate the code in memory.. this will allow you to compile the code several times.. but will not generate a .pdb
cp.GenerateInMemory = true;
in alternative if you have no need to able to locate the assembly on disk you can ask the compiler to dump all the code in the temp directory and generate a temp name for the dll (wich will always be unique)
cp.TempFiles = new TempFileCollection(Path.GetTempPath(), false);
//cp.OutputAssembly = "eventHandler.dll";
in both this cases to access the dll and it's types you can get it from the compiler results
Assembly assembly = cr.CompiledAssembly;
not explicitly loading is necessary
but if non of this situations apply and you must and a physical .dll with a .pdp in a known folder.. the only advice i can give you it to put a version number on the dll..
and in case you don't have a simple way to control the amount of times the dll was compiled you can always resort to a timestamp..
cp.OutputAssembly = "eventHandler"+DateTime.Now.ToString("yyyyMMddHHmmssfff")+".dll";
of course you must realize that every time you compile a new .dll will be loaded into memory and wont be unloaded unless you use separate app domains.. but that goes out of scope for this question..