Wednesday 17 February 2010

Custom Proxy Generation using "RealProxy"

[Download Full Solution]

I've been looking around the top side of the WCF client proxy for a nice place to hook in contract specific caching. I was hoping to find soemthing similar to the IOperationInvoker on the server end. Unfortunately I couldn't find anything that allowed for me to fully divert the flow and avoid a service call. You have the ability to inspect the parameter listing (pre-validation), and the message (further validation or header injection) after serialization (which you can also adjust) but at no point can you just return a value within the pipeline...


I gave up my search for a solution within the framework itself and focused on the proxy end which seems to have been useful. This sample runs through a few things which together make up a way in which we can either inject a caching layer on a per-contract basis, or something generic like what I've put together here. The end result is that you can specify a "local" implementation of what you would otherwise do on the server. This way I can keep things simple on the consumer side (you need to provide something that returns the data). For sending results I've added an interface that can be used to feed data into any appropriate cache on the local side. The current behaviour model for this sample is to query the server if the local version returns no result, and skip local if there is no return type.

First order of business is to write an implementation of the IChannelFactory<TChannel>

using System;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceModel.Description;

namespace CA.NGommans.Wcf.Caching
{
    /// <summary>
    /// Caching channel factory
    /// </summary>
    /// <typeparam name="T">Type to generate</typeparam>
    public class CachingChannelFactory<T> : ChannelFactory<T>
    {
        /// <summary>
        /// Get/Set local caching implementation
        /// </summary>
        public T LocalCacheImplementation { get; set; }

        #region Constructors

        public CachingChannelFactory()
            : base("*")
        { }

        public CachingChannelFactory(T localImplementation)
            : base("*")
        {
            LocalCacheImplementation = localImplementation;
        }

        // Suppressed the rest for simplicity

        #endregion

        /// <summary>
        /// Bridge cache proxy onto service proxy
        /// </summary>
        /// <param name="address">Underlying service proxy address</param>
        /// <param name="via">Underlying serivce proxy via</param>
        /// <returns>Caching proxy</returns>
        public override T CreateChannel(EndpointAddress address, Uri via)
        {
            T underlier = base.CreateChannel(address, via);
            SwitchingProxy builder = new SwitchingProxy(typeof(T), underlier, LocalCacheImplementation);
            T proxy = (T)builder.GetTransparentProxy();
            return proxy;
        }
    }
}

The key above is the creation of the underlying/base proxy and assignment of it as one of the parameters against our "SwitchingProxy". This is the proxy which will flip between local and remote implementations.

Below is the implementation of "SwitchingProxy" which does the heavy lifting for us and handles invocation of our local implementation and remote proxy.

using System;
using System.Reflection;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Messaging;
using System.Runtime.Remoting.Proxies;
using System.Security;

namespace CA.NGommans.Wcf.Caching
{
    /// <summary>
    /// Switching proxy provides an intermediary
    /// </summary>
    [SecurityCritical(SecurityCriticalScope.Everything)]
    internal sealed class SwitchingProxy : RealProxy, IRemotingTypeInfo
    {
        private Type proxiedType;
        private object remoteProxy = null;
        private object localProxy = null;

        /// <summary>
        /// A proxy that switches between a local and remote source
        /// </summary>
        /// <param name="type">type to switch</param>
        /// <param name="remote">remote proxy</param>
        /// <param name="local">local implementation</param>
        public SwitchingProxy(Type type, object remote, object local)
            : base(type)
        {
            this.proxiedType = type;
            remoteProxy = remote;
            localProxy = local;
        }

        /// <summary>
        /// Provides mapping capabilities
        /// </summary>
        /// <param name="message">Message to process</param>
        /// <returns>Result message</returns>
        public override IMessage Invoke(IMessage message)
        {
            IMessage result = null;

            IMethodCallMessage methodCall = message as IMethodCallMessage;
            MethodInfo method = methodCall.MethodBase as MethodInfo;

            // Check local proxy - note we ignore methods with null return types
            if (localProxy != null && method.ReturnType != null && method.DeclaringType.IsAssignableFrom(localProxy.GetType()))
            {
                // Invoke service call
                object callResult = method.Invoke(localProxy, methodCall.InArgs);
                if (callResult != null)
                {
                    LogicalCallContext context = methodCall.LogicalCallContext;
                    result = new ReturnMessage(callResult, null, 0, context, message as IMethodCallMessage);
                }
            }

            // Invoke remote proxy
            if (result == null)
            {
                if (remoteProxy != null)
                {
                    object callResult = method.Invoke(remoteProxy, methodCall.InArgs);
                    LogicalCallContext context = methodCall.LogicalCallContext;
                    result = new ReturnMessage(callResult, null, 0, context, message as IMethodCallMessage);

                    // Optionally set to cache if we are comply with the IRemoteResponse interface
                    if (result != null && localProxy != null && localProxy is IRemoteResponse)
                    {
                        ((IRemoteResponse)localProxy).InvocationResponse(method, methodCall.InArgs, callResult);
                    }
                }
                else
                {
                    NotSupportedException exception = new NotSupportedException("Remote proxy is not defined");
                    result = new ReturnMessage(exception, message as IMethodCallMessage);
                }
            }
            return result;
        }

