Click here to Skip to main content
11,642,216 members (62,217 online)
Click here to Skip to main content

Tagged as

Tracking “active” clients using method parameter inspection in a WCF web service

, 22 Jul 2014 CPOL 6.5K 3
Rate this:
Please Sign up or sign in to vote.
A client-tracking solution for a session-less WCF web service

Introduction

Web services are recommended to be stateless, that is, clients should not rely or depend upon session information in the service side.

But the truth is, in almost every client/server application, session information is necessary and should be stored somewhere: in a database, in a separate service process, in another server, etc.

In an application I’m working on, which is a document management (DM) system, there was a need to track which users were using a web service (part of the application).

Why the need of tracking users, why avoid the use of sessions in the web service and how a solution was implemented is explained in the following sections.

Background

The mentioned application is composed of 4 layers, but either the 4 or just 3 layers may be traversed depending on how the application is accessed: web or desktop.

The primary communication interface of the DocumentServer service (DSS) is .NET Remoting.

The document management application was born as a 3-layer native .NET application (desktop), but after some time a web UI was becoming more and more required, so a web service layer was introduced, mainly for 2 reasons:

  • As an interface between the web UI and the DSS layer.
  • As a point of integration with other (external) systems.

The web service layer was built with WCF in mind, and designed from scratch to be stateless, so no sessions would be handled there. All session information was going to be handled by the underlying DSS layer, as it was done from the beginning with the native .net clients.

In its first version, the web client was allowed to do just read-only operations, like search or view documents, so the stateless nature of the web service didn't impact in any client activity.

The problem appeared later, when the web client evolved with the need of modifying data, and upload files for server-side processing.

To put a little more background information on this topic: the desktop version of the client is able to scan images, do image-processing (deskew, despeckle, line removal, etc.), OCR and barcode recognition, annotations, digital signatures, and so on.

Trying to implement many of these features in a pure web client environment was (almost?) impossible, so the approach was to upload the images to the web server and process them there.

The problem

Uploading (large) images to a web service is not a trivial task, but after setting up the correct timeout values, transfer quotas, defining a specific MessageContract for the upload operation and use the correct binding (one that supports streaming like the predefined basicHttpBinding) the upload part was up and running. By the way, if you want to know more about the WCF upload/download process using streaming, check this inspiring blog from Stefano Ricciardi http://stefanoricciardi.com/2009/08/28/file-transfer-with-wcp/

So, what worried me more was how to keep track of the uploaded files, commit them to the next layer when necessary or dispose them when the client stopped using the service for whatever reason.

The logical component that should handle uploaded files would be a file manager, a component that could do all that was explained before.

But instead of reinventing the wheel I thought that there might be some already existing code that could be adapted for this task. That’s how I thought of the Microsoft.Practices.EnterpriseLibrary.Caching CacheManager.

The setup

Before explaining the setup, let’s answer this: why use this cache manager to handle files?

There are several reasons:

  1. It could easily handle file references (file keys -> file paths).
  2. It could check expired items after a configurable timeout.
  3. The cached items could be linked to a file (although this was not used in the end, it was a nice choice to have at hand.)
  4. It could call a custom callback that we could use to evaluate items and, eventually, dispose files (this replaced reason 3).
  5. Most of the code was already written, so little additional code was needed.

Explaining in detail how this cache manager works is beyond the scope of this post, but just as a side note, when items are added to the cache, a callback can be specified to the cache's Add method. This callback is called when an item needs to be refreshed so the system can evaluate whether it must be kept in or removed from the cache.

The logic implemented in the callback is totally up to the programmer, and its use will be explained later because it’s integrated with the client tracking mechanism used by the web service.

In order to use the CacheManager, I downloaded Microsoft Enterprise Library, and after installing it, I referenced the following assemblies in the Web Service project:

  • Microsoft.Practices.EnterpriseLibrary.Caching
  • Microsoft.Practices.EnterpriseLibrary.Common
  • Microsoft.Practices.EnterpriseLibrary.ServiceLocation

Then, using the configuration tool (right click on the web.config file -> Edit Enterprise Library Configuration)  I added the CacheManager required entries in the web.config file, as shown below:

<configuration>
  <configSections>
    <section name="cachingConfiguration" type="Microsoft.Practices.EnterpriseLibrary.Caching.Configuration.CacheManagerSettings, Microsoft.Practices.EnterpriseLibrary.Caching, Version=5.0.505.0, Culture=neutral, PublicKeyToken=3ceaadfbabdfd6a6" requirePermission="true" />
  </configSections>
  <cachingConfiguration defaultCacheManager="Cache Manager">
    <cacheManagers>
      <add name="MyCacheManager" type="Microsoft.Practices.EnterpriseLibrary.Caching.CacheManager, Microsoft.Practices.EnterpriseLibrary.Caching, Version=5.0.505.0, Culture=neutral, PublicKeyToken=3ceaadfbabdfd6a6"
        expirationPollFrequencyInSeconds="60" maximumElementsInCacheBeforeScavenging="1000"
        numberToRemoveWhenScavenging="100" backingStoreName="NullBackingStore" />
    </cacheManagers>
    <backingStores>
      <add type="Microsoft.Practices.EnterpriseLibrary.Caching.BackingStoreImplementations.NullBackingStore
