Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Question about Assembly loading & Resolving #62391

Closed
jogibear9988 opened this issue Dec 4, 2021 · 25 comments
Closed

Question about Assembly loading & Resolving #62391

jogibear9988 opened this issue Dec 4, 2021 · 25 comments
Labels
area-AssemblyLoader-coreclr question Answer questions and provide assistance, not an issue with source code or documentation.

Comments

@jogibear9988
Copy link

Hello,

I've a question about best practice to do some Assembly Loading.

I've a complex software wich uses many different Assemblies. I want be able to replace some of the DLLs at runtime, but I don't want to use a own Assembly load Context. I thought I could hijack Assembly loading via AppDomain.Resolve, but it is only called when the Assemblies are not directly found.
Is there a Callback wich is use before a Assemblie is loaded, even when it is found in the File System? Cause I want to load them in a way so the File is not locked.

@dotnet-issue-labeler dotnet-issue-labeler bot added area-AssemblyLoader-coreclr untriaged New issue has not been triaged by the area owner labels Dec 4, 2021
@ghost
Copy link

ghost commented Dec 4, 2021

Tagging subscribers to this area: @vitek-karas, @agocke, @VSadov
See info in area-owners.md if you want to be subscribed.

Issue Details

Hello,

I've a question about best practice to do some Assembly Loading.

I've a complex software wich uses many different Assemblies. I want be able to replace some of the DLLs at runtime, but I don't want to use a own Assembly load Context. I thought I could hijack Assembly loading via AppDomain.Resolve, but it is only called when the Assemblies are not directly found.
Is there a Callback wich is use before a Assemblie is loaded, even when it is found in the File System? Cause I want to load them in a way so the File is not locked.

Author: jogibear9988
Assignees: -
Labels:

area-AssemblyLoader-coreclr, untriaged

Milestone: -

@teo-tsirpanis
Copy link
Contributor

teo-tsirpanis commented Dec 4, 2021

Hello, I persume you avoid using AssemblyLoadContexts because you want your project to work in both modern .NET and .NET Framework?

Unfortunately there is no such solution that works in both frameworks. If you want to control first-chance assembly loading on modern .NET, you have to create an AssemblyLoadContext and override its Load method, (as far as I know at least) there is no other way.

I want to load them in a way so the File is not locked

I use this on my code and it works:

using var file = File.OpenRead("MyAssembly.dll");
alc.LoadFromStream(file);

AssemblyLoadContext.LoadFromStream reads the entire stream into memory even if it gets a FileStream; only LoadFromAssemblyPath does obtain a lock on the file.

@jogibear9988
Copy link
Author

@teo-tsirpanis no, our software is completely migrated to netcore.
My problem is, some Assemblys reference other and so on. I can only explicit load Assemblys from a byte array. Referenced Assemblies are load automaticly when found on file system.

I now solved it, via moving the whole application and all assemblies in a subdirectly and create a loader program. So all assemblies are not found and so the AssemblyLoadContext.Resolve is called.

@jogibear9988
Copy link
Author

jogibear9988 commented Dec 5, 2021

But I thought maybe this would be easier, so I don't need an extra loader, I only wanted to change how the default AssemblyLoadContext loads the Assemblies from FileSystem (it should use a ByteArray), so it does not lock the files.

@AaronRobinsonMSFT
Copy link
Member

@jogibear9988 There are a lot of options in the assembly load arena. I'd recommend reading the following: https://docs.microsoft.com/dotnet/core/dependency-loading/overview. It discusses all the potential points of access and even the algorithm.

@jogibear9988
Copy link
Author

@AaronRobinsonMSFT
Is there also a way to tell DotNet the Path to the PDB File, if I load the Assembly from a byte-array?

@jogibear9988
Copy link
Author

Sorry.... Found it. Assembly.Load has 2 Byte Arrays as parameter...

@jogibear9988
Copy link
Author

jogibear9988 commented Dec 6, 2021

I've created this class, maybe it is usefull for someone...
I've moved all Assemblys to a subdirectory ("lib") and load all assemblys via the Loader...

using System;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Runtime.Loader;

namespace MCC.AssemblyLoader
{
	public static class AssemblyLoader
	{
		static AssemblyLoader()
		{
			Initialize();
		}

		public static readonly ConcurrentDictionary<string, Assembly> AssemblysCache = new ConcurrentDictionary<string, Assembly>();
		public static readonly ConcurrentDictionary<Assembly, string> AssemblysPathCache = new ConcurrentDictionary<Assembly, string>();

		public static string ApplicationDirectory { get; private set; }

		public static string ApplicationLibDirectory { get; private set; }

		public static string DotNetRuntimeDirectory { get; private set; }

		public static string AspNetRuntimeDirectory { get; private set; }

		private static void Initialize()
		{
			ApplicationDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location));
			if (!ApplicationDirectory.EndsWith(Path.DirectorySeparatorChar.ToString()))
				ApplicationDirectory += Path.DirectorySeparatorChar;

			ApplicationLibDirectory = Path.Combine(ApplicationDirectory, "lib");
			var a = typeof(int).Assembly;
			var f = new FileInfo(a.Location);
			var fi = FileVersionInfo.GetVersionInfo(a.Location);

			DotNetRuntimeDirectory = Path.GetDirectoryName(typeof(int).Assembly.Location);

			var dotNetRuntimeDirectoryWithoutVersion = Path.GetDirectoryName(DotNetRuntimeDirectory);
			var version = DotNetRuntimeDirectory.Substring(dotNetRuntimeDirectoryWithoutVersion.Length + 1);

			AspNetRuntimeDirectory = Path.Combine(Path.GetDirectoryName(dotNetRuntimeDirectoryWithoutVersion), "Microsoft.AspNetCore.App", version);

			AssemblyLoadContext.Default.Resolving += Default_Resolving;
			//AssemblyLoadContext.Default.ResolvingUnmanagedDll += Default_ResolvingUnmanagedDll;
		}

		//private static IntPtr Default_ResolvingUnmanagedDll(Assembly arg1, string arg2)
		//{
		//    return IntPtr.Zero;
		//}

		private static Assembly Default_Resolving(AssemblyLoadContext arg, AssemblyName args)
		{
			try
			{
				var name = args.Name.Split(',')[0];
				var dll_name = name + ".dll";
				var file = Path.Combine(ApplicationDirectory, "lib", dll_name);
				var pdbFile = Path.Combine(ApplicationDirectory, "lib", name + ".pdb");
				if (AssemblysCache.TryGetValue(file.ToLower(), out var assembly))
					return assembly;
				if (File.Exists(file))
				{
					lock (AssemblysCache)
					{
						if (!AssemblysCache.TryGetValue(file.ToLower(), out assembly))
						{
							var bytes = File.ReadAllBytes(file);
							if (File.Exists(pdbFile))
							{
								var pdbBytes = File.ReadAllBytes(pdbFile);
								assembly = Assembly.Load(bytes, pdbBytes);
							}
							else
								assembly = Assembly.Load(bytes);
							AssemblysCache.TryAdd(file.ToLower(), assembly);
							AssemblysPathCache.TryAdd(assembly, file);
							return assembly;
						}
						else
						{
							return assembly;
						}
					}
				}

				if (File.Exists(Path.Combine(DotNetRuntimeDirectory, dll_name)))
					return Assembly.LoadFrom(Path.Combine(DotNetRuntimeDirectory, dll_name));

				if (File.Exists(Path.Combine(AspNetRuntimeDirectory, dll_name)))
					return Assembly.LoadFrom(Path.Combine(AspNetRuntimeDirectory, dll_name));
			}
			catch (Exception ex)
			{
				Console.WriteLine(ex);
			}
			return null;
		}

		public static Assembly GetOrLoadAssembly(string dll, bool reload = false)
		{
			Assembly assembly;
			if (!reload)
			{
				if (AssemblysCache.TryGetValue(dll.ToLower(), out assembly))
					return assembly;
			}

			lock (AssemblysCache)
			{
				if (reload || !AssemblysCache.TryGetValue(dll.ToLower(), out assembly))
				{
					var bytes = File.ReadAllBytes(dll);
					assembly = Assembly.Load(bytes);
					if (assembly != null)
					{
						AssemblysCache[dll.ToLower()] = assembly;
						AssemblysPathCache.TryAdd(assembly, dll);
					}
				}
				return assembly;
			}
		}

		public static string GetAssemblyPath(Assembly assembly)
		{
			if (!string.IsNullOrEmpty(assembly.Location))
				return assembly.Location;
			AssemblysPathCache.TryGetValue(assembly, out var path);
			return path;

		}
	}
}

@vitek-karas
Copy link
Member

Couple of notes on this:

  • The reason there's no way to intercept assembly loading in the default load context is to avoid hard to diagnose issues. If it were possible, it would be really easy to break the app with seemingly simple change. And it would be hard to debug why. This is mainly due to issues with loading framework itself. For the system to work, the framework assemblies must be consistent, custom loading algorithm could easily change this and introduce subtle behavioral differences. (For example, such a hook would be used to resolve things like System.Console, System.IO and similar, what if there was an exception generated from the hook itself, and in order to throw the exception the system would have to call the hook again... either stack overflow, or other really bad behavior).
  • Force loading everything from memory comes with obvious performance cost. I just want to make sure you're aware of this. It's not only the fact that the entire assembly must be read from disk (otherwise it's memory mapped and only used parts are loaded), but lot of framework assemblies contain R2R code (AOT native code to improve startup), currently loading these from memory may force an additional memory copy due to requirements on the layout of the native code in memory.

@jogibear9988
Copy link
Author

Couple of notes on this:

  • The reason there's no way to intercept assembly loading in the default load context is to avoid hard to diagnose issues. If it were possible, it would be really easy to break the app with seemingly simple change. And it would be hard to debug why. This is mainly due to issues with loading framework itself. For the system to work, the framework assemblies must be consistent, custom loading algorithm could easily change this and introduce subtle behavioral differences. (For example, such a hook would be used to resolve things like System.Console, System.IO and similar, what if there was an exception generated from the hook itself, and in order to throw the exception the system would have to call the hook again... either stack overflow, or other really bad behavior).
  • Force loading everything from memory comes with obvious performance cost. I just want to make sure you're aware of this. It's not only the fact that the entire assembly must be read from disk (otherwise it's memory mapped and only used parts are loaded), but lot of framework assemblies contain R2R code (AOT native code to improve startup), currently loading these from memory may force an additional memory copy due to requirements on the layout of the native code in memory.

But maybe you have seen, I load the Framework assemblies direct from the disk:

		if (File.Exists(Path.Combine(DotNetRuntimeDirectory, dll_name)))
				return Assembly.LoadFrom(Path.Combine(DotNetRuntimeDirectory, dll_name));

			if (File.Exists(Path.Combine(AspNetRuntimeDirectory, dll_name)))
				return Assembly.LoadFrom(Path.Combine(AspNetRuntimeDirectory, dll_name));

so the issue should not be present?

@vitek-karas
Copy link
Member

Sorry - I missed that part - yes, in that case the performance should not take a big hit.

@jogibear9988
Copy link
Author

I know find, that my loading approach is barely to simple.

see issue: dotnet/SqlClient#1424

there is also a runtimes folder, and some dll's needed to be loaded from this directory (the ones in the app dir are only stubs?).
but how can i find out where in the runtimes folder to search at first?

I thought maybe AssemblyDependencyResolver would help, but I did not think so.

I also found this: #1050 (comment) does it mean the complete resolving strategy is only in unmanged code?

@teo-tsirpanis
Copy link
Contributor

Let's take a step back, why do you need to load all assemblies from memory?

@jogibear9988
Copy link
Author

I don't need to load all from memory, but I need to load some of the assemblies from memory.

But I thought it would be a good idea, to move the whole program into a subdirectory so I can implement my own assembly loader. But now I see that the whole assembly loading Process in netcore is much complexer.

Don't know what the best solution now is.
So maybe I should switch back to the normal assembly loading, and move only my DLLs wich I need to be replaceable, to a directory so the native loader does not find them?

The best would be if the native assembly loader would have a callback wich the DLL it found, and I could load the Assembly how I like.

@teo-tsirpanis
Copy link
Contributor

Which are these "some" assemblies and why do you want to load them from memory?

@jogibear9988
Copy link
Author

This are some assemblies from our application. I want to load the from memory so I could replace them later in the file system and load new Versions of them. I don't want to create an assembly load context, I only want to load the new ones into memory, and then use them. This all works, with my loader, but now SqlServer access didn't work any more (I tested with sqlite), cause I loaded the sqlserver dll from the application dir and not the on from the "runtimes" directory.

@jogibear9988
Copy link
Author

This assemblyloader now works...
But I think I will switch to only move the DLL's I need to reload to a subdirectory

using System;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Runtime.Loader;

namespace MCC.AssemblyLoader
{
	public static class AssemblyLoader
	{
		static AssemblyLoader()
		{
			Initialize();
		}

		public static readonly ConcurrentDictionary<string, Assembly> AssemblysCache = new ConcurrentDictionary<string, Assembly>();
		public static readonly ConcurrentDictionary<Assembly, string> AssemblysPathCache = new ConcurrentDictionary<Assembly, string>();

		public static string ApplicationDirectory { get; private set; }

		public static string ApplicationLibDirectory { get; private set; }

		public static string DotNetRuntimeDirectory { get; private set; }

		public static string AspNetRuntimeDirectory { get; private set; }

		public static string PlattformDir { get; private set; }

		public static string[] PlattformSearchDirs { get; private set; }

		public static string PlattformNativeDir { get; private set; }

	   

		private static void Initialize()
		{
			ApplicationDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
			if (!ApplicationDirectory.EndsWith(Path.DirectorySeparatorChar.ToString()))
				ApplicationDirectory += Path.DirectorySeparatorChar;

			ApplicationLibDirectory = Path.Combine(ApplicationDirectory, "lib");
			var a = typeof(int).Assembly;
			var f = new FileInfo(a.Location);
			var fi = FileVersionInfo.GetVersionInfo(a.Location);

			DotNetRuntimeDirectory = Path.GetDirectoryName(typeof(int).Assembly.Location);

			var dotNetRuntimeDirectoryWithoutVersion = Path.GetDirectoryName(DotNetRuntimeDirectory);
			var version = DotNetRuntimeDirectory.Substring(dotNetRuntimeDirectoryWithoutVersion.Length + 1);
			var v = Version.Parse(version);
			var shortVersion = v.Major + "." + v.Minor;

			AspNetRuntimeDirectory = Path.Combine(Path.GetDirectoryName(dotNetRuntimeDirectoryWithoutVersion), "Microsoft.AspNetCore.App", version);

			AssemblyLoadContext.Default.Resolving += Default_Resolving;

			PlattformDir = "win";
			if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
				PlattformDir = "linux";
			else if (RuntimeInformation.IsOSPlatform(OSPlatform.FreeBSD))
				PlattformDir = "freebsd";
			else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
				PlattformDir = "osx";

			PlattformNativeDir = PlattformDir + "-" + RuntimeInformation.OSArchitecture.ToString().ToLower();

			PlattformSearchDirs = new[] { "net6.0", "net5.0", "netcoreapp3.1", "netcoreapp2.1", "netcoreapp2.0", "netstandard2.0" };
			//AssemblyLoadContext.Default.ResolvingUnmanagedDll += Default_ResolvingUnmanagedDll;
		}

		//private static IntPtr Default_ResolvingUnmanagedDll(Assembly arg1, string arg2)
		//{
		//    return IntPtr.Zero;
		//}

		private static Assembly Default_Resolving(AssemblyLoadContext arg, AssemblyName args)
		{
			try
			{
				var name = args.Name.Split(',')[0];
				if (AssemblysCache.TryGetValue(name.ToLower(), out var assembly))
					return assembly;

				var dll_name = name + ".dll";
				string file = null;
				string pdbFile = null;
				for (int i = 0; i < PlattformSearchDirs.Length; i++)
				{
					string sd = PlattformSearchDirs[i];
					file = Path.Combine(ApplicationDirectory, "lib", "runtimes", PlattformDir, "lib", sd, dll_name);
					if (File.Exists(file))
					{
						pdbFile = Path.Combine(ApplicationDirectory, "lib", "runtimes", PlattformDir, "lib", sd, name + ".pdb");
						break;
					}
					file = null;
				}
				if (file == null)
				{
					file = Path.Combine(ApplicationDirectory, "lib", dll_name);
					pdbFile = Path.Combine(ApplicationDirectory, "lib", name + ".pdb");
				}
				
				if (File.Exists(file))
				{
					lock (AssemblysCache)
					{
						if (!AssemblysCache.TryGetValue(name.ToLower(), out assembly))
						{
							var bytes = File.ReadAllBytes(file);
							if (File.Exists(pdbFile))
							{
								var pdbBytes = File.ReadAllBytes(pdbFile);
								assembly = Assembly.Load(bytes, pdbBytes);
							}
							else
								assembly = Assembly.Load(bytes);
							AssemblysCache.TryAdd(name.ToLower(), assembly);
							AssemblysPathCache.TryAdd(assembly, file);
							return assembly;
						}
						else
						{
							return assembly;
						}
					}
				}

				file = Path.Combine(DotNetRuntimeDirectory, dll_name);
				if (File.Exists(file))
				{
					assembly = Assembly.LoadFrom(file);
					AssemblysCache.TryAdd(name.ToLower(), assembly);
					AssemblysPathCache.TryAdd(assembly, file);
					return assembly;
				}

				file = Path.Combine(AspNetRuntimeDirectory, dll_name);
				if (File.Exists(file))
				{
					assembly = Assembly.LoadFrom(file);
					AssemblysCache.TryAdd(name.ToLower(), assembly);
					AssemblysPathCache.TryAdd(assembly, file);
					return assembly;
				}
			}
			catch (Exception ex)
			{
				Console.WriteLine(ex);
			}

			Console.WriteLine("Could not load Assembly: " + args.Name + " - this does not need to be an error.");
			return null;
		}

		public static Assembly GetOrLoadAssembly(string dll, bool reload = false)
		{
			Assembly assembly;
			if (!reload)
			{
				if (AssemblysCache.TryGetValue(dll.ToLower(), out assembly))
					return assembly;
			}

			lock (AssemblysCache)
			{
				if (reload || !AssemblysCache.TryGetValue(dll.ToLower(), out assembly))
				{
					var bytes = File.ReadAllBytes(dll);
					assembly = Assembly.Load(bytes);
					if (assembly != null)
					{
						AssemblysCache[dll.ToLower()] = assembly;
						AssemblysPathCache.TryAdd(assembly, dll);
					}
				}
				return assembly;
			}
		}

		public static string GetAssemblyPath(Assembly assembly)
		{
			if (!string.IsNullOrEmpty(assembly.Location))
				return assembly.Location;
			AssemblysPathCache.TryGetValue(assembly, out var path);
			return path;

		}
	}
}

@teo-tsirpanis
Copy link
Contributor

I could replace them later in the file system and load new Versions of them

To load these new versions you have to restart the process. Dynamically loading and unloading assemblies can be done only via an AssemblyLoadContext.

If you want to implement something like an auto-update, I would suggest to place each version to a separate folder, close the old version and start the new, without replacing files.

Why are you avoiding using ALCs anyway? You said earlier that your codebase is fully migrated to modern .NET, and from personal experience I can tell you that working with them is a delight.

@jogibear9988
Copy link
Author

jogibear9988 commented Dec 7, 2021

I thought it would work... but it does not.
It now finds the correct managed DLL, but the unmanged could not be loaded:
image

but also the resolver for unmanged dlls is not called:

        AssemblyLoadContext.Default.ResolvingUnmanagedDll += Default_ResolvingUnmanagedDll;
    }

    private static IntPtr Default_ResolvingUnmanagedDll(Assembly arg1, string arg2)
    {
        return IntPtr.Zero;
    }

if an breakpoint in this function, but it is not raised.

@jogibear9988
Copy link
Author

jogibear9988 commented Dec 7, 2021

I could replace them later in the file system and load new Versions of them

To load these new versions you have to restart the process. Dynamically loading and unloading assemblies can be done only via an AssemblyLoadContext.

no, I can load multiple versions of an assembly. I don't need to unload. This will be cleaned when the software is restared at some time. The little bit more memory usage does not matter for us at the moment. The Loading already works, the only thing wich does not work, is sql server cause it's implentation dll is in the runtimes dir, and now cause it could not load it's unmanged code.

If you want to implement something like an auto-update, I would suggest to place each version to a separate folder, close the old version and start the new, without replacing files.

it has nothing to do with auto update.

Why are you avoiding using ALCs anyway? You said earlier that your codebase is fully migrated to modern .NET, and from personal experience I can tell you that working with them is a delight.

I don't need them.

@teo-tsirpanis
Copy link
Contributor

As shown in https://docs.microsoft.com/en-us/dotnet/core/dependency-loading/loading-managed, Assembly.Load(byte[], byte[]) loads each assembly in its own AssemblyLoadContext.

If you really don't want to use your own ALCs (and I will stress again that your existing approach IMHO looks inefficient and unnecessarily complicated), you can try this, after loading an assembly from memory:

AssemblyLoadContext.GetLoadContext(assembly).ResolvingUnmanagedDll += Default_ResolvingUnmanagedDll;

You can load native libraries using methods in the System.Runtime.InteropServices.NativeLibrary class.

@jogibear9988
Copy link
Author

should the AssemblyLoadContext.Default.ResolvingUnmanagedDll be called for a DLLImport in Microsoft.Data.SqlClient?

@jogibear9988
Copy link
Author

jogibear9988 commented Dec 7, 2021

Now it works.

    AssemblyLoadContext.GetLoadContext(assembly).ResolvingUnmanagedDll += Default_ResolvingUnmanagedDll;

was the key.
Thx @teo-tsirpanis

But I think I will switch to, that I only move the DLLs wich are needed to be replaced, into a subdirectory. So the whole AssemblyLoader will get simpler again.
But much learned about AssemblyLoading in NetCore

using System;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Runtime.Loader;

namespace MCC.AssemblyLoader
{
	public static class AssemblyLoader
	{
		static AssemblyLoader()
		{
			Initialize();
		}

		public static readonly ConcurrentDictionary<string, Assembly> AssemblysCache = new ConcurrentDictionary<string, Assembly>();
		public static readonly ConcurrentDictionary<Assembly, string> AssemblysPathCache = new ConcurrentDictionary<Assembly, string>();

		public static string ApplicationDirectory { get; private set; }

		public static string ApplicationLibDirectory { get; private set; }

		public static string DotNetRuntimeDirectory { get; private set; }

		public static string AspNetRuntimeDirectory { get; private set; }

		public static string PlattformDir { get; private set; }

		public static string[] PlattformSearchDirs { get; private set; }

		public static string PlattformNativeDir { get; private set; }

	   

		private static void Initialize()
		{
			ApplicationDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
			if (!ApplicationDirectory.EndsWith(Path.DirectorySeparatorChar.ToString()))
				ApplicationDirectory += Path.DirectorySeparatorChar;

			ApplicationLibDirectory = Path.Combine(ApplicationDirectory, "lib");
			var a = typeof(int).Assembly;
			var f = new FileInfo(a.Location);
			var fi = FileVersionInfo.GetVersionInfo(a.Location);

			DotNetRuntimeDirectory = Path.GetDirectoryName(typeof(int).Assembly.Location);

			var dotNetRuntimeDirectoryWithoutVersion = Path.GetDirectoryName(DotNetRuntimeDirectory);
			var version = DotNetRuntimeDirectory.Substring(dotNetRuntimeDirectoryWithoutVersion.Length + 1);
			var v = Version.Parse(version);
			var shortVersion = v.Major + "." + v.Minor;

			AspNetRuntimeDirectory = Path.Combine(Path.GetDirectoryName(dotNetRuntimeDirectoryWithoutVersion), "Microsoft.AspNetCore.App", version);

			AssemblyLoadContext.Default.Resolving += Default_Resolving;

			PlattformDir = "win";
			if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
				PlattformDir = "linux";
			else if (RuntimeInformation.IsOSPlatform(OSPlatform.FreeBSD))
				PlattformDir = "freebsd";
			else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
				PlattformDir = "osx";

			PlattformNativeDir = PlattformDir + "-" + RuntimeInformation.OSArchitecture.ToString().ToLower();

			PlattformSearchDirs = new[] { "net6.0", "net5.0", "netcoreapp3.1", "netcoreapp2.1", "netcoreapp2.0", "netstandard2.0" };
			AssemblyLoadContext.Default.ResolvingUnmanagedDll += Default_ResolvingUnmanagedDll;
		}

		private static IntPtr Default_ResolvingUnmanagedDll(Assembly arg1, string dllName)
		{
			var file = Path.Combine(ApplicationDirectory, "lib", "runtimes", PlattformNativeDir, "native", dllName);
			if (File.Exists(file))
			{
				return System.Runtime.InteropServices.NativeLibrary.Load(file);
			}
			return IntPtr.Zero;
		}

		private static Assembly Default_Resolving(AssemblyLoadContext arg, AssemblyName args)
		{
			try
			{
				var name = args.Name.Split(',')[0];
				if (AssemblysCache.TryGetValue(name.ToLower(), out var assembly))
					return assembly;

				var dll_name = name + ".dll";
				string file = null;
				string pdbFile = null;
				for (int i = 0; i < PlattformSearchDirs.Length; i++)
				{
					string sd = PlattformSearchDirs[i];
					file = Path.Combine(ApplicationDirectory, "lib", "runtimes", PlattformDir, "lib", sd, dll_name);
					if (File.Exists(file))
					{
						pdbFile = Path.Combine(ApplicationDirectory, "lib", "runtimes", PlattformDir, "lib", sd, name + ".pdb");
						break;
					}
					file = null;
				}
				if (file == null)
				{
					file = Path.Combine(ApplicationDirectory, "lib", dll_name);
					pdbFile = Path.Combine(ApplicationDirectory, "lib", name + ".pdb");
				}
				
				if (File.Exists(file))
				{
					lock (AssemblysCache)
					{
						if (!AssemblysCache.TryGetValue(name.ToLower(), out assembly))
						{
							var bytes = File.ReadAllBytes(file);
							if (File.Exists(pdbFile))
							{
								var pdbBytes = File.ReadAllBytes(pdbFile);
								assembly = Assembly.Load(bytes, pdbBytes);
							}
							else
								assembly = Assembly.Load(bytes);
							var loadContext = AssemblyLoadContext.GetLoadContext(assembly);
							if (loadContext != AssemblyLoadContext.Default)
								loadContext.ResolvingUnmanagedDll += Default_ResolvingUnmanagedDll;
							AssemblysCache.TryAdd(name.ToLower(), assembly);
							AssemblysPathCache.TryAdd(assembly, file);
							return assembly;
						}
						else
						{
							return assembly;
						}
					}
				}

				file = Path.Combine(DotNetRuntimeDirectory, dll_name);
				if (File.Exists(file))
				{
					assembly = Assembly.LoadFrom(file);
					AssemblysCache.TryAdd(name.ToLower(), assembly);
					AssemblysPathCache.TryAdd(assembly, file);
					return assembly;
				}

				file = Path.Combine(AspNetRuntimeDirectory, dll_name);
				if (File.Exists(file))
				{
					assembly = Assembly.LoadFrom(file);
					AssemblysCache.TryAdd(name.ToLower(), assembly);
					AssemblysPathCache.TryAdd(assembly, file);
					return assembly;
				}
			}
			catch (Exception ex)
			{
				Console.WriteLine(ex);
			}

			Console.WriteLine("Could not load Assembly: " + args.Name + " - this does not need to be an error.");
			return null;
		}

                    public static Assembly GetOrLoadAssembly(string dll, bool reload = false)
                    {
                        Assembly assembly;
                        var name = Path.GetFileNameWithoutExtension(dll).ToLower();
                        if (!reload)
                        {
                            if (AssemblysCache.TryGetValue(name, out assembly))
                                return assembly;
                        }
            
                        lock (AssemblysCache)
                        {
                            if (reload || !AssemblysCache.TryGetValue(name, out assembly))
                            {
                                var bytes = File.ReadAllBytes(dll);
                                assembly = Assembly.Load(bytes);
                                if (assembly != null)
                                {
                                    AssemblysCache[name] = assembly;
                                    AssemblysPathCache.TryAdd(assembly, dll);
                                }
                            }
                            return assembly;
                        }
                    }

		public static string GetAssemblyPath(Assembly assembly)
		{
			if (!string.IsNullOrEmpty(assembly.Location))
				return assembly.Location;
			AssemblysPathCache.TryGetValue(assembly, out var path);
			return path;

		}
	}
}

@teo-tsirpanis
Copy link
Contributor

Tell me about your app if you want, what underlying reason motivated you to implement this custom assembly loader?

@jogibear9988
Copy link
Author

jogibear9988 commented Dec 8, 2021

We have a complex APP, with over 100 Projects in the solution.
The initial Assembly loading/unloading was developed in Net Framework using AppDomains. In this app, at first our different AppDomains communicated via WCF, later we converted this into a Websocket communication between the Parts.

But after switching to NetCore, we decided to Drop the communication Overhead, and switched to direct interface calls. So at first I tried to load my Services (the ones wich were in different appdomains before), into AssemblyLoadContexts, but th eProblem is, they refrence each other, and when then an Assembly is loaded to another LoadContext, they types are not the same (same as I load a assembly multiple times to one load context).
So at first we dropped the support for Reloading of assemblies.

But now I thought an easier solution would be only to reload the new Assembly (and leave the old one in memory). Initially I thouhght AssemblyResolve would be called for all Assemblies, then I saw it's only called for the ones wich are not found... And so I started workin on this.
But for me this now works, but I think I will change, so I move my own DLLs (wich are the only ones I need to reload) to a subdirectory, and only Load them via my LoadContext.

One more Information, we only need the reload during the development of our customer solution, later when the software runs for weeks, or months we will not use this.

@agocke agocke closed this as completed Jun 14, 2022
@ghost ghost removed the untriaged New issue has not been triaged by the area owner label Jun 14, 2022
@jkotas jkotas added the question Answer questions and provide assistance, not an issue with source code or documentation. label Jun 14, 2022
@ghost ghost locked as resolved and limited conversation to collaborators Jul 15, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-AssemblyLoader-coreclr question Answer questions and provide assistance, not an issue with source code or documentation.
Projects
Archived in project
Development

No branches or pull requests

6 participants