rulururu

post Best Practice: Config File Settings

August 4th, 2008

Filed under: Best Practices, How To, blackpearl — Colin

My current project is an ASP.NET application which integrates with K2 blackpearl processes to handle various workflow requirements and the app has a very rich object model consisting of 100+ business classes that the workflows must interact with. While it is conceivable that I could create a SmartObject Service wrapper for all of these entities, the work involved would be massive and couldn’t be justified for the project (especially since the object model is undergoing a lot of flux). So, rather than reinvent the wheel, my workflows use the existing object model for all data access.

Now, one tricky aspect of reusing the external assemblies is that they rely on configuration settings which are typically stored in the Web.config file. It utilizes settings from both the <connectionStrings> section as well as application settings from the <appSettings> section. So how do you make it so these external assemblies can find the information they are looking for when they are running under K2 blackpearl?

The answer is pretty simple, you need to add all these settings to the K2HostServer.config file (you did something very similar with K2.NET 2003 as well), but there are a few caveats:

When your assemblies require settings from the appSettings section

You can just add your settings to the appSettings section of the K2HostServer.config file and be done with it, but I do not recommend this method as it blends K2 settings with your custom settings and increases the likelyhood that a mistake will be made either when adding, updating or removing these settings. Instead, I recommend that you take advantage of the file attribute of the appSettings section. This allows you to specify a secondary file where settings are pulled from (for duplicate settings, the setting in this external file is used).

To implement this method, you would modify the appSettings section of K2HostServer.config like this:

<appSettings file="environment.config"> 
	<add key="addomain" value="[Domain]"/>
	<add key="applicationdomain" value="[Domain]"/>
	<add key="dependancyservice" value="SourceCode.Hosting.Services.DependancyService"/>
	<add key="port" value="5555"/>
	<add key="sessiontimeout" value="20"/>
	<add key="useassemblyregistration" value="false"/>
	<add key="AlwaysUseDefaultLabel" value="true"/>
	<add key="dbconnretries" value="100"/>
	<add key="dbconnretrydelay" value="30000"/>
</appSettings>

and then you create a file named what you specified in the file attribute (environment.config in this example). All the settings file contains is another appSettings section like this:

<appsettings>
	<add key="[Your App Setting]" value="[Your value]"/>
</appsettings>

This leads to a much cleaner separation of external application settings and K2 settings.

When your assemblies require connections from the connectionStrings section

K2 blackpearl encrypts the connectionString section of the K2HostServer.config, so it isn’t as obvious how to add additional connection strings since the whole section is encrypted.

Here is a sample of what a K2 blackpearl connectionStrings section looks like:

  <connectionStrings configProtectionProvider="RsaProtectedConfigurationProvider">
    <EncryptedData Type="http://www.w3.org/2001/04/xmlenc#Element"
      xmlns="http://www.w3.org/2001/04/xmlenc#">
      <EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#tripledes-cbc" />
      <KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
        <EncryptedKey xmlns="http://www.w3.org/2001/04/xmlenc#">
          <EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#rsa-1_5" />
          <KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
            <KeyName>Rsa Key</KeyName>
          </KeyInfo>
          <CipherData>
            <CipherValue>[CipherValue]</CipherValue>
          </CipherData>
        </EncryptedKey>
      </KeyInfo>
      <CipherData>
        <CipherValue>[CipherValue]</CipherValue>
      </CipherData>
    </EncryptedData>
  </connectionStrings>

So with everything encrypted like that, how do you add the other connection strings your assemblies depend on?

First, you have to know what the unencrypted K2 connection string is.
For most setups, this string is:

<add 
    connectionString="Data Source=[SQLSERVER];Initial Catalog=HostServer;Integrated Security=SSPI;Pooling=True;" 
    providerName="System.Data.SqlClient" 
    name="HostserverDB"/>

(You replace the [SQLSERVER] text with your actual SQL Server.) If your setup is different, then you will need to enter in the appropriate connection string.

So, to add your own connection strings, you completely replace the connectionString section in the config file so that it looks something like:

<connectionStrings>
  <!-- K2 Connection Strings -->
  <add 
    connectionString="Data Source=[SQLSERVER];Initial Catalog=HostServer;Integrated Security=SSPI;Pooling=True;"
    providerName="System.Data.SqlClient" 
    name="HostserverDB"/>
 
  <!-- Custom Connection Strings -->
  <add name="[Connection Name]" connectionString="[Connection String]"/>
</connectionStrings>

Then you simply restart the K2 Service. K2 then re-encrypts the connectionStrings section, so that the next time you open up the K2HostServer.config file, you’ll just see an encrypted block again, but that block will still contain your custom connection strings.

