WF4 Spike: Activity Versioning, GAC and Loose XAML

In agile software development, a spike is a story that cannot be estimated until a development team runs a timeboxed investigation. The output of a spike story is an estimate for the original story.
- SearchSoftwareQuality.com Definitions

Update 1/8/2011: For a solution to these problems see this post.  Special thanks to Krisragh for his help with this post.

For this spike I want to answer the following questions

  • What happens when a V1 workflow loads a V2 activity?
  • What happens when a V2 workflow cannot find a V2 activity but a V1 activity is available?
  • What difference if any does it make if the activity is deployed in the GAC?
  • What difference does it make if you use Compiled or Loose XAML

endpoint.tv - WF4 XAML Assembly Resolution & The Case of the Unexpected Activity! | Channel 9

Get Microsoft Silverlight

I will run through a number of scenarios with various options. For each scenario the format is Activity (version), Host (version), Deploy (option), XAML option (which one runs first)

Deployment options
Application Base Deploy the activity assembly to the same directory as the workflow application
GAC Deploy the activity assembly in the GAC
XAML options
Compiled XAML A .XAML file that has a XamlAppDef build task in Visual Studio and is deployed in the assembly – this is the default setting for XAML
Loose XAML .XAML file that is deployed as a file and loaded by ActivityXamlServices.Load
Project artifacts
Host XamlAssemblyResolution.exe Workflow Console Application with WorkflowCompiled.xaml and WorkflowLoose.xaml
Activity AcvitityLibrary1.dll Contains a custom activity named GetTypeInfo

Scenario 1: Activity V1, Host V1, Deploy Application Base, Compiled/Loose

Expected: Both Compiled and Loose should use V1

SNAGHTML511c75e

Scenario 2: Activity V1, Host V1, Deploy GAC (V1), Compiled/Loose

Expected: Both Compiled and Loose should use V1 from the GAC even though the Activity DLL is in the Application Base

Actual: The activity was loaded from the GAC.  For more info see How the Runtime Locates Assemblies.

SNAGHTML512596f

Scenario 3: Activity V2, Host V1, Deploy Application Base, Loose

Expected: Activity V1 from the GAC will be used for both loose and compiled

Actual: Not Expected! When you run the Loose XAML first, it will load V2 from the file and the Compiled XAML will load V1. 
Why does Loose XAML load a different activity version when run before compiled XAML?

SNAGHTML513b5f0

Scenario 4: Activity V2, Host V1, Deploy Application Base, Compiled

Expected: Activity V1 from the GAC will be used for both loose and compiled

Actual: Behaves as expected

SNAGHTML5156523

Scenario 5: Activity V2, Host V1, Deploy GAC (V1/V2), Loose

Expected: Activity V1 from the GAC will be used for both loose and compiled because the host was built for V1

Actual: Not Expected! When you run the Loose XAML first, it will load V2 from the GAC and the Compiled XAML will load V1. 
Why does Loose XAML load a different activity version when run before compiled XAML?

SNAGHTML522c8c5

Scenario 6: Activity V2, Host V1, Deploy GAC (V1/V2), Compiled

Expected: Activity V1 from the GAC will be used for both loose and compiled because the host was built for V1

Actual: Behaves as expected

Scenario 7: Activity V2, Host V1, Deploy GAC (V2), Loose/Compiled

Expected: Compiled and Loose will fail because V1 is not available

Actual: Not Expected!  Both Compiled and Loose loaded V2 from the GAC even though they were not built for V2 of the activity 
Why do both Compiled and Loose XAML load versions of the activity than what they were built with?

SNAGHTML5294969

Scenario 8: Activity V2, Host V1, Deploy Application Base, Loose/Compiled

Expected: Compiled and Loose will fail because V1 is not available

Actual: Not Expected!  Both Compiled and Loose loaded V2 from the application base
Why do both Compiled and Loose XAML load versions of the activity than what they were built with?

SNAGHTML53dba54

Scenario 9: Activity V1, Host V2, Deploy Application Base, Loose/Compiled

Expected: Compiled and Loose will fail because V2 is not available

Actual: Not Expected!  Both Compiled and Loose loaded V1 from the application base
Why do both Compiled and Loose XAML load versions of the activity than what they were built with?

SNAGHTML541ae24

Scenario 10: Activity V1, Host V2, Deploy GAC (V1), Loose/Compiled

Expected: Compiled and Loose will fail because V2 is not available

Actual: Not Expected!  Both Compiled and Loose loaded V1 from the GAC
Why do both Compiled and Loose XAML load versions of the activity than what they were built with?

SNAGHTML5486939

