Wednesday, March 18, 2009

WCF / Windsor Integration: Adding Behaviours

Continuing my love fest with the Windsor WCF facility, today I'm going to show you how to add behaviours to your WCF service. Behaviours are the core extension point for WCF and allow you to do pretty much anything with your service. Here's a nice brief description by Mehran Nikoo (it was the first thing that popped up on google :)

The WCF Facility automatically discovers any registered WCF behaviours and adds them to the service. The really nice thing about this is that because the behaviours are also IoC hosted components, the container can automatically provide their dependencies just like any other component. If you need to use any of your services inside the behaviour, simply inject it through the constructor as normal.

Let's look at an example where we want to log the entry and exit from an operation. Here's a simple service:

[ServiceContract]
public interface IHelloService
{
    [OperationContract]
    string Hello(string name);
}
public class HelloService : IHelloService
{
    private readonly ILogger logger;
    public HelloService(ILogger logger)
    {
        this.logger = logger;
    }
    public string Hello(string name)
    {
        logger.WriteLine("In Hello()");
        return string.Format("Hello {0}", name);
    }
}

It has a single operation 'Hello' that takes a name and returns 'Hello <name>'. It also has a dependency on ILogger which it uses to log that the Hello operation has been invoked.

Now lets create a WCF behaviour:

public class TestEndpointBehaviour : IEndpointBehavior
{
    private readonly TestCallContextInitializer callContextInitializer;
    public TestEndpointBehaviour(TestCallContextInitializer callContextInitializer)
    {
        this.callContextInitializer = callContextInitializer;
    }
    public void Validate(ServiceEndpoint endpoint){}
    public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters){}
    public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime){}
    public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
    {
        foreach (var operation in endpointDispatcher.DispatchRuntime.Operations)
        {
            operation.CallContextInitializers.Add(callContextInitializer);
        }
    }
}

We want to write to the logger before and after every operation invocation, to do that we're creating an Endpoint behaviour. The Endpoint behaviour gives you an opportunity, the ApplyDispatchBehaviour method, to add a 'CallContextInitializer' that can intercept operation invocations.  Note that we resolve our CallContextInitializer from the IoC container by expecting it to be injected via a constructor parameter.

As an aside, I can't help feeling that the WCF behaviour API is overly complex, it hardly invites you in :) There's no way I would have worked out how to do this without looking at the documentation. Not at all the pit of success.

Rant over, here's our CallContextInitializer:

 

public class TestCallContextInitializer : ICallContextInitializer
{
    private readonly ILogger logger;
    public TestCallContextInitializer(ILogger logger)
    {
        this.logger = logger;
    }
    public object BeforeInvoke(InstanceContext instanceContext, IClientChannel channel, Message message)
    {
        logger.WriteLine("Before Invoke");
        return null;
    }
    public void AfterInvoke(object correlationState)
    {
        logger.WriteLine("After Invoke");
    }
}

The ICallContextInitializer interface has two methods: BeforeInvoke and AfterInvoke where which allow us to execute any actions before and after the operation is invoked. Here we're just using the logger to write some interesting stuff out. Note that because the TestCallContextInitializer is also an IoC component we can resolve any of our services as constructor arguments again. Here we using the same ILogger service that our HelloService uses.

Here's the configuration and a little test to show it all working:

[TestFixture]
public class WCFIntegration_Spikes
{
    private const string tcpEndpointAddress = "net.tcp://localhost:4321/HelloService";
    [SetUp]
    public void SetUp()
    {
    }
    [Test, Explicit]
    public void Simple_TCP_Service()
    {
        var container = new WindsorContainer()
            .AddFacility<WcfFacility>()
            .Register(
                Component.For<ILogger>().ImplementedBy<Logger>(),
                Component.For<TestCallContextInitializer>(),
                Component.For<TestEndpointBehaviour>(),
                Component.For<IHelloService>().ImplementedBy<HelloService>()
                    .ActAs(new DefaultServiceModel().AddEndpoints(
                        WcfEndpoint.BoundTo(new NetTcpBinding { PortSharingEnabled = false }).At(tcpEndpointAddress)
                    ))
            );
        var client = ChannelFactory<IHelloService>.CreateChannel(
            new NetTcpBinding { PortSharingEnabled = false }, new EndpointAddress(tcpEndpointAddress));
        Console.WriteLine(client.Hello("Mike"));
        container.Dispose();
    }                
}

We first register the WcfFacility as usual. Then we register our components. Note that we don't need to do any special configuration for our EndpointBehaviour, the WCF Facility takes care of this for us.

When we run the test we get this output to the console:

Before Invoke
In Hello()
After Invoke
Hello Mike

As expected our behaviour was installed and invoked.

Integrating WCF and Windsor like this provides a very slick way of doing web services. I'm very impressed with Craig Neuwirt's work, what a star. Shame he doesn't blog more about it.

2 comments:

preet sangha said...

I was describing to a colleague how to implement a WCF based logging solution a few weeks ago - and here you go again reading my mind and the making it better. Very good post.

Mike Hadlow said...

Thanks Preet. I'm going to be doing a workshop in May on WCF/Windsor integration at the progressive .net tutorials in London. So expect more posts on the subject.