views:

315

answers:

0

I have implemented a custom lifestyle for Windsor that will allow me to scope components to the current WCF operation. The biggest problem is that the call context initializer is executed after the service has been constructed which means that it cannot have dependencies on components with a perWcfLifestyle because the context for the perWcfLifestyle hasn't been configured yet.

What I would like to do is to initialiaze the perWcfLifestyle before the service is instantiated so that I can take full advantage of Windsor to create all the dependencies for me rather than have the service explicitly call Windsor as it needs to do currently.

I've included the code for this below. It looks like a lot but there are only a few pertinent pieces:

  1. ServiceBehavior - Integrates with WCF.
  2. LifestyleManager - Integrates with Windsor.
  3. CallContextInitializer - Initialises the call context from WCF.
  4. CallContext - Provides access to the current context information (thread static).
  5. CallContextItem - Stores the allocated component for the given call context, and also provides access to some cleanup semantics.

So how can I improve this so that my service and all it's dependencies can be created by Windsor within the context of a WCF operation?

namespace PerWcfOperationLifestyle
{
    public class ServiceBehavior : IServiceBehavior
    {
        public void ApplyDispatchBehavior(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
        {
            var callContextInitializer = new CallContextInitializer();
            foreach (var channelDispatcherBase in serviceHostBase.ChannelDispatchers)
            {
                var channelDispatcher = channelDispatcherBase as ChannelDispatcher;
                if (null != channelDispatcher)
                {
                    foreach (var endpointDispatcher in channelDispatcher.Endpoints)
                    {
                        foreach (var dispatchOperation in endpointDispatcher.DispatchRuntime.Operations)
                        {
                            dispatchOperation.CallContextInitializers.Add(callContextInitializer);
                        }
                    }
                }
            }
        }

        public void Validate(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
        { }

        public void AddBindingParameters(
            ServiceDescription          serviceDescription,
            ServiceHostBase             serviceHostBase,
            Collection<ServiceEndpoint> endpoints,
            BindingParameterCollection  bindingParameters
        )
        { }
    }

    public class CallContextInitializer : ICallContextInitializer
    {
        public object BeforeInvoke(InstanceContext instanceContext, IClientChannel channel, Message message)
        {
            CallContext.StartContext();
            return null;
        }

        public void AfterInvoke(object correlationState)
        {
            CallContext.EndContext();
        }
    }

    public class LifestyleManager : AbstractLifestyleManager
    {
        private readonly string id = "PerWcfOperationLifestyleManager-" + Guid.NewGuid();

        public override object Resolve(CreationContext context)
        {
            var wcfContext = CallContext.Current;

            if (null == wcfContext)
            {
                throw new InvalidOperationException(
                    "PerWcfOperationLifestyleManager.Resolve(...) must be called from within a WCF operation."
                );
            }

            var component = wcfContext.GetComponent(id);
            if (null == component)
            {
                component = base.Resolve(context);
                if (null != component)
                {
                    wcfContext.RegisterComponent(id, component, releaseComponent);
                }
            }

            return component;
        }

        public override void Release(object instance)
        {
            //
            // Do nothing. The component should be released when the context ends.
            // Use the private releaseComponent method.
            //
        }

        public override void Dispose()
        { }

        private void releaseComponent(string componentId, object component)
        {
            if (false == string.Equals(id, componentId))
            {
                throw new InvalidOperationException("The component was not created with this lifestyle manager.");
            }
            base.Release(component);
        }
    }

    public class CallContext
    {
        [ThreadStatic]
        private static CallContext current;
        private Dictionary<string, CallContextItem> callContextItems;

        public static CallContext Current
        {
            get { return current; }
            private set { current = value; }
        }

        public CallContext()
        {
            callContextItems = new Dictionary<string, CallContextItem>();
        }

        public static void StartContext()
        {
            if (null != Current)
            {
                throw new InvalidOperationException();
            }
            Current = new CallContext();
        }

        public static void EndContext()
        {
            if (null == Current)
            {
                throw new InvalidOperationException();
            }
            foreach (var callContextComponent in Current.callContextItems.Values)
            {
                callContextComponent.DoRelease();
            }
            Current.callContextItems.Clear();
            Current = null;
        }

        public void RegisterComponent(string id, object component, Action<string, object> releaseAction)
        {
            callContextItems.Add(id, new CallContextItem(id, component, releaseAction));
        }

        public object GetComponent(string id)
        {
            CallContextItem callContextItem;
            if (callContextItems.TryGetValue(id, out callContextItem))
            {
                return callContextItem.Component;
            }
            return null;
        }
    }

    public class CallContextItem
    {
        public string                 Id            { get; private set; }
        public object                 Component     { get; private set; }
        public Action<string, object> ReleaseAction { get; private set; }

        public CallContextItem(string id, object component, Action<string,object> releaseAction)
        {
            Id            = id;
            Component     = component;
            ReleaseAction = releaseAction;
        }

        public void DoRelease()
        {
            if (null != ReleaseAction)
            {
                ReleaseAction(Id, Component);
            }
        }
    }
}