Thursday, June 05, 2008

Binsor Method Interceptor Macro

So, I just picked up a story that is one of those really annoying feature requests-- you know the kind: adding log statements to analyze performance of about 40 key use cases scattered all over the app. Its the kind of story every developer dreads and the worst part is that it is throw away code. In a week when data has been collected there will be another story coming down the pipe to revert those changes. While I could've spent the day hunting down the 40 culprits and performing mind numbing cut and paste code injection I decided instead to attempt an implementation that would stick around and have potential uses in other scenarios.

Since our application uses Windsor entirely configured through Ayende's Binsor scripts I figured I'd take a stab at writing my own binsor macro for interceptors. The functionality I required was that only specific methods of a service need to be logged for time and occasionally other contextual information dumped.

I know I could perform the same action by creating a class that implements IInterceptor and registering it in Binsor like:
IoC.Container.Kernel.GetHandler('textProvider').ComponentModel.Interceptors.Add(InterceptorReference(MyInterceptor))

But I wanted something more flexible. Ultimately I want to be able to craft a custom binsor config file and not have to perform any code changes. This way when the story comes down the pipe to revert changes it is as simple as replacing a config file. Additionally having the ability to throw interceptors on specific methods is an enticing feature should we ever need to peer down the business end of the app running in production.

The syntax I was after looked something like this:

component 'textProvider', ITextProvider, HelloWorldProvider:
intercept 'GetText':
Log.Info('Invoking: ' + invocation.Method.Name + ' at ' + DateTime.Now())
stopwatch.Start()
invocation.Proceed()
Log.Info('Duration: ' + stopwatch.Stop())

Make sense? If not, I'll paraphrase in English: I want to intercept the GetText method on my ITextProvider service and perform the block of code that logs the invocation, lets the invocation proceed and then logs the duration of the invocation. Essentially my goal is to inject that block of code as a delegate into a class implementing IInterceptor. Its actually easier than it sounds!

The first step was to create the MethodInterceptor. This is the class that implements IInterceptor:

public class MethodInterceptor : IInterceptor

{

public delegate void InterceptHandler(IInvocation invocation);

private readonly string methodName;

private readonly InterceptHandler interceptHandler;

public MethodInterceptor(string methodName, InterceptHandler interceptHandler)

{

this.methodName = methodName;

this.interceptHandler = interceptHandler;

}

public void Intercept(IInvocation invocation)

{

if(methodName.Equals(invocation.Method.Name))

interceptHandler(invocation);

else

invocation.Proceed();

}

}


Notice the delegate? Thats the magic... otherwise, its pretty straight forward.

Step 2 is to create something that will do the Windsor configuration for us. This class needs to:

  • Add a new component for the interceptor
  • Add the dependency values to the interceptor component's configuration
  • Register the interceptor component as an interceptor on the parent component
If you are unclear about interceptors in Windsor I suggest reading up on them in the Windsor documentation.

public class MethodInterceptExtension : IComponentExtension

{

private const string INTERCEPTOR = "interceptor";

private const string INTERCEPTORS = "interceptors";

private readonly MethodInterceptor.InterceptHandler interceptHandler;

private readonly string methodName;

public MethodInterceptExtension(string methodName, MethodInterceptor.InterceptHandler interceptHandler)

{

this.methodName = methodName;

this.interceptHandler = interceptHandler;

}

public void Apply(Component component)

{

var interceptorKey = component.Name + methodName + "Interceptor";

AbstractConfigurationRunner.IoC.Container.Kernel.AddComponent(interceptorKey, typeof (MethodInterceptor));

AbstractConfigurationRunner.IoC.Container.Kernel.GetHandler(interceptorKey).AddCustomDependencyValue("methodName", methodName);

AbstractConfigurationRunner.IoC.Container.Kernel.GetHandler(interceptorKey).AddCustomDependencyValue("interceptHandler", interceptHandler);

AddInterceptorConfigItem(FindOrCreateInterceptorsConfigItem(component), interceptorKey);

}

private static IConfiguration FindOrCreateInterceptorsConfigItem(Component component)

{

foreach (IConfiguration configChild in component.Configuration.Children)

{

if (INTERCEPTORS.Equals(configChild.Name))

return configChild;

}

var interceptorsConfig = new MutableConfiguration(INTERCEPTORS);

component.Configuration.Children.Add(interceptorsConfig);

return interceptorsConfig;

}

private static void AddInterceptorConfigItem(IConfiguration parent, string interceptorKey)

{

parent.Children.Add(new MutableConfiguration(INTERCEPTOR, "${" + interceptorKey + "}"));

}

}



Finally we need to create the actual Macro. Binsor has a very handy base class called BaseBinsorExtensionMacro that I will inherit from so most of the heavy lifting is done for me

public class InterceptMacro : BaseBinsorExtensionMacro<MethodInterceptExtension>

{

public InterceptMacro() : base("intercept", false, new[] {"component"})

{

}

protected override bool ExpandExtension(ref MethodInvocationExpression extension, MacroStatement macro, MacroStatement parent, ref Statement expansion)

{

extension.Arguments.Add(macro.Arguments[0]);

extension.Arguments.Add(BuildInterceptHandler(macro));

return true;

}

private static Expression BuildInterceptHandler(MacroStatement macro)

{

var e = new BlockExpression();

e.Parameters.Add(new ParameterDeclaration("invocation", new SimpleTypeReference("Castle.Core.Interceptor.IInvocation")));

e.Body = macro.Block;

return e;

}

}


In order to understand this class you have to know that behind the scenes what happens is that by default a new instance of a MethodInterceptExtension is created and injected into the Component being registered (ITextProvider in my example). Unfortunately by default Binsor does not know how many or what parameters to pass into the constructor. So we want to modify the call to construct the MethodInterceptExtension so that it takes in the method name that we want to intercept along with a delegate that actually does the intercepting.

The InterceptorMacro just takes the first argument passed to it as the method name we want to intercept. Now for some magic: the macro wraps it's body block in a delegate. Not to be outdone it then modifies the call to construct a new MethodInterceptExtension to accept two new parameters (method name and delegate). Now the MethodInterceptExtension will be constructed with the method name and delegate injected into it. Cool huh??

These three classes are pretty straight forward and the additional functionality in Binsor is incredibly useful. Plus rather than spending a day writing boring log statements I learned heaps about Binsor and Windsor. Tomorrow I will actually use this and craft up a binsor container configuration file in record time...

1 comments:

Anonymous said...

Между прочим, лучший способ обезопасить себя от слежки - использовать Подавитель мобильных

 
s