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
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)
Expected: Both Compiled and Loose should use V1
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.
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?
Actual: Behaves as expected
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?
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?
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?
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?
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?
Expected: Workflow will run V2 from GAC
Actual: Compiled and Loose loaded V2 from the GAC as expected when Compiled ran first
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?
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.
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.
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.