One rarely used type in the .NET Framework is the RealProxy. It allows us to intercept any invocation done against the generated transparent proxy (retrieved from RealProxy.GetTransparentProxy()). We get complete control over the real invocation, including skipping it, logging it or retrying it. This is, oddly, something I abuse often enough to simplify using a base class.
using System;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Runtime.Remoting.Messaging;
using System.Runtime.Remoting.Proxies;
namespace RealProxyDemo {
public abstract class ProxyBase<T> : RealProxy where T : class {
protected static readonly Type InstanceType = typeof(T);
private readonly T _instance;
protected T Instance {
get { return _instance; }
}
protected ProxyBase(T instance) : base(InstanceType) {
_instance = instance;
}
public override IMessage Invoke(IMessage msg) {
var methodCallMessage = msg as IMethodCallMessage;
if (methodCallMessage != null)
return InvokeMethodCall(methodCallMessage);
var ifaceList = msg.GetType().GetInterfaces().Select(i => i.Name).ToArray();
var messageType = msg.GetType().Name;
var messageInterfaces = String.Join(", ", ifaceList);
throw new NotImplementedException(String.Format(
CultureInfo.InvariantCulture,
"Unknown message type '{0}' was passed to the ProxyBase. It implements {1}, of which none was supported.",
messageType, messageInterfaces
));
}
protected virtual IMethodReturnMessage InvokeMethodCall(IMethodCallMessage msg) {
var args = msg.Args;
try {
var result = InvokeMethodBase(msg.MethodBase, Instance, args);
return new ReturnMessage(result, args, msg.ArgCount, msg.LogicalCallContext, msg);
} catch (TargetInvocationException targetEx) {
var realEx = targetEx.InnerException ?? targetEx;
return new ReturnMessage(realEx, msg);
} catch (Exception ex) {
return new ReturnMessage(ex, msg);
}
}
protected virtual object InvokeMethodBase(MethodBase methodBase, Object subject, Object[] args) {
return methodBase.Invoke(subject, args);
}
}
}
Imagine a scenario where we need to log every method invocation on a IList<T>; we could just create a wrapping object that would log and forward any calls to the wrapped list. However, it would be better with a solution that works for more types than just IList<T>. Here comes a limitation using the RealProxy approach, it only supports interfaces and types derived from MarshalByRefObject.
using System;
using System.Collections.Generic;
using System.Runtime.Remoting.Messaging;
namespace RealProxyDemo {
public static class Program {
public static void Main() {
var someList = new List<String>();
var loggingList = Log<IList<String>>(someList);
loggingList.Add("test1");
loggingList.Add("test2");
foreach(var item in loggingList)
Console.WriteLine("Item: {0}", item);
}
public static T Log<T>(T instance) where T : class {
var loggingProxy = new LoggingProxy<T>(instance);
return (T)loggingProxy.GetTransparentProxy();
}
}
public class LoggingProxy<T> : ProxyBase<T> where T : class {
public LoggingProxy(T instance) : base(instance) {
}
protected override IMethodReturnMessage InvokeMethodCall(IMethodCallMessage msg) {
Console.Write("Calling {0}(", msg.MethodName);
for (var i = 0; i < msg.InArgCount; ++i)
Console.Write("{0}: '{1}'", msg.GetInArgName(i), msg.InArgs[i]);
Console.WriteLine(")");
// Forward the call to actually execute it.
return base.InvokeMethodCall(msg);
}
}
}
Another scenario is when the invocation fails with a specific exception, and it's possible to try and execute it again without introducing any unwanted side effects. This was originally built for a cms that threw an exception when an application closes the login session even if another is still using it. In this case it was possible to execute a call to open a new session, and retry the invocation that originally failed. A much simpler scenario would be our FailAlot class, which fails about 80% of the time.
using System;
using System.Runtime.Remoting.Messaging;
namespace RealProxyDemo {
public static class Program {
public static void Main() {
var failAlot = new FailAlot();
var retryingProxy = new RetryingProxy<IFailAlot>(failAlot);
var retryAlot = (IFailAlot)retryingProxy.GetTransparentProxy();
Console.WriteLine("First: {0}", retryAlot.Try());
Console.WriteLine("Second: {0}", retryAlot.Try());
Console.WriteLine("Third: {0}", retryAlot.Try());
}
}
public interface IFailAlot {
Int32 Try();
}
public class RetryException : Exception { }
public class FailAlot : IFailAlot {
private readonly Random _random = new Random();
private Int32 _tryCount;
public Int32 Try() {
_tryCount++;
// Fail with 80% probability.
if (_random.Next(10) <= 8)
throw new RetryException();
var oldTryCount = _tryCount;
_tryCount = 0;
return oldTryCount;
}
}
public class RetryingProxy<T> : ProxyBase<T> where T : class {
public RetryingProxy(T instance)
: base(instance) {
}
protected override IMethodReturnMessage InvokeMethodCall(IMethodCallMessage msg) {
IMethodReturnMessage result;
do {
result = base.InvokeMethodCall(msg);
} while (result.Exception is RetryException);
return result;
}
}
}
Other scenarios includes...
- ... a proxy that caches invocation results in a unit-of-work container, like a HttpContext.Items (which is a per-request dictionary).
- ... a proxy that serves old invocation results if the real invocation fails.
- ... a proxy that validates, and optionally modify, arguments and results.