        #region IRemotingTypeInfo Members

        /// <summary>
        /// Checks whether the proxy that represents the specified object type can be cast
        /// to the type represented by the defined proxy.
        /// </summary>
        /// <param name="toType">The type to cast to.</param>
        /// <param name="o">The object for which to check casting.</param>
        /// <returns>true if cast will succeed; otherwise, false.</returns>
        bool IRemotingTypeInfo.CanCastTo(Type toType, object o)
        {
            bool result = true;
            if (!toType.IsAssignableFrom(this.proxiedType))
            {
                RealProxy objRef = RemotingServices.GetRealProxy(remoteProxy);
                if (objRef is IRemotingTypeInfo)
                {
                    result = ((IRemotingTypeInfo)objRef).CanCastTo(toType, o);
                }
                else
                {
                    result = false;
                }
            }
            return result;
        }

        /// <summary>
        /// Gets the fully qualified type name of the server object in a System.Runtime.Remoting.ObjRef.
        /// </summary>
        string IRemotingTypeInfo.TypeName
        {
            get { return this.proxiedType.FullName; }
            set { }
        }

        #endregion
    }
}

A few noteworthy items above include the implementation of IRemotingTypeInformation which delegates casting checks off to the underlying proxy since we comply with the specific service contract, and delegate the rest onward anyway. The Invoke code is is reasonably self-explanatory and simply toggles between returning data from our local implementation and the remote one. I've added a rather simple interface (IRemoteResponse) with one method called InvocationResponse to allow for found data from a service to be passed back to the local implementation if it implements this interface.

There was a second way that I was considering interaction with the underlying proxy and that was by unwrapping it (similar to how IRemotingTypeInfo.CanCastTo is implemented). This could prune down the code a bit, and possibly avoid some additional overhead. I haven't had a chance to test the behaviour in this approach which would essentially store the RealProxy implementation and call it's Invoke method directly from the Invoke method of SwitchingProxy. Of course, any alterations in the underlying proxy and you may experience adverse behaviour with this (likely a bit faster) alternate approach.

The bottom line is that we can very easily setup a proxy by instantiating an instance of CachingChannelFactory and following the same procedure as with ChannelFactory itself.

using System;
using System.ServiceModel;
using CA.NGommans.Wcf.Caching;
using ConsoleApplication.ServiceReference1;

namespace ConsoleApplication
{
    class Program
    {
        static void Main(string[] args)
        {
            CachingChannelFactory<IService1> ccf = new CachingChannelFactory<IService1>(new CachedService());
            IService1 service = ccf.CreateChannel();

            // Note proxy still works with IClientChannel and therefore ICommunicationObject/IDisposable contracts
            using (service as IClientChannel)
            {
                Console.WriteLine("Service Proxy returned: {0}", service.GetData(1));
                Console.WriteLine("Service Proxy returned: {0}", service.GetData(1));
                Console.WriteLine("Service Proxy returned: {0}", service.GetData(1));
                Console.WriteLine("Service Proxy returned: {0}", service.GetData(2));
                Console.WriteLine("Service Proxy returned: {0}", service.GetData(1));
                Console.WriteLine("Service Proxy returned: {0}", service.GetData(2));
                Console.WriteLine("Service Proxy returned: {0}", service.GetData(2));
            }
            Console.Write("\n\nPress Enter to continue...");
            Console.Read();
        }
    }
}

As for results, here's the output outlining where the local implementation was touched:

Output of ConsoleApplication.exe
==Cache lookup missed for key: 1
<<Saving server response to cache: You entered: 1
Service Proxy returned: You entered: 1
>>Serving data from cache for key: 1
Service Proxy returned: You entered: 1
>>Serving data from cache for key: 1
Service Proxy returned: You entered: 1
==Cache lookup missed for key: 2
<<Saving server response to cache: You entered: 2
Service Proxy returned: You entered: 2
>>Serving data from cache for key: 1
Service Proxy returned: You entered: 1
>>Serving data from cache for key: 2
Service Proxy returned: You entered: 2
>>Serving data from cache for key: 2
Service Proxy returned: You entered: 2


Press Enter to continue...

As you can see above, the service was only queried twice. The balance of the above queries hit our local implementation.

The sample referenced at the top includes the code for "CachedService" which just implements the Service1 interface generated by default as part of the WcfService project template.

No comments:

Post a Comment