(WF4.5) Using CSharpValue and CSharpReference in .Net 4.5 – Compiling expressions–and changes in Visual Studio generated XAML
I’ve been publicizing for a while that Visual Studio 11 (still in Beta) supports C# expressions in workflow designer. Of course you might also possibly want to use C# or VB expressions by writing a workflow in code, instead of by building it in Visual Studio. There are actually a couple tricks to doing this, so that is today’s topic – how to!
Recently we looked at the WF XAML which is produced by Visual Studio 2010, and noticed that it had a couple things in there specifically for the Visual Basic compiler to understand namespaces and assembly references for Visual Basic expressions.
Today, if we look at the WF XAML which is produced by Visual Studio 2011 for a C# .Net 4.5 project, we’ll see that it is quite different!
Here is a hello world example, and I will highlight the keywords that deserve discussion today.
<Activity mc:Ignorable="sap sap2010 sads" x:Class="WorkflowConsoleApplication1.Workflow1"
xmlns="https://schemas.microsoft.com/netfx/2009/xaml/activities"
xmlns:mc="https://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:sads="https://schemas.microsoft.com/netfx/2010/xaml/activities/debugger"
xmlns:sap="https://schemas.microsoft.com/netfx/2009/xaml/activities/presentation"
xmlns:sap2010="https://schemas.microsoft.com/netfx/2010/xaml/activities/presentation"
xmlns:sco="clr-namespace:System.Collections.ObjectModel;assembly=mscorlib"
xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml">
<x:Members>
<x:Property Name="user" Type="InArgument(x:String)" />
</x:Members>
<sap2010:ExpressionActivityEditor.ExpressionActivityEditor>C#</sap2010:ExpressionActivityEditor.ExpressionActivityEditor>
<sap2010:WorkflowViewState.IdRef>WorkflowConsoleApplication1.Workflow1_1</sap2010:WorkflowViewState.IdRef>
<TextExpression.NamespacesForImplementation>
<sco:Collection x:TypeArguments="x:String">
<x:String>System</x:String>
<x:String>System.Collections.Generic</x:String>
<x:String>System.Data</x:String>
<x:String>System.Linq</x:String>
<x:String>System.Text</x:String>
</sco:Collection>
</TextExpression.NamespacesForImplementation>
<TextExpression.ReferencesForImplementation>
<sco:Collection x:TypeArguments="AssemblyReference">
<AssemblyReference>Microsoft.CSharp</AssemblyReference>
<AssemblyReference>System</AssemblyReference>
<AssemblyReference>System.Activities</AssemblyReference>
<AssemblyReference>System.Core</AssemblyReference>
<AssemblyReference>System.Data</AssemblyReference>
<AssemblyReference>System.Runtime.Serialization</AssemblyReference>
<AssemblyReference>System.ServiceModel</AssemblyReference>
<AssemblyReference>System.ServiceModel.Activities</AssemblyReference>
<AssemblyReference>System.Xaml</AssemblyReference>
<AssemblyReference>System.Xml</AssemblyReference>
<AssemblyReference>System.Xml.Linq</AssemblyReference>
<AssemblyReference>mscorlib</AssemblyReference>
<AssemblyReference>WorkflowConsoleApplication1</AssemblyReference>
</sco:Collection>
</TextExpression.ReferencesForImplementation>
<WriteLine sap2010:WorkflowViewState.IdRef="WriteLine_1">
<InArgument x:TypeArguments="x:String">
<mca:CSharpValue x:TypeArguments="x:String">"Hello! " + user</mca:CSharpValue>
</InArgument>
<sads:DebugSymbol.Symbol>d3xjOlx1c2Vyc1x0aWxvdmVsbFxkb2N1bWVudHNcdmlzdWFsIHN0dWRpbyAxMVxQcm9qZWN0c1xXb3JrZmxvd0NvbnNvbGVBcHBsaWNhdGlvbjFcV29ya2Zsb3dDb25zb2xlQXBwbGljYXRpb24xXFdvcmtmbG93MS54YW1sAikDLg8CAQErBytVAgEC</sads:DebugSymbol.Symbol>
</WriteLine>
<sap2010:WorkflowViewState.IdRef>WorkflowConsoleApplication1.Workflow1_1</sap2010:WorkflowViewState.IdRef>
<sap2010:WorkflowViewState.ViewStateManager>
<sap2010:ViewStateManager>
<sap2010:ViewStateData Id="WriteLine_1" sap:VirtualizedContainerService.HintSize="211,59" />
<sap2010:ViewStateData Id="WorkflowConsoleApplication1.Workflow1_1" sap:VirtualizedContainerService.HintSize="251,139" />
</sap2010:ViewStateManager>
</sap2010:WorkflowViewState.ViewStateManager>
</Activity>
Now, for the key to the above diagram, explaining each interesting piece.
sap2010:ExpressionActivityEditor.ExpressionActivityEditor="C#"
This is a new XAML attached propertyto indicate to Visual Studio that the workflow was created as a C# workflow, and that all expressions inside of the workflow are C# expressions. It can appear either as an XML attribute or an XML element, depending on how the XAML was generated.
<TextExpression.NamespacesForImplementation>
This is the new, more explicit way of representing the namespaces you’ve imported in the ‘Imports’ control which pops up at the bottom of the designer. The reason this has changed is because it actually fixes some bugs in the WF4 approach of automatically deriving your imported expression namespaces based on xmlns references, which we used for VB expressions in .Net 4 workflows.
<TextExpression.ReferencesForImplementation> and <AssemblyReference>
And similarly this is the new, more explicit way of representing the assemblies your workflow expressions reference, which in the past, for a workflow created in Visual Studio in .Net 4 and built using XamlBuildTask was derived from the information included in the compilation process somehow (hazy on details) , and for .Net 4 XAML workflows which were loaded using XamlServices.Load() or similar, was derived from the assemblies referenced in xmlns attributes. I would imagine this also helps fix some fairly obscure bugs, but I don’t know the details.
OK, so now we understand what VS is doing, let’s see how we would do the same in a code C# workflow.
Attempt 1:
static void Main(string[] args)
{
// Using DynamicActivity for this sample so that we can have an
// InArgument, and also do everything without XAML at all
DynamicActivity codeWorkflow = new DynamicActivity
{
Name = "MyScenario.MyDynamicActivity",
Properties =
{
new DynamicActivityProperty
{
Name = "user",
Type = typeof(InArgument<string>),
},
},
Implementation = () => new WriteLine
{
Text = new CSharpValue<string>
{
ExpressionText = "\"hello ! \" + user"
},
}
};
WorkflowInvoker.Invoke(
codeWorkflow,
new Dictionary<string, object>
{
{ "user", "tim" }
});
}
This looks like it should work, right? It’s just like using VBValue, right? Well…. wrong. What the? (I was rather surprised when I first saw this.)
“Expresssion Activity type ‘CSharpValue`1’ requires compilation in order to run”?? What does it mean? And why didn’t we hit this error when we ran the workflow as built using Workflow Designer in VS?
The answer is that VS cheats... in a good way! Visual Studio actually precompiles all of the C# expressions in your workflow into expression trees. And embeds those expression trees in the secret XAML resources inside your assembly which are the real way that your workflow’s implementation is loaded at runtime. This is good because your precompiled workflows run faster! But that sounds like a big problem for us, because we aren’t Visual Studio, how are we going to embed all that information in the workflow we just painstakingly wrote?
While the bad news is we have to take extra steps to compile workflows before running them, the good news is that we don’t have to precompile our workflow using VS. We can ‘precompile’ at runtime, by copy pasting a little code.
For a DynamicActivity loaded via ActivityXamlServices.Load(), you must use one of the new overloads that takes ActivityXamlServiceSettings:
DynamicActivity dynamicActivity = ActivityXamlServices.Load(s, new ActivityXamlServicesSettings{ CompileExpressions = true } ); // (not yet sure when passing LocationReferenceEnvironment is required)
For a DynamicActivity not got via ActivityXamlServices.Load(), it goes like this:
static void Compile(DynamicActivity dynamicActivity)
{
TextExpressionCompilerSettings settings = new TextExpressionCompilerSettings
{
Activity = dynamicActivity,
Language = "C#",
ActivityName = dynamicActivity.Name.Split('.').Last() + "_CompiledExpressionRoot",
ActivityNamespace = string.Join(".", dynamicActivity.Name.Split('.').Reverse().Skip(1).Reverse()),
RootNamespace = null,
GenerateAsPartialClass = false,
AlwaysGenerateSource = true,
};
TextExpressionCompilerResults results =
new TextExpressionCompiler(settings).Compile();
if (results.HasErrors)
{
throw new Exception("Compilation failed.");
}
ICompiledExpressionRoot compiledExpressionRoot =
Activator.CreateInstance(results.ResultType,
new object[] { dynamicActivity }) as ICompiledExpressionRoot;
CompiledExpressionInvoker.SetCompiledExpressionRootForImplementation(
dynamicActivity, compiledExpressionRoot);
}
Now there’s a couple bits of that code which may look a little mysterious. What are ActivityName and ActivityNamespace for? And what is results.ResultType?
ActivityName and ActivityNamespace turn out to together be the name of a new type. As in System.Type, and in our case it is a dynamically generated runtime type. (Why a runtime generated type is required, I don’t yet know… but I have suspicions that it supports the compiled XAML in assembly scenario.)
Secondly, what does SetCompiledExpressionRootForImplementation do? This helper method is setting an attached property on the dynamicActivity, one which defines an instance of the type.
Now hopefully we can see what the object tree we just created looks like if we serialize out the XAML... oops! The attached property doesn’t serialize? What gives? I think this is a bug, we’ll see. Stay tuned?
Anyway that's a bit of a strange piece of code, but the good news is still that it should work - we can compile and run C# expressions in .Net 4.5.
Comments
Anonymous
May 24, 2012
The comment has been removedAnonymous
May 25, 2012
Hi Cinnio, I think you can do the same procedure for compiling your WorkflowService, but -probably you need to target it upon WorkflowService.Body -probably you need to call SetCompiledExpressionRoot instead of SetCompiledExpressionRootForImplementation TimAnonymous
June 08, 2012
I have tried this out, and for people who want to compile expressions from XAMLX for WorkflowServiceHost, there are actually THREE changes to the code in this article. They are:
- target WorkflowService.Body (which is type Activity)
- call SetCompiledExpresssionRoot (instead of SetCompiledExpressionRootImplementation)
- in TextExpressionCompilerSettings you must set ForImplementation = false. ForImplementation = true is for compiling expression on ActivityBuilders or DynamicActivities only, it should not be true for compiling a XAMLX activity. (If you're using WorkflowServiceHost and not using a XAMLX file, but instead are newing up a specific custom activity type from an ActivityLibrary you shouldn't hit any of these problems with precompilation, since ActivityLibrary activities are precompiled already when they are added as part of the assembly.)
Anonymous
February 07, 2013
MSDN Documentation for compiling the C# expressions: msdn.microsoft.com/.../jj591618.aspx Note that there are some esoteric scenarios where even this, does not work.Anonymous
January 29, 2015
But what if I get an activity directly from WorkflowDesigner. How can I set CompileExpressions property?