In one of our applications we had a custom Assembly Resolve event that looks like below.
While far from perfect, this worked OK (meaning: good enough) in our application when we were targeting NET FX 4.6.2
AppDomain.CurrentDomain.AssemblyResolve += (sender, args) =>
{
string fileName = args.Name.Split(',').FirstOrDefault();
if (!string.IsNullOrEmpty(fileName))
{
fileName += ".dll";
FileInfo fileInfo = new FileInfo(Assembly.GetEntryAssembly().Location);
string filePath = null;
//If block should just make sure that filePath is set to an existing file...
//left it in in case I misread something in there...
if (fileInfo.Directory != null)
{
filePath = Path.Combine(fileInfo.Directory.FullName, fileName);
if (!File.Exists(filePath))
{
filePath = Path.Combine(standardAddInsDirectory, fileName);
if (!File.Exists(filePath))
{
filePath = Path.Combine(customerAddInsDirectory, fileName);
if (!File.Exists(filePath))
{
filePath = null;
}
}
}
}
//at this point filePath should be either null OR the full path to the dll
Assembly assembly = null;
if (!string.IsNullOrEmpty(filePath))
{
assembly = Assembly.LoadFile(filePath); //this line raised AssemblyResolve again
}
return assembly;
}
return null;
};
However, we had to update to 4.8, and once there, we got a stackoverflow exception because Assembly.Load(path) did raise an assembly resolve event for the exact same assembly we did try to load.
The exact case that brought our code down was "Microsoft.Extensions.Primitives 3.0.0.0", but we only shipped version 2.2.
We did call Assembly.Load on the 2.2 version and the event was raised again for the same assembly / version ("Microsoft.Extensions.Primitives 3.0.0.0").
I wanted to write a small MVS / Testcase afterwards but was not able to - I could not figure out how to declare an assembly (or a dependency) so that I came into the same situation.
I used strong naming, version mismatches, dependcies,...but to no luck.
Could you please help with the following two questions:
Did the assembly load / Assembly Load Event handling change with
4.8 OR could this have been an issue with differently restored nuget packages?
How can I build assemblies to force such behavior and have a testcase for it?
Related
Our localization team was trying to use LocBaml (.NET Framework version, 4.6.1) to localize some resources. They kept on getting errors saying "Could not find file or assembly..." So, I looked into it, saw the note that the x.resources.dll file had to be in same directory as x.dll. ("x" just means some name). Tried that, still go the same error. I then built a debug version and also downloaded the .NET source code. Turns out some exception was occuring in the guts of .NET. If I could summarize, it was failing when trying to do Assembly.Load("x"). So I wrote a program trying to duplicate the situation...
using System;
using System.Reflection;
using System.Linq;
namespace Nothing
{
class Loader
{
public static void Main(string[] args)
{
try
{
if (args.Count() == 1)
{
Assembly asm = Assembly.Load(args[0]);
Console.WriteLine($"Name of asembly: {asm.FullName}");
}
else
{
Console.WriteLine("Need to specify filename");
}
}
catch (Exception x)
{
Console.WriteLine("Exception: {0}", x);
}
}
}
}
The file was named AsmLoader.cs and compiled to AsmLoader.exe.
Well from the directory x.dll was in, I typed in \path\to\AsmLoader.exe x.dll and \path\to\AsmLoader.exe x. Same error, "Could not find file or assembly..."
Looked at the stack trace for the exception and saw that "codebase" was an argument for some function on the stack. Gave it a thought, and copied AsmLoader.exe to the same directory as x.dll.
Gave .\AsmLoader.exe x.dll a try..still same error. Remembered that the argument to the exception was just "x". Tried .\AsmLoader.exe x .... bingo... worked. For grins, copied LocBaml.exe and it's .config file to the same directory, and tried .\LocBaml x.resources.dll ... ding, ding, ding... success. Finally.
So, for now, I'll just tell the localiztion team to copy LocBaml to the same directory as the files and all should be good.
However, I can't help but feel this could somehow be solved with code. How can I make changes to the code in the example so that AsmLoader.exe doesn't have to be in the same directory as the DLL I want to load? I had even changed my path environment variable to ensure AsmLoader.exe and both x.dll directories were in the path. That didn't work...
So what do I need to change for it to work in my base program...and then maybe I can do the same for LocBaml...???
Well, I came up with a solution to add an AssemblyResolve event handler to the current app domain. Solved it for this simple example and my rebuilt LocBaml...
In main, add:
AppDomain.CurrentDomain.AssemblyResolve += LoadFromCurrentDirectory;
Implement LoadFromCurrentDirectly like:
static Assembly LoadFromCurrentDirectory(object sender, ResolveEventArgs args)
{
string name = args.Name;
bool bCheckVersion = false;
int idx = name.IndexOf(',');
if (idx != -1)
{
name = name.Substring(0, idx);
bCheckVersion = true;
}
string sCurrentDir = Directory.GetCurrentDirectory();
if (!name.EndsWith(".dll", StringComparison.OrdinalIgnoreCase) && !name.EndsWith(".exe"))
{
string[] exts = { ".dll", ".exe" };
foreach( string ext in exts)
{
string tryPath = Path.Combine(sCurrentDir, name + ext);
if (File.Exists(tryPath))
{
name = name += ext;
break;
}
}
}
string path = Path.Combine(sCurrentDir, name);
if (!string.IsNullOrEmpty(path) && File.Exists(path))
{
Assembly assembly = Assembly.LoadFrom(path);
if (assembly != null & bCheckVersion)
{
if (assembly.FullName != args.Name)
return null;
}
return assembly;
}
else
{
var reqAsm = args.RequestingAssembly;
if (reqAsm != null)
{
string requestingName = reqAsm.GetName().FullName;
Console.WriteLine($"Could not resolve {name}, {path}, requested by {requestingName}");
}
else
{
Console.WriteLine($"Could not resolve {args.Name}, {path}");
}
}
return null;
}
I'm sure it could be optimized to add a global list of directories, or to use the path when searching for files to load. However, for our use case, works just fine. When I added it to our LocBaml code, it solved the loading problem for us...also got rid of necessity of copying the en\x.resources.dll files to our output directory. As long as we run the program from the output directory, LocBaml finishes parsing.
I have a project which needs to load extra assemblies dynamically at runtime for reflection purposes. I need custom code because the path to the DLLs is not known by the framework. This code works fine with normal DLLs, they load fine and I can reflect over them. However, when I attempt to load types which statically uses embedded resources (i.e. a resx) this code fails.
Without my custom assembly resolution code this works fine. Here is my assembly resolution code:
static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
{
bool isName;
string path = GetAssemblyLoadInfo(args, out isName);
return isName ? Assembly.Load(path) : Assembly.LoadFrom(path);
}
static string GetAssemblyLoadInfo(ResolveEventArgs args, out bool isAssemblyName)
{
isAssemblyName = false;
var assemblyName = new AssemblyName(args.Name);
string path = String.Concat(new FileInfo(DllPath).Directory, "\\", assemblyName.Name, ".dll");
if (File.Exists(path))
{
return path;
}
if (path.EndsWith(".resources.dll"))
{
path = path.Replace(".resources.dll", ".dll");
if (File.Exists(path)) return path;
}
var assemblyLocation = AssemblyLocations.FirstOrDefault(al => al.Name.FullName == assemblyName.FullName);
if (null == assemblyLocation)
{
isAssemblyName = true;
return args.Name;
}
else
{
return assemblyLocation.Location;
}
}
Here is a link to a project which recreates the entire issue:
https://drive.google.com/file/d/0B-mqMIMqm_XHcktyckVZbUNtZ28/view?usp=sharing
Once you download the project, you first need to build TestLibrary, and then run ConsoleApp4. It should work fine and write the string "This is the value of the resource" to the console, which comes from the resx file. However, uncomment line 23 in Program.cs and run it again and it will fail with an exception, which indicates that it failed to load the embedded resources.
The solution in this question solved my issue:
AppDomain.CurrentDomain.AssemblyResolve asking for a <AppName>.resources assembly?
Basically, add the following code to the assembly being loaded:
[assembly: NeutralResourcesLanguage("en-GB", UltimateResourceFallbackLocation.MainAssembly)]
I'm writing a WinForms program that uses MEF to load assemblies. Those assemblies are not located in the same folder than the executable.
As I need to perform some file maintenance, I implemented some code in the file Program.cs, before loading the actual WinForm, so the files (even if assemblies) are not loaded (or shouldn't if they are) by the program.
I'm performing two operations:
- Moving a folder from one location to an other one
- Unzipping files from an archive and overwrite dll files from the folder moved (if file from the archive is newer than the one moved)
The problem is that after moving the folder, files in it are locked and cannot be overwritten. I also tried to move files one by one by disposing them when the move is finished.
Can someone explain me why the files are blocked and how I could avoid that
Thanks
private static void InitializePluginsFolder()
{
if (!Directory.Exists(Paths.PluginsPath))
{
Directory.CreateDirectory(Paths.PluginsPath);
}
// Find archive that contains plugins to deploy
var assembly = Assembly.GetExecutingAssembly();
if (assembly.Location == null)
{
throw new NullReferenceException("Executing assembly is null!");
}
var currentDirectory = new FileInfo(assembly.Location).DirectoryName;
if (currentDirectory == null)
{
throw new NullReferenceException("Current folder is null!");
}
// Check if previous installation contains a "Plugins" folder
var currentPluginsPath = Path.Combine(currentDirectory, "Plugins");
if (Directory.Exists(currentPluginsPath))
{
foreach (FileInfo fi in new DirectoryInfo(currentPluginsPath).GetFiles())
{
using (FileStream sourceStream = new FileStream(fi.FullName, FileMode.Open))
{
using (FileStream destStream = new FileStream(Path.Combine(Paths.PluginsPath, fi.Name), FileMode.Create))
{
destStream.Lock(0, sourceStream.Length);
sourceStream.CopyTo(destStream);
}
}
}
Directory.Delete(currentPluginsPath, true);
}
// Then updates plugins with latest version of plugins (zipped)
var pluginsZipFilePath = Path.Combine(currentDirectory, "Plugins.zip");
// Extract content of plugins archive to a temporary folder
var tempPath = string.Format("{0}_Temp", Paths.PluginsPath);
if (Directory.Exists(tempPath))
{
Directory.Delete(tempPath, true);
}
ZipFile.ExtractToDirectory(pluginsZipFilePath, tempPath);
// Moves all plugins to appropriate folder if version is greater
// to the version in place
foreach (var fi in new DirectoryInfo(tempPath).GetFiles())
{
if (fi.Extension.ToLower() != ".dll")
{
continue;
}
var targetFile = Path.Combine(Paths.PluginsPath, fi.Name);
if (File.Exists(targetFile))
{
if (fi.GetAssemblyVersion() > new FileInfo(targetFile).GetAssemblyVersion())
{
// If version to deploy is newer than current version
// Delete current version and copy the new one
// FAILS HERE
File.Copy(fi.FullName, targetFile, true);
}
}
else
{
File.Move(fi.FullName, targetFile);
}
}
// Delete temporary folder
Directory.Delete(tempPath, true);
}
Check the implementation of the GetAssemblyVersion() method used in this part of code:
if (File.Exists(targetFile))
{
if (fi.GetAssemblyVersion() > new FileInfo(targetFile).GetAssemblyVersion())
{
// If version to deploy is newer than current version
// Delete current version and copy the new one
// FAILS HERE
File.Copy(fi.FullName, targetFile, true);
}
}
fi variable has type FileInfo, GetAssemblyVersion() looks like an extension method. You should check how assembly version is retrieved from the file. If this method loads an assembly it should also unload it to release the file.
The separate AppDomain is helpful if you need to load the assembly, do the job and after that unload it. Here is the GetAssemblyVersion method implementation:
public static Version GetAssemblyVersion(this FileInfo fi)
{
AppDomain checkFileDomain = AppDomain.CreateDomain("DomainToCheckFileVersion");
Assembly assembly = checkFileDomain.Load(new AssemblyName {CodeBase = fi.FullName});
Version fileVersion = assembly.GetName().Version;
AppDomain.Unload(checkFileDomain);
return fileVersion;
}
The following implementation of the GetAssemblyVersion() could retrieve the assembly version without loading assembly into your AppDomain. Thnx #usterdev for the hint. It also allows you to get the version without assembly references resolve:
public static Version GetAssemblyVersion(this FileInfo fi)
{
return AssemblyName.GetAssemblyName(fi.FullName).Version;
}
You have to make sure that you are not loading the Assembly into your domain to get the Version from it, otherwise the file gets locked.
By using the AssemblyName.GetAssemblyName() static method (see MSDN), the assembly file is loaded, version is read and then unloaded but not added to your domain.
Here an extension for FileInfo doing so:
public static Version GetAssemblyVersion(this FileInfo fi)
{
AssemblyName an = AssemblyName.GetAssemblyName(fi.FullName);
return an.Version;
}
The below statement locks the file
destStream.Lock(0, sourceStream.Length);
but after that you havent unlocked the file. Perhaps that is the cause of your problem.
I would start checking if you program has actually already loaded the assembly.
two suggestions:
1 - Call a method like this before calling your InitializePluginsFolder
static void DumpLoadedAssemblies()
{
var ads = AppDomain.CurrentDomain.GetAssemblies();
Console.WriteLine(ads.Length);
foreach (var ad in ads)
{
Console.WriteLine(ad.FullName);
// maybe this can be helpful as well
foreach (var f in ad.GetFiles())
Console.WriteLine(f.Name);
Console.WriteLine("*******");
}
}
2 - In the first line of Main, register for AssemblyLoad Event and dump Loaded Assembly in the event handler
public static void Main()
{
AppDomain.CurrentDomain.AssemblyLoad += OnAssemlyLoad;
...
}
static void OnAssemlyLoad(object sender, AssemblyLoadEventArgs args)
{
Console.WriteLine("Assembly Loaded: " + args.LoadedAssembly.FullName);
}
You definitely load assembly using AssemblyName.GetAssemblyName, unfortunately .NET has no conventional ways of checking assembly metadata without loading assembly. To avoid this you can:
Load assembly in separated AppDomain as Nikita suggested, I can add: load it with ReflectionOnlyLoad
Or get assembly version using Mono.Cecil library as Reflector does
Just for completeness: actually you can load assembly into same AppDomain without locking assembly file in two stage: read file contents into byte[] and using Assembly.Load(byte[] rawAssembly) but this way has serious "Loading Context" issues and what will you do with several loaded assemblies :)
Here is how my application folders looks like:
Application:
+ App.exe
+ App.exe.config
Application/Plugins:
+ Plugin1 (folder)
Application/Plugins/Plugin1:
+ Plugin1.dll
+ SomeDll.dll
So main application App.exe looking for plugins folder and load {PluginName}.dll into memory to run it. This plugin usually uses it's own dependant assemblies which must be loaded (like SomeDll.dll). It appears that it make serious troubles sometimes. I receive exception that for example dependant assembly of dependant assembly cannot be found and I don't know why.
For example, My plugin must load lots of additional dlls becouse plugin runs OwinSelfHost service.
So it must load for example:
System.Web.Http.Owin
Owin
Microsoft.Owin
Microsoft.Owin.Host.HttpListener
Microsoft.Owin.Hosting
and when load Microsoft.Owin.Hosting then throw exception that cannot load Microsoft.Owin
Exception looks like:
Could not load file or assembly 'Microsoft.Owin, Version=2.0.2.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35' or one of it's dependencies. File not found.
I wrote this method to resolve assemblies. It is tweaked to fit my needs.
It basically hooks a AssemblyResolve event to the current application domain to retrieve an requested assembly from a list of directories.
There is no easy way to find where the assembly file that match the namespace to resolve, except by loading an assembly file and check to which namespace it belongs to.
Plus, it discards some unwanted assemblies (like serializers, resources...) and detects dlls or exes that are not .NET assemblies.
A better approach would consist in using the Global Assembly Cache, but we want our plugins to be fully moveable. So here it is.
public static class AssemblyResolver
{
internal static void Hook(params string[] folders)
{
AppDomain.CurrentDomain.AssemblyResolve += (sender, args) =>
{
// Check if the requested assembly is part of the loaded assemblies
var loadedAssembly = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(a => a.FullName == args.Name);
if (loadedAssembly != null)
return loadedAssembly;
// This resolver is called when an loaded control tries to load a generated XmlSerializer - We need to discard it.
// http://connect.microsoft.com/VisualStudio/feedback/details/88566/bindingfailure-an-assembly-failed-to-load-while-using-xmlserialization
var n = new AssemblyName(args.Name);
if (n.Name.EndsWith(".xmlserializers", StringComparison.OrdinalIgnoreCase))
return null;
// http://stackoverflow.com/questions/4368201/appdomain-currentdomain-assemblyresolve-asking-for-a-appname-resources-assembl
if (n.Name.EndsWith(".resources", StringComparison.OrdinalIgnoreCase))
return null;
string assy = null;
// Find the corresponding assembly file
foreach (var dir in folders)
{
assy = new[] { "*.dll", "*.exe" }.SelectMany(g => Directory.EnumerateFiles(dir, g)).FirstOrDefault(f =>
{
try { return n.Name.Equals(AssemblyName.GetAssemblyName(f).Name, StringComparison.OrdinalIgnoreCase); }
catch (BadImageFormatException) { return false; /* Bypass assembly is not a .net exe */ }
catch (Exception ex) { throw new ApplicationException("Error loading assembly " + f, ex); }
});
if (assy != null)
return Assembly.LoadFrom(assy);
}
throw new ApplicationException("Assembly " + args.Name + " not found");
};
}
}
Here is how it works:
AssemblyResolver.Hook("\Plugins", "\CommonReferences");
Everytime some assemblies needs to be resolved, it will get the one that is loaded in memory, otherwise it will search in any given folders.
You can use "AssemblyResolve Event" (Method 3):
https://support.microsoft.com/en-us/kb/837908
I have a structure:-
\bin\debug\abc.exe and
\Libs\win32\xyz.dll.
Now I need to refer xyz.dll so as to run my abc.exe. I tried with "probing" tag in app.config but in that case the possibility was only when I had 'Libs' folder in 'debug' folder i.e. where .exe is present. But I want to come 2 folders out from .exe and then go into \Libs\win32 to refer to .dll . Please suggest me what should I do.
One option is handling AssemblyResolve event, every time .NET couldn't find required assembly in current path, it will trigger AssemblyResolve event:
{
// Execute in startup
AppDomain.CurrentDomain.AssemblyResolve += CurrentDomainOnAssemblyResolve;
}
private Assembly CurrentDomainOnAssemblyResolve(object sender, ResolveEventArgs args)
{
string RESOURCES = ".resources";
try
{
/* Extract assembly name */
string[] sections = args.Name.Split(new char[] { ',' });
if (sections.Length == 0) return null;
string assemblyName = sections[0];
/* If assembly name contains ".resource", you don't need to load it*/
if (assemblyName.Length >= RESOURCES.Length &&
assemblyName.LastIndexOf(RESOURCES) == assemblyName.Length - RESOURCES.Length)
{
return null;
}
/* Load assembly to current domain (also you can use simple way to load) */
string assemblyFullPath = "..//..//Libs//" + assemblyName;
FileStream io = new FileStream(assemblyNameWithExtension, FileMode.Open, FileAccess.Read);
if (io == null) return null;
BinaryReader binaryReader = new BinaryReader(io);
Assembly assembly = Assembly.Load(binaryReader.ReadBytes((int)io.Length));
return assembly;
}
catch(Exception ex)
{}
}
*Another option is loading all of your required assemblies to current domain at your project start-up.
You use ..\ in the file path to move up a directory.
So if you're in \bin\debug\abc.exe then your reference to \Libs\win32\xyz.dll would be
..\..\Libs\win32\xyz.dll
This should only be necessary when building your projects, when it's built if your executable is referencing the dll correctly it only needs to be put in the same folder as the dll.
Unless of course you're using dllimport or something where you need to know the exact path of the dll during runtime.