[C#] I wish I knew : RealProxy
I’ve been a professional C# developer for years now, but I’m still discovering trivial stuffs. In “I wish I knew”, I describe a feature that I missed.
The Gang Of Four defined a lot of design patterns. Among them is one that I use quite often: the Proxy pattern. For years I’ve been dreaming of an easy way to automatically create a proxy over any class.
My dream almost came true yesterday, thanks to System.Runtime.Remoting.Proxies.RealProxy
The target class
I’ll be honest from the beginning: the major drawback of RealProxy
is that the target class must derive from MarshalByRefObject
. This is a serious limitation and this is why this solution is far from perfect. But I think it’s good to know that it exists anyway.
Here is my sample class that will be “proxied”:
class Baby : MarshalByRefObject
{
public string Name
{
get { return "Annabelle"; }
}
public void Sleep()
{
throw new InvalidOperationException("Teething in progress");
}
}
The proxy class
RealProxy
is an abstract class, you must derive a concrete class and do what you need in the proxy. Every call to the target class will pass through your proxy class first.
In this article, I’ll create a simple proxy that logs the invocation to the console, but you could do many other things. As you’ll see the proxy isn’t written for a particular target Type
, and can be reused with any other target class (as soon as it derives from MarshalByRefObject
).
First, let’s create that class:
class LoggingProxy : RealProxy
{
readonly object target;
LoggingProxy(object target)
: base(target.GetType())
{
this.target = target;
}
...
}
Then, we need to implement the abstract method Invoke
:
public override IMessage Invoke(IMessage msg)
{
var methodCall = msg as IMethodCallMessage;
if (methodCall != null)
{
return HandleMethodCall(methodCall); // <- see further
}
return null;
}
RealProxy
is designed for remoting purposes, that’s why it deals with messages. In our case, we simply invoke the members through reflection, when an IMethodCallMessage
arrives.
Now, we can do the invocation on the target
instance:
IMessage HandleMethodCall(IMethodCallMessage methodCall)
{
Console.WriteLine("Calling method {0}...", methodCall.MethodName);
try
{
var result = methodCall.MethodBase.Invoke(target, methodCall.InArgs);
Console.WriteLine("Calling {0}... OK", methodCall.MethodName);
return new ReturnMessage(result, null, 0, methodCall.LogicalCallContext, methodCall);
}
catch(TargetInvocationException invocationException)
{
var exception = invocationException.InnerException;
Console.WriteLine("Calling {0}... {1}", methodCall.MethodName, exception.GetType());
return new ReturnMessage(exception, methodCall);
}
}
As you can see, this is nothing more than a call to MethodInfo.Invoke()
surrounded by a try
/catch
.
Finally, let’s add a helper method to make proxy creation easier:
public static T Wrap<T>(T target)
where T : MarshalByRefObject
{
return (T) new LoggingProxy(target).GetTransparentProxy();
}
Here, GetTransparentProxy()
is a public method of RealProxy
that returns the proxy instance. This is where the magic happens!
The sample program
Now let’s try this wonder!
class Program
{
static void Main(string[] args)
{
var baby = LoggingProxy.Wrap(new Baby());
Console.WriteLine("Name = {0}", baby.Name);
try
{
baby.Sleep();
}
catch (Exception e)
{
Console.WriteLine("Exception = {0}", e.Message);
}
}
}
That’s all!
You just need to pass the target instance to Wrap()
and it returns a proxy that implements all the public members of the target class.
Here is the console output:
Calling method get_Name...
Calling get_Name... OK
Name = Annabelle
Calling method Sleep...
Calling Sleep... System.InvalidOperationException
Exception = Teething in progress
Conclusion
I not sure I could ever accept the requirement of making the target class derive from MarshalByRefObject
. Too bad, because it’s a very neat solution.
Please note another limitation: I didn’t try, but I guess methods with out
or ref
would be difficult to handle in the Invoke()
implementation.
Finally, here are a few applications that immediately come to mind:
- Logging (we just saw an example)
- Profiling (just use a
Stopwatch
) - Concurrency (surround invocations with
lock
)
Thanks Franck for sharing this trick ;-)