Reflection only assembly loading in .Net Core - c#

I have a .Net Framework WPF application that I'm currently migrating to .Net6. At startup it examines certain assemblies in the executable folder looking for any with a custom assembly attribute. Those that have this are then loaded into the current appdomain. (Note that some of these assemblies may already be in the appdomain, as they are projects in the running application's solution).
This is the 4.x code:
private void LoadAssemblies(string folder)
{
AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve +=
(s, e) => Assembly.ReflectionOnlyLoad(e.Name);
var assemblyFiles = Directory.GetFiles(folder, "*.Client.dll");
foreach (var assemblyFile in assemblyFiles)
{
var reflectionOnlyAssembly = Assembly.ReflectionOnlyLoadFrom(assemblyFile);
if (ContainsCustomAttr(reflectionOnlyAssembly))
{
var assembly = Assembly.LoadFrom(assemblyFile);
ProcessAssembly(assembly);
}
}
}
The custom assembly attribute (that this code is looking for) has a string property containing a path to a XAML resource file within that assembly. The ProcessAssembly() method adds this resource file to the application's merged dictionary, something like this:
var resourceUri = string.Format(
"pack://application:,,,/{0};component/{1}",
assembly.GetName().Name,
mimicAssemblyAttribute.DataTemplatePath);
var uri = new Uri(resourceUri, UriKind.RelativeOrAbsolute);
application.Resources.MergedDictionaries.Add(new ResourceDictionary { Source = uri });
Just to reiterate, all this works as it should in the .Net 4.x application.
.Net6 on the other hand doesn't support reflection-only loading, nor can you create a second app domain in which to load the assemblies. I rewrote the above code by loading the assemblies being examined into what I understand is a temporary, unloadable context:
private void LoadAssemblies(string folder)
{
var assemblyFiles = Directory.GetFiles(folder, "*.Client.dll");
using (var ctx = new TempAssemblyLoadContext(AppDomain.CurrentDomain.BaseDirectory))
{
foreach (var assemblyFile in assemblyFiles)
{
var assm = ctx.LoadFromAssemblyPath(assemblyFile);
if (ContainsCustomAttr(assm))
{
var assm2 = Assembly.LoadFrom(assemblyFile);
ProcessAssembly(assm2);
}
}
}
}
private class TempAssemblyLoadContext : AssemblyLoadContext, IDisposable
{
private AssemblyDependencyResolver _resolver;
public TempAssemblyLoadContext(string readerLocation)
: base(isCollectible: true)
{
_resolver = new AssemblyDependencyResolver(readerLocation);
}
public void Dispose()
{
Unload();
}
protected override Assembly Load(AssemblyName assemblyName)
{
var path = _resolver.ResolveAssemblyToPath(assemblyName);
if (path != null)
{
return LoadFromAssemblyPath(path);
}
return null;
}
protected override IntPtr LoadUnmanagedDll(string unmanagedDllName)
{
var path = _resolver.ResolveUnmanagedDllToPath(unmanagedDllName);
if (path != null)
{
return LoadUnmanagedDllFromPath(path);
}
return IntPtr.Zero;
}
}
(Note the ProcessAssembly() method is unchanged).
This code "works" in so much as it goes through the motions without crashing. However at a later point when the application starts creating the views, I get the following exception:
The component '..ModeSelectorView' does not have a resource identified by the URI '/.;component/views/modeselector/modeselectorview.xaml'.
This particular view resides in a project of this application's solution, so the assembly will already be in the appdomain. The assembly also contains that custom attribute so the above code will be trying to load it, although I believe that Assembly.LoadFrom() should not load the same assembly again?
Just in case, I modified the "if" block in my LoadAssemblies() method to ignore assemblies already in the app domain:
if (ContainsCustomAttr(assm) && !AppDomain.CurrentDomain.GetAssemblies().Contains(assm))
Sure enough, a breakpoint shows that the assembly in question (containing that view) is ignored and not loaded into the app domain. However I still get the same exception further down the line.
In fact I can comment out the entire "if" block so no assemblies are being loaded into the app domain, and I still get the exception, suggesting that it's caused by loading the assembly into that AssemblyLoadContext.
Also, a breakpoint shows that context is being unloaded via its Dispose() method, upon dropping out of the "using" block in the LoadAssemblies() method.
Edit: even with the "if" block commented out, a breakpoint at the end of the method shows that all the assemblies being loaded by ctx.LoadFromAssemblyPath() are ending up in AppDomain.Current. What am I not understanding? Is the context part of the appdomain and not a separate "area"? How can I achieve this "isolated" loading of assemblies in a similar way to the "reflection only" approach that I was using in .Net 4.x?

Okay, so I found the answer, which is to use MetadataLoadContext. This is essentially the .Net Core replacement for reflection-only loading:
private void LoadAssemblies(string folder)
{
// The load context needs access to the .Net "core" assemblies...
var allAssemblies = Directory.GetFiles(RuntimeEnvironment.GetRuntimeDirectory(), "*.Client.dll").ToList();
// .. and the assemblies that I need to examine.
var assembliesToExamine = Directory.GetFiles(folder, "NuIns.CoDaq.*.Client.dll");
allAssemblies.AddRange(assembliesToExamine);
var resolver = new PathAssemblyResolver(allAssemblies);
using (var mlc = new MetadataLoadContext(resolver))
{
foreach (var assemblyFile in assembliesToExamine)
{
var assm = mlc.LoadFromAssemblyPath(assemblyFile);
if (ContainsCustomAttr(assm))
{
var assm2 = Assembly.LoadFrom(assemblyFile);
AddMimicAssemblyInfo(assm2);
}
}
}
}

Related

Assembly Loading in .NET Core

Using VS2017 RC, .NET Core
I am trying to load an assembly from a file.
The dependencies of this assembly are in the same folder.
I am using AssemblyLoadContext.Default.LoadFromAssemblyPath.
I realize LoadFromAssemblyPath exclusively loads the requested assembly, ignoring its dependencies; any attempt to iterate through the assembly types fails with a System.Reflection.ReflectionTypeLoadException.
LoaderExceptions contains a list of System.IO.FileNotFoundException.
I'm curious as to why this is the case, since all the required files are in the same folder.
I also tried to load all *.dll files in a folder, but some surprisingly fail with a System.IO.FileLoadException.
What am I doing wrong?
Edit: I wouldn't want to rely on the .deps file (thus ruling out DependencyContext). Is it possible?
Well what works for me is to register a handle with the Resolving event and load required assemblies on demand when LoadFromAssemblyPath needs dependencies. Be aware that this my solution from hours of trial and error, so it might not be the most ideal way. It works for me by now though. Here's my code:
AssemblyLoadContext.Default.Resolving += (context, name) =>
{
// avoid loading *.resources dlls, because of: https://github.com/dotnet/coreclr/issues/8416
if (name.Name.EndsWith("resources"))
{
return null;
}
var dependencies = DependencyContext.Default.RuntimeLibraries;
foreach (var library in dependencies)
{
if (IsCandidateLibrary(library, name))
{
return context.LoadFromAssemblyName(new AssemblyName(library.Name));
}
}
var foundDlls = Directory.GetFileSystemEntries(new FileInfo(<YOUR_PATH_HERE>).FullName, name.Name + ".dll", SearchOption.AllDirectories);
if (foundDlls.Any())
{
return context.LoadFromAssemblyPath(foundDlls[0]);
}
return context.LoadFromAssemblyName(name);
};
}
private static bool IsCandidateLibrary(RuntimeLibrary library, AssemblyName assemblyName)
{
return (library.Name == (assemblyName.Name))
|| (library.Dependencies.Any(d => d.Name.StartsWith(assemblyName.Name)));
}
The IsCandidateLibrary() bit originates from there:
http://www.michael-whelan.net/replacing-appdomain-in-dotnet-core/
I think you could omit this and the whole DependencyContext part, but it acts as a cache and avoids reloading the same assemblies over and over again. So i kept it.
There is a great enhancement in .Net Core 3.0+, wire AssemblyLoadContext.Default.Resolving event as given below and all dependencies will be resolved and loaded:
AssemblyLoadContext.Default.Resolving += (context, name) => {
string assemblyPath = $"{pluginFolder}\\{name.Name}.dll";
if (assemblyPath != null)
return context.LoadFromAssemblyPath(assemblyPath);
return null;
};
Remember to define the variabe pluginFolder
Solution2
You can use the AssemblyDependencyResolver class and resolve dependendencies including ones in .deps.json:
var resolver = new AssemblyDependencyResolver(pluginPath);
AssemblyLoadContext.Default.Resolving += (context, name) => {
string assemblyPath = resolver.ResolveAssemblyToPath(name);
if (assemblyPath != null)
return context.LoadFromAssemblyPath(assemblyPath);
return null;
};

Manipulating files block them

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 :)

Dynamically load assemblies in ASP.NET 5

I used to have some code which scanned the bin directory of my application for assemblies which weren't loaded in the AppDomain yet and loaded them. It basically looked like:
foreach (var assemblyPath in Directory.GetFiles("path\to\bin", "*.dll"))
{
var inspected = Assembly.ReflectionOnlyLoadFrom(assemblyPath);
Assembly.Load(inspected.GetName());
}
I skipped the try/catch clauses, etc for brevity.
This allowed me to drop assemblies in the bin folder at run-time with implementations for certain interfaces and let the IoC container pick them up automatically. Now with the new Roslyn magic, there are no physical DLL's anymore when debugging. Is there any way to retrieve assembly names, project names or dependency names (in project.json) dynamically.
I guess I have to implement something like this example in the Entropy repo, but I don't know how to implement it for my scenario.
You can use the IAssemblyLoadContextAccessor interface to load ASP.NET 5 class library (.xproj) projects dynamically. The following example code works with Beta 4:
public class Startup
{
public void Configure(IApplicationBuilder app)
{
var assemblyLoadContextAccessor = app.ApplicationServices.GetService<IAssemblyLoadContextAccessor>();
var loadContext = assemblyLoadContextAccessor.Default;
var loadedAssembly = loadContext.Load("NameOfYourLibrary");
}
}
What you are looking for is ILibraryManager implementation which provides access to the complete graph of dependencies for the application. This is already flowed through the ASP.NET 5 DI system. So, you can reach out to it from there.
Sample usage can be found inside RoslynCompilationService.
I solved this issue partly using the ILibraryManager as suggested by #tugberk. I changed the approach a bit which dropped the need of scanning the bin folder for new assemblies. I just want all the loaded assemblies in the current AppDomain.
I injected an instance of the ILibraryManager interface in my type finder class and used the GetReferencingLibraries() method with the name of the core assembly, which is referenced by all the other assemblies in the application.
A sample implementation can be found here, where this is the important part:
public IEnumerable<Assembly> GetLoadedAssemblies()
{
return _libraryManager.GetReferencingLibraries(_coreAssemblyName.Name)
.SelectMany(info => info.Assemblies)
.Select(info => Assembly.Load(new AssemblyName(info.Name)));
}
For .net core users, here is my code for loading assemblies from a specific path. I had to use directives, as it's slightly different for .Net Framework and .Net Core.
In your class header you'll need to declare the using something similar to:
#if NET46
#else
using System.Runtime.Loader;
#endif
And in your function something similar to the following:
string assemblyPath = "c:\temp\assmebly.dll";
#if NET46
Assembly assembly = Assembly.LoadFrom(assemblyPath);
#else
AssemblyLoadContext context = AssemblyLoadContext.Default;
Assembly assembly = context.LoadFromAssemblyPath(assemblyPath);
#endif
Its not ASP.NET but it can be converted easily to asp.net.
bellow if function for loading an assembly, and invoke a method inside a class on that assembly.
private static FormCustomized loadLayout(global::System.String layoutFilename, global::System.String layoutNameSpace)
{
FormCustomized mainForm = default;
Type typeMainLayout = default;
FileInfo layoutFile;
layoutFile = new FileInfo(layoutFilename);
layoutFile.Refresh();
if (!layoutFile.Exists)
{
MessageBox.Show("Layout file not found. You need to reinstall the program");
return default;
}
try
{
Assembly assemblyRaw = Assembly.LoadFrom(layoutFilename);
AssemblyLoadContext context = AssemblyLoadContext.Default;
Assembly assembly = context.LoadFromAssemblyPath(layoutFilename);
Type typeMainLayoutIni = assembly.GetType(layoutNameSpace + ".initializeLayoutClass");
Object iniClass = Activator.CreateInstance(typeMainLayoutIni, true);
MethodInfo methodInfo = typeMainLayoutIni.GetMethod("AssembliesToLoadAtStart");
enVars.assemblies = (Dictionary<string, Environment.environmentAssembliesClass>)methodInfo.Invoke(iniClass, default);
typeMainLayout = assembly.GetType(layoutNameSpace + ".mainAppLayoutForm");
mainForm = Activator.CreateInstance(typeMainLayout, enVars) as FormCustomized;
}
catch (Exception ex)
{
return default;
}
return default;
}

Reflection not working on assembly that is loaded using Assembly.LoadFrom

I have a library that contains some reflection code which inspects an Asp.Net's primary assembly, any referenced assemblies and does cool stuff. I'm trying to get the same exact code to execute in a console application while still reflecting on an Asp.Net's assemblies and I'm seeing odd results. I've got everything wired up and the code executes, however the reflection code returns false when I know it should be returning true as I'm stepping through it in the debugger.. It's driving me nuts and I can't figure out why reflection is exhibiting different behavior when running from the console app.
Here's a perfect example of some reflection code that gets all of the types that are area registrations in an Asp.Net application (type.IsSubclassOf(typeof(System.Web.Mvc.AreaRegistration))). This returns true for several types when executing in the app domain of an Asp.Net application, however it returns false for those same types when executed under the console application, but still reflecting on those same Asp.Net types.
I've also tried using the Assembly.ReflectionOnlyLoadFrom method but even after writing all the code to manually resolve referenced assemblies the reflection code shown below returns false on types that it should be returning true for.
What can I try to make this work?
public static Assembly EntryAssembly { get; set; } // this is set during runtime if within the Asp.Net domain and set manually when called from the console application.
public CodeGenerator(string entryAssemblyPath = null)
{
if (entryAssemblyPath == null) // running under the Asp.Net domain
EntryAssembly = GetWebEntryAssembly(); // get the Asp.Net main assembly
else
{
// manually load the assembly into the domain via a file path
// e:\inetpub\wwwroot\myAspNetMVCApp\bin\myApp.dll
EntryAssembly = Assembly.LoadFrom(entryAssemblyPath);
}
var areas = GetAreaRegistrations(); // returns zero results under console app domain
... code ...
}
private static List<Type> GetAreaRegistrations()
{
return EntryAssembly.GetTypes().Where(type => type.IsSubclassOf(typeof(System.Web.Mvc.AreaRegistration)) && type.IsPublic).ToList();
}
This has to do with the assembly context in which LoadFrom loads assemblies. Dependencies loaded during LoadFrom will not be used when resolving "regular" assemblies in the Load context.
The same appies the ReflectionOnly overloads, which load into the ReflectionOnly context.
For detailed information see https://stackoverflow.com/a/2493855/292411, and Avoid Assembly.LoadFrom; instead use Assembly.Load for an issue with LoadFrom similar to yours.
When I ran into this issue I switched to using Load and demanded "plugin" assemblies to be in the same path as the executable; I don't know if there are tricks to make things work if the assemblies are in different paths.
Ok, after a lot of debugging I've got this working! It turned out that my library project was compiling against Asp.Net MVC 4.0 even though Nuget and the properties window claimed 5.1. Nuget/MS fail again. The Asp.Net MVC application that my library is reflecting on is using MVC 5.1 so when the Assembly.LoadFrom and the AssemblyResolve event ran it was loading two versions of System.Web.Mvc.dll into the LoadFrom context (4.0 & 5.1) and this caused the IsSubclassOf() method to return false when the expected result should have been true.
The very odd error I mentioned in the comments above while debugging: The type 'System.Web.Mvc.AreaRegistration' exists in both 'System.Web.Mvc.dll' and 'System.Web.Mvc.dll' now makes sense, but only after the fact.
The way I finally tracked this down was by writing out all of the assemblies that AssemblyResolve was called upon to resolve and noticed that System.Web.Mvc.dll was not in the list. I fired up the Assembly Binding Log Viewer and was clearly able to see that System.Web.Mvc.dll was being loaded twice.
In retrospect, one should just skip all the custom logging and just use the Assembly Binding Log Viewer to verify only one of each assembly is being loaded and that it's the correct version your expecting.
Figuring out how to use AssemblyResolve properly was a nightmare so here is my unfinished, but working code for posterity.
public class CodeGenerator
{
public static string BaseDirectory { get; set; }
public static string BinDirectory { get; set; }
static CodeGenerator()
{
BinDirectory = "bin";
// setting this in a static constructor is best practice
AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve;
}
public CodeGenerator(string entryAssemblyPath = null, string baseDirectory = null, string binDirectory = null)
{
if (string.IsNullOrWhiteSpace(baseDirectory))
BaseDirectory = AppDomain.CurrentDomain.BaseDirectory;
else
BaseDirectory = baseDirectory;
if (string.IsNullOrWhiteSpace(binDirectory) == false)
BinDirectory = binDirectory;
if (entryAssemblyPath == null) // running under the Asp.Net domain
EntryAssembly = GetWebEntryAssembly(); // get the Asp.Net main assembly
else
{
// manually load the assembly into the domain via a file path
// e:\inetpub\wwwroot\myAspNetMVCApp\bin\myApp.dll
EntryAssembly = Assembly.LoadFrom(entryAssemblyPath);
}
var areas = GetAreaRegistrations(); // reflect away!
... code ...
}
static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
{
try
{
if (args == null || string.IsNullOrWhiteSpace(args.Name))
{
Logger.WriteLine("cannot determine assembly name!", Logger.LogType.Debug);
return null;
}
AssemblyName assemblyNameToLookFor = new AssemblyName(args.Name);
Logger.WriteLine("FullName is {0}", Logger.LogType.Debug, assemblyNameToLookFor.FullName);
// don't load the same assembly twice!
var domainAssemblies = AppDomain.CurrentDomain.GetAssemblies();
var skipLoading = false;
foreach (var dAssembly in domainAssemblies)
{
if (dAssembly.FullName.Equals(assemblyNameToLookFor.FullName))
{
skipLoading = true;
Logger.WriteLine("skipping {0} because its already loaded into the domain", Logger.LogType.Error, assemblyNameToLookFor.FullName);
break;
}
}
if (skipLoading == false)
{
var requestedFilePath = Path.Combine(Path.Combine(BaseDirectory, BinDirectory), assemblyNameToLookFor.Name + ".dll");
Logger.WriteLine("looking for {0}...", Logger.LogType.Warning, requestedFilePath);
if (File.Exists(requestedFilePath))
{
try
{
Assembly assembly = Assembly.LoadFrom(requestedFilePath);
if (assembly != null)
Logger.WriteLine("loaded {0} successfully!", Logger.LogType.Success, requestedFilePath);
// todo: write an else to handle load failure and search various probe paths in a loop
return assembly;
}
catch (FileNotFoundException)
{
Logger.WriteLine("failed to load {0}", Logger.LogType.Error, requestedFilePath);
}
}
else
{
try
{
// ugh, hard-coding, but I need to get on with the real programming for now
var refedAssembliesPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), #"Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5.1");
requestedFilePath = Path.Combine(refedAssembliesPath, assemblyNameToLookFor.Name + ".dll");
Logger.WriteLine("looking for {0}...", Logger.LogType.Warning, requestedFilePath);
Assembly assembly = Assembly.LoadFrom(requestedFilePath);
if (assembly != null)
Logger.WriteLine("loaded {0} successfully!", Logger.LogType.Success, requestedFilePath);
// todo: write an else to handle load failure and search various probe paths in a loop
return assembly;
}
catch (FileNotFoundException)
{
Logger.WriteLine("failed to load {0}", Logger.LogType.Error, requestedFilePath);
}
}
}
}
catch (Exception e)
{
Logger.WriteLine("exception {0}", Logger.LogType.Error, e.Message);
}
return null;
}
}

Using MEF to load DLLs with embedded libraries

I am currently writing an application suite with a plugin system that loads plugins at runtime using the MEF framework.
I have currently setup one of my top level WPF applications to embed it's referenced DLLs as embedded resources and load them at runtime using the method described here.
This works fine and I get my single file WPF application that runs fine.
However, another of my top level console applications uses the MEF framework to load plugins at runtime (the WPF application is fixed and includes the plugins explicitly). My plugins have several dependencies themselves on various libraries and the extensions folder that the console application loads the plugins from is littered with all the various library dlls.
I would like to embed the dependencies of each plugin within itself so that my extensions directory contains only the top level DLL files. The method that I have used above does not cater for this approach as the plugin component cannot find the required dependency as it is only the executing assembly that is being searched for these embedded resources.
My current OnResolveAssembly method looks like this:
public static Assembly OnResolveAssembly(object sender, ResolveEventArgs args)
{
Assembly executingAssembly = Assembly.GetExecutingAssembly();
var assemblyName = new AssemblyName(args.Name);
string path = assemblyName.Name + ".dll";
if (assemblyName.CultureInfo.Equals(CultureInfo.InvariantCulture) == false)
{
path = String.Format(#"{0}\{1}", assemblyName.CultureInfo, path);
}
using (Stream stream = executingAssembly.GetManifestResourceStream(path))
{
if (stream == null)
return null;
var assemblyRawBytes = new byte[stream.Length];
stream.Read(assemblyRawBytes, 0, assemblyRawBytes.Length);
return Assembly.Load(assemblyRawBytes);
}
}
I'm thinking that the best way to proceed would be to add in functionality to keep track of all assemblies loaded in a list and once a new assembly has been loaded in this way, recursively do the same; load any embedded DLLs within those as you go. You can then add these DLLs to the list which will act as a cache.
Is there perhaps a better way to proceed with this?
I have implemented a very similar solution to yours and it works very fine for me. As you can see I keep track of already loaded assemblies in a _references dictionary.
In my case, I do not need to "eagerly" load all embedded dependencies in any recursive way, but rather my embedded assemblies do register themselves with the application host on-demand.
public static class ApplicationHost
{
private static readonly Dictionary<string, Assembly> _references = new Dictionary<string, Assembly>();
[STAThread]
private static void Main()
{
AppDomain.CurrentDomain.AssemblyResolve += (sender, args) => _references.ContainsKey(args.Name) ? _references[args.Name] : null;
RegisterAssemblyAndEmbeddedDependencies();
// continue application bootstrapping...
}
public static void RegisterAssemblyAndEmbeddedDependencies()
{
var assembly = Assembly.GetCallingAssembly();
_references[assembly.FullName] = assembly;
foreach (var resourceName in assembly.GetManifestResourceNames())
{
using (var resourceStream = assembly.GetManifestResourceStream(resourceName))
{
var rawAssembly = new byte[resourceStream.Length];
resourceStream.Read(rawAssembly, 0, rawAssembly.Length);
var reference = Assembly.Load(rawAssembly);
_references[reference.FullName] = reference;
}
}
}
}

Categories

Resources