Click here to Skip to main content
15,880,905 members
Articles / All Topics

Dynamic Proxy (or Getting Crazy with Dynamics and Expressions)

Rate me:
Please Sign up or sign in to vote.
5.00/5 (1 vote)
13 Oct 2010CPOL3 min read 9.5K   1   1
Dynamic Proxy (or getting crazy with Dynamics and Expressions)

If you’ve read my earlier posts, you’ll see I have a somewhat childish fascination (crush if you will) with the .NET Expressions and dynamics APIs. I think there is limitless potential with these two platforms and in this post, I will demonstrate the two working together by creating a small interception “framework” built on a dynamic proxy object provider…

My goal is to create a class deriving from DynamicObject. When operations are executed against an object of type DyanamicObject, there is opportunity to intercept the call and do something meaningful; here I am going to just forward the call to the target object. What is this target object? It’s going to be a type fed to the generic class deriving from DynamicObject.

So the “real” object (the interception target) is going to be simple:

image

Ignore the attributes for now… just take note that there is nothing special about this class. No class attributes, no interfaces… just a POCO with a property and two methods that do rather boring things.

The interception “framework” is a single class:

C#
public delegate void BeforeSetDelegate(string name, object value); 

   /// <summary>
   /// A class that will delegate operations to an instance of T.
   /// </summary>
   public class DynamicProxy<T> : DynamicObject
   {
       public event BeforeSetDelegate OnBeforeSet; 

       /// <summary>
       /// The instance of T that will be proxied.
       /// </summary>
       private T instance = default(T); 

       public DynamicProxy()
       {
           instance = Activator.CreateInstance<T>();
       } 

       /// <summary>
       /// 
       /// </summary>
       /// <param name="binder"></param>
       /// <param name="result"></param>
       /// <returns></returns>
       public override bool TryGetMember(GetMemberBinder binder, out object result)
       {
           result = null; 

           PropertyInfo info = instance.GetType().GetProperty(binder.Name); 

           if(info == null)
               return false; 

           result = info.GetValue(instance, null); 

           return true;
       } 

       /// <summary>
       /// 
       /// </summary>
       /// <param name="binder"></param>
       /// <param name="value"></param>
       /// <returns></returns>
       public override bool TrySetMember(SetMemberBinder binder, object value)
       {
           if(OnBeforeSet != null)
               OnBeforeSet(binder.Name, value); 

           PropertyInfo info = instance.GetType().GetProperty(binder.Name); 

           if(info == null)
               return false; 

           foreach (var att in info.GetCustomAttributes(typeof(ValidationAttribute), true))
           {
               var vatt = att as ValidationAttribute;
               if(!vatt.IsValid(value))
               {
                   throw new ValidationException("Parameter failed validation.", vatt, value);
               }
           } 

           info.SetValue(instance, value, null); 

           return true;
       } 

       /// <summary>
       /// 
       /// </summary>
       /// <param name="binder"></param>
       /// <param name="args"></param>
       /// <param name="result"></param>
       /// <returns></returns>
       public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args,
           out object result)
       {
           MethodInfo info = instance.GetType().GetMethod(binder.Name); 

           for(int i = 0; i < args.Length; i++)
           {
               var param = info.GetParameters()[i]; 

               foreach(var att in param.GetCustomAttributes(typeof(ValidationAttribute),
                   true))
               {
                   ValidationAttribute vatt = att as ValidationAttribute;
                   bool isValid = vatt.IsValid(args[i]); 

                   if (!isValid)
                   {
                       throw new ValidationException(String.Format(
                       "Parameter: ({0}) failed validation.", param.Name), vatt, args[0]);
                   }
               }
           } 

           result = info.Invoke(instance, args); 

           return true;
       }
   }

Ahhh… so that’s how the interception target is created! In the constructor of this class. So if I want to proxy say “SomeClass”, I can use the following code:

image

Now, what if I want to fire an event before a property is set? Holy Smokes! This allows us to! Since DynamicProxy intercepts ALL calls, if I wire up an event, I can raise it before forwarding on the call to the actual object.

image

This is where the magic happens. Before any property is executed on the target object, the message gets relayed here… in TrySetMember. Here I raise the OnBeforeSet event which I subscribed to earlier. So when I make the call “d.Name = ‘String one’” I will get the following output:

image

So it tells me the name of the property being set. The second line of the output is simply the value of the Name property. Here is what’s happening under the hood: when I set a dynamic property the DynamicProxy class is intercepting the call, then it forwards the set to the real object (object of type T). But it’s totally transparent to the caller (aside from using the dynamic keyword when creating the object); the caller is just setting the “Name” property on the SomeClass object.

So what more can we do with this interception technique? How about data validation (could be used as pre/post conditions, for instance). If you take a look back at SomeClass, the property, Name, and a parameter to the function “DoSomethingAndReturn” are both decorated with attributes from the System.ComponentModel.DataAnnotations namespace: StringLength and Range, respectively. I bet you can see where I am going with this… If a property on the target object is decorated with one of these ValidationAttributes, I will check the validity of the data before setting the property; throwing an exception if validation fails. If you look at the “TrySetMember” operation of the DynamicProxy class, you’ll see I find all custom attributes of type “ValidationAttribute” and check the validity of the data being provided (remember: TrySetMember will have the value of the property). Nothing special here…I just call IsValid() with the data provided (StringLength and Range both derive from ValidationAttribute).

image

To demonstrate the validation, I am going to change the code in Main slightly to purposely fail the StringLength(10) rule applied to the “Name” property of “SomeClass.” If I run it, I will get the following output:

image

So it told me which validator failed and the expression??? Where did that come from? Ooooh, wait…that’s the actual StringLength attribute displayed as a mathematical expression! How useful is that!? I get the validator that failed along with why (the expression…telling me (26 > 0 and 26 < 10). Remember: The StringLength attribute describes the maximum length of a string…and 0 is obviously the minimum. This makes sense but how did it know? With a simple extension method I created to craft the expression from the attribute:

image

… and on and on. You can take this as far as you want to go… and I’ve only scratched the surface.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
GeneralMy vote of 5 Pin
AWdrius13-Oct-10 22:48
AWdrius13-Oct-10 22:48 
Interesting example. What about intellisense, would you still get a nice auto-completion with you proxied object?

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.