Testing

webTiger Logo Wide

Reloading Workflows With VB Expressions In A WCF Host

Workflow

Loading (well, reloading) workflows that include Visual Basic expressions in a WCF hosted service is not as straightforward as one might expect. If you have created your XAML workflow based on an ActivityBuilder or a DynamicActivity then the Visual Basic settings aren’t configured automatically.

Microsoft provide details about this known issue and potential fixes here. This article expands upon that initial resolution, attempting to detect dependent assemblies from the XAML markup (and then using reflection for any nested dependencies of those first-level dependencies!)

The basis of this article relies Microsoft’s standard approach to persistence and tracking, but as a quick aside I’d like to mention a shortcoming I’ve found with it when implementing workflow solutions.

Microsoft’s default approach expects implementers to reload the workflow from the original XAML before resuming the workflow from the persistence store. These XAML workflows could be files that are altered over time and might not be version controlled. Consider a scenario where business analysts develop workflows using a re-hosted designer, for example, and the development team have no control over the publishing environment. This could lead to a situation where a persisted workflow cannot be resumed because the workflow structure has been modified in a way that breaks the persisted state.

This being the case, I generally add a table to the workflow database used for persistence and tracking and then serialise my XAML (and some other additional metadata I find useful) into that table, linking the data to the workflow instance ID. That way, I can always reload the exact workflow that was originally executed for a particular workflow instance being persisted.

Back to the topic at hand… For nearly all implementations I’ve worked on, the workflow engine that hosts and runs my workflows has been exposed as a WCF service. As you’ll note from Microsoft’s knowledge-base article referenced above, the workflow runtime won’t load or configure any Visual Basic settings for us in this case so any workflows that include VB expressions may fail to run. To overcome this we can manually detect any dependent assemblies, register the VB settings for them and then associate the settings with the root activity in the workflow.

First, we need to try to detect the dependencies the workflow has. It is assumed that the root activity has already been loaded (see serialising and reloading XAML workflows), and now it is only the references that need to be detected and resolved.

The root activity element of the workflow should be specified similar to the XAML fragment below, so it should be pretty easy to detect and extract the assembly references:

<Activity x:Class='{x:Null}'
    xmlns='http://schemas.microsoft.com/netfx/2009/xaml/activities'
    xmlns:s='clr-namespace:System;assembly=mscorlib'
    xmlns:cr='clr-namespace:Custom.Routines;assembly=MyRoutines'
    xmlns:cl='clr-namespace:Custom.Logging;assembly=MyLogs'
    xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
    <!--content omitted-->
</Activity>Code language: HTML, XML (xml)

As you will notice, the references are clearly identifiable and a method similar to the one below can be used to extract a collection of the names of the assemblies and namespaces the workflow consumes.

/// <summary>
/// Returns a collection of references that are declared on the root activity 
/// element in the supplied XAML markup. 
/// <para>
/// For each key-value pair in the collection, the key is the assembly name and 
/// the value is a collection of namespaces to reference.
/// </para>
/// </summary>
/// <param name="markup">The textual XAML markup for the activity.</param>
private SortedList<string, List<string>> GetReferences(string markup)
{
    SortedList<string, List<string>> refs = new SortedList<string, List<string>>();

    if (string.IsNullOrWhiteSpace(markup)) return refs;

    int start = markup.IndexOf("<Activity ") + 10;
    int end = markup.IndexOf(">", start + 1);

    if (start < 0 || end < 0 || start >= end) return refs;

    string attributes = markup.Substring(start, end - start);
    
    int offset = 0;
    string scope;
    string assemblyName;
    string dependentNamespace;

    while (attributes.IndexOf("xmlns", offset) != -1)
    {
        start = attributes.IndexOf("xmlns", offset) + 5;
        start = attributes.IndexOf("\"", start) + 1;
        end = attributes.IndexOf("\"", start);

        if (start >= end) continue; // Not a dependency.

        scope = attributes.Substring(start, end - start);
        offset = end + 1;

        start = scope.IndexOf("clr-namespace:") + 14;
        end = scope.IndexOf(";assembly=", start);

        if (start < 0 || start >= end) continue;

        dependentNamespace = scope.Substring(start, end - start);
        assemblyName = scope.Substring(end + 10); // +10 to start after the assembly= prefix

        if (!refs.ContainsKey(assemblyName))
        {
            refs.Add(assemblyName, new List<string>());
        }

        if (!refs[assemblyName].Contains(dependentNamespace))
        {
            refs[assemblyName].Add(dependentNamespace);
        }
    }

    return refs;
}Code language: C# (cs)

Once we’ve got a list of the references that need to be added, we can create a new Visual Basic settings object and associate it with out workflow’s root activity.

This works on the principle that a XAML file has been created in Visual Studio or a re-hosted workflow designer and the root element is an Activity, a DynamicActivity or an ActivityBuilder. (Note that a separate method is required for the activity builder because it doesn’t inherit from the Activity base class.)

/// <summary>
/// Returns a <see cref="Microsoft.VisualBasic.Activities.VisualBasicSettings"/>
/// object that includes reference imports for the supplied collection of assembly 
/// names and namespaces.
/// </summary>
/// <param name="refs">
/// A collection of references to import. 
/// <para>
/// For each key-value pair in the collection, the key is the assembly name and 
/// the value is a collection of namespaces to reference.
/// </para>
/// </param>
private VisualBasicSettings CreateVbSettings(SortedList<string, List<string>> refs)
{
    VisualBasicSettings settings = new VisualBasicSettings();

    foreach (string assemblyName in refs.Keys)
    {
        foreach (string dependentNamespace in refs[assemblyName])
        {
            settings.ImportReferences.Add(new VisualBasicImportReference
            {
                Assembly = assemblyName,
                Import = dependentNamespace
            });
        }
    }

    return settings;
}

/// <summary>
/// Initialises a <see cref="Microsoft.VisualBasic.Activities.VisualBasicSettings"/>
/// object, adding import references for dependencies detected in the supplied XAML 
// markup, and then registers it with the specified root activity.
/// </summary>
/// <param name="activity">The activity to register the VB settings with.</param>
/// <param name="markup">The textual XAML markup for the activity.</param>
private void RegisterVbRefs(Activity activity, string markup)
{
    SortedList<string, List<string>> refs = GetReferences(markup);
    VisualBasicSettings settings = CreateVbSettings(refs);
    DynamicActivity dynamicActivity = activity as DynamicActivity;

    if (dynamicActivity != null)
    {
        // For dynamic activities, the workflow is in the 'Implementation' 
        // so VB settings need to be added there.
        VisualBasic.SetSettingsForImplementation(dynamicActivity, settings);
    }
    else
    {
        VisualBasic.SetSettings(activity, settings);
    }
}

/// <summary>
/// Initialises a <see cref="Microsoft.VisualBasic.Activities.VisualBasicSettings"/>
/// object, adding import references for dependencies detected in the supplied XAML 
/// markup, and then registers it with the specified activity builder implementation.
/// </summary>
/// <param name="builder">The activity builder to register the VB settings with.</param>
/// <param name="markup">The textual XAML markup for the activity.</param>
private void RegisterVbRefsForBuilder(ActivityBuilder builder, string markup)
{
    SortedList<string, List<string>> refs = GetReferences(markup);
    VisualBasicSettings settings = CreateVbSettings(refs);
    VisualBasic.SetSettingsForImplementation(builder, settings);
}Code language: PHP (php)

That’s it. With the Visual Basic settings added to the configuration, the workflow should run as expected.