Windows Workflow Foundation has an out-of-the-box set of runtime services. One of the most popular is the Tracking Service, which is defined in the class SqlTrackingService. The SqlTrackingService class represents a fully functional tracking service. You can use this service to collect and store tracking information and tracking profiles, and provide them when requested by the workflow runtime engine. The SQL tracking service writes tracking data sent to it by the runtime tracking infrastructure to a SQL database. Well, I just copied the definition of SqlTrackingService from Windows SDK Documentation. In fact, there are a lot of information about SqlTrackingService.
However, it is not the first time that our customers question us about the user Identity who made some interaction on some workflow instance. Well, I think that SqlTrackingService is a good starting point for reflection about this requirement. Let's see: the best solution is to have an extensibility point in the SqlTrackingService that allows me to associate with the tracking event being saved more information. Unfortunately, this is not possible, but you can always write your own tracking service. Deep Dive in Tracking Services reading this article.
So, I decided to write my own tracking service. Since I decided to implement it as an extension of SqlTrackingService, my tracking service requires that SqlTrackingService is registered in the WorkflowRuntime. What I want is that my tracking service writes in a Sql Server table the identity information for each event written by SqlTrackingService. So let's start for the database and define the table
CREATE TABLE [dbo].[AuditingTrackingEvent]
(
[WorkflowID] [uniqueidentifier] NOT NULL,
[EventOrder] [int] NOT NULL,
[Username] [varchar](100) NOT NULL,
[EventDate] [datetime] NOT NULL,
)
GO
CREATE CLUSTERED INDEX IX_AuditingTrackingEvent_WorkflowID_EventOrder
ON AuditingTrackingEvent(WorkflowID, EventOrder)
GO
The column EventDate is not necessary, since the tables associated with the SqlTrackingService already record this information.
Now I have to implement my service, which I will call SqlAuditingService, extending the abstract class TrackingService
public class SqlAuditingService : TrackingService
{
protected override TrackingProfile GetProfile(Guid workflowInstanceId)
{
throw new Exception("The method or operation is not implemented.");
}
protected override TrackingProfile GetProfile(Type workflowType, Version profileVersionId)
{
throw new Exception("The method or operation is not implemented.");
}
protected override TrackingChannel GetTrackingChannel(TrackingParameters parameters)
{
throw new Exception("The method or operation is not implemented.");
}
protected override bool TryGetProfile(Type workflowType, out TrackingProfile profile)
{
throw new Exception("The method or operation is not implemented.");
}
protected override bool TryReloadProfile(Type workflowType, Guid workflowInstanceId, out TrackingProfile profile)
{
throw new Exception("The method or operation is not implemented.");
}
}
and my channel, SqlAuditingChannel, extending the abstract class TrackingChannel
public class SqlAuditingChannel : TrackingChannel
{
protected override void InstanceCompletedOrTerminated()
{
throw new Exception("The method or operation is not implemented.");
}
protected override void Send(TrackingRecord record)
{
throw new Exception("The method or operation is not implemented.");
}
}
The SqlAuditingService must have the same implementation as SqlTrackingService in respect to profile management. In fact, I want my SqlAuditingService to behave as a wrapper to SqlTrackingService. We have two approaches here:
1) inspect the code (via Reflector) in SqlTrackingService and duplicate it in our implementation;
2) Use reflection to invoke the equivalent methods on SqlTrackingService (you have to use reflection because of the visibility of the methods in the class SqlTrackingService).
It's up to you. (BTW: in our implementation we choose approach 1).
The main method is the GetTrackingChannel, which must return an instance of SqlAuditingChannel
protected override TrackingChannel GetTrackingChannel(TrackingParameters parameters)
{
return new SqlAuditingChannel(this);
}
In the SqlAuditingChannel, in the implementation of the Send methos, we must write to the database the identity information
protected override void Send(TrackingRecord record)
{
string userName = GetCurrentUsername();
WriteInDatabase(record, userName);
}
One possible implementation for the method GetCurrentUsername is
private string GetCurrentUsername()
{
string login = string.Empty;
if (HttpContext.Current != null)
{
login = HttpContext.Current.User.Identity.Name;
}
else
{
login = Thread.CurrentPrincipal.Identity.Name;
}
return login;
}
The implementation depends on your scenario, but for example if you are hosting the WF Runtime in IIS, and using the ManualWorkflowScheduler and using the Membership Provider, this implementation works fine.
The implementation depends on your scenario, but for example if you are hosting the WF Runtime in IIS, and using the ManualWorkflowScheduler and the Membership Provider, this implementation works fine.
This is a simplified version of the auditing service. Of course in the real world you must deal with some aspects
- Transactionality: if the SqlTrackingService was configured with IsTransactional = true, then your SqlAuditingChannel must be transactional either. For this you must implement the IPendingWork interface
- SharedConnection: if the SqlTrackingService and the SqlPersistenceService shares the same connection, avoiding to use distributed transaction, using the service SharedConnectionWorkflowCommitWorkBatchService, then you must use the same connection. This is a big deal, since this service was not designed to be open and allow other runtime services to use the connection and the transaction in use. In the real implementation we had to use reflection to get access to the connection and transaction. As an humble suggestion to the WF Team, please design the SharedConnectionWorkflowCommitWorkBatchService to be used by other services.
- At last, you must provide some kind of helper to explore and merge the native information of the Tracking service and our Auditing service. Remember that the column EventOrder (combined with WorkflowID) is the column that allows you to correlate events between the two services.
You can email me if you want to know more about the complete implementation.