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:
- A WCF service hosted on another process: since different processes allow you
to load different versions of assemblies.
- Strong-named assemblies: official .NET technique to solve this issue.
- 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.
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:
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
.
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
:
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
.
[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
.
[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)]
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.
[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)]
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);
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.