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:
- It could easily handle file references (file keys -> file paths).
- It could check expired items after a configurable timeout.
- 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.)
- It could call a custom callback that we could use to evaluate items and, eventually, dispose files (this replaced reason 3).
- 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:
- 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.
- 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:
- Items that represent uploaded files (as shown in the table above).
- 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:
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. 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 ...
Public Sub ApplyClientBehavior ...
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 Each op As DispatchOperation In dispatchRuntime.Operations
Dim op2 As DispatchOperation = op
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 ...
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)
End If
End If
End Sub
Public Function BeforeCall(operationName As String, inputs() As Object) As Object Implements System.ServiceModel.Dispatcher.IParameterInspector.BeforeCall
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
pi = pis(paramIndex)
If pi.Name.ToLower = "sessionid" Then
If paramIndex <= inputs.Length - 1 Then
Try
sessionID = inputs(paramIndex)
If sessionID <> String.Empty Then
If operationName.ToLower <> "disconnect" Then
Dim ci = CacheManager.GetItem(CACHE_SESSION_KEY & "_" & sessionID)
If ci IsNot Nothing Then
ci.RefreshLastUsedTime()
End If
Else
CacheManager.RemoveItem(CACHE_SESSION_KEY & "_" & sessionID)
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:
- The very first client connects, thus activates the web service.
- The
SessionTrackerInspectorAttribute.ApplyDispatchBehavior
is executed, attaching a ParameterInspector to every method in the WS contract. - 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. - The web service initializes the
CacheManager
.
- The web service passes the user credentials to the next layer (DSS) for authentication.
- The DSS layer validates the user against the database.
- The user is validated.
- A SessionID is generated in the DSS layer.
- Before returning control to the client, the
AfterCall
ParameterInspector method of the Login operation is called. - This method checks that the operation invoked was in fact
Login
, so the return value must be the SessionID that needs to be tracked. - The web service creates and stores a new cache item (token) that represents the client using the service.
- The control is returned to the client.
When the client uploads a file, this is what happens:
- The client request to store a new document page (file). The operation called is
UploadTempFile
in the web service. - 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.
- The token is found, so the lifetime of this item is automatically extended.
- The uploaded file is stored in a temporary folder in the web service.
- A cache key (string) is generated for the file and stored in the cache.
- 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 expirationPollFrequencyInSecond
s 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):
- A cache item refresh event is triggered.
- The callback (cb) associated with the cache item is called.
- The cb reads the SessionID bound to the cache item.
- The cb looks up the session token in the cache.
- If the token is found (which means, the client is using the service), the item (file) is re-cached.
- 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.