Udostępnij za pośrednictwem


WCF Extensibility – Initializers (instance context, channel, call context)

This post is part of a series about WCF extensibility points. For a list of all previous posts and planned future ones, go to the index page .

And we’re reaching the end of this series. To close it, I’ll cover some of the lesser used extensibility points in WCF, which would be too short for a full post. This time I’ll cover the “initializer” interfaces, which can be used to, well, initialize some component of the WCF runtime. Those are the IInstanceContextInitializer, ICallContextInitializer and IChannelInitializer, which will be covered in detail below.

And since there will be multiple samples in this post, the usual disclaimer goes ahead: they are simple samples for illustrating the topics of this post, not production-ready code. I tested them for a few contracts and they worked, but I cannot guarantee that they will work for all scenarios – please let me know if you find a bug or something missing. There are some shortcuts I did in them to make the sample smaller, such as not using real resource files in the call context initializer sample, and, as usual, error checking has been kept to a minimum.

IInstanceContextInitializer

The example from the previous post showed how we can completely control the instance context life cycle with the IInstanceContextProvider interface. There are some scenarios, however, where all we want is to simply perform some initialization code when a new instance context is created – essentially, only implement the InitializeInstanceContext method in the instance context provider. Such scenarios can involve attaching an extension to the instance context, or possibly doing some custom throttling. For those cases, having to implement the whole instance context provider is a lot of work, so for this scenario WCF also provides (yet) another extensibility point, the IInstanceContextInitializer interface, which is called whenever a new instance context is created. The interface declaration is shown below.

  1. public interface IInstanceContextInitializer
  2. {
  3.     void Initialize(InstanceContext instanceContext, Message message);
  4. }

The Initialize method in the instance context initializer is called when a new instance context is created. If there is a custom instance context provider, the provider’s InitializeInstanceContext method will be called first, then all of the initializers in the dispatch runtime will be called.

To add an instance context initializer to the WCF runtime, you need to add it to the InstanceContextInitializers property in the DispatchRuntime object. They’re typically added in endpoint or service behaviors, as shown in the example below.

  1. class MyBehavior : IEndpointBehavior
  2. {
  3.     public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
  4.     {
  5.         endpointDispatcher.DispatchRuntime.InstanceContextInitializers.Add(new MyInstanceContextInitializer());
  6.     }
  7. }

And that’s probably all you need to know about instance context initializers.

IChannelInitializer

Just like the instance context initializer, the channel initializer can be used to perform some initialization on channels. The IChannelInitializer, however, can be used both at the client and at the server side. At the former, it’s called either when CreateChannel is called on a channel factory (directly or indirectly via a class derived from ClientBase<TChannel>). At the latter it’s called when a new session is created (for session-less bindings, that’s essentially for every new request). Channel initializers are usually used to track existing client sessions. The interface definition is shown below.

  1. public interface IChannelInitializer
  2. {
  3.     void Initialize(IClientChannel channel);
  4. }

The Initialize method on the channel initializer is called when a new channel is created, and unlike the instance context initializer, it isn’t given the message (on the client there isn’t such a message when the channel is initialized), only the client channel which has been created. Applications will typically add a listener to the channel’s Closed (or Closing) event to know when the channel is being recycled.

Channel initializers on the server are bound to the ChannelDispatcher object, and typically accessed in endpoint behaviors. On the client side they’re bound to the ClientRuntime object directly, and also typically accessed in endpoint behaviors, as shown in the code below.

  1. public class MyBehavior : IEndpointBehavior
  2. {
  3.     public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
  4.     {
  5.         clientRuntime.ChannelInitializers.Add(new MyChannelInitializer());
  6.     }
  7.  
  8.     public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
  9.     {
  10.         endpointDispatcher.ChannelDispatcher.ChannelInitializers.Add(new MyChannelInitializer());
  11.     }
  12. }

Real world scenario: tracking session objects and detecting client disconnection

I’ve found this question in the MSDN forums and in some other places as well: how to determine when a client disconnected from a (session-full) service? As the answerer for that post suggested, the channel initializer is a good option for that. On the initializer we can register to the closed event on the channel, which will be called when the client disconnects (i.e., closes the channel).

For this sample I chose the same contract as in the post about instance context providers, but this time we’ll use a real session-full binding instead of having all the trouble of creating a custom provider (this sample is about channel initializers after all).

  1. [ServiceContract(SessionMode = SessionMode.Required)]
  2. public interface IStackCalculator
  3. {
  4.     [OperationContract]
  5.     void Enter(double value);
  6.     [OperationContract]
  7.     double Add();
  8.     [OperationContract]
  9.     double Subtract();
  10.     [OperationContract]
  11.     double Multiply();
  12.     [OperationContract]
  13.     double Divide();
  14. }

This time the server will not only trace the operations which are happening, but also the number of clients which are connected at a given time, with that information coming from our channel initializer class.

  1. private void Trace(string format, params object[] args)
  2. {
  3.     string text = string.Format(CultureInfo.InvariantCulture, format, args);
  4.     Console.WriteLine("[{0} client(s) connected] {1}", ClientTrackerChannelInitializer.ConnectedClientCount, text);
  5. }

The channel initializer itself will just count the number of clients connected at a given time. When a new channel is created, the Initialize method is called and the code increments the counter. It then starts listening to both Closed and Faulted events on the channel, so that if the client disconnects gracefully or not the code will be notified, so that the counter can be decremented.

  1. class ClientTrackerChannelInitializer : IChannelInitializer
  2. {
  3.     internal static int ConnectedClientCount = 0;
  4.  
  5.     public void Initialize(IClientChannel channel)
  6.     {
  7.         ConnectedClientCount++;
  8.         Console.WriteLine("Client {0} initialized", channel.SessionId);
  9.         channel.Closed += ClientDisconnected;
  10.         channel.Faulted += ClientDisconnected;
  11.     }
  12.  
  13.     static void ClientDisconnected(object sender, EventArgs e)
  14.     {
  15.         Console.WriteLine("Client {0} disconnected", ((IClientChannel)sender).SessionId);
  16.         ConnectedClientCount--;
  17.     }
  18. }

To set up the channel initializer, an endpoint behavior:

  1. class ClientTrackerEndpointBehavior : IEndpointBehavior
  2. {
  3.     public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
  4.     {
  5.         endpointDispatcher.ChannelDispatcher.ChannelInitializers.Add(new ClientTrackerChannelInitializer());
  6.     }
  7. }

And to test the implementation, we create two client channels and connect both to the service. When we close the first, the Closed event is fired and the tracker decrements the number of connected clients at the server.

  1. static void Main(string[] args)
  2. {
  3.     string baseAddress = "https://" + Environment.MachineName + ":8000/Service";
  4.     ServiceHost host = new ServiceHost(typeof(StackCalculator), new Uri(baseAddress));
  5.     WSHttpBinding binding = new WSHttpBinding(SecurityMode.None);
  6.     binding.ReliableSession.Enabled = true;
  7.     ServiceEndpoint endpoint = host.AddServiceEndpoint(typeof(IStackCalculator), binding, "");
  8.     endpoint.Behaviors.Add(new ClientTrackerEndpointBehavior());
  9.     host.Open();
  10.     Console.WriteLine("Host opened");
  11.  
  12.     ChannelFactory<IStackCalculator> factory = new ChannelFactory<IStackCalculator>(binding, new EndpointAddress(baseAddress));
  13.     IStackCalculator proxy1 = factory.CreateChannel();
  14.     Console.WriteLine("Created first client");
  15.     proxy1.Enter(5);
  16.     proxy1.Enter(8);
  17.     proxy1.Multiply();
  18.     Console.WriteLine();
  19.  
  20.     IStackCalculator proxy2 = factory.CreateChannel();
  21.     Console.WriteLine("Created second channel");
  22.     proxy2.Enter(5);
  23.     proxy2.Enter(2);
  24.     proxy2.Divide();
  25.     Console.WriteLine();
  26.  
  27.     Console.WriteLine("Disconnecting the first client");
  28.     ((IClientChannel)proxy1).Close();
  29.     Console.WriteLine();
  30.  
  31.     Console.WriteLine("Using the second proxy again");
  32.     proxy2.Enter(10);
  33.     proxy2.Multiply();
  34.     Console.WriteLine();
  35.  
  36.     Console.WriteLine("Closing the second client");
  37.     ((IClientChannel)proxy2).Close();
  38.  
  39.     factory.Close();
  40.     host.Close();
  41. }

And that’s it for the channel initializer.

ICallContextInitializer

The last of the initializer interfaces is not about initializing any of the WCF runtime objects, but the context in which the service operation will be executed. And by context it just means the thread – this is the interface which lets the implementer to set any thread-local variables which can be used by the service operations as they’re executed – thanks to the authors of this post about this interface, since I had never used it before, for helping me find out its usage. Any properties of the Thread class, such as the current [UI] culture, impersonation (current principal), or any other variables marked with ThreadStaticAttribute are a good candidate for initialization by this extensibility point, whose interface is shown below.

  1. public interface ICallContextInitializer
  2. {
  3.     void AfterInvoke(object correlationState);
  4.     object BeforeInvoke(InstanceContext instanceContext, IClientChannel channel, Message message);
  5. }

When an incoming request arrives, after the instance provider hands WCF the service instance to be used, BeforeInvoke is called so that the initializer can, well, initialize the context of the call. The context is preserved throughout some of the runtime components, including the dispatch message formatter, any parameter inspectors and the operation invoker. The method returns an object, the correlation state, which will be passed to the AfterInvoke method, which is called after the same components (invoker, parameter inspector, formatter). It’s on AfterInvoke that the initializer will typically restore the state of the thread to what it was before the BeforeInvoke call.

To add a call context initializer to the WCF runtime, you need to add it to the CallContextInitializers property of the DispatchOperation object. This is typically done either inside an operation or an endpoint behavior, as shown in the example below.

  1. class MyBehavior : IEndpointBehavior
  2. {
  3.     public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
  4.     {
  5.         foreach (DispatchOperation operation in endpointDispatcher.DispatchRuntime.Operations)
  6.         {
  7.             operation.CallContextInitializers.Add(new MyCallContextInitializer());
  8.         }
  9.     }
  10. }

Real world scenario: supporting Accept-Language header in requests

This is one of the cases where a call context initializer can be useful. By binding the HTTP Accept-Language header to the thread culture, we can use the resource management libraries in .NET to load strings from the culture expected by the client. This is a simple application that does that. The service is very simple, with a single operation which takes three parameters and return a Person object. The constructor of that class may throw an exception if any of the parameters is invalid, and we’ll wrap that in a WebFaultException to return the message to the client.

  1. [ServiceContract]
  2. public interface ITest
  3. {
  4.     [WebGet]
  5.     Person CreatePerson(string name, string email, string dateOfBirth);
  6. }
  7. public class Service : ITest
  8. {
  9.     public Person CreatePerson(string name, string email, string dateOfBirth)
  10.     {
  11.         try
  12.         {
  13.             return new Person(name, email, DateTime.Parse(dateOfBirth));
  14.         }
  15.         catch (ArgumentException e)
  16.         {
  17.             throw new WebFaultException<string>(e.Message, HttpStatusCode.BadRequest);
  18.         }
  19.     }
  20. }

The exception message is retrieved from a simulated resource manager (I didn’t find out quickly how to have multiple resource languages in a VS project to enable a quick F5 experience, and since it wasn’t overly relevant to the topic I decided to have a mock one).

  1. class StringRetriever
  2. {
  3.     public static IResourceStrings GetResources()
  4.     {
  5.         switch (Thread.CurrentThread.CurrentCulture.TwoLetterISOLanguageName)
  6.         {
  7.             case "es":
  8.                 return new SpanishStrings();
  9.             case "pt":
  10.                 return new PortugueseStrings();
  11.             default:
  12.                 return new DefaultStrings();
  13.         }
  14.     }
  15. }
  16.  
  17. interface IResourceStrings
  18. {
  19.     string GetInvalidName();
  20.     string GetInvalidEMail();
  21.     string GetInvalidDateOfBirth();
  22. }

In order to add the call context initializer to the runtime, I used an endpoint behavior in this sample, as shown below.

  1. class GlobAwareEndpointBehavior : IEndpointBehavior
  2. {
  3.     public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
  4.     {
  5.         foreach (DispatchOperation operation in endpointDispatcher.DispatchRuntime.Operations)
  6.         {
  7.             operation.CallContextInitializers.Add(new GlobAwareCallContextInitializer());
  8.         }
  9.     }
  10. }

The call context initializer itself is shown below. During BeforeInvoke, the code looks at the HttpRequestMessageProperty from the request to see if it has an Accept-Language header. If it does, it will try to get a CultureInfo object from the value of the header (notice that this implementation doesn’t handle ‘q’ values or multiple languages), and set it to the CurrentCulture property of the current thread. On the AfterInvoke method, the initializer restores the original culture.

  1. class GlobAwareCallContextInitializer : ICallContextInitializer
  2. {
  3.     public void AfterInvoke(object correlationState)
  4.     {
  5.         CultureInfo culture = correlationState as CultureInfo;
  6.         if (culture != null)
  7.         {
  8.             Thread.CurrentThread.CurrentCulture = culture;
  9.         }
  10.     }
  11.  
  12.     public object BeforeInvoke(InstanceContext instanceContext, IClientChannel channel, Message message)
  13.     {
  14.         object correlationState = null;
  15.  
  16.         object prop;
  17.         if (message.Properties.TryGetValue(HttpRequestMessageProperty.Name, out prop))
  18.         {
  19.             var httpProp = prop as HttpRequestMessageProperty;
  20.             string acceptLanguage = httpProp.Headers[HttpRequestHeader.AcceptLanguage];
  21.             CultureInfo requestCulture = null;
  22.             if (!string.IsNullOrEmpty(acceptLanguage))
  23.             {
  24.                 requestCulture = new CultureInfo(acceptLanguage);
  25.                 correlationState = Thread.CurrentThread.CurrentCulture;
  26.                 Thread.CurrentThread.CurrentCulture = requestCulture;
  27.             }
  28.         }
  29.  
  30.         return correlationState;
  31.     }
  32. }

To test it we can host the service, add an endpoint and add the appropriate behavior. In this case, since we’re using a HTTP endpoint, we add both the WebHttpBehavior (to make it a Web HTTP endpoint) and our GlobAwareEndpointBehavior, which will add the call context initializer. Then we send a few requests, one which should succeed, and one for each possible error message which the service can return.

  1. static void Main(string[] args)
  2. {
  3.     string baseAddres = "https://" + Environment.MachineName + ":8000/Service";
  4.     ServiceHost host = new ServiceHost(typeof(Service), new Uri(baseAddres));
  5.     ServiceEndpoint endpoint = host.AddServiceEndpoint(typeof(ITest), new WebHttpBinding(), "");
  6.     endpoint.Behaviors.Add(new WebHttpBehavior { DefaultOutgoingResponseFormat = WebMessageFormat.Json });
  7.     endpoint.Behaviors.Add(new GlobAwareEndpointBehavior());
  8.     host.Open();
  9.     Console.WriteLine("Host opened");
  10.  
  11.     string[] allRequests = new string[]
  12.     {
  13.         "name=John+Doe&email=john@doe.com&dateOfBirth=1970-01-01",
  14.         "name=&email=john@doe.com&dateOfBirth=1970-01-01",
  15.         "name=John+Doe&email=john&dateOfBirth=1970-01-01",
  16.         "name=John+Doe&email=john@doe.com&dateOfBirth=1470-01-01",
  17.     };
  18.  
  19.     foreach (string lang in new string[] { null, "en-US", "es-ES", "pt-BR" })
  20.     {
  21.         if (lang != null)
  22.         {
  23.             Console.WriteLine("Accept-Language: {0}", lang);
  24.         }
  25.  
  26.         foreach (string request in allRequests)
  27.         {
  28.             WebClient c = new WebClient();
  29.             if (lang != null)
  30.             {
  31.                 c.Headers[HttpRequestHeader.AcceptLanguage] = lang;
  32.             }
  33.  
  34.             try
  35.             {
  36.                 Console.WriteLine(c.DownloadString(baseAddres + "/CreatePerson?" + request));
  37.             }
  38.             catch (WebException e)
  39.             {
  40.                 Console.WriteLine(new StreamReader(e.Response.GetResponseStream()).ReadToEnd());
  41.             }
  42.         }
  43.  
  44.         Console.WriteLine();
  45.     }
  46. }

And that’s it for the last of the initializers.

[Code in this post]

[Back to the index]

Comments

  • Anonymous
    June 12, 2012
    I have an issue with the InstanceContextInitializer not executing the Initialize method. Is this something that is a known issue? I have posted the issue on Stackoverflow here:stackoverflow.com/.../extended-instancecontext-initalize-method-never-firesAny help would be appreciated.
  • Anonymous
    June 19, 2012
    Hi Erik N-P, I replied to the question on SO at stackoverflow.com/.../751090, and apparently your issue has been solved.
  • Anonymous
    September 18, 2014
    I know this is a couple years old, but I also have an issue with the Initialize method not being called, though I would suspect its something I'm doing (or not doing) on the client. I've posted a stackoverflow question as well here:stackoverflow.com/.../wcf-session-based-services-client-trackingAny help pointing me in the right direction is appreciated. Thanks!