Scenario 11: Activity V1/V2, Host V2, Deploy GAC (V2), Application Base (V1), Compiled

Expected: Workflow will run V2 from GAC

Actual: Compiled and Loose loaded V2 from the GAC as expected when Compiled ran first

SNAGHTML5a828d1

Scenario 12: Activity V1/V2, Host V2, Deploy GAC (V2), Application Base (V1), Loose

Expected: Workflow will run V2 from GAC

Actual: Not Expected!  Loose loaded V1 from Application Base and Compiled loaded V2 from the GAC
Why do both Compiled and Loose XAML load versions of the activity than what they were built with?

SNAGHTML5a7583a

Investigations

Why does Loose XAML load a different activity version when run before compiled XAML?

A: Because the generated class _XamlStaticHelper specifically tried to load the version that was referenced at compile time.

When you look at the activity assembly reference in XAML you will see that it does not include the version or public key token

 xmlns:a="clr-namespace:ActivityLibrary1;assembly=ActivityLibrary1"

How does the version get referenced in compiled XAML?

The XamlAppDef build task will generate a file that creates a class which represents the compiled workflow.  My workflow is WorkflowCompiled.xaml so the generated file (located under obj\x86) is WorkflowCompiled.g.cs.  Contained in that file is a line of code that reveals where the XAML comes from

 System.IO.Stream initializeXaml = typeof(WorkflowCompiled).Assembly.GetManifestResourceStream(resourceName);

When this class reads the XAML it uses a XamlSchemaContext to help it interpret the XAML and it gets the context from a generated class called _XamlStaticHelper. 

 System.Xaml.XamlSchemaContext schemaContext = XamlStaticHelperNamespace._XamlStaticHelper.SchemaContext;

if we open the _XAMLAssemblyResolution.g.cs file and look at the _XamlStaticHelper.SchemaContext property we can see what is going on in the body of the method you see this.

 if ((AssemblyList.Count > 0)) {
                    xsc = new System.Xaml.XamlSchemaContext(AssemblyList);
                }

There is an AssemblyList property! And what does it contain? We see from the LoadAssemblies method that the XamlAppDef build task has generated a fully qualified reference to ActivityLibrary1 (version 1) – here is a cleaned up version of the code

 private static IList<Assembly> LoadAssemblies()
{
    var assemblyList = new List<Assembly>();
    assemblyList.Add(
        Load("ActivityLibrary1, Version=1.0.0.0, Culture=neutral, PublicKeyToken=c18b97d2d48a43ab"));

    // Many other assemblies here
    assemblyList.Add(Assembly.GetExecutingAssembly());
    return assemblyList;
}

Because this code is generated at build time, the Compiled XAML will first try to load the specific version of the activity it was created with.

Why do both Compiled and Loose XAML load versions of the activity than what they were built with?

In spite of the fact that the compiled host assembly specifies a version of the activity library should be loaded; our testing shows that a compiled will load older or newer versions of the activity if the specific version is not available, and loose XAML will load any assembly with a matching name (even one that does not have a strong name).

The _XamlStaticHelper.Load() method is the reason why this happens

 private static Assembly Load(string assemblyNameVal)
{
    var assemblyName = new AssemblyName(assemblyNameVal);
    var publicKeyToken = assemblyName.GetPublicKeyToken();
    Assembly asm = null;
    try
    {
        asm = Assembly.Load(assemblyName.FullName);
    }
    catch (Exception)
    {
        // Can't load it? Try a version independent load
        var shortName = new AssemblyName(assemblyName.Name);
        if (publicKeyToken != null)
        {
            shortName.SetPublicKeyToken(publicKeyToken);
        }

        asm = Assembly.Load(shortName);
    }

    return asm;
}

It first tries to load the assembly using the full name and if that fails it catches the exception and tries to load it with the name and public key token but no version.  This is different than the typical CLR behavior which requires a specific version match.

Summary

To wrap this up I have to say… be careful.  WF4 workflows do not follow the same rules for assembly versioning that you might expect. 

  • Compiled XAML will try to load the specified version if available.  If not, they will do a version independent load but will respect the PublicKeyToken so they won’t load assemblies with the wrong signature

  • Loose XAML will load any matching assembly based on name alone with no respect for version or public key token.  If the AppDomain has previously loaded the type from some other mechanism (CLR or XAML) then the Loose XAML will always load the type previously loaded.

  • If you have to use Loose XAML and you want to be sure that you are loading a type of a certain version and/or PublicKeyToken you could use some code with a CLR reference to those types to cause the types to load into the AppDomain prior to calling ActivityXamlServices.Load