Assembly.Load and FileNotFoundException

September 25th, 2012 by

I’m experimenting with AppDomains to be able to load multiple versions of the same assembly in one application. I’ll write a bigger post about my findings later, but there’s one thing I encountered very early in the experiments: using Assembly.Load on a newly created AppDomain immediately leads to a FileNotFoundException. It took me a while to figure out why that happened and I’d like to share my experience with you.

This does not work

Let me first show you what didn’t work:

using System;
using System.IO;
using AppDomainTest.Crm4.Interfaces;

namespace AppDomainTest.Application
{
    class Program
    {
        static void Main(string[] args)
        {
            // Create a new AppDomainSetup with its ApplicationBase set to the
            // subdirectory "Crm4" of the main appliation's ApplicationBase.
            AppDomainSetup adSetup = new AppDomainSetup();
            adSetup.ApplicationBase =
                Path.Combine(AppDomain.CurrentDomain.SetupInformation.ApplicationBase, "Crm4");

            // Create the new AppDomain
            AppDomain crm4AppDomain = AppDomain.CreateDomain("Crm4Service", null, adSetup);

            // Try to load the assembly "AppDomainTest.Crm4.Service.dll"
            // from the subfolder "Crm4".
            // This will immediately lead to a FileNotFoundException...
            crm4AppDomain.Load("AppDomainTest.Crm4.Service");
        }
    }
}

Which resulted in: Could not load file or assembly 'AppDomainTest.Crm4.Service, Version=1.0.4645.27468, Culture=neutral, PublicKeyToken=null' or one of its dependencies. The system cannot find the file specified.

Wait a sec? I didn’t provide a version, culture or public key token? The only way it can give me this FileNotFoundException, is by actually loading an assembly it is telling me it can’t find!? How’s that possible?

Well, it turns out I’m hiding something from myself. Let me give you the none-working code again, with this little bit extra added…

using System;
using System.IO;
using AppDomainTest.Crm4.Interfaces;

namespace AppDomainTest.Application
{
    class Program
    {
        static void Main(string[] args)
        {
            // Create a new AppDomainSetup with its ApplicationBase set to the
            // subdirectory "Crm4" of the main appliation's ApplicationBase.
            AppDomainSetup adSetup = new AppDomainSetup();
            adSetup.ApplicationBase =
                Path.Combine(AppDomain.CurrentDomain.SetupInformation.ApplicationBase, "Crm4");

            // Create the new AppDomain
            AppDomain crm4AppDomain = AppDomain.CreateDomain("Crm4Service", null, adSetup);

            // Try to load the assembly "AppDomainTest.Crm4.Service.dll"
            // from the subfolder "Crm4".
            // This will immediately lead to a FileNotFoundException...
            Assembly crm4ServiceAssembly = crm4AppDomain.Load("AppDomainTest.Crm4.Service");
        }
    }
}

Explanation

Spot the difference? AppDomain.Load returns an Assembly and that’s where all goes wrong. Two AppDomains can’t just throw stuff at each other. The entire reason AppDomains exist is to be able to “sandbox” certain functionality within one application. Communication between AppDomains happens (almost) transparently to the user (the programmer…) using channels and proxies, but not entirely. You need to be aware that you can either pass objects by value (they need to implement ISerialize or be declared Serializable) to another AppDomain, or by reference, in which case the class needs to extend MarshalByRefObj.

Is an Assembly a MarshalByRefObj? No, but it is marked Serializable and implements ISerializable:

[SerializableAttribute]
[ClassInterfaceAttribute(ClassInterfaceType.None)]
[ComVisibleAttribute(true)]
[PermissionSetAttribute(SecurityAction.InheritanceDemand, Unrestricted = true)]
public abstract class Assembly : _Assembly, 
	IEvidenceFactory, ICustomAttributeProvider, ISerializable

So what I think happens is the following:

  • crm4AppDomain.Load("AppDomainTest.Crm4.Service") is called and successfully loads the requested assembly in the new AppDomain.
  • After it has loaded the assembly (and its dependencies), it returns the loaded Assembly.
  • The Assembly has to cross the boundary between the two AppDomains. The Remoting logic inspects the object that needs to cross: does it extend MarshalByRefObj? No, but does it implement ISerializable? Yes! Serialize the Assembly and push the result to the other AppDomain.
  • The other AppDomain receives the serialized Assembly (which probably is just the FQN of the assembly) and tries to deserialize it. Deserializing an Assembly probably comes down to loading it using its FQN.
  • This other AppDomain (the main AppDomain) can’t find the assembly (which is good, separation works!) and throws a FileNotFoundException. Bummer.

One solution

What can you do? Well, you probably have a main point of entry for the functionality in the assembly you want to load in another AppDomain. What worked best for me was to define an interface (IService in my case) in a separate assembly, which is known to both the main application and the external assembly I want to load in the separate AppDomain. This is the shared knowledge.

namespace AppDomainTest.Common.Interfaces
{
    public interface IService
    {
        void Setup();
    }
}

Next, I defined an actual Service class which implements the IService interface and extends the MarshalByRefObj class. So, both the main application and the external assembly know about the interface, but they don’t know about each other. Great separation.

using System;
using AppDomainTest.Common.Interfaces;

namespace AppDomainTest.Crm4.Service
{
    public class Service : MarshalByRefObject, IService
    {
        #region IService Members

        public void Setup()
        {
            Console.WriteLine("Crm4.Service AppDomain: {0}", AppDomain.CurrentDomain.FriendlyName);
        }

        #endregion
    }
}

Finally, there’s a convenience method on AppDomain, called AppDomain.CreateInstanceAndUnwrap. It takes the AssemblyName as a first argument and the TypeName as a second (including namespace). So, I call IService crm4Service = (IService)crm4AppDomain.CreateInstanceAndUnwrap("AppDomainTest.Crm4.Service", "AppDomainTest.Crm4.Service.Service"); from the main AppDomain and everything is done for me in the new AppDomain: loading the assembly and its dependencies, instantiating a Setup object, returning it to the main AppDomain (as a transparent proxy object).

using System;
using System.IO;
using AppDomainTest.Crm4.Interfaces;

namespace AppDomainTest.Application
{
    class Program
    {
        static void Main(string[] args)
        {
            // Create a new AppDomainSetup with its ApplicationBase set to the
            // subdirectory "Crm4" of the main appliation's ApplicationBase.
            AppDomainSetup adSetup = new AppDomainSetup();
            adSetup.ApplicationBase =
                Path.Combine(AppDomain.CurrentDomain.SetupInformation.ApplicationBase, "Crm4");

            // Create the new AppDomain
            AppDomain crm4AppDomain = AppDomain.CreateDomain("Crm4Service", null, adSetup);

            // Instantiate the Service type in the remote AppDomain and get a handle.
            IService crm4Service = (IService)crm4AppDomain.CreateInstanceAndUnwrap("AppDomainTest.Crm4.Service", "AppDomainTest.Crm4.Service.Service");
        }
    }
}

So, that’s it. I’ve put my experiments on Github. I’m actually working on a WCF service that needs to service both a Dynamics CRM 4 and CRM 2011 instance and I ran into trouble getting it to work with the same assembly-names but differing versions.

2 Responses to “Assembly.Load and FileNotFoundException”

  1. eg01st Says:

    Hi! I’ve the same problem in my applictaion with plugins. Unfortunatelly, your solution will works only if all people, who write plugins will give name “Service” to class whith imlement IService. And when someone will name it like “MyService” and implement IService – he doesn’t get what he expected. I meen, that the main application doesn’t have to know anything about plugins, only that they implement IService.

  2. Dewey Says:

    Hi Thijs, thanks, I ran into a similar issue and your post came up in my searching.

    Just to note, you may prefer not to have each guest object subclass the MarshalByRef class (e.g., you’re trying use code authored by others or the object lives in its own hierarchy and so can’t subclass MarshalByRef). Instead, you can use a proxying object loaded from the host assembly via CreateInstanceFrom or CreateInstanceFromAndUnwrap.

    This may help:

    http://msdn.microsoft.com/en-us/library/bb763046(v=vs.110).aspx

Leave a Reply