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
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
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.
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?
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
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?
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?
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?
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?
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?
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
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?
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