Partager via


WCF Extensibility – IInstanceContextProvider

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 now back in the middle of the WCF runtime, and at the last few posts for this series. This time we’ll look at the instance context provider. The instance context represents the service which is running, but often (as most inner components in WCF) you don’t need to know anything about them, until you do. Unfortunately the MSDN documentation for the instance context isn’t very clear (it lists what it has, but doesn’t show many examples on where it can be used), but I’ve found that the post on instance context from Dan Rigsby to be a good introduction on the topic, where he lists some usages and properties of that class, so I won’t repeat that here.

The instance context provider, represented by the aptly named interface IInstanceContextProvider, is a way for a service to change how those contexts can be created. Similar to the IInstanceProvider (which defines how instances of the service class are created), it allows for tweaking the context creation, sharing of contexts between multiple calls, among other things, but the canonical usage of the instance context provider is to allow the sharing of service sessions between different clients.

Public implementations in WCF

As with most runtime extensibility points, there are no public implementations in WCF. There are three internal ones, however, which are chosen depending on the value of the InstanceContextMode property of a ServiceBehaviorAttribute which can be applied to the service class – you can use a tool such as ILSpy or Reflector to get more information on the implementation of those classes.

Note that when a custom IInstanceContextProvider is set in the dispatch runtime, the instance context mode set in the service behavior attribute is ignored.

Interface definition

  1. public interface IInstanceContextProvider
  2. {
  3.     InstanceContext GetExistingInstanceContext(Message message, IContextChannel channel);
  4.     void InitializeInstanceContext(InstanceContext instanceContext, Message message, IContextChannel channel);
  5.     bool IsIdle(InstanceContext instanceContext);
  6.     void NotifyIdle(InstanceContextIdleCallback callback, InstanceContext instanceContext);
  7. }

The first method in the instance context provider to be called is GetExistingInstanceContext, whenever a new message arrives to the service. For that message the provider can either return an existing instance context which will be reused, or return null (or Nothing) – in that case, a new instance context is created by the WCF runtime and passed to the InitializeInstanceContext method, so that it can be properly initialized. Notice that each new instance context created by the runtime counts towards the ServiceThrottle.MaxConcurrentInstances quota.

When the WCF runtime is done with the instance context, it will call IsIdle to check whether the instance context can be disposed. If the method return true, then the context will be closed and recycled, and the number of concurrent instances (bound by ServiceThrottle.MaxConcurrentInstances) will be decremented. If the method returns false, then NotifyIdle will be called. In this case, the runtime will pass a callback that the instance context provider must call whenever the instance context is done and ready to be recycled. When the callback is invoked, the runtime will again invoke IsIdle to check whether it can dispose and recycle the instance context.

How to add instance context providers

Instance context providers apply only at the server side, and they’re bound to 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.InstanceContextProvider = new MyInstanceContextProvider();
  6.     }
  7. }

Real world scenario: simulating sessions in session-less bindings

The canonical usage of the instance context provider (and the one in the WCF samples) is for two different client to be able to share a session object on the server. I decided to improve on that example, to, beside enable sharing session, also enable it to happen in bindings which don’t support sessions (such as BasicHttpBinding). In this example I’m using a custom SOAP header to identify the session, but another scenario where this could definitely be useful would be where a cookie was used for identification.

Anyway, let’s revive once again the calculator. But this time, to make things more interesting for sessions, let’s make it a calculator which works with postfix operators, using the Reverse Polish notation (RPN). So instead of having the usual operations taking two parameters, our contract is shown below. The operation Enter adds a parameter to a stack, and the operations take the top two parameters from the stack, process them, and store the result back in the stack.

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

And example helps to understand this concept. If we want to calculate ((4*5) / (3+2)), we’d call the following operations:

  • Enter(4)
  • Enter(5)
  • Multiply()
  • Enter(3)
  • Enter(2)
  • Add()
  • Divide()

Now, this new calculator has a concept of a stack. If we use a normal session-less binding (such as BasicHttpBinding or WebHttpBinding) to implement this contract, the operation calls would fail, because every call goes to a new instance (thus the stack would be empty). We cannot use the InstanceContextMode.PerSession property of the ServiceBehaviorAttribute, because for session-less bindings this is essentially the same as InstanceContextMode.Single. We could also have a static stack, but in this case the operations by two different clients would clobber each other. What we really need is to simulate a session over those bindings which don’t support them (notice that another way to do that is to create a protocol channel that changes the shape of the underlying channel from session-less to session-full; but as I mentioned before, if you can avoid writing channels, you should do always go for the alternative).

The approach I’m using in this sample is to keep a session alive for some time after the last message for that session arrived. This way, the client can send a series of messages which share the same header, and if they arrive close enough to each other, they will get the same instance context in the server (and consequently the same service instance). Each time a new message for that session is processed, the “expiration” of the session is extended. A background thread keeps checking the sessions to see which ones are expired, and marks those which are for removal.

Before we move on, the usual disclaimer: this is a sample for illustrating the topic of this post, this is not production-ready code. I tested it for a few contracts and it worked, but I cannot guarantee that it will work for all scenarios – please let me know if you find a bug or something missing. There are some security implications in keeping multiple instance contexts alive for a certain time period which are not addressed (if many different clients connect to the server, it would quickly exceed its MaxConcurrentInstances quota). Also, as usual, error checking has been kept to a minimum.

First, let’s look at the service code – it’s not as trivial as the “normal” calculator, but it’s quite simple nonetheless. Since the service class doesn’t need to deal with sessions itself (that’s why we’re using a custom instance context provider), it just implements the stack calculator as it normally would.

  1. public class StackCalculator : IStackCalculator
  2. {
  3.     Stack<double> values = new Stack<double>();
  4.     public void Enter(double value)
  5.     {
  6.         this.values.Push(value);
  7.         Console.WriteLine("Adding {0} to stack - {1}", value, GetStack());
  8.     }
  9.  
  10.     public double Add()
  11.     {
  12.         return this.Process((x, y) => x + y, "+");
  13.     }
  14.  
  15.     public double Subtract()
  16.     {
  17.         return this.Process((x, y) => x - y, "-");
  18.     }
  19.  
  20.     public double Multiply()
  21.     {
  22.         return this.Process((x, y) => x * y, "*");
  23.     }
  24.  
  25.     public double Divide()
  26.     {
  27.         return this.Process((x, y) => x / y, "/");
  28.     }
  29.  
  30.     private double Process(Func<double, double, double> operation, string symbol)
  31.     {
  32.         double op2 = this.values.Pop();
  33.         double op1 = this.values.Pop();
  34.         double result = operation(op1, op2);
  35.         this.values.Push(result);
  36.         Console.WriteLine("{0} {1} {2} = {3} - {4}", op1, symbol, op2, result, GetStack());
  37.         return result;
  38.     }
  39.  
  40.     private string GetStack()
  41.     {
  42.         StringBuilder sb = new StringBuilder();
  43.         sb.Append('[');
  44.         sb.Append(string.Join(",", this.values));
  45.         sb.Append(']');
  46.         return sb.ToString();
  47.     }
  48. }

The test program will just use the channel factory and channel as it would in a session-full scenario, but this time it will use a session-less binding, BasicHttpBinding – but we’ll add an endpoint behavior which will do the trick with the instance contexts. The client code is also similar, but we’ll use an operation context scope to add a header to all requests from the client, so that all the requests can go to the same server session.

  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.     var binding = new BasicHttpBinding();
  6.     ServiceEndpoint endpoint = host.AddServiceEndpoint(typeof(IStackCalculator), binding, "");
  7.     endpoint.Behaviors.Add(new SharedSessionEndpointBehavior());
  8.     host.Open();
  9.     Console.WriteLine("Host opened");
  10.  
  11.     ChannelFactory<IStackCalculator> factory = new ChannelFactory<IStackCalculator>(binding, new EndpointAddress(baseAddress));
  12.     IStackCalculator proxy = factory.CreateChannel();
  13.  
  14.     using (new OperationContextScope((IContextChannel)proxy))
  15.     {
  16.         OperationContext.Current.OutgoingMessageHeaders.Add(
  17.             MessageHeader.CreateHeader(
  18.                 Constants.HeaderName,
  19.                 Constants.HeaderNamespace,
  20.                 "abcdef"));
  21.  
  22.         proxy.Enter(2);
  23.         proxy.Enter(3);
  24.         proxy.Add();
  25.         proxy.Enter(4);
  26.         proxy.Subtract();
  27.         Console.WriteLine();
  28.     }
  29. }

The endpoint behavior will add our custom instance context provider to the runtime. And like the “official” sample, it will also add a message inspector which will be used to notify our session that the call has completed.

  1. class SharedSessionEndpointBehavior : IEndpointBehavior
  2. {
  3.     public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
  4.     {
  5.         SharedSessionInstanceContextProvider extension = new SharedSessionInstanceContextProvider();
  6.         endpointDispatcher.DispatchRuntime.InstanceContextProvider = extension;
  7.         endpointDispatcher.DispatchRuntime.MessageInspectors.Add(extension);
  8.     }
  9. }

Now, for the instance context provider class. The class stores a dictionary of instance contexts keyed by the identifier send by the client. When a new message arrives, we first try to fetch the header to find if the client sent any session identifier. If it did, we check if the session is active. If it is, we’ll retrieve a SharedInstanceContextInfo from the instance context extension list (see more about extensions in the previous post) and increment the number of instances which are using it. The extension object is a perfect place for this scenario, since it will live alongside the instance context object. If we didn’t find a session for the client (either because it didn’t send any identifier, or because there’s no instance context for the given identifier), then the method returns null, which signals WCF that it’s time to create a new instance context for that request.

  1. class SharedSessionInstanceContextProvider : IInstanceContextProvider, IDispatchMessageInspector
  2. {
  3.     private readonly static object SyncRoot = new object();
  4.     Dictionary<string, InstanceContext> instanceContexts = new Dictionary<string, InstanceContext>();
  5.     Thread expirationThread;
  6.  
  7.     public InstanceContext GetExistingInstanceContext(Message message, IContextChannel channel)
  8.     {
  9.         int headerIndex = message.Headers.FindHeader(Constants.HeaderName, Constants.HeaderNamespace);
  10.         if (headerIndex >= 0)
  11.         {
  12.             string instanceId = message.Headers.GetHeader<string>(headerIndex);
  13.  
  14.             lock (SyncRoot)
  15.             {
  16.                 if (this.instanceContexts.ContainsKey(instanceId))
  17.                 {
  18.                     InstanceContext context = this.instanceContexts[instanceId];
  19.                     SharedInstanceContextInfo info = context.Extensions.Find<SharedInstanceContextInfo>();
  20.                     info.IncrementBusyCount();
  21.                     return context;
  22.                 }
  23.             }
  24.         }
  25.  
  26.         return null;
  27.     }
  28. }

If the previous method (GetExistingInstanceContext) returns null, then WCF will create a new instance context, and pass it to the instance context provider so that it can be initialized. In this method, we first try to fetch the instance id send by the client; if there is none, we just create a new random one, then create a SharedInstanceContextInfo which will live alongside with the instance context as one of its extensions – this is similar to the code from the official sample. But to use the timed expiration, we’ll increment the “busy count” of the context info twice – one for the request which caused the new context to be created, and one so that the context will always be “alive”, until the expiration thread finally marks it down. We then add the new instance context to the provider’s dictionary, and start the expiration thread, if it hasn’t yet. Finally, the code will add a handler to the Closing event of the context: when WCF decides that it’s time to recycle it, we’ll be notified and we can remove it from the provider’s dictionary.

  1. public void InitializeInstanceContext(InstanceContext instanceContext, Message message, IContextChannel channel)
  2. {
  3.     string instanceId;
  4.     int headerIndex = message.Headers.FindHeader(Constants.HeaderName, Constants.HeaderNamespace);
  5.     if (headerIndex >= 0)
  6.     {
  7.         instanceId = message.Headers.GetHeader<string>(headerIndex);
  8.     }
  9.     else
  10.     {
  11.         instanceId = Guid.NewGuid().ToString();
  12.     }
  13.  
  14.     SharedInstanceContextInfo info = new SharedInstanceContextInfo(instanceId, instanceContext);
  15.     info.IncrementBusyCount(); // one for the current caller
  16.     info.IncrementBusyCount(); // one for the expiration timer
  17.  
  18.     instanceContext.Extensions.Add(info);
  19.  
  20.     lock (SyncRoot)
  21.     {
  22.         this.instanceContexts.Add(instanceId, instanceContext);
  23.         if (this.expirationThread == null)
  24.         {
  25.             this.expirationThread = new Thread(RemoveExpiredInstanceContexts);
  26.             this.expirationThread.Start();
  27.         }
  28.     }
  29.  
  30.     instanceContext.Closing += delegate(object sender, EventArgs e)
  31.     {
  32.         lock (SyncRoot)
  33.         {
  34.             this.instanceContexts.Remove(instanceId);
  35.             if (this.instanceContexts.Count == 0)
  36.             {
  37.                 this.expirationThread.Abort();
  38.                 this.expirationThread = null;
  39.             }
  40.         }
  41.     };
  42. }

The expiration thread method is a simple loop which every second looks at the instance contexts in the provider’s dictionary. If the context info associated with it is expired, then the context is marked to be removed, which is done by decrementing the “busy count” one last time so that it reaches zero.

  1. private void RemoveExpiredInstanceContexts()
  2. {
  3.     try
  4.     {
  5.         while (true)
  6.         {
  7.             lock (SyncRoot)
  8.             {
  9.                 List<SharedInstanceContextInfo> toRemove = new List<SharedInstanceContextInfo>();
  10.  
  11.                 foreach (var key in this.instanceContexts.Keys)
  12.                 {
  13.                     InstanceContext context = this.instanceContexts[key];
  14.                     SharedInstanceContextInfo info = context.Extensions.Find<SharedInstanceContextInfo>();
  15.                     toRemove.Add(info);
  16.                 }
  17.  
  18.                 foreach (var info in toRemove)
  19.                 {
  20.                     if (info.IsExpired())
  21.                     {
  22.                         info.DecrementBusyCount(); // let it get to 0
  23.                     }
  24.                 }
  25.             }
  26.  
  27.             Thread.CurrentThread.Join(1000); // check again in 1 second
  28.         }
  29.     }
  30.     catch (ThreadAbortException) { }
  31. }

The last two methods in the IInstanceContextProvider interface simply delegate the processing to the context info associated with the extension.

  1. public bool IsIdle(InstanceContext instanceContext)
  2. {
  3.     var info = instanceContext.Extensions.Find<SharedInstanceContextInfo>();
  4.     return info.IsIdle;
  5. }
  6.  
  7. public void NotifyIdle(InstanceContextIdleCallback callback, InstanceContext instanceContext)
  8. {
  9.     SharedInstanceContextInfo info = instanceContext.Extensions.Find<SharedInstanceContextInfo>();
  10.     info.SetIdleCallback(callback);
  11. }

You recall that the instance context provider also implements IDispatchMessageInspector – and we use its methods to decrement the “busy count” for the instance context information (which was incremented on either GetExistingInstanceContext or InitializeInstanceContext).

  1. public object AfterReceiveRequest(ref Message request, IClientChannel channel, InstanceContext instanceContext)
  2. {
  3.     return instanceContext;
  4. }
  5.  
  6. public void BeforeSendReply(ref Message reply, object correlationState)
  7. {
  8.     InstanceContext instanceContext = (InstanceContext)correlationState;
  9.     SharedInstanceContextInfo info = instanceContext.Extensions.Find<SharedInstanceContextInfo>();
  10.     info.DecrementBusyCount();
  11. }

That’s the instance context provider. A lot of the processing, though, happens on the extension class, SharedInstanceContextInfo, which is shown below. It stores the instance context associated with it, its expiration time and the number of active calls (plus the expiration one) in the “busy count”.

  1. class SharedInstanceContextInfo : IExtension<InstanceContext>
  2. {
  3.     internal static readonly int SecondsToIdle = 10;
  4.     DateTime expiration;
  5.     int busyCount;
  6.     InstanceContext instanceContext;
  7.     InstanceContextIdleCallback idleCallback;
  8.  
  9.     public SharedInstanceContextInfo(InstanceContext instanceContext)
  10.     {
  11.         this.instanceContext = instanceContext;
  12.         this.UpdateExpiration();
  13.         this.busyCount = 0;
  14.     }
  15. }

The busy / idle operations are interesting in the extension. Whenever a new message arrives, the instance context provider will increment the busy count in the extension. When the message processing finishes (on the dispatch message inspector), or with the expiration thread finds the instance context to be expired, they will decrement the busy count. Once it reaches zero, if any callback had been set by the WCF runtime, it will be called, which will cause WCF to call IsIdle again and, unless new messages arrived for the same instance identifier, the instance context will be marked for recycling.

  1. public void IncrementBusyCount()
  2. {
  3.     this.busyCount++;
  4. }
  5.  
  6. public void DecrementBusyCount()
  7. {
  8.     this.busyCount--;
  9.     this.CheckIdle();
  10. }
  11.  
  12. public void SetIdleCallback(InstanceContextIdleCallback callback)
  13. {
  14.     this.idleCallback = callback;
  15.     this.CheckIdle();
  16. }
  17.  
  18. private void CheckIdle()
  19. {
  20.     if (this.busyCount == 0 && this.idleCallback != null)
  21.     {
  22.         InstanceContextIdleCallback callback = this.idleCallback;
  23.         this.idleCallback = null;
  24.         if (callback != null)
  25.         {
  26.             try
  27.             {
  28.                 this.IsIdle = true;
  29.                 callback(this.instanceContext);
  30.             }
  31.             finally
  32.             {
  33.                 this.IsIdle = false;
  34.             }
  35.         }
  36.     }
  37. }

And that’s it. On the code gallery I’ve augmented the test program to perform more operations: have multiple instance contexts at the same time, verify that the sessions actually expire after a certain time, and other examples.

Coming up

Initializer interfaces in the WCF runtime

[Code in this post]

[Back to the index]

Comments

  • Anonymous
    June 11, 2012
    please tell me whether you can use this approach to service which is on IIS. As far as I know IIS can create a new instance of service and can lead to the fact that requests will be divided. Or am I mistaken?
  • Anonymous
    June 12, 2012
    Yes, IIS may recycle the services, but AFAIK it won't do that while there are active threads hanging on the service. I know that using the Reliable Messaging option in WSHttpBinding works under IIS, so I assume that this approach should work as well. But as usual, test before using it on production.