Updating a task from .Net

Introduction

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.

Main Article

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.

# Copyright 2012 Oracle Corporation. 
# All Rights Reserved. 
# 
# Provided on an 'as is' basis, without warranties or conditions of any kind, 
# either express or implied, including, without limitation, any warranties or 
# conditions of title, non-infringement, merchantability, or fitness for a 
# particular purpose. You are solely responsible for determining the 
# appropriateness of using and assume any risks. You may not redistribute. 

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.

dotnet-ft-form31e9

Add Your Comment