In this previous post I showed how to use the TaskQueryService to query tasks. In this post, I am going to take this one step further and update the task payload and process the task. In order to do this, we need to use both the TaskQueryService and the TaskService. This introduces a couple of new challenges that we need to deal with.
Let’s take a look at the basic outline of the code first, then drill into the challenges. First, we need to authenticate to the engine, as we did in the previous example. This is done by calling the authenticate operation on the TaskQueryService web service.
TaskQueryServiceClient tqs = new TaskQueryServiceClient("TaskQueryServicePort"); // provide credentials for ws-security authentication to WLS to call the web service tqs.ClientCredentials.UserName.UserName = "weblogic"; tqs.ClientCredentials.UserName.Password = "welcome1"; // set up the application level credentials that will be used to get a session on BPM (not WLS) credentialType cred = new credentialType(); cred.login = "weblogic"; cred.password = "welcome1"; cred.identityContext = "jazn.com"; // authenticate to BPM Console.WriteLine("Authenticating..."); workflowContextType ctx = tqs.authenticate(cred); Console.WriteLine("Authenticated to TaskQueryService");
Next, we need to retrieve the task that we want to update. In this example, I am just hard coding the task number. Then we call the getTaskDetailsByNumber operation on the TaskQueryService web service, passing in the context we got back from the authenticate operation, and the task number.
taskDetailsByNumberRequestType request = new taskDetailsByNumberRequestType(); request.taskNumber = "200873"; request.workflowContext = ctx; task task = tqs.getTaskDetailsByNumber(request);
Now that we have the task, we want to update the payload. In this example, I up just updating one of the string parameters in the payload to contain the text “changed in .net” and then updating the payload in our local copy of the task.
TaskService.TaskServiceClient ts = new TaskService.TaskServiceClient("TaskServicePort"); System.Xml.XmlNode[] payload = (System.Xml.XmlNode[])task.payload; payload.ElementAt(0).ChildNodes.Item(1).InnerText = "changed in .net"; task.payload = payload;
Now to actually update the real task on the server, we need to call the updateTask operation on the TaskService web service and pass it our locally updated task. This call will return back a new task object which represents the updated task.
// update task TaskService.taskServiceContextTaskBaseType updateTaskRequest = new TaskService.taskServiceContextTaskBaseType(); updateTaskRequest.workflowContext = ctx; updateTaskRequest.task = task; TaskService.task updatedTask = ts.updateTask(updateTaskRequest);
Now, we want to take an action on the task, in this case I have just hardcoded the “OK” action. To have the task processed, we call the updateTaskOutcome operation on the TaskService web service, again we pass in the context and the updated task object.
// complete task TaskService.updateTaskOutcomeType updateTaskOutcomeRequest = new TaskService.updateTaskOutcomeType(); updateTaskOutcomeRequest.workflowContext = ctx; updateTaskOutcomeRequest.outcome = "OK"; updateTaskOutcomeRequest.Item = updatedTask; ts.updateTaskOutcome(updateTaskOutcomeRequest);
So, this all looks relatively straight forward and if you have followed our custom worklist sample then the code probably looks pretty similar to the Java code in that sample. But unfortunately, this code will not work as is.
The problem we have here is to do with the way web services work in .Net. For each of the two web services that we want to use, the TaskQueryService and the TaskService, we need to add a service reference to our .Net solution. When we add the service reference, we need to create a namespace, and they need to be unique. So we end up with two definitions of task in two different namespaces, i.e. we get a TaskQueryService.task and a TaskService.task. These are in fact exactly the same and came from the same Java object, but because of the way web service references work, .Net does not think they are the same object, and you cannot cast from one to the other.
This creates an issue for us, as we get our workflowContext object from the TaskQueryService but we need to provide it to the TaskService. There is no way to get it from the TaskService. If you invest five or ten minutes into searching the web, you will discover this is a fairly common issue encountered in .Net when using web services.
So what do we do?
My initial approach was to just write some logic to manually convert the objects. That looked something like this:
public static TaskService.workflowContextType convertWorkflowContextType(TaskQueryService.workflowContextType input) { TaskService.workflowContextType output = new TaskService.workflowContextType(); output.credential = convertCredentialType(input.credential); output.locale = input.locale; output.timeZone = input.timeZone; output.token = input.token; return output; }
This does not look too bad, but the issue is in the size of these objects. Notice that the credential is a complex type and I need another method like this to copy it. So in order to actually implement this method for just the workflowContext and the task objects, we would need several hundred lines of ugly boring boilerplate code. So I gave up on this method.
My second approach was to use reflection to do a deep copy on the objects. This looked promising and I found several samples online, but again, I ran into issues. First, it had problems with arrays. Once I fixed this, it then had problems with enumerated types. Again, this was getting pretty ugly, so I abandoned this method too.
Next, I turned to an open source (MIT-license) project called AutoMapper which addresses this very issue. I found that investing a few minutes in learning how to use AutoMapper resolved my issues completely. So this is the approach I have adopted. Here is the code that configures the AutoMapper to handle our two types we are discussing, and all of the embedded subtypes we need:
// set up the automapper AutoMapper.Mapper.CreateMap<TaskQueryService.workflowContextType, TaskService.workflowContextType>(); AutoMapper.Mapper.CreateMap<TaskQueryService.credentialType, TaskService.credentialType>(); AutoMapper.Mapper.CreateMap<TaskQueryService.task, TaskService.task>(); AutoMapper.Mapper.CreateMap<TaskQueryService.attachmentType, TaskService.attachmentType>(); AutoMapper.Mapper.CreateMap<TaskQueryService.callbackType, TaskService.callbackType1>(); AutoMapper.Mapper.CreateMap<TaskQueryService.customAttributesType, TaskService.customAttributesType>(); AutoMapper.Mapper.CreateMap<TaskQueryService.documentType, TaskService.documentType>(); AutoMapper.Mapper.CreateMap<TaskQueryService.EvidenceType, TaskService.EvidenceType>(); AutoMapper.Mapper.CreateMap<TaskQueryService.processType, TaskService.processType>(); AutoMapper.Mapper.CreateMap<TaskQueryService.commentType, TaskService.commentType>(); AutoMapper.Mapper.CreateMap<TaskQueryService.identityType, TaskService.identityType>(); AutoMapper.Mapper.CreateMap<TaskQueryService.ucmMetadataItemType, TaskService.ucmMetadataItemType>(); AutoMapper.Mapper.CreateMap<TaskQueryService.systemAttributesType, TaskService.systemAttributesType>(); AutoMapper.Mapper.CreateMap<TaskQueryService.actionType, TaskService.actionType2>(); AutoMapper.Mapper.CreateMap<TaskQueryService.displayInfoType, TaskService.displayInfoType>(); AutoMapper.Mapper.CreateMap<TaskQueryService.shortHistoryTaskType, TaskService.shortHistoryTaskType>(); AutoMapper.Mapper.CreateMap<TaskQueryService.assignmentContextType, TaskService.assignmentContextType1>(); AutoMapper.Mapper.CreateMap<TaskQueryService.assignmentContextTypeValueType, TaskService.assignmentContextTypeValueType1>(); AutoMapper.Mapper.CreateMap<TaskQueryService.collectionTargetType, TaskService.collectionTargetType>(); AutoMapper.Mapper.CreateMap<TaskQueryService.collectionTargetActionType, TaskService.collectionTargetActionType>(); AutoMapper.Mapper.CreateMap<TaskQueryService.preActionUserStepType, TaskService.preActionUserStepType>(); AutoMapper.Mapper.CreateMap<TaskQueryService.systemMessageAttributesType, TaskService.systemMessageAttributesType>(); AutoMapper.Mapper.CreateMap<TaskQueryService.flexfieldMappingType, TaskService.flexfieldMappingType>(); AutoMapper.Mapper.CreateMap<TaskQueryService.scaType, TaskService.scaType>(); AutoMapper.Mapper.CreateMap<TaskQueryService.UpdatableEvidenceAttributesType, TaskService.UpdatableEvidenceAttributesType>(); // check automapper config is valid AutoMapper.Mapper.AssertConfigurationIsValid();
But I really don’t want to have all the AutoMapper code messing up my nice clean class. So I went one step further and implemented some implicit operators so that I can write my code like I showed at the start of this article and pretend that this issue does not even exist. Here is the code to implement implicit operators to covert from TaskQueryService.task to TaskService.task and from TaskQueryService.workflowContext to TaskService.workflowContext:
namespace TaskService { partial class workflowContextType { public static implicit operator workflowContextType(TaskQueryService.workflowContextType from) { return AutoMapper.Mapper.Map(from); } } partial class task { public static implicit operator task(TaskQueryService.task from) { return AutoMapper.Mapper.Map(from); } } }
So here is the completed class:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using ConsoleApplication1.TaskQueryService; using System.Web.Services; using System.ServiceModel.Security; using System.ServiceModel.Security.Tokens; namespace ConsoleApplication1 { class Class1 { static void Main(string[] args) { Console.WriteLine("Sample C# TaskQueryService client"); init(); // set up the TaskQueryService client // Note that this constructor refers to an endpoint configuration that is defined in the app.config // which was created by Visual Studio when you added the web service reference. // You have to edit the app.config to set the security mode to "TransportCredentialOnly" // and the transport clientCredentialType to "Basic" TaskQueryServiceClient tqs = new TaskQueryServiceClient("TaskQueryServicePort"); // provide credentials for ws-security authentication to WLS to call the web service tqs.ClientCredentials.UserName.UserName = "weblogic"; tqs.ClientCredentials.UserName.Password = "welcome1"; // set up the application level credentials that will be used to get a session on BPM (not WLS) credentialType cred = new credentialType(); cred.login = "weblogic"; cred.password = "welcome1"; cred.identityContext = "jazn.com"; // authenticate to BPM Console.WriteLine("Authenticating..."); workflowContextType ctx = tqs.authenticate(cred); Console.WriteLine("Authenticated to TaskQueryService"); // get task taskDetailsByNumberRequestType request = new taskDetailsByNumberRequestType(); request.taskNumber = "200873"; request.workflowContext = ctx; task task = tqs.getTaskDetailsByNumber(request); // get TaskService TaskService.TaskServiceClient ts = new TaskService.TaskServiceClient("TaskServicePort"); // update the payload System.Xml.XmlNode[] payload = (System.Xml.XmlNode[])task.payload; payload.ElementAt(0).ChildNodes.Item(1).InnerText = "changed in .net"; task.payload = payload; // update task TaskService.taskServiceContextTaskBaseType updateTaskRequest = new TaskService.taskServiceContextTaskBaseType(); updateTaskRequest.workflowContext = ctx; updateTaskRequest.task = task; TaskService.task updatedTask = ts.updateTask(updateTaskRequest); // complete task TaskService.updateTaskOutcomeType updateTaskOutcomeRequest = new TaskService.updateTaskOutcomeType(); updateTaskOutcomeRequest.workflowContext = ctx; updateTaskOutcomeRequest.outcome = "OK"; updateTaskOutcomeRequest.Item = updatedTask; ts.updateTaskOutcome(updateTaskOutcomeRequest); // all done Console.WriteLine(); Console.WriteLine("Press enter to exit"); Console.Read(); } private static void init() { // set up the automapper AutoMapper.Mapper.CreateMap<TaskQueryService.workflowContextType, TaskService.workflowContextType>(); AutoMapper.Mapper.CreateMap<TaskQueryService.credentialType, TaskService.credentialType>(); AutoMapper.Mapper.CreateMap<TaskQueryService.task, TaskService.task>(); AutoMapper.Mapper.CreateMap<TaskQueryService.attachmentType, TaskService.attachmentType>(); AutoMapper.Mapper.CreateMap<TaskQueryService.callbackType, TaskService.callbackType1>(); AutoMapper.Mapper.CreateMap<TaskQueryService.customAttributesType, TaskService.customAttributesType>(); AutoMapper.Mapper.CreateMap<TaskQueryService.documentType, TaskService.documentType>(); AutoMapper.Mapper.CreateMap<TaskQueryService.EvidenceType, TaskService.EvidenceType>(); AutoMapper.Mapper.CreateMap<TaskQueryService.processType, TaskService.processType>(); AutoMapper.Mapper.CreateMap<TaskQueryService.commentType, TaskService.commentType>(); AutoMapper.Mapper.CreateMap<TaskQueryService.identityType, TaskService.identityType>(); AutoMapper.Mapper.CreateMap<TaskQueryService.ucmMetadataItemType, TaskService.ucmMetadataItemType>(); AutoMapper.Mapper.CreateMap<TaskQueryService.systemAttributesType, TaskService.systemAttributesType>(); AutoMapper.Mapper.CreateMap<TaskQueryService.actionType, TaskService.actionType2>(); AutoMapper.Mapper.CreateMap<TaskQueryService.displayInfoType, TaskService.displayInfoType>(); AutoMapper.Mapper.CreateMap<TaskQueryService.shortHistoryTaskType, TaskService.shortHistoryTaskType>(); AutoMapper.Mapper.CreateMap<TaskQueryService.assignmentContextType, TaskService.assignmentContextType1>(); AutoMapper.Mapper.CreateMap<TaskQueryService.assignmentContextTypeValueType, TaskService.assignmentContextTypeValueType1>(); AutoMapper.Mapper.CreateMap<TaskQueryService.collectionTargetType, TaskService.collectionTargetType>(); AutoMapper.Mapper.CreateMap<TaskQueryService.collectionTargetActionType, TaskService.collectionTargetActionType>(); AutoMapper.Mapper.CreateMap<TaskQueryService.preActionUserStepType, TaskService.preActionUserStepType>(); AutoMapper.Mapper.CreateMap<TaskQueryService.systemMessageAttributesType, TaskService.systemMessageAttributesType>(); AutoMapper.Mapper.CreateMap<TaskQueryService.flexfieldMappingType, TaskService.flexfieldMappingType>(); AutoMapper.Mapper.CreateMap<TaskQueryService.scaType, TaskService.scaType>(); AutoMapper.Mapper.CreateMap<TaskQueryService.UpdatableEvidenceAttributesType, TaskService.UpdatableEvidenceAttributesType>(); // check automapper config is valid AutoMapper.Mapper.AssertConfigurationIsValid(); } } namespace TaskService { partial class workflowContextType { public static implicit operator workflowContextType(TaskQueryService.workflowContextType from) { return AutoMapper.Mapper.Map<TaskQueryService.workflowContextType, workflowContextType>(from); } } partial class task { public static implicit operator task(TaskQueryService.task from) { return AutoMapper.Mapper.Map<TaskQueryService.task, task>(from); } } } }
Where to next? My next step is to take this approach and apply it to writing a human task user interface in ASP.NET C# and integrate that into the BPM Workspace as shown in the example below.
Thank you for your unique information. When to expect your next post about integration of custom user interface written in C# into the BPM Workspace? If possible, point out where I can get more information about that.
Hi thanks for your comment. I am working on posting it soon. I can give you the raw information if you need it.
Thank you for your response.
Such information is simply not available elsewhere. Many thanks in advance.
Mark,
I could like to get the raw information on this, if you could share. Thanks a lot.
Hi, do you mean the VS project?
Pingback: Writing a Human Task UI in .Net (C#/ASP.NET) or in fact anything other than ADF | RedStack
Mark, you mentioned you are getting ready to post the details about integration of custom user interface written in C# into the BPM Workspace? and you can provide the raw information about it. I could use that info now, if you could email me. Thanks a lot.
Take a look at the latest post
Hi Mark,
You can solve the problem of converting from TaskQueryService.task to TaskService.task, and from TaskQueryService.workflowContext to TaskService.workflowContext, in a much simpler way. Actually, you don’t even need to convert anything at all. When you add a Service Reference in VS, if you click “Show All Files” next to the Solution Explorer, you’ll notice that there is a file called “Reference.svcmap”, which essentially stores service metadata, including the WSDL that you have specified (and from which the types are derived), inside . By default, a single MetadataSource/WSDL is used, but you can manually edit this file to include both WSDLs, i.e.:
Then, when you update your Service Reference, types that are common to both WSDL will only be generated once, and will be included in the same namespace, thereby solving the issue of duplicated types.
Thank you very much for sharing this tip!
<MetadataSources>
<MetadataSource Address=”http://ip:8001/integration/services/TaskQueryService/TaskQueryService?WSDL” Protocol=”http” SourceId=”1″ />
<MetadataSource Address=”http://ip:8001/integration/services/TaskService/TaskServicePort?WSDL” Protocol=”http” SourceId=”2″ />
</MetadataSources>