It would be nice if you could turn off encryption or if you could separate your connection strings from K2’s connection strings, but unfortunately, this isn’t possible. There is no equivalent to the file attribute for the <connectionStrings> section. The nearest is the configSource attribute which allows you to specify that settings come from a separate file, but unlike the file attribute on appSettings which performs a merge of the two, the configSource attribute is a replacement for what would otherwise be in the connectionStrings section. So you have to move all connectionStrings (including the K2 connection string) to this external file. And yes, the external connetionStrings file still gets encrypted.

For that reason, I don’t see much benefit in using the configSource attribute for K2 blackpearl. I do recommend including a commented out copy of your connectionStrings section in the .config file (perhaps don’t include the accounts/passwords used for security), so that people viewing the file can at least see what connections the encrypted section contains.

post Bug: Deleting Data Fields with Same Name as Workflow

July 29th, 2008

Filed under: K2, blackpearl, bugs — Colin

Ack! It’s almost the end of the month and I haven’t even posted a single blog entry. I’ve been too busy with my current project and also finishing up my chapter for the K2 book we’re writing.

But, I did run across a bug today I thought I’d do a quick blurb about.

If you have a Data Field with the same name as your workflow such as this (Note the ResidualAmount and ResidualAmount data field):

Data Fields

And then you delete the Data Field with the same name (ResidualAmount) you end up with this:

 Deleted Deal DataFields

So now all your process level Data Fields seem to have been deleted! This is actually just a bug in the UI and if you right click on Data Fields and select Refresh the rest of your fields will reappear. So definitely not a major bug, but still, it freaked me out for a second.

post K2 blackpearl Client Impersonation - Suggested Design Pattern

June 26th, 2008

Filed under: How To, K2, Technical, blackpearl — Colin

The K2 blackpearl client API has impersonation functionality that I make heavy use of in my current project. A sample might look like this:

View CodeCSHARP
    //Open a client connection (using a helper function) 
    Connection conn = OpenK2ClientConnection(); 
 
    //Impersonate the desired user 
    conn.ImpersonateUser(userName); 
 
    //Do whatever work you need to do as the impersonated user 
 
    //Revert back to the original user 
    conn.RevertUser();

This works quite well, but you have to always be sure to call conn.RevertUser() at the end of your impersonation call so that you revert back to the original user (assuming you plan on reusing the same connection). If you don’t, you’ll continue to use the impersonated user context which will obviously lead to unintended consequences. To be safer, a try/finally makes for a better approach.

View CodeCSHARP
    //Open a client connection (using a helper function) 
    Connection conn = OpenK2ClientConnection(); 
    try 
    { 
        //Impersonate the desired user 
        conn.ImpersonateUser(userName); 
 
        //Do whatever work you need to do as the impersonated user 
    } 
    finally 
    { 
        //Revert back to the original user 
        conn.RevertUser(); 
    }

The above code should always correctly revert our user, but it’s kind of a pain to write, results in a lot of “plumbing”/duplicate code and increases the chances implementing it will be overlooked somwhere. To overcome this, I recommend something like the following pattern be used:

