Pretending to Be Something You're Not - Real Proxy

I recently did some work revamping the Caching mechanism that was initially developed in XPOS by Phil and I and later moved to the IQ.Framework, but how it actually works is pretty interesting so I thought I’d share with you the concept of RealProxy.

RealProxy is a class from the System.Runtime.Remoting.Proxies namespace that basically allows you to programatically intercept method calls sent to another class by running those calls through a Proxy. RealProxy is an abstract class that you can extend to do whatever you need it for, and in extending it you need to define the Invoke method:

  • IMessage Invoke(IMessage message): this is the method that gets called any time a call to your “object” is made. The IMessage container defines the message (usually a ConstructorCallMessage or a MethodCallMessage), which includes what method was called, and the array of parameters passed in. From there you can do what you will with it, either call the actual method on a concrete class, or override the call altogether. The IMessage you return will define the return value of the method (null if it’s a void return), as well as any out parameters that may have been altered in the process.

Pretty powerful stuff. So, how did we use it with caching? Well, being that we can intercept and modify the method being called as well as the return value, all we really do is check a cache (be it in-memory, on disk, or otherwise) to see if we’ve already called that method, and if so we can return a value from the cache instead of having to hit the service again.

Create a cache key from the method invocation
1
2
3
4
5
6
7
8
CacheKey key = CreateCacheKey(methodCallMessage);
if (_cache.HasData(key))
{
  trace.TraceInformation("Cache Hit for {0}", key);
  return _cache.GetData(key);
}
trace.TraceInformation("Cache Miss for {0}", key);
return CallService(methodCallMessage, key);

Of course, there’s variations of the method call – if you called GetProductByID(123) and GetProductByID(456) you’d expect two different results – so we created a CacheKey object that contains references to the service as well as the parameters passed in to differentiate between specific method calls.

Validate cache key
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public CacheKey(string key, string service, params object[] parameters)
{
  if (key == null || key == string.Empty)
      throw new InvalidKeyException("Key must not be null or an 
          empty string");
  _key = key;
  _service = service;
  _parameters = parameters;
}

public override bool Equals(object obj)
{
  if (!(obj is CacheKey))
      return false;
  CacheKey toCompare = (CacheKey)obj;
  if (toCompare._key != this._key)
      return false;
  if (toCompare._service != this._service)
      return false;
  if (toCompare._parameters.Length != this._parameters.Length)
      return false;
  for (int i = 0; i < toCompare._parameters.Length; i++)
  {
      if (!toCompare._parameters[i].Equals(this._parameters[i]))
          return false;
  }
  return true;
}

After all that, we wanted to make an easy way to identify a interface method as “cachable”, so we created our own Attribute that can be used to decorate any method.

Attribute to indicate a method is “cachable”
1
2
3
4
5
6
7
8
9
10
11
12
13
14
[AttributeUsage(AttributeTargets.Method, Inherited = false, 
 AllowMultiple = false)]
public sealed class CacheableAttribute : Attribute
{
    public String CacheKey { get; private set; }
    public CacheableAttribute(String cacheKey)
    {
        CacheKey = cacheKey;
    }
}

//Usage
[Cacheable(CacheKey="SomeString")]
ProductTransport GetProductByID(int ProductID);

I’m not sure if we’re actually using this in production or not, but it’s a pretty slick way of making service calls cacheable. Of course this only addresses caching of data from the server to the client, and does it in a fairly transparent way, which as a side effect removes some of the control from the developer. But with the initial way the CacheProxy is setup requires a Cache be passed in, meaning it could be stored globally and managed as necessary (i.e. set objects to expire, check the cache before making service calls, etc.).

Comments