3

I'm trying to build a small tool for comparing types in a bunch of assemblies. For this purpose I created two subfolders and put the respective dlls there:

  • ..\Dlls\v1.1
  • ..\Dlls\v1.2

where .. is the application folder

I also created a proxy object:

public class ProxyDomain : MarshalByRefObject
{
    public Assembly LoadFile(string assemblyPath)
    {
        try
        {
            //Debug.WriteLine("CurrentDomain = " + AppDomain.CurrentDomain.FriendlyName);
            return Assembly.LoadFile(assemblyPath);
        }
        catch (FileNotFoundException)
        {
            return null;
        }
    }
}

and used it for loading in the following routine that should load an dll and get all types declared in it:

private static HashSet<Type> LoadAssemblies(string version)
{
    _currentVersion = version;

    var path = Path.Combine(Environment.CurrentDirectory, Path.Combine(_assemblyDirectory, version));

    var appDomainSetup = new AppDomainSetup
    {
        ApplicationBase = Environment.CurrentDirectory,     
    };

    var evidence = AppDomain.CurrentDomain.Evidence;
    var appDomain = AppDomain.CreateDomain("AppDomain" + version, evidence, appDomainSetup);           

    var proxyDomainType = typeof(ProxyDomain);
    var proxyDomain = (ProxyDomain)appDomain.CreateInstanceAndUnwrap(proxyDomainType.Assembly.FullName, proxyDomainType.FullName);
    _currentProxyDomain = proxyDomain;
    
    var assemblies = new HashSet<Type>();
    
    var files = Directory.GetFiles(path, "*.dll");

    foreach (var file in files)
    {
        try
        {
            var assembly = proxyDomain.LoadFile(file);
            if (assembly != null)
            {
                assemblies.UnionWith(assembly.DefinedTypes.Where(t => t.IsPublic));
            }
        }
        catch (Exception)
        {
        }
    }
    return assemblies;
}

So far nothing unusual... but it didn't work like that in this case (probably because of the subfolders) so I searched a bit and found that a settting in the app.config might help so I tried to add two probing paths:

<runtime>
  <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
    <probing privatePath="Dlls\v1.1;Dlls\v1.2" />
  </assemblyBinding>
</runtime>

Now there wasn't any FileNotFoundExpections anymore but as the dlls have the same names it loaded only dlls from the first subdirectory (v1.1) into both domains so I removed it and instead tried to implement the AppDomain.CurrentDomain.AssemblyResolve event handler like that:

AppDomain.CurrentDomain.AssemblyResolve += (sender, e) =>
{
    var path = Path.Combine(Environment.CurrentDirectory, Path.Combine(_assemblyDirectory, _currentVersion));
    path = Path.Combine(path, e.Name.Split(',').First());
    path = path + ".dll";
    var assembly = _currentProxyDomain.LoadFile(path);
    return assembly;
};

But unfortunatelly with this event handler I created an infinite loop and it calls itself each time it tries to load a dll it cannot find. I have no more ideas what else I could try.


UPDATE-1

As @SimonMourier suggested I tried to use a custom .config for my new AppDomains and created two more *.configs like:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <startup>
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
  </startup>
  <runtime>
    <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
      <probing privatePath="Dlls\v1.1" />
    </assemblyBinding>
  </runtime>
</configuration>

I named them v1.1.config and v1.2.config. Then I set the new ConfigurationFile property:

var appDomainSetup = new AppDomainSetup
{
    ApplicationBase = Environment.CurrentDirectory,
    ConfigurationFile = Path.Combine(Environment.CurrentDirectory, string.Format("{0}.config", version)),        
};

I've set the option Copy to Output Directory to Copy always ;-)

It didn't work so I googled and tried another suggestion:

AppDomain.CurrentDomain.SetData("APP_CONFIG_FILE", appDomainSetup.ConfigurationFile);

but this didn't help either. Still FileNotFoundException like my custom configs weren't there.

Using the SetConfigurationBytes method instead had also no effect:

var domainConfig = @"
    <configuration>
        <startup>
            <supportedRuntime version=""v4.0"" sku="".NETFramework,Version=v4.5"" />
        </startup>
        <runtime>
            <assemblyBinding xmlns=""urn:schemas-microsoft-com:asm.v1"">
              <probing privatePath=""Dlls\{0}"" />
            </assemblyBinding>
        </runtime>
    </configuration>";

domainConfig = string.Format(domainConfig, version).Trim();
var probingBytes = Encoding.UTF8.GetBytes(domainConfig);
appDomainSetup.SetConfigurationBytes(probingBytes);

However if I call the GetData method the new appdomain uses the custom .config:

Debug.WriteLine("Current config: " + AppDomain.CurrentDomain.GetData("APP_CONFIG_FILE"));

It outputs the path that I set via ConfigurationFile


UPDATE-2

This is really confusing. The Stack Trace reveals that despite of what the GetData returs the Assembly.LoadFile still uses the original .config:

=== LOG: This bind starts in default load context. LOG: Using application configuration file:

C:[...]\bin\Debug\MyApp.exe.Config

LOG: Using host configuration file: LOG: Using machine configuration file from

C:\Windows\Microsoft.NET\Framework\v4.0.30319\config\machine.config.


UPDATE-3

OK, I did some more experimenting and found out that by implementing @SimonMourier suggestion it indeed worked. The FileNotFoundException wasn't thrown by the LoadFile method in the ProxyDomain class but in the Main method of my app. I gues the assembly and the types are allowed to live only within the ProxyDomain context and cannot be transfered into the main domain as I have tried.

public IEnumerable<Type> LoadFile(string assemblyPath)
{
    //try
    {
        // does't throw any exceptions
        var assembly = Assembly.LoadFile(assemblyPath);
        // returning the assembly itself or its types will throw an exception in the main application
        return assembly.DefinedTypes;
    }
    //catch (FileNotFoundException)
    {
      //  return null;
    }
}

Method in the main domain:

private static HashSet<Type> LoadAssemblies(string version)
{
    // omitted

    foreach (var file in files)
    {
        //try
        {

            // the exception occurs here, when transfering types between domains:
            var types = proxyDomain.LoadFile(file);         
            assemblies.UnionWith(types.Where(t => t.IsPublic));
        }
    }

    // omitted
}

I'll need to rewrite the comparison algorithm now by at least the assemblies can be loaded ;-)

Community
  • 1
  • 1
t3chb0t
  • 16,340
  • 13
  • 78
  • 118
  • @SimonMourier if you post your comment as an answer I can accept it ;-) You may also find the 3rd update interesing explaining why I thought I wouldn't work at first. – t3chb0t Sep 30 '15 at 09:16

2 Answers2

3

If you don't want to recompile the assemblies with a different strong name, than you can load them into different AppDomains, with different setup configuration, and different .config files, exactly like IIS does with multiple web sites, each one in its one AppDomain, completely independent, all hosted in only one AppPool process - w3wp.exe.

Simon Mourier
  • 132,049
  • 21
  • 248
  • 298
0

Strong name your dll . Make 2 exe to run separate app domain.

Otherwise there is no way to reference same dll name twice. You have to rename new dll with _new.dll and then you can use it with fully qualified names pace class method name format.

Meaning rename all v2 dll

Jin Thakur
  • 2,711
  • 18
  • 15
  • Those are third party dlls and I had to recompile them to create strong names. I'd rather avoid this. I also don't want to reference those dlls but just load them programmatically into two additional domains that I create for this purpose because like you've said, it's not possible to load more then one dll with the same name into the same domain. I also think renaming wouldn't help because the dlls (about 80) reference each other so I have to brute force load them. – t3chb0t Sep 30 '15 at 06:07
  • Loading them into two different domains (with different .config) seems a decent idea if you don't want to recompile – Simon Mourier Sep 30 '15 at 06:31
  • @SimonMourier I tried your suggestion, unfortunatelly without success :-( See my UPDATE-1 to the question. – t3chb0t Sep 30 '15 at 07:58