This is my second post on custom Workflow Activities. In my first post I made it easy to myself: I’ve given you some links where you can find more information on developing custom activities. Those links have a lot of information and do a very good job at introducing their subjects. I didn’t feel like repeating what was allready written.
Today I’ll introduce you to some more advanced scenario’s: I’ll explain the ActivityCodeGeneratorAtribute , and the WorkflowCompilerParameters.UserCodeCompileUnits property. Both poperties allow you to extend your workflow duing compilation.
The workflow compilation process
To understand what exactly happens when using Activity code generation, we will first see what happens when compiling a normal Workflow project.
Again, I will forward you to two articles allready available on the internet which give a good introduction to the subject. After that I will have a deeper dive into the process.
The two introducory articles are WF – Compile and execute an activity/workflow at runtime and Authoring Workflows
Using code generation to construct your activity
As you may be aware of, you can interfere with the compilation process of your custom activity by attaching a ActivityCodeGenerator attribute to your custom activity:
[ActivityCodeGenerator(typeof(ConvertToGreyscaleCodeGenerator))]
public class ConvertToGreyscale : Activity
{
// More code here ...
}
The class specified in the typeof-declaration must be a class inherited from the ActivityCodeGenerator class. In your class you must override the base classes GenerateCode method:
public override void GenerateCode(CodeGenerationManager manager, object obj)
{
// Your code here ...
}
Where does this interfere with the compilation process? As you can see from the signature of the GenerateCode method, you receive as a second parameter an object of the type of Activity you are trying to compile!!! Where does this come from? And what is in the first CodeGenerationManager parameter?
The workflow compilation process: a detailed look
So, what actually happens inside the compilation project? To get an answer to this question I investigated the workflow assemblies with Reflector and this is what I came up with:
- Step 1: seperate the provided files in two arrays:
- a first array with XOML files
- a second array with other files
- Step 2: execute some setup code. This code generaly makes types needed for compilation, like types you reference in your code, known to the compilation process.
- Step 3: generate a temporary assembly from those two arrays
- Step 3.1: generate a CodeCompileUnit unit from the provided xoml files
- Step 3.2: generate code files from this CodeCompileUnit
- Step 3.3: generate an assembly in memory from these generated code files and the provided code files
- Step 4: make the generated assembly and its types known to the compilation process
- Step 5: perform a definitive compilation
- Step 5.1: for each type in the temporary assembly (see Step 3) that is an Activity
- Step 5.1.1: do some setup
- Step 5.1.2: perform the validation
- Step 5.1.3: add it to a list of activities
- Step 5.2: generate, again, a CodeCompileUnit unit from the provided xoml files
- Step 5.3: for each type in the list
- Step 5.3.1: if the type (which is an Activity) has no parent, apply each known ActivityCodeGenerator for that type, providing it the temporary generated activity instance and the CodeCompileUnit unit incapsulated in the CodeGenerationManager parameter
- Step 5.4: generate code files from the UserCodeCompileUnits and the CodeCompileUnit unit from
- Step 5.5: generate the definitive assembly from these generated code files and the original “other files” array from Step 1
- Step 6: return the definitive assembly
What can we assert from all this?
Assertion 1: The Activity derived object you receive in the GenerateCode method is incomplete
This was more or less to be expected. The codegeneration is intended to give you the opportunity to extend the type, so by definition the type can not be complete. By analyzing the compilation process we got confirmation of this assumption: the type of the object provided to the ActivityCodeGenerator in Step 5.3.1 is the type generated in the temporary assembly in Step 3.
Assertion 2: The CodeGenerationManager object you receive only contains the types defined in xoml files
As you can see, one of the first things that happen are seperating the XOML files and the other files in different lists during Step 1 of the compilation process. Then, in Step 5.2 of the process, a CodeCompileUnit unit is generated from the list of files containing the XOML files, and this unit is provided to the CodeGenerationManager handed to the ActivityCodeGenerator derived class.
This immedately shows that if you provide now XOML files, then the CodeCompileUnit will be empty and you will receive to CodeDOM elements in the CodeGenerationManager.
This also shows up as an empty CodeNamespaceCollection in the debugger:
Bug or feature?
From the pseudo code of the compilation process you can see that each xoml file is transformed to it’s CodeDOM model. Unfortunately, each namespace of the xoml file is always added to a collection of namespaces, even if an earlier xoml file allready resulted in adding the same namespace.As a result, the method GetCodeTypeDeclaration of the ActivityCodeGenerator class never returns the CodeTypeDeclaration of any type declared in subsequent namespaces.
For example:
I provide two XOML files to the compiler:File 1
<ns0:ConvertToGreyscalex:Class="HFK.CustomActivities.Activity1"File 2
<ns0:ConvertToGreyscalex:Class="HFK.CustomActivities.Activity2"Then, when the comilation process arrives at the custom code generation I set a breakpoint and look into the provided CodeGenerationManager, I see the following:
Indeed: two namespaces with the same name. Then when you ask the GetCodeTypeDeclaration method to look for the CodeTypeDeclaration of your second class with name “HFK.CustomActivities.Activity2”, it looks in the collection for the corresponding namespace, then in that namespace for teh class declaration but only finds the Activity1 class and concludes, wrongly, that the Activity2 class is not available. It doesn’t go looking into the other equally named namespace.
Assertion 3: Only the ActivityCodeGenerator for the root activities are called from the compilation process
If you look in the workflow DLL’s and follow the compilation process, you will ultimately get to some lines of code similar to the following:
foreach (Activity activity in list)
{
if (activity.Parent == null)
{
foreach (ActivityCodeGenerator generator
in manager.GetCodeGenerators(activity.GetType()))
{
generator.GenerateCode(manager, activity);
}
}
}
Indeed, before calling the codegenerators, the compilation project checks of the activity has no parents. In a workflow, the activity with no parents is the root activity. So consequently, only the root acivity has it’s codegenerators called.
Assertion 4: Wait a minute, you say that only the ActivityCodeGenerator for the root activities are called from the compilation process?
Aha !! You where paying attention !! Indeed, assertion 3 is really strange because that would mean that codegenerators for classes inside a workflow would never get called, and anyone having used Microsofts WebServiceInputActivity activity knows for a fact this isn’t true.
Well, the assertion is true, but the above conclusion is wrong. I didn’t tell you everything…
Wait, there’s more…
What I didn’t mention was the CompositeActivityCodeGenerator applied to the CompositeActivity. And this CompositeActivity just happens to live in the inheritance tree of the SequentialWorkflowActivity activity which is the root for all sequential activities. And for those using StateMachineWorkflowActivity as the root activity, don’t worry: CompositeActivity activity is also in its inheritance tree.
The GenerateCode method of the CompositeActivityCodeGenerator class has a loop which calls the GenerateCode method for all ActivityCodeGenerator objects of the root activity’s enabled child activities. So, allthough the ActivityCodeGenerators aren’t called directly through the compilation project, they get called through the CompositeActivityCodeGenerator of the root activity.
Assertion 5: You can only adapt classes that are defined in a xoml file
This is actualy a too hard statement. The truth being that you can adapt any class that was originaly defined as partial.
Using Visual Studio too generate files for your Activity derived classes, when generating a class in code that is not a partial class. If you then generate a partial class in the same namespace you will get a compiler message saying that a non partial class was allready defined. Only when you generate a XOML defined activity does Visual Studio generate a partial class and can you extend that with your own partial class CodeDOM statements.
Of course, if you completely define your own Activity classes in code and define them as being partial, you can extend them through the CodeDOM.
Downloads
In the following download you can find code which demonstrates all 5 assertions:
To execute the assertions do the following:
- In the Main method, put in comments all lines excepts the one for the assertion you want to test
- In the method GenerateCode of the class CustomActivityCodeGenerator, put in comment all lines except the one for the assertion you wanrt to test
- Compile the code
- Execute the compiler console application and watch the printed messages
Links
Code generation
System.Workflow.ComponentModel.Compiler namespace?
Workflow compilation
WF – Compile and execute an activity/workflow at runtime
Authoring Workflows
Changing the directory where the WorkflowComiler creates the dll