NullBackingStore, Microsoft.Practices.EnterpriseLibrary.Caching, Version=5.0.505.0, Culture=neutral, PublicKeyToken=3ceaadfbabdfd6a6"
        name="NullBackingStore" />
    </backingStores>
  </cachingConfiguration>

The manager is configured to check items every 60 seconds. This value doesn’t specify the lifetime of cached items; instead it tells the manager how often to check if there are any expired items.

Item expiration time is specified when adding it to the cache. For example the following code will add an item (ci) with a specific key (key), the callback to call when the item needs to be refreshed (_itemRefreshHandler) and an expiration time of 20 minutes:

 _cache.Add(key, ci, CacheItemPriority.Normal, _itemRefreshHandler, New SlidingTime(TimeSpan.FromMinutes(20)))

So if the item just added is not accessed in our code in 20 minutes, the cache manager will call the callback (_itemRefreshHandler) and this procedure will determine whether the item should be disposed permanently or can be re-cached.

The process

When a client uploads an image, its filename is stored in the cache with a key that uniquely identifies it, and the file itself is stored into a temporary folder. The generated key is then returned to the client.

Later, when the client wants to save the document (which contains some metadata and all the uploaded files), it has to pass the collected keys to the web service and it would take care of moving the temporary files previously uploaded to the next service layer. This will store the files permanently and save the document in the database. After the job is done, the keys corresponding to the files are removed from the cache and the temporary files deleted from the web service folder.

The following graphic shows the communication path between components.

The cache approach resulted in these benefits:

  • The web service didn’t need to store any session information on its side, just keep the files in a temporary folder and their keys in the cache manager.
  • If the client didn’t save the document before a certain time, the cache would dispose the uploaded files automatically, preventing the WS to keep unused files and resources. The file disposal was done in the callback mentioned earlier, when the cache item is refreshed by the cache manager and the client owning that file is found to be inactive.

The caveat

The cache solution worked well when the time between page uploading and document saving was relatively small (20 minutes was the configured cache item expiration time), but if the wait was more than that the cache manager would start cleaning up the files and the document saving process would fail.

So the question was: how to keep the items in the cache active while a user was still using the service? Even more, how to tell if a user was still connected when there was no session information kept in the service side?

One easy answer could be: Modify all contract operations to keep track of the SessionID passed in, by calling some tracking procedure in top of every method, before doing any other operation... 

... Well, that would have been overkill. The contract has 56 methods, and it's planned to grow. It's not a good idea to edit all of them.

But what if we could intercept the calls the clients make to the web service, and inspect the SessionID parameter passed to the WS methods? That's where the WCF extensibility magic came in.

The solution

The implemented solution can be described in two parts:
  1. Implementing a hook in the WCF processing pipeline that allows us to check the parameters (and return values) of the calls made by the clients to the web service.
  2. Modify the use of the cache to take advantage on the information gathered by our hook.

WCF allows hooking into many places inside the processing pipeline. For example: before calling a method, after executing it, etc.  But its power goes further allowing the hook to be implemented only for certain (or all) methods, entire contracts, endpoints or even for the whole service. For more information, Carlos Figueira has an excellent blog about WCF extensibility at http://blogs.msdn.com/b/carlosfigueira/archive/2011/03/14/wcf-extensibility.aspx

Before the solution was implemented, the cache only stored items representing uploaded files.

This is the basic structure of a cache item:

Key Filename SessionID
UPDF092948 /tmpfiles/HFD0938.JPG SID01
UPDF937820 /tmpfiles/HDP9284.JPG SID01
UPDF829642 /tmpfiles/HJD3989.JPG SID02

Note: Each cache item has a property that binds it to a SessionID. This is what relates the files to the corresponding client.

After the modifications were implemented, the cache allowed storing two kinds of items:

  1. Items that represent uploaded files (as shown in the table above).
  2. Items that represent active clients.

When a client first connects to the WS, an item of the type 2 is created and stored in the cache.

This allows us to keep track of which user is using the web service. 

But in order to avoid disposing that item, we needed a way to tell whether the client was still using the service.

That’s where the WCF hook comes in. This hook was designed as a parameter inspector, which is composed of two classes:

  1. ParameterInspector: This class peeps on the parameters passed to every method in the WS contract and, if a SessionID parameter is found to be there, the cache item associated to that session is refreshed.
  2. SessionTrackerInspectorAttribute class: This attribute-derived class is applied to the web service implementation class. Its job is to iterate over the contract operations and attach the ParameterInspector class to them when the service is loaded for the first time.