View CodeCSHARP
    Connection conn = OpenK2ClientConnection(); 
    //Impersonate 
    using( Impersonation.Impersonate(userName, conn ) 
    { 
        //Do whatever work you need to do as the impersonated user 
    }

The result is a significant reduction in lines coded and guaranteed user revertion. This is accomplished by creating a custom class (Impersonation) which implements IDisposable and which always calls RevertUser at disposal. I use this approach in my BWT framework and it works flawlessly.

Here is my implementation of the Impersonation class:

View CodeCSHARP
    public class Impersonation : IDisposable 
    { 
        private Connection _conn = null; 
 
        private Impersonation(Connection conn) 
        { 
            _conn = conn; 
        } 
 
        #region Impersonate Methods 
 
        public static Impersonation Impersonate(string userName, Connection conn) 
        { 
            conn.ImpersonateUser(userName); 
 
            return new Impersonation(conn); 
        } 
 
        #endregion Impersonate Methods 
 
        #region IDisposable Members 
 
        public void Dispose() 
        { 
            _conn.RevertUser(); 
        } 
 
        #endregion 
    }

post Retrieving the Action Result DataField in an Activity

June 19th, 2008

Filed under: K2, Technical, blackpearl — Colin

I recently had the need to access the action that was selected by a user in a prior client event within the same Activity and the solution wasn’t as obvious as I thought it would be so I thought I’d post it for others.

Every activity which has a client event gets two DataFields added to it (Action Result and Outcome). So my first thought was that I could just retrieve the action the user selected using this code:

View CodeCSHARP
string action = (string) activityInstanceDestination.DataFields["Action Result"].Value;

However, this always resulted in an empty string. I eventually figured out that the action result is actually stored in Slot level DataFields.

View CodeCSHARP
            string actionResult = null; 
 
            foreach (SourceCode.KO.Slot slot in destination.ActivityInstance.WorklistSlots) 
            { 
                if( slot.DataFields.Contains("Action Result") ) 
                { 
                    actionResult = (string)slot.DataFields["Action Result"].Value; 
                    if( !String.IsNullOrEmpty(actionResult) ) 
                    { 
                        break; 
                    } 
                } 
            }

There might be a better way to associate a slot to a particular ActivityInstanceDestination, but I haven’t found it yet, so I just loop through all the slots for the activity until I find an Action Result that isn’t empty. If you know a better solution, please let me know!

An alternative approach which works if you are using an InfoPath form is to access the value the user selected from the XML data (as described in this post), but that approach is far more work and isn’t necessarily as authoritative as the action result in the DataField.

post SmartObject/Custom Service Brokers and Versioning

June 19th, 2008

Filed under: K2, blackpearl — Colin

I received a question regarding custom service brokers and versioning and my response seemed like a good blog topic.

Quick SmartObject Overview 

With blackpearl, a new concept called SmartObjects were introduced which is a fairly easy mechansim to access external data. This post isn’t meant to be a comprehensive look at SmartObjects, but some understanding of how they work is going to be required so here’s my fifteen second explanation.

SmartObjects are basically just a surfacing of data in a form which can be easily consumed by K2. They consist of fields (data) and methods to retrieve that data.

SmartObjects don’t interact with the external data source directly, instead they must interact with a SmartObject Service (service broker). The service broker is where you write all the actual code to retrieve data from the external system and hand it off to the smart object. A service broker is just a .NET assembly which has a class which inherits from ServiceAssemblyBase and which you register with blackpearl.

So that’s my quick overview. If you want to know more about writing your own service brokers, then check out my blog post about my Dynamic Web Service Broker.

Versioning in blackpearl

K2 has always had strong versioning capabilities in its workflow engine and one of the features (although sometimes I wish it were otherwise) is that the version of the workflow which starts is the version until completion (warts and all). So a natural question is how do SmartObjects and SmartObject Services factor in to a versioned workflow?

If you think about it for a moment, you’ll see that attempting to force versioning of external data systems you potentially have no control over introduces some unique challenges.

If blackpearl forced versioning, then something as simple as a column name change in the database you are accessing could break all currently running workflows which attempt to access the old column name. But at the same time, your workflow likely depends on a particular field in a SmartObject so if it isn’t versioned and the field name changes then the workflow is going to break! We’re at a bit of an impass… if we don’t allow changes then accessing external data systems which have had structural changes might break, if we do allow changes then our SmartObjects might break.

This is the same issue that ORMs face, and the K2 solution is conceptually similar. Always version SmartObjects, but make the mapping between the objects and the data flexible. This flexible mapping is achieved within the custom service broker, but it’s up to the author to implement it.

So first, let’s define some basic terminology:

A SmartObject service/service broker (think of this as a database) encapsulates one or more service objects (think of these as each being a single database table) which each contain methods (think of these as the Create/Retrieve/Update/Delete operations). Service brokers support versioning at the service object (table) level.

This is probably best explained with an example:

Example

Imagine that I have an external database of employees and I need to be able to retrieve information about them. So I might create a service broker with these characteristics:

  • Service Broker: EmployeeData
  • Service Object: Employees
  • Method: Get(string employeeId)

 And I create a SmartObject to access this:

  • SmartObject: Employee
  • Fields: EmployeeId, FirstName, LastName, HireDate
  • Method: Get(EmployeeId)

 And I write a workflow which consumes the Employee SmartObject and everything is glorious for a time. But then my company decides that the EmployeeData database isn’t going to only contain data for employees in a single company, but is going to employ data for employees in multiple companies and that now instead of employeeId being the primary key in the Employees table it is going to be a composite key of EmployeeId &  CompanyId.

This leads to two issues:

  • The Get(string employeeId) method on my service object is no longer sufficient to retrieve unique employees, I need to change it’s signature to Get(string employeeId, int companyId)
  • If I change the Get method, any currently running workflows that try to retrieve employee data are going to break because they expect to pass in only the EmployeeId.

Luckily, the blackpearl team recognized this issue and the solution lies within my service broker implementation. I need to update the old implementation so that it still accepts a single argument of employeeId, but have it pass in a hardcoded companyId in its query to the database. This solves the disconnect between my existing process instances and my datasource. Next I need to add a new implementation so that future workflows can support the two arguments properly (no hardcoded companyId). So after those changes my service broker looks like this:

  • Service Broker: EmployeeData
  • Service Object: Employees (version 1), Employees (version 2)
  • Methods:
    • Employees (version 1)
      • Get(string employeeId) - same public interface, but behind the scenes I’m passing in a hardcoded companyId parameter to the database
    • Employees (version 2)
      • Get(string employeeId, int companyId)

I now must redeploy my service broker to my K2 server(s). Then I update my SmartObject:

  • SmartObject: Employee
  • Fields: EmployeeId, FirstName, LastName, HireDate, CompanyId
  • Method: Get(EmployeeId, CompanyId)

I then redeploy my SmartObjects. Previously deployed workflows will continue to use the previous version of my SmartObject which is accessing version 1 of my Employees service object, while new workflows I deploy will use the latest version of my SmartObject which is accessing version 2 of my Employees service object.

Hopefully that makes sense….

Summary

So in summary,

  • SmartObjects are always versioned
    • If you don’t also version your service objects however, then SmartObject versioning is extremely brittle as any change to a service object may break the mappings to the SmartObject
  • Service Broker assemblies (.dll) are not versioned within blackpearl
    • Service Brokers authors can implement versioning of service objects, but this isn’t automatic.
    • Service Brokers authors must maintain support for all versions of service objects (at least until there are no more process instances using SmartObjects which access that version) and ensure that all versions work as expected against their data sources.

Hope that helps clear some of the confusion on this topic.

post Debugging External .dll’s Referenced by K2 blackpearl

June 12th, 2008

Filed under: Deployment, blackpearl, troubleshooting — Colin

I’ve indicated in previous posts, that I prefer to keep all of my custom code for my blackpearl processes in external .dll’s. The benefits to this approach are numerous: better integration with source control, better for team development, IntelliSense seems to work better, faster editing of code, more obvious structure and naming conventions just to name a few. 

But in order to debug these files, you do have to go through a few additional steps because blackpearl doesn’t automatically copy over the .pdb files to the workflow server. blackpearl creates a process specific folder for these .dlls at the time a particular process version is first started up, so one option is to copy the .pdb files directly to that folder, but blackpearl basically wipes out that directory every time it is stopped & started (and you can’t deploy updates/fixes if you choose this approach without deploying another version of the workflow because the .dlls get locked by the blackpearl service).

 So, while this isn’t a recommended approach on a production server, I place my custom .dlls and .pdb files in the root Host Server\Bin directory. Any .dlls found in this directory are used instead of the process specific set of .dlls. Here is batch file code which will automate this process:

@ECHO OFFREM ------------------------------------------------------------ 

REM -------------BEGIN CONFIGURATION SECTION-------------------- 

REM -------------Files To Copy Config--------------------------- 

set sourceDir="[The source directory of your .dlls (usually the path to your bin\debug dir)" 

set sourceFilter=[A filter to choose which .dlls to copy, usually just *] 

REM -------------BlackPearl Server Config----------------------- 

set k2ServerName=[ServerName] 

set targetDrive=[Shared Drive of K2 server] 

set workDir=%targetDrive%\Program Files\K2 blackpearl\Host Server\Bin\ 

REM -------------END CONFIGURATION SECTION---------------------- 

REM ------------------------------------------------------------ 

set targetDir=%workDir% 

%targetDrive% 

cd %targetDir% 

ECHO ---------------------------------------- 

ECHO Backing up existing files to %backupDir% 

ECHO ---------------------------------------- 

REM UNCOMMENT OUT THESE LINES IF YOU WANT TO BACKUP OLD DLLs 

REM set datetime=%DATE:~4,2%%DATE:~7,2%%DATE:~-4% %TIME:~0,2%%TIME:~3,2%%TIME:~6,2% 

REM set backupDir=%targetDir%\backup\%datetime% 

REM mkdir "%backupDir%" 

REM copy "%targetDir%\Capital*.*" "%backupDir%" 

ECHO ---------------------------------------- 

ECHO Copying new files 

ECHO ---------------------------------------- 

sc %k2ServerName% stop "K2 [blackpearl] Server" 

copy "%sourceDir%\%sourceFilter%.dll" "%targetDir%" /Y 

copy "%sourceDir%\%sourceFilter%.pdb" "%targetDir%" /Y 

sc %k2ServerName% start "K2 [blackpearl] Server" 

pause

The only issue with this approach is that you have to remember to remove the .dlls from your Host Server bin directory if you want .dlls deployed through the blackpearl deployment process to take precedence, but I feel like the advantages make it worth it. This allows you to change the .dlls and then deploy them to the server quickly in order to test your fixes.

Please note, I am only recommending this approach for development servers! You need to be very cautious of using this approach in production as it forces all versions of your workflow to use the .dlls in this directory rather than the .dlls deployed with the workflow version. This can lead to incompatibilities and workflows erroring out if you aren’t careful.

post Bug: ADO.NET SmartObject Provider and Joins

May 5th, 2008

Filed under: K2, blackpearl, bugs — Colin

So in a previous blog posting, I discussed creating joins using the SourceCode.SmartObjectClient API, but I also said that you could use the ADO.NET data provider as a simpler alternative. This weekend however, I found a pretty serious bug with the ADO.NET provider around joins which is large enough that I am now recommending that you avoid using the ADO.NET provider until it gets fixed.

Example of problem code:

View CodeCSHARP
SOConnection conn = OpenSmartClientConnection("Activity_Instance, 
    Process_Instance, Activity_Instance_Destination");   
 
SOCommand cmd = new SOCommand(); 
cmd.Connection = conn; 
cmd.CommandText = @" 
SELECT * FROM Process_Instance.List PI 
LEFT JOIN Activity_Instance_Destination.List AID ON 
    PI.ProcessInstanceID = AID.ProcessInstanceID 
LEFT JOIN Activity_Instance.List AI ON 
    AID.ProcessInstanceID = AI.ProcessInstanceID 
    AND AID.ActivityInstanceID = AI.ActivityInstanceID";   
 
using (SODataReader dr = cmd.ExecuteReader()) 
{ 
    DataSet ds = new DataSet("test"); 
    ds.Load(dr, LoadOption.OverwriteChanges, "Process_Instance"); 
}

The data in these tables are: 9 process instances, 3 activity_instance_destinations (each correspond to one of the 9 process instances), and 20 activity_instances (3 of which have destinations). So, I’d expect that the above query should return 9 rows, but instead it returns 13.

The problem is that the data provider ignores the second part of the ON (if I reverse the order it returns 11 rows). It doesn’t matter if I wrap it in () either.

If I craft the query using SmartObjectClientServer and joins then it works correctly, so it isn’t a problem with the SmartObjects themselves. Something like:

View CodeCSHARP
JoinDetails jd = new JoinDetails();   
 
jd.From = processInstances; 
Join join = new Join();   
 
join.AddCondition( 
    processInstances.Properties["ProcessInstanceID"], 
    activityDestinations.Properties["ProcessInstanceID"]);   
 
join.From = processInstances; 
join.To = activityDestinations; 
join.Scope = JoinScope.left; 
join.Type = JoinType.outer;   
 
Join join2 = new Join(); 
join2.From = activityDestinations; 
join2.To = activityInstances;   
 
join2.AddCondition( 
    activityDestinations.Properties["ActivityInstanceID"], 
    activityInstances.Properties["ActivityInstanceID"]);   
 
join2.AddCondition( 
    activityDestinations.Properties["ProcessInstanceID"], 
    activityInstances.Properties["ProcessInstanceID"]);   
 
join2.Scope = JoinScope.left; 
join2.Type = JoinType.outer;   
 
jd.Joins.Add(join); 
jd.Joins.Add(join2);   
 
SmartObjectList destinationsList = soServer.ExecuteList(jd,null);

works and properly returns 9 rows.

For those of you who love using the ADO.NET provider, there is a workaround (suggested by Jason Apergis), which is to move the conditions from the ON to the WHERE clause, like so:

SELECT * FROM Process_Instance.List PI 
LEFT JOIN Activity_Instance_Destination.List AID ON 
    PI.ProcessInstanceID = AID.ProcessInstanceID 
LEFT JOIN Activity_Instance.List AI ON 
    AID.ProcessInstanceID = AI.ProcessInstanceID 
    AND AID.ActivityInstanceID = AI.ActivityInstanceID 
WHERE ( 
    AID.ProcessInstanceID IS NULL 
    OR ( 
        AID.ProcessInstanceID = AI.ProcessInstanceID 
        AND AID.ActivityInstanceID = AI.ActivityInstanceID 
    ) 
)

But you shouldn’t have to write your queries to support a poor SQL language implementation on K2’s part though.

I have opened a trouble ticket with K2 and will update this blog entry with a resolution.

UPDATE:

This has been confirmed as a bug by K2 (it took a month!) and is listed as a high priority bug. No ETA on a fix though. For future reference, the internal bug number is TFS 16957.

ruldrurd
Next Page »
Powered by WordPress, Web Design by Laurentiu Piron
Entries (RSS) and Comments (RSS)