I'd like to dynamically load the latest installed version of an assembly in the GAC using reflections.
So far I've found multiple working ways to accomplish this but they're all having their specific downsides.
The easiest solution is using the Assembly.LoadWithPartialName() method. But this method is obsolete since .NET Framework 2:
var assembly = Assembly.LoadWithPartialName("Microsoft.WindowsAzure.ServiceRuntime");
Another possibility is to use Assembly.Load() (as recommended by the obsolete warning) and call the different assembly versions with their fully qualified assembly name in a try/catch block to get the latest installed version. This screams for maintenance and just looks dirty:
Assembly assembly = null;
try
{
assembly = Assembly.Load("Microsoft.WindowsAzure.ServiceRuntime, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35");
}
catch (FileNotFoundException) { }
try
{
assembly = Assembly.Load("Microsoft.WindowsAzure.ServiceRuntime, Version=1.7.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35");
}
catch (FileNotFoundException) { }
Last but not least there's another solution I found here on SO using the Assembly.LoadFrom() method and then basically importing the assembly manager module fusion.dll to figure the path of the latest version. This seems to be way too much for such a "simple" task.
Isn't there any better solution to accomplish that without using an obsolete method, creating a maintenance hell with magic strings or by calling unmanaged code?
Thanks in advance!
Fusion.dll... ugh. I would find your target framework version and enumerate all of the gas directories and simply query them based on the root assembly name so that your only magic string is the assembly name. Then, use Assembly.ReflectionOnlyLoadFrom static method to compare the versions of until you find the latest one and then use the Assembly.Load method with the appropriate file path arguments. You may have trouble with this using click-once download unless you have given the app the appropriate trust.
// List of all the different types of GAC folders for both 32bit and 64bit
// environments.
List<string> gacFolders = new List<string>() {
"GAC", "GAC_32", "GAC_64", "GAC_MSIL",
"NativeImages_v2.0.50727_32",
"NativeImages_v2.0.50727_64"
};
foreach (string folder in gacFolders)
{
string path = Path.Combine(#"c:\windows\assembly", folder);
if(Directory.Exists(path))
{
Response.Write("<hr/>" + folder + "<hr/>");
string[] assemblyFolders = Directory.GetDirectories(path);
foreach (string assemblyFolder in assemblyFolders)
{
Response.Write(assemblyFolder + "<br/>");
}
}
}
See this other answer here on stack overflow. I used this to enumerate the gac successfully.
Related
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.
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.
I am currently trying to write a small service, which uses CefSharp (v57.0.0) to render HTML to a PDF file and followed the instructions to use "Any CPU" in my project (Feature Request - Add AnyCPU Support).
In my project I used the following assembly resolver that seems to work fine (it loads CefSharp.Core.dll, CefSharp.dll during initialisation):
// Will attempt to load missing assembly from either x86 or x64 subdir
private static Assembly Resolver(object sender, ResolveEventArgs args)
{
if (args.Name.StartsWith("CefSharp", StringComparison.Ordinal))
{
string assemblyName = args.Name.Split(new[] { ',' }, 2)[0] + ".dll";
string archSpecificPath = Path.Combine(
AppDomain.CurrentDomain.SetupInformation.ApplicationBase,
Environment.Is64BitProcess ? "x64" : "x86",
assemblyName);
var outputAssembly = File.Exists(archSpecificPath) ? Assembly.LoadFile(archSpecificPath) : null;
return outputAssembly;
}
return null;
}
For the initialisation of CefSharp I set exactly the same values like in the example:
var settings = new CefSettings()
{
// By default CefSharp will use an in-memory cache, you need to specify a Cache Folder to persist data
CachePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "CefSharp\\Cache")
};
// Perform dependency check to make sure all relevant resources are in our output directory.
Cef.Initialize(settings, performDependencyCheck: true, browserProcessHandler: null);
However, if I start my simple test, I get the following error code:
Message: System.Exception : Unable to locate required Cef/CefSharp dependencies:
Missing:CefSharp.BrowserSubprocess.exe
Missing:CefSharp.BrowserSubprocess.Core.dll
Missing:CefSharp.Core.dll
Missing:CefSharp.dll
Missing:icudtl.dat
Missing:libcef.dll
Executing Assembly Path:D:\projects\CefService\bin\Debug\x86
Any ideas what might be happening here and how to solve the problem?
The message is pretty clear, other assemblies could not be loaded.
Here are some generic instructions on how to do that:
load native ones (e.g. libcef.dll) first with LoadLibrary and FreeLibrary
see if loading a managed one will automatically load other managed ones it depends, else handle them (tedious)
You might be interested in these tools for spotting dependencies:
http://www.dependencywalker.com/
https://github.com/isindicic/DependencyWalker.Net
How do I determine the dependencies of a .NET application?
First of all I need to emphasize that this is slightly different question than the one in this thread. Additionally, installing KB2468871 doesn't help.
I tried to simplify this problem as much as possible. In general it about loading PCL assemblies in Desktop application with Assembly.LoadFile(...).
Let's say there is a .NET 4.0 Console Application (called "C"). It references .NET 4.0 assembly (called "N4") and PCL assembly (called "PCL").
where N4 looks like this:
using System.Linq;
namespace N4
{
public class ClassInN4
{
public static string Greet()
{
return new string(
"hello from N4"
.ToCharArray()
.Select(char.ToUpper)
.ToArray()
);
}
}
}
PCL looks like this:
using System.Linq;
namespace PCL
{
public class ClassInPCL
{
public static string Greet()
{
return new string(
"hello from pcl"
.ToCharArray()
.Select(char.ToUpper)
.ToArray()
);
}
}
}
and C look like this:
using System;
using System.IO;
using System.Reflection;
using N4;
using PCL;
namespace C
{
internal class Program
{
private static void Main(string[] args)
{
Test();
Console.ReadLine();
}
private static void Test()
{
Test("N4", ClassInN4.Greet);
Test("PCL", ClassInPCL.Greet);
}
private static void Test(
string title,
Func<string> generator)
{
try
{
Console.WriteLine(
"{0}: {1}", title, generator());
}
catch (Exception e)
{
Console.WriteLine(
"{0}: {1} -> {2}", title, e.GetType(), e.Message);
}
}
}
}
When you run this application you get absolutely correct results:
N4: HELLO FROM N4
PCL: HELLO FROM PCL
Let's add AssemblyResolve event to CurrentDomain in Program.Main:
AppDomain.CurrentDomain.AssemblyResolve += (_, a) => {
var fileName = Path.GetFullPath(
new AssemblyName(a.Name).Name + ".data");
Console.WriteLine("Probing '{0}'", fileName);
return
File.Exists(fileName)
? Assembly.LoadFile(fileName)
: null;
};
So, what it does if assembly cannot be found it tries to load it from ".data" file.
Let's go application folder and rename "N4.dll" to "N4.data" and run "C.exe".
Probing 'C:\xxx\C\bin\Debug\N4.data'
N4: HELLO FROM N4
PCL: HELLO FROM PCL
So it goes through AssemblyResolve and finally loads "N4.data" and works as good as original.
Let's revert "N4.data" to "N4.dll" and rename "PCL.dll" to "PCL.data" and...
Probing 'C:\xxx\C\bin\Debug\PCL.data'
N4: HELLO FROM N4
Probing 'C:\xxx\C\bin\Debug\System.Core.data'
Probing 'C:\xxx\C\bin\Debug\System.Core.data'
Probing 'C:\xxx\C\bin\Debug\System.Core.data'
PCL: System.IO.FileNotFoundException -> Could not load file or assembly 'System.Core, Version=2.0.5.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e, Retargetable=Yes' or one of its dependencies. The system cannot find the file specified.
Please note that PCL assembly was loaded just fine, the problem is, it only can't find its dependencies (System.Core) anymore.
It's like Assembly.LoadFile(fileName) is-a no-no if loaded assembly is portable.
Did anyone have this problem? Did anyone solved this problem?
You can find all files here.
EDIT:
Thanks to leppie for forcing me to check other options. I actually wanted to be sure that I'm not lying while I answer "Yeah, yeah, I tried". Apparently it was worth checking.
From Suzanne Cook's .NET CLR Notes:
Be careful - these aren't the same thing.
LoadFrom() goes through Fusion and can be redirected to another assembly at a different path but with that same identity if one is already loaded in the LoadFrom context.
LoadFile() doesn't bind through Fusion at all - the loader just goes ahead and loads exactly* what the caller requested. It doesn't use either the Load or the LoadFrom context.
You can return the System.Core assembly of your platform (e.g. version 4.0.0.0 for .NET Framework 4.0) from the AssemblyResolve event, when asked for the 2.0.5.0 version.
I am loading all my referenced assemblies stored as resources via Load(byte[]), which also fails to resolve the 2.0.5.0 assembly, and I retrieve both System and System.Core from AppDomain.CurrentDomain.GetAssemblies().
I think you are getting these issues because:
You're getting an exception because you haven't got the latest .NET updates.
http://www.paraesthesia.com/archive/2013/01/21/using-portable-class-libraries-update-net-framework.aspx
Take note of the WSUS part - you may think you have the latest updates, but you don't cause your WSUS server is out of date.
This patch may help, but you're better just to get all the .net updates:
http://support.microsoft.com/kb/2468871
(from a comment above)
Try LoadFrom(...) rather than LoadFile(...) / Load(byte[]) and see if that fixes your issue? :)
I had the same problem and ended up with the following solution: invoke the following code before dynamically loading the PCL assembly.
Assembly.Load("System, Version=2.0.5.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e, Retargetable=Yes");
Assembly.Load("System.Core, Version=2.0.5.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e, Retargetable=Yes");
If any other dependency is missing when you load your PCL assembly, you just need to add a line to code above.
For some strange and ununderstandable reason, it works.
My task: Find all Forms (WindowsForm or WPF, doesn't matter) in a dll or exe file and return that list. In theory that works (meaning: if I've an assembly with a WPF or WindowsForm my code manages to get all Forms, TextBoxes, Labels etc. ). When it comes to "real" assemblies it fails. I get FileNotFound exceptions when calling GetExportedTypes() for every "custom" assembly (.NET assemblies are found, no problems there). I already use GetReferencedAssemblies() to load the referenced assemblies (Reflection.Assembly.LoadFrom) and yes it does work (all assemblies are found and loaded into the AppDomain) but it doesn't help.
I checked the version numbers (they match), I copied my executable and the assembly into one directory with all referenced assemblies, doesn't work.
Here is my code, maybe someone figures out what I'm (obviously) doing wrong:
foreach (AssemblyName reference in selectedAssembly.GetReferencedAssemblies())
{
if (System.IO.File.Exists(
System.IO.Path.GetDirectoryName(selectedAssembly.Location) +
#"\" + reference.Name + ".dll"))
{
System.Reflection.Assembly.LoadFrom(
System.IO.Path.GetDirectoryName(selectedAssembly.Location) +
#"\" + reference.Name + ".dll");
}
else if (System.IO.File.Exists(#"C:\dll\" + reference.Name + ".dll"))
{
System.Reflection.Assembly.LoadFrom(#"C:\dll\" + reference.Name + ".dll");
}
else
{
System.Reflection.Assembly.ReflectionOnlyLoad(reference.FullName);
}
selectedAssembly.GetExportedTypes();
}
at first check if the referenced dll exists in the directory where the assembly is, if not check if it exists in C:\dll and if it's not there try and use the GAC. it does work and I've no errors from there but as soon as I come to GetExportedTypes it fails with a FileNotFound exception on the first custom library.
*edit 1 what do I mean by "real assemblies": I mean assemblies which are more complex and have references to non-standard-.NET libraries/assemblies
Thanks for the hint to fuslogvw.exe Hans Passant but what do you mean by "with code like this"?
okay I used fuslogvw.exe and I get two exceptions for every single dll that is referenced by the "selectedAssembly".
The first one says something like
"The binding starts in LoadFrom-context
The image owned by the system isn't searched in LoadFrom-Context"
the other logentry says that the dll referenced by the selectedAssembly couldn't be found and it tried to download it from application's base path and all directories below...but not from it's actual location...so, key question: how do I change the Load-context to LoadFrom? And why is .NET so stubborn on this? I mean the assemblies are loaded in the AppDomain, it shouldn't care about the actual location of the assembly.
okay problem solved. Here is the solution:
http://ayende.com/blog/1376/solving-the-assembly-load-context-problem
I implemented that into my existing class (removed the static-keyword and put the body of the Init method into my method), compiled it and it worked.
Thanks for your help guys.
okay problem solved. Here is the solution: http://ayende.com/blog/1376/solving-the-assembly-load-context-problem
I implemented that into my existing class (removed the static-keyword and put the body of the Init method into my method), compiled it and it worked.
Thanks for your help guys.
just in case the website will someday be unavailable, here is the sourcecode from ayende
static Dictionary<string, Assembly>assemblies;
public static void Init()
{
assemblies = new Dictionary<string, Assembly>();
AppDomain.CurrentDomain.AssemblyLoad += new AssemblyLoadEventHandler(CurrentDomain_AssemblyLoad);
AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(CurrentDomain_AssemblyResolve);
}
static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
{
Assembly assembly = null;
assemblies.TryGetValue(args.Name, out assembly);
return assembly;
}
static void CurrentDomain_AssemblyLoad(object sender, AssemblyLoadEventArgs args)
{
Assembly assembly = args.LoadedAssembly;
assemblies[assembly.FullName] = assembly;
}
I would recommend using Reflector to see which references you may not have loaded. For instance, you are only loading the referenced assemblies that the current assembly is looking at. Do you step down through each child to find their referenced assemblies as well? The FileNotFound error is probably pointing you in the direction of a type that is declared in another assembly that isn't loaded.