The code of the SessionTrackerInspectorAttribute class (2) is this:

Imports System.ServiceModel.Description
Imports System.ServiceModel.Dispatcher
 
Public Class SessionTrackerInspectorAttribute
    Inherits Attribute
    Implements IContractBehavior
 
    Public Sub AddBindingParameters ... 'resumed for readability.. nothing done here
 
    Public Sub ApplyClientBehavior ... 'resumed for readability.. nothing done here
 
    Public Sub ApplyDispatchBehavior(contractDescription As System.ServiceModel.Description.ContractDescription, endpoint As System.ServiceModel.Description.ServiceEndpoint, dispatchRuntime As System.ServiceModel.Dispatcher.DispatchRuntime) Implements System.ServiceModel.Description.IContractBehavior.ApplyDispatchBehavior
        'for every operation defined in the service contract, attach our paramenterInspector class
        For Each op As DispatchOperation In dispatchRuntime.Operations
            Dim op2 As DispatchOperation = op 'local var to avoid compiler warning regarding linq
            'get the operation corresponding to the current dispatchOp in the contract
            Dim contractOp = (From o In contractDescription.Operations Where o.Name = op2.Name Select o).SingleOrDefault
            If contractOp IsNot Nothing Then
                op.ParameterInspectors.Add(New ParameterInspector(contractOp))
            End If
        Next
    End Sub
 
    Public Sub Validate ... 'resumed for readability.. nothing done here
 End Class

And this is the code of the ParameterInspector class (1):

Imports System.ServiceModel.Description
Imports System.ServiceModel.Dispatcher
Imports System.Reflection
 
Public Class ParameterInspector
    Implements IParameterInspector
    Private _od As OperationDescription
    Public Sub New(od As OperationDescription)
        _od = od
    End Sub
 
    Public Sub AfterCall(operationName As String, outputs() As Object, returnValue As Object, correlationState As Object) Implements System.ServiceModel.Dispatcher.IParameterInspector.AfterCall
        If operationName.ToLower = "login" Then
            If returnValue IsNot Nothing AndAlso returnValue.ToString.Length > 0 Then
                Dim sessionID As String = returnValue.ToString
                CacheManager.AddSessionTrackerObject(CACHE_SESSION_KEY & "_" & sessionID, sessionID) 'add the new sessionID to the cache
            End If
        End If
    End Sub
 
    Public Function BeforeCall(operationName As String, inputs() As Object) As Object Implements System.ServiceModel.Dispatcher.IParameterInspector.BeforeCall
        'check operation parameters. If SessionID is present, keep track of that sid by using the cacheManager
        If inputs Is Nothing Then
            Return Nothing
        End If
 
        Dim sessionID As String
        If _od IsNot Nothing AndAlso _od.SyncMethod IsNot Nothing Then
            Dim paramIndex As Integer
            Dim pi As ParameterInfo
            Dim pis() As ParameterInfo = _od.SyncMethod.GetParameters()
            If pis Is Nothing Then
                Return Nothing
            End If
            For paramIndex = 0 To pis.Length - 1 'look for the sessionid parameter in the method parameters
                pi = pis(paramIndex)
                If pi.Name.ToLower = "sessionid" Then
                    'now that we have the index, check that parameter in the inputs() array
                    If paramIndex <= inputs.Length - 1 Then
                        Try
                            sessionID = inputs(paramIndex) 'get the sessionID parameter value
                            If sessionID <> String.Empty Then
                                If operationName.ToLower <> "disconnect" Then
                                    'get the sessionTracker object associated the sid
                                    Dim ci = CacheManager.GetItem(CACHE_SESSION_KEY & "_" & sessionID)
                                    If ci IsNot Nothing Then    'if the sid exists in the cache, refresh its last used timestamp
                                        ci.RefreshLastUsedTime()
                                    End If
                                Else
                                    CacheManager.RemoveItem(CACHE_SESSION_KEY & "_" & sessionID) 'removes the sessionTracker object from the cache
                                End If
                            End If
                        Catch ex As Exception
                            Return Nothing
                        End Try
                    End If
                End If
            Next
        End If
        Return Nothing
    End Function
End Class

A couple of notes about this code:

  • All cache items (files or session identifiers) are wrapped in a class called CacheItem.
  • The CacheItem class has a property SessionID, that identified which session it belongs to.
  • CacheManager is a wrapper class around the real the Microsoft.Practices.EnterpriseLibrary.Caching CacheManager
  • AddSessionTrackerObject is a method in this wrapper that is used to create a custom CacheItem..nothing interesting to mention here really.

The service class implementation is then adorned with the SessionTrackerInspectorAttribute like this:

    <ServiceBehavior(ConcurrencyMode:=ConcurrencyMode.Multiple, 
        InstanceContextMode:=InstanceContextMode.PerCall), SessionTrackerInspector()>
    Public Class DDWS

        Implements IDigitalDocsWebService

The SessionTrackerInspectorAttribute class implements the IContractBehavior interface from the System.ServiceModel.Description namespace, which allows our hooking mechanism to attach to the contract of our DDWS service.

When the service is activated for the first time, the ApplyDispatchBehavior method is called. This method receives the contract description as a parameter and this contract description is used to iterate through all the operations defined in the contract and attach them a parameter inspector.

The ParameterInspector class implements the IParameterInspector interface from the System.ServiceModel.Dispatcher  namespace, which has two methods:

  • AfterCall: This method is executed right after the web service returns from the invoked operation and before the response is sent to the client. This gives us a chance to check a return value of a specific method of our contract: Login, which turns out to be the first method a client should call in order to use our service. The return value of this method is the SessionID that the client will use for subsequent calls. By intercepting this value we can create a cache item representing the connected client.
  • BeforeCall: This method is executed after the client invokes a method in the web service, but just before the message is dispatched to the corresponding operation in the service's implementation class. This gives us a chance to analyze the parameters passed to the operation a see if a SessionID is there. If a SessionID is found, the cache item representing the session is refreshed, so the other items (mainly uploaded files) that depend on it are kept safe.

The whole process can be visualized in this graphic:

  1. The very first client connects, thus activates the web service.
    1. The SessionTrackerInspectorAttribute.ApplyDispatchBehavior is executed, attaching a ParameterInspector to every method in the WS contract.
    2. The BeforeCall ParameterInspector method of the Login operation is executed, but since this operation doesn’t have a SessionID parameter, BeforeCall has nothing to do.
    3. The web service initializes the CacheManager.
  2. The web service passes the user credentials to the next layer (DSS) for authentication.
  3. The DSS layer validates the user against the database.
  4. The user is validated.
  5. A SessionID is generated in the DSS layer.
    1. Before returning control to the client, the AfterCall ParameterInspector method of the Login operation is called. 
    2. This method checks that the operation invoked was in fact Login, so the return value must be the SessionID that needs to be tracked.
    3. The web service creates and stores a new cache item (token) that represents the client using the service.
  6. The control is returned to the client.

When the client uploads a file, this is what happens:

  1. The client request to store a new document page (file). The operation called is UploadTempFile in the web service.
  2. The BeforeCall ParameterInspector method is executed for the UploadTempFile operation; the SessionID is detected in the parameter list, so a lookup for the session token in the cache is performed.
    1. The token is found, so the lifetime of this item is automatically extended.
  3. The uploaded file is stored in a temporary folder in the web service.
    1. A cache key (string) is generated for the file and stored in the cache.
  4. The file key is returned to the client for future reference.

And next is the procedure when the cache launches an item refresh request. The cache’s expirationPollFrequencyInSeconds is set to 60 seconds, which means that the CacheManager will check items every minute to see which ones need to be kept or removed from the cache (the procedure described is for cache items representing files, not session IDs):

  1. A cache item refresh event is triggered.
  2. The callback (cb) associated with the cache item is called.
  3. The cb reads the SessionID bound to the cache item.
  4. The cb looks up the session token in the cache.
    1. If the token is found (which means, the client is using the service), the item (file) is re-cached.
    2. If the token is not found, the item is removed and the associated file is deleted.

Conclusion

You may be asking: why not use a session-aware web service instead of doing all this stuff?

Although WCF allows using sessions in the web service, those sessions aren’t like ASP.NET sessions.

WCF sessions are service instances, and the state is part of each service instance, with all the complexity of stateful channels, reliable messaging and so on.

Enabling session support in our web service would mean that another session-aware component was being introduced to the system besides the already existing ASP.NET and the DocumentServer service. This would have added complexity since more timeouts would have to be set up and eventually synchronized with other session-aware layers.

Also, WCF sessions had mean more resources used in the WS layer, and we wanted it to be fast, lightweight, and of easy integration with other external systems that may not be designed to use web service sessions.

The solution may seem complex at first, but it is not… just a couple of classes to set up the WCF hook, a cache manager wrapper that could handle file storage/deletion and a good understanding of the WCF extensibility model is all that was required to make the code run.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Alekz
Architect
Argentina Argentina
No Biography provided

You may also be interested in...

Comments and Discussions

 
-- There are no messages in this forum --
| Advertise | Privacy | Terms of Use | Mobile
Web01 | 2.8.150731.1 | Last Updated 22 Jul 2014
Article Copyright 2014 by Alekz
Everything else Copyright © CodeProject, 1999-2015
Layout: fixed | fluid