2

I am creating a console app which will look in a specified directory to find and load all assemblies (dll files) which implement a given IRqServiceWorker interface, which simply defines a void DoWork(string params) function. The problem is this directory will contain tons of assembly files, most of which do not implement the interface, so I don't want them all loaded into memory. So basically, I first want to find which assemblies implement the interface, save those to a list, and then load up all assemblies in that list.

For my first attempt, I simply just loaded every assembly, checked if it implemented my interface, saved those to a list, and then then created an instance of each class and call the DoWork() function on it. Here is the code to do that:

static void Main(string[] args)
{
    var assembliesRootDirectory = Path.GetFullPath(@"..\..\..\..\TestFiles\ServiceWorkerAssemblies");
    var rqServiceWorkerAssemblyFilePaths = new List<string>();
    var rqServiceWorkerInstances = new List<IRqServiceWorker>();

    // Load all assemblies and record the ones that implement our IRqServiceWorker interface.
    foreach (var assemblyFilePath in Directory.GetFiles(assembliesRootDirectory, "*.dll", SearchOption.AllDirectories))
    {
        try
        {
            // If this assembly contains a class that implements IRqServiceWorker, add it to our list.
            var assembly = Assembly.LoadFrom(assemblyFilePath);
            if (assembly.GetTypes().Any(t => t.IsClass && t.GetInterfaces().Contains(typeof(IRqServiceWorker))))
                rqServiceWorkerAssemblyFilePaths.Add(assemblyFilePath);
        }
        catch (Exception ex)
        { }
    }

    // Create an instance of every class that implements IRqServiceWorker.
    foreach (var assemblyFilePath in rqServiceWorkerAssemblyFilePaths)
    {
        try
        {
            // Get an instance of all types in this assembly that implement IRqServiceWorker and have a parameterless constructor.
            var assembly = Assembly.LoadFile(assemblyFilePath);
            var rqServiceWorkerTypes = assembly.GetTypes().Where(t => t.IsClass && t.GetInterfaces().Contains(typeof(IRqServiceWorker)) && t.GetConstructor(Type.EmptyTypes) != null);
            rqServiceWorkerInstances.AddRange(rqServiceWorkerTypes.Select(t => Activator.CreateInstance(t) as IRqServiceWorker).Where(i => i != null));
        }
        catch (Exception ex)
        { }
    }

    // Call the DoWork function on every class that implements IRqServiceWorker.
    foreach (var instance in rqServiceWorkerInstances)
    {
        try
        {
            instance.DoWork(assembliesRootDirectory);
        }
        catch (Exception ex)
        { }
    }
}

This works, but it ends up loading EVERY assembly into memory, so it quickly bloats the memory footprint of my app. Since an assembly cannot be unloaded, the proper solution seems to be to load the assemblies into a new temporary app domain, create my list of assemblies that implement IRqServiceWorker there, and then unload that temporary app domain to unload all of those extra assemblies from memory. Once I have that list, I can then load only the assemblies I care about using the technique above. Unless there is some other way to check if a dll implements an interface without loading it into an app domain?

I have found many posts that talk about how to load an assembly into a new app domain 1 2 3 4 5 6, but have not been able to get any of them to work.

Here is one method I have tried for loading assemblies into a new app domain:

private static void Main(string[] args)
{
    var assembliesRootDirectory = Path.GetFullPath(@"..\..\..\..\TestFiles\ServiceWorkerAssemblies");
    var rqServiceWorkerAssemblyFilePaths = new List<string>();
    var rqServiceWorkerInstances = new List<IRqServiceWorker>();

    // Create a temporary app domain to load all assemblies into, and then unload it when we are done with it.
    var tempAppDomain = AppDomain.CreateDomain("tempAppDomain");
    foreach (var assemblyFilePath in Directory.GetFiles(assembliesRootDirectory, "*.dll", SearchOption.AllDirectories))
    {
        try
        {
            // Load the assembly into our temporary app domain.
            var tempAssemblyName = AssemblyName.GetAssemblyName(assemblyFilePath);
            var assembly = tempAppDomain.Load(tempAssemblyName);

            // If this assembly contains a class that implements IRqServiceWorker, add it to our list of assemblies to load into the real app domain.
            if (assembly.GetTypes().Any(t => t.IsClass && t.GetInterfaces().Contains(typeof(IRqServiceWorker))))
                rqServiceWorkerAssemblyFilePaths.Add(assemblyFilePath);
        }
        catch (Exception ex)
        {}
    }

    // Unload the temp app domain to release all of the assemblies from memory.
    AppDomain.Unload(tempAppDomain);

    // Force garbage collection of the temp app domain we just unloaded.
    GC.Collect();
    GC.WaitForPendingFinalizers();

    // Load only the assemblies we care about now.....
}

I also tried creating the new app domain using an AppDomainSetup:

var appDomainSetup = new AppDomainSetup();
appDomainSetup.ApplicationName = "temp";
appDomainSetup.ApplicationBase = AppDomain.CurrentDomain.BaseDirectory;
appDomainSetup.PrivateBinPath = assembliesRootDirectory;
appDomainSetup.CachePath = Path.Combine(assembliesRootDirectory, "cache" + Path.DirectorySeparatorChar);
appDomainSetup.ShadowCopyFiles = "true";
appDomainSetup.ShadowCopyDirectories = assembliesRootDirectory;
var tempAppDomain = AppDomain.CreateDomain("tempAppDomain", null, appDomainSetup);

As well as tried using a Proxy that inherits from MarshalByRefObject:

public class ProxyDomain : MarshalByRefObject
{
    public Assembly GetAssembly(string assemblyPath)
    {
        try
        {
            return Assembly.LoadFrom(assemblyPath);
        }
        catch (Exception ex)
        {
            throw;
        }
    }
}

static void Main(string[] args)
{
    var assembliesRootDirectory = Path.GetFullPath(@"..\..\..\..\TestFiles\ServiceWorkerAssemblies");
    var rqServiceWorkerAssemblyFilePaths = new List<string>();
    var rqServiceWorkerInstances = new List<IRqServiceWorker>();

    var domainSetup = new AppDomainSetup { PrivateBinPath = assembliesRootDirectory };
    var tempAppDomain = AppDomain.CreateDomain("temp", AppDomain.CurrentDomain.Evidence, domainSetup);
    foreach (var assemblyFilePath in Directory.GetFiles(assembliesRootDirectory, "*.dll", SearchOption.AllDirectories))
    {
        try
        {
            var domain = (ProxyDomain)(tempAppDomain.CreateInstanceAndUnwrap(typeof(ProxyDomain).Assembly.FullName, typeof(ProxyDomain).FullName));
            var assembly = domain.GetAssembly(assemblyFilePath);

            // If this assembly contains a class that implements IRqServiceWorker, add it to our list of assemblies to load into the real app domain.
            if (assembly.GetTypes().Any(t => t.IsClass && t.GetInterfaces().Contains(typeof(IRqServiceWorker))))
                rqServiceWorkerAssemblyFilePaths.Add(assemblyFilePath);
        }
        catch (Exception ex)
        { }
    }

    // Unload the temp app domain to release all of the assemblies from memory.
    AppDomain.Unload(tempAppDomain);

    // Force garbage collection of the temp app domain we just unloaded.
    GC.Collect();
    GC.WaitForPendingFinalizers();

    // Load only the assemblies we care about now.....
}

But all of these methods throw the following exception on tempAppDomain.Load(tempAssemblyName); and domain.GetAssembly(assemblyFilePath);

{"Could not load file or assembly 'Class1, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null' or one of its dependencies. The system cannot find the file specified.":"Class1, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"}

Class1.dll is the first assembly in the assemblyRootDirectory, and it does implement IRqServiceWorker. I'll note too though that assemblyRootDirectory is outside of my application directory (e.g. my app would be at "C:\MyApp\MyApp.exe" and the assemblies at "C:\Assemblies").

Any thoughts on what I am doing wrong here? Or if there is an easier/better way to see if a dll implements a given interface? Any help/thoughts are appreciated. Thanks.


Answer

Using the information provided in the link in D Stanley's comment, I was able to figure out a solution. His post is about using MEF, which I didn't want to do, but also provides the tidbit I needed, which was to use the Mono.Cecil nuget package. This package is able to read an assembly's information without actually loading it into memory.

Here is the full code for the solution I ended up with:

static void Main(string[] args)
{
    var assembliesRootDirectory = Path.GetFullPath(@"..\..\..\..\TestFiles\ServiceWorkerAssemblies");
    var rqServiceWorkerAssemblyFilePaths = new List<string>();
    var rqServiceWorkerInstances = new List<IRqServiceWorker>();

    var assembliesToNotLoad = new List<string>()
    {
        "RQ.ServiceWorker.Required.dll"
    };

    // Loop through all of the assembly files and record which ones implement IRqServiceWorker.
    foreach (var assemblyFilePath in Directory.GetFiles(assembliesRootDirectory, "*.dll", SearchOption.AllDirectories))
    {
        // Skip processing the RQ.ServiceWorker.Required assembly that contains the IRqServiceWorker interface.
        if (assembliesToNotLoad.Contains(Path.GetFileName(assemblyFilePath)))
            continue;

        try
        {
            // Read the assembly info without loading the assembly into memory, by using the Mono.Cecil package.
            var assemblyDefinition = Mono.Cecil.AssemblyDefinition.ReadAssembly(assemblyFilePath);

            // If this assembly contains a class that implements IRqServiceWorker, add it to our list of assemblies to load.
            if (assemblyDefinition.Modules.Any(m => m.Types.Any(t => t.IsClass && t.Interfaces.Any(i => i.FullName.Equals(typeof(IRqServiceWorker).FullName)))))
                rqServiceWorkerAssemblyFilePaths.Add(assemblyFilePath);
        }
        // Eat all exceptions.
        catch (Exception ex)
        { }
    }

    // Force garbage collection to clear the memory allocated for all of the assembly definitions read above.
    GC.Collect();
    GC.WaitForPendingFinalizers();
    System.Threading.Thread.Sleep(10);  // Sleeping the thread allows the garbage collection to cleanup more garbage sooner for whatever reason.

    // Load all of the assemblies that have a class that implements IRqServiceWorker.
    foreach (var assemblyFilePath in rqServiceWorkerAssemblyFilePaths)
    {
        try
        {
            // Get an instance of all types in this assembly that implement IRqServiceWorker and have a parameterless constructor.
            var assembly = Assembly.LoadFile(assemblyFilePath);
            var rqServiceWorkerTypes = assembly.GetTypes().Where(t => t.IsClass && t.GetInterfaces().Contains(typeof(IRqServiceWorker)) && t.GetConstructor(Type.EmptyTypes) != null);
            rqServiceWorkerInstances.AddRange(rqServiceWorkerTypes.Select(t => Activator.CreateInstance(t) as IRqServiceWorker).Where(i => i != null));
        }
        catch (Exception ex)
        { }
    }

    // Loop over every class that implements IRqServiceWorker and call DoWork() on it.
    foreach (var instance in rqServiceWorkerInstances)
    {
        try
        {
            instance.DoWork(assembliesRootDirectory);
        }
        catch (Exception ex)
        { }
    }
}

So while I didn't solve the problem I was having with trying to load an assembly into a different app domain (i.e. using pure vanilla .Net code), I feel this is a much more elegant solution. Thanks for the help!

Community
  • 1
  • 1
deadlydog
  • 22,611
  • 14
  • 112
  • 118
  • 2
    How about [this answer](http://stackoverflow.com/questions/14619052/how-to-read-mef-metadata-in-a-dll-plugin-without-copying-the-entire-dll-into-mem)? – D Stanley Oct 30 '14 at 18:09

0 Answers0