Share via


InstanceContextSharing

Download sample

This sample demonstrates how to use the IInstanceContextProvider interface to share InstanceContext objects across multiple calls and clients. This sample demonstrates how a client application can create and send a unique identifier to the service. The service then associates that identifier with a specific InstanceContext object. The client can then pass the identifier to another client. This client can then place the context identifier in a header that is sent to the same service. That service uses the context identifier to associate the second call with the first instance context object and therefore the service object.

NoteNote:

The setup procedure and build instructions for this sample are located at the end of this topic.

Implement the IInstanceContextProvider interface to provide the appropriate InstanceContext object to the system. Typically, this interface is implemented to support shared sessions, enable service instance pooling, control lifetimes of service instances, or to group contexts among clients.

To insert the custom IInstanceContextProvider, create a behavior (such as an IEndpointBehavior or IServiceBehavior) and use the behavior to assign the IInstanceContextProvider object to the System.ServiceModel.Dispatcher.DispatchRuntime.InstanceContextProvider property.

This sample uses a IServiceBehavior on a custom ShareableAttribute attribute to insert the IInstanceContextProvider.

The ShareableAttribute instantiates a CalculatorExtension object, which implements IInstanceContextProvider, and iterates through each EndpointDispatcher and sets each InstanceContextProvider property to the CalculatorExtension object just created. This is shown in the following sample code.

//Apply the custom IInstanceContextProvider to the EndpointDispatcher.DispatchRuntime
public void ApplyDispatchBehavior(ServiceDescription description, ServiceHostBase serviceHostBase)
{
    CalculatorExtension extension = new CalculatorExtension();
    foreach (ChannelDispatcherBase dispatcherBase in serviceHostBase.ChannelDispatchers)
    {
        ChannelDispatcher dispatcher = dispatcherBase as ChannelDispatcher;
        foreach (EndpointDispatcher endpointDispatcher in dispatcher.Endpoints)
        {
            endpointDispatcher.DispatchRuntime.InstanceContextProvider = extension;
            endpointDispatcher.DispatchRuntime.MessageInspectors.Add(extension);
        }
    }
}

Each call from the client goes through the CalculatorExtension object to determine which InstanceContext is used to serve that particular message.

The sample uses two clients, Client1 and Client2 to demonstrate the sharing. The sequence in which the two clients interact is shown in the following code. Note that both the client and server use a CustomHeader to communicate the unique identifier for the InstanceContext and use a random number generator utility to generate a unique 32-byte identifier.

public static class CustomHeader
{
    public static readonly String HeaderName = "InstanceId";
    public static readonly String HeaderNamespace = "http://Microsoft.ServiceModel.Samples/Sharing";
}

static string NewInstanceId()
{
    byte[] random = new byte[256 / 8];
    randomNumberGenerator.GetBytes(random);
    return Convert.ToBase64String(random);
}
  1. Client1 creates an OperationContextScope so it can add headers. It then generates a unique Id and then adds that Id as a value to its list of outgoing headers.

    //Create a new 1028 bit strong InstanceContextId that we want the server to associate 
    //the InstanceContext that processes all messages from this client.
    String uniqueId = NewInstanceId();
    
    MessageHeader Client1InstanceContextHeader = MessageHeader.CreateHeader(
        CustomHeader.HeaderName,
        CustomHeader.HeaderNamespace,
        uniqueId);
    
    try
    {
        using (new OperationContextScope(client1.InnerChannel))
        {
            //Add the header as a header to the scope so it gets sent for each message.
            OperationContext.Current.OutgoingMessageHeaders.Add(Client1InstanceContextHeader);
            ...
        }
    }
    

    Then it invokes DoCalculations that calls several operations on the remote server. For each operation invoked, the custom header and the id generated is sent.

  2. Client1 invokes Add operation, which is the first call on this channel.

  3. The server receives the message and invokes CalculatorExtension.GetExistingInstanceContext with the channel and the message. The extension checks whether the channel is attached to an InstanceContext. If not, the extension tries to look up the custom header and checks the cache to see if it has an InstanceContext for that id. In this case the cache is empty and so null is returned. Note that the extension actually stores an AddressableInstanceContextInfo (defined later in the source) to keep extra information about the InstanceContext and to coordinate between multiple threads before the InstanceContext is created.

    // If the channel has a session, we bind the session to a particular InstanceContext
    // based on the first message, and then route all subsequent messages on that session to
    // the same InstanceContext.
    bool hasSession = (channel.SessionId != null);
    if (hasSession)
    {
        info = channel.Extensions.Find<AddressableInstanceContextInfo>();
        if (info != null)
        {
            ...
        }
    }
    
    // If this is the first message of a session, or is using a datagram channel, look in
    // the message headers to see if there is a header with an instance id.
    int headerIndex = message.Headers.FindHeader(CustomHeader.HeaderName, CustomHeader.HeaderNamespace);
    
    // If there was a header, extract the instanceId.
    string instanceId = null;
    if (headerIndex != -1)
    {
        instanceId = message.Headers.GetHeader<string>(headerIndex);
    }
    
    ...
    
    // Check our table to see if we recognize the instance id.
    lock (this.ThisLock)
    {
        if ((instanceId == null) || !this.contextMap.TryGetValue(instanceId, out info))
        {
            isNew = true;
            ...
        }
        ...
    }
    ...
    if (isNew)
    {
        // This tells WCF to create a new InstanceContext and call InitializeInstanceContext.
        return null;
    }
    
  4. The server then creates a new InstanceContext and calls CalculatorExtension.InitializeInstanceContext. The InitializeInstanceContext method notifies the extension of the newly created InstanceContext. This retrieves the AddressableInstanceContextInfo either from the channel (for session channels) or the cache (for datagram channels) and adds it to the InstanceContext's extension collection. This is done so that the server can quickly retrieve the Id for any InstanceContext. If the channel has a session, the extension adds the channel to the InstanceContext's IncomingChannels collection so that Windows Communication Foundation (WCF) does not close the InstanceContext until the channel is closed. The sample also hooks up with the Closed event of InstanceContext so that it can be removed from the cache in case it is closed explicitly. Now this InstanceContext is used to process and reply to the message.

    if (hasSession)
    {
        // Since this is a new InstanceContext, we could not add the channel in
        // GetExistingInstanceContext, so add it here.
        instanceContext.IncomingChannels.Add(channel);
    
        // If we have a session, we stored the info in the channel, so just look it up
        // there.
        info = channel.Extensions.Find<AddressableInstanceContextInfo>();
    }
    else
    {
        // Otherwise, if we don't have a session, look the info up again in the table.
        ...
    }
    
    // Now that we have the InstanceContext, we can link it to the
    // AddressableInstanceContextInfo and vice versa.
    if (info != null)
    {
        instanceContext.Extensions.Add(info);
        info.SetInstanceContext(instanceContext);
    }
    
    // When the InstanceContext starts closing, remove it from the table.
    //
    // Generally we will already have the lock because Close will happen inside
    // CallIdleCallback.  However, if someone just closes the InstanceContext explicitly
    // before it goes idle, we will not have the lock.  Since modifying Dictionary is not
    // thread-safe, we lock here.
    instanceContext.Closing += delegate(object sender, EventArgs e)
    {
        lock (this.ThisLock)
        {
            this.contextMap.Remove(info.InstanceId);
        }
    };
    
  5. Client1 then calls its second operation Subtract. Once again, CalculatorExtension.GetExistingInstanceContext is called with the new message. The header is retrieved and this time the lookup succeeds. It returns the InstanceContext from the cache. WaitForInstance ensures that the first call has finished its InitializeInstanceContext call. WCF uses this InstanceContext to process the rest of the message.

    if (hasSession)
    {
        info = channel.Extensions.Find<AddressableInstanceContextInfo>();
        if (info != null)
        {
            // We may be processing a second message before the first message has finished
            // initializing the InstanceContext.  Wait here until the first message is
            // done.  If the first message has already finished initializing, this returns
            // immediately.
            info.IncrementBusyCount();
            return info.WaitForInstanceContext();
        }
    } 
    
  6. Client1 calls Subtract and Delete operations and they use the same InstanceContext by repeating step 5.

  7. The client then creates another channel (Client2) and once again creates an OperationContextScope and adds the same Id to its OutgoingMessageHeaders collection. So the same Id that was used by Client1 is now sent across with all calls made on Client2.

  8. Client2 calls Add(), Subtract(), Multiply() and Divide() operations and using the same logic in step 7, all of them are serviced by the InstanceContext created by the first client. The server calls CalculatorExtension.GetExistingInstanceContext. The extension finds the header and looks up the InstanceContext associated with it. This InstanceContext is used to dispatch the message.

    // If this is the first message of a session, or is using a datagram channel, look in
    // the message headers to see if there is a header with an instance id.
    int headerIndex = message.Headers.FindHeader(CustomHeader.HeaderName, CustomHeader.HeaderNamespace);
    
    // If there was a header, extract the instanceId.
    string instanceId = null;
    if (headerIndex != -1)
    {
        instanceId = message.Headers.GetHeader<string>(headerIndex);
    }
    
    // Remember if we created a new AddressableInstanceContextInfo.
    bool isNew = false;
    
    // Check our table to see if we recognize the instance id.
    lock (this.ThisLock)
    {
        if ((instanceId == null) || !this.contextMap.TryGetValue(instanceId, out info))
        {
            ...
        }
        ...
    }
    ...
    if (isNew)
    {
        // ...
    }
    else
    {
        InstanceContext instanceContext = info.WaitForInstanceContext();
        ...
        return instanceContext;
    }
    

To set up, build, and run the sample

  1. Ensure that you have performed the One-Time Setup Procedure for the Windows Communication Foundation Samples

  2. To build the solution, follow the instructions in Building the Windows Communication Foundation Samples.

  3. To run the sample in a single- or cross-machine configuration, follow the instructions in Running the Windows Communication Foundation Samples.

Footer image

Send comments about this topic to Microsoft.
© Microsoft Corporation. All rights reserved.