Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / programming / exceptions

Inter-Application Domain Communication

5.00/5 (1 vote)
8 Jul 2013CPOL3 min read 21.4K  
Use the AppDomain to load another .NET assembly version.

Introduction

Sometimes in our applications, we want to load the assembly dynamically at runtime. What we can do is to use the Assembly.Load method. But there are some cases that may cause compatibility issues. Fox example, your main application uses a version of UnityContainer and the assembly you want to load dynamically uses another version of UnityContainer. This will result in failing to load the assembly.

Solution

There are several solutions for this:

  1. A WCF service hosted on another process: since different processes allow you to load different versions of assemblies.
  2. Strong-named assemblies: official .NET technique to solve this issue.
  3. New AppDomain: since different AppDomains can isolate types and assemblies.

Solution 1 looks like a little bit of an overkill for a simple case.

For solution 2, it required to sign all assemblies and register them in the GAC. In our case, we want to avoid the overhead it introduces.

Solution 3 is a lightweight approach. We will discuss how to implement it in this article.

C#
var domain = AppDomain.CreateDomain("TestService");
var tester = (ITester)domain.CreateInstanceAndUnwrap(
    typeof(Tester).Assembly.FullName,
    typeof(Tester).FullName);

You can also specify the default assembly search path by:

C#
var setup = new AppDomainSetup { ApplicationBase = GetPath() };
var domain = AppDomain.CreateDomain("TestService", null, setup);
var tester = (ITester)domain.CreateInstanceAndUnwrap(
   typeof(Tester).Assembly.FullName,
   typeof(Tester).FullName);

Intra-Process Remoting

In order to invoke the type instance in the other AppDomain, we need to let the type inherit from MarshalByRefObject.

C#
public class Tester : MarshalByRefObject, ITester

.NET will create a remoting proxy for you automatically. So you can use the type as normal. But the type instantiated in the remote domain is dependent on the client’s domain garbage collector. If the client domain crashes, the remote object won’t get released. So .NET provides a mechanism to avoid this. The remote object will self-destruct after five minutes of non-use. Since in our case the client runs in the default AppDomain, when it ends, the whole process will end too. So we can use the following code to disable the default self destructive behavior. Within the class, we can override InitializeLifetimeService and return null:

C#
public override object InitializeLifetimeService()
{
    return null;
}

For reference types, we can inherit from MarshalByRefObject. But how about those value types? Actually, most value types are remotable by default. Such as int, string, enum, etc. But for struct type, we need to mark it as Serializable.

C#
[Serializable]
public struct Result
{
    public string Outcome;
} 

Besides the delegates are Serializable too. Actually, we can mark the reference type with Serializable as well. It will also support the remoting operation. But the runtime semantics are different. With MarshalByRefObject, the client code will call the remote object via the proxy and the remote object runs in the new AppDomain. But with Serializable applied to all types, the serialization process will create a new instance in the client AppDomain. Which means all subsequent calls will be executed in the client AppDomain.

Exception Handling

To avoid marking all types as Serializable, we choose the Marshalling approach. For value types, mark them as Serializable. It’s working to let the AppDomains communicate with each other. In the case where an exception is thrown, we may need some special handling for making the exception Serializable. For a system defined exception, .NET already handles that. Nothing is needed to make them Serializable. But for user defined exceptions, we need to handle them: if the exceptions do not have a custom property, what we need to do is to mark them as Serializable. For example, let’s say we define an exception called RetryableException.

C#
[Serializable]
[ComVisible(true)]
public class RetryableException : Exception
{
    public RetryableException(){}

    public RetryableException(string message) : base(message) { }

    public RetryableException(string message, Exception ex) : base(message, ex)  { }

    [SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)]
    // Constructor should be protected for unsealed classes, private for sealed classes.
    // (The Serializer invokes this constructor through reflection, so it can be private)
    protected RetryableException(SerializationInfo info,
        StreamingContext context)
        : base(info, context)
    {
    }
}

If the exceptions has a custom property, we need to add some implementation in the serializer invoked constructor and override

GetObjectData(SerializationInfo info, 
StreamingContext context)
which is defined in the ISerializable interface.

C#
[Serializable]
[ComVisible(true)]
public class RetryableException : Exception
{
    public Action RetryTask { get; set; }
    public Exception Exception { get; set; }
    public string Name { get; set; }

    public RetryableException(){}

    public RetryableException(string message) : base(message) { }

    public RetryableException(string message, Exception ex) : base(message, ex)  { }

    public RetryableException(Action retry, Exception e, string name)
    {
        RetryTask = retry;
        Exception = e;
        Name = name;
    }

    [SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)]
    // Constructor should be protected for unsealed classes, private for sealed classes.
    // (The Serializer invokes this constructor through reflection, so it can be private)
    protected RetryableException(SerializationInfo info,
        StreamingContext context)
        : base(info, context)
    {
        Exception = (Exception)info.GetValue("Exception", typeof(Exception));
        RetryTask = (Action)info.GetValue("RetryTask", typeof(Action));
        Name = info.GetString("Name");
    }

    [SecurityPermissionAttribute(SecurityAction.Demand, SerializationFormatter = true)]
    public override void GetObjectData(SerializationInfo info, StreamingContext context)
    {
        if (info == null)
        {
            throw new ArgumentNullException("info");
        }

        info.AddValue("Exception", Exception, typeof(Exception));
        info.AddValue("RetryTask", RetryTask, typeof(Action));
        info.AddValue("Name", Name);

        // MUST call through to the base class to let it save its own state
        base.GetObjectData(info, context);
    }
}

[Serializable] and ISerializable

Be aware that [Serializable] and ISerializable are not inheritable. Which means no matter which class the defined class inherits from, if you want it to be Serializable, you still need to mark it as Serializable. Even if it implements the ISerializable interface.

Points of Interest

In this article, we discussed how to use an AppDomain to load another .NET assembly version which already loads in the default AppDomain. And we also discussed about the differences between applying MarshalByRefObject and [Serializable] in order to support remoting.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)