Wednesday, May 20, 2009

Tales from the Developers Crypt:
(Another one of those postings that never made it from about a year ago...)

Hosting XAML only workflows services in ASP.NET

Recently I was tasked with building a lab that showed how to build a XAML only based WCF service and host it inside of ASP.NET for the purpose of following the SOA pillar of service composition, and agility. I always taught that this could be done; quite frankly I have even heard various colleagues say this could be done. However I never actually saw it implemented, thus the reasoning for this blog. Below I describe the pains and glories of hosting declarative marked-up Xaml based workflows. My intention is that you read through this simple check list in hopes that you won’t run into any ‘gotchas’ when you implement this. So happy workflowin!!!

The easiest way to pass in a Xaml file is to use the WorkflowServicehostFactory inside a ASP.NET “.svc” file. All you have to do is apply the “service=”Filename.xoml” attribute and away you go… Well almost.
Depending on your XOML, you may need to include TypeProviders, something that can’t be done simply pointing to a XOML file, thus using this approach I was blocked. To solve this issue, I had to create my own XamlWFHostFactory class by simply deriving from the WorkflowServiceHostFactory class and overriding the CreateServiceHost method with code such as this:
public class XamlWFHostFactory : WorkflowServiceHostFactory
{
public override ServiceHostBase CreateServiceHost(string constructorString, Uri[] baseAddresses)
{

TypeProvider typeProv = new TypeProvider(System.Web.HttpContext.Current);
typeProv.AddAssembly(System.Reflection.Assembly.GetExecutingAssembly());
typeProv.AddAssemblyReference("MovieFlowService.dll");

}
}
Once I had my typeProviders, any custom code behind or references to classes that exist inside other .Net assemblies can be resolved. However I didn’t come to this conclusion right away.

Simply passing in a XOML file caused all kinds of errors, however I only saw one inside the browser when testing the WF/Service was XAML validation failed. Now prior to trying the host the XOML file inside the WorkflowServiceHostFactory, my workflow ran fine with a custom windows form based host. I just knew that the XOML file was fine and that the validation was a generic error.
So I said, this is no problem, I’ll just listen for the WF validation errors and write them out to an ASP.NET Trace. What I learned immediately for debugging is that there is no way to get the validation errors on the WorkflowServiceHostFactory, because the error events are caught by the internal WorkflowServiceHost that the factory creates by default. The default host isn’t bubbling up the events to the WorkflowServiceHostFactory where my Event handling code is listening thus I had to create my own WFHost. This was done easily enough by creating a class that derives from the WorkflowServiceHost and overrind the InitializeRuntime to listen for the WorkflowValidation errors:
Public class XamlWFHost : WorkflowServiceHost
{
public XamlWFHost(string wfPath, string rulesPath, ITypeProvider provider, Uri[] baseaddresses ): base (wfPath, rulesPath, provider, baseaddresses )
{ }

protected override void InitializeRuntime()
{
try
{
// HttpContext.Current.Trace.Warn("Trace is good- InitializeRuntime()");
base.InitializeRuntime();
}
catch (WorkflowValidationFailedException ex)
{

StringBuilder errors = new StringBuilder();
foreach (System.Workflow.ComponentModel.Compiler.ValidationError error in ex.Errors)
{
errors.AppendLine(error.ToString());
}
string errMsg = "Validation Errors: " + errors.ToString();
Debug.WriteLine(errMsg);
System.Web.HttpContext.Current.Trace.Write(errMsg);
}
}
In the overriden InitializeRuntime() method and catch the ValidationErrors here because they are not caught by default within your HostFactory. Also if something goes wrong with your WCF ServiceHost, like your channel faults for some reason it’s also not a bad idea to listen for the HostFaulted event as well. From WCF I’ve learned that when you set a break point inside the Faulted event method, you can see all the exceptions that have occurred from WCF's perspective.
Something else to note, when trying to write out Trace statements to the ASP.NET Trace context, found inside the HttpContext, it was null. The reason being was that WCF was not turned on to support ASP.NET compatibility, nor was the web configuration enabled to support this. This is equivalent to turning on all the ASP.Net intrinsic such as tracing, Request, Response, Sessions state and etc. You don’t need to turn on ASP.Net compatibility to use WorkflowServices, in my case I wanted to write out details simply out to the Browser using the TraceContext inside of ASP.NET.
To use the System.Web.HttpContext. we must specify in WCF to use the ASP.NET compatibility which posed another problem. The ASP.Net compatibility is enabled using a ServiceBehavior. Well first off, how do I get to the ServiceHost, after all this is a WorkflowServiceHost not simply a WCF ServiceHost.
Well that’s the answer, the WorkflowServiceHost allows you to access the description and ServiceEndpoints as if you’re working with the ServiceHost class yourself. In fact the workflow service host derives from the ServiceHostBase class. So just add the behaviors you’d like. Also if you wanted to access the workflow runtime, as long as you’ve configured a behavior for it such as within the configuration file upon startup:






You can access it inside of code such as this:

WorkflowRuntimeBehavior wfrBehavior =
wfSvcHost.Description.Behaviors.Find();
WorkflowRuntime wfr = wfrbehavior.WorkflowRuntime;

So going back to the ASP.NET compatibility issue, we can add this behavior through the ASPNetCompatibility attribute inside code such as this code here:
AspNetCompatibilityRequirementsAttribute behavior = new AspNetCompatibilityRequirementsAttribute();
behavior.RequirementsMode = AspNetCompatibilityRequirementsMode.Required ;
wfSvcHost.Description.Behaviors.Add(behavior);
As I was going through my code, I also noticed that in order to use the TypeProvider classes, I needed to pass in either null, or a class that implements the IServiceProvider interface. It just so happens the the System.Web.HttpContext actually implements this interface.
Some last pointers about hosting XOML Inside an ASP.NET workflowServiceHost, I had several problems when using Root names inside the XOML that contained some combination of NS.Class as the root node name. I also found that in order to get it to work properly, the Root node name inside the XOML file had to match the name of Root Activity, and the namespace prefix matched namespace within XmlDefinitionsAttribute inside the Host project assemblyinfo.cs file.


Inside you Hosting project inside the AssemblyInfo.cs file make sure that the ns0: prefix matches within the assemblyInfo XmlnsDefintion attribute:
[assembly: System.Workflow.ComponentModel.Serialization.XmlnsDefinition("clr-namespace:MovieFlowService", "MovieFlowService")]
Also note: that when using a custom host, I can leave off the Assembly part, while here inside the workflow service host, this didn't work when I left off the Assembly=... part off.
Another issue I ran into was that if you build the WF using the VS.NET WF designer with XAML separation, verify that within the WF Service Properties page you change the NameSpace.ServiceName to simply ServiceName, which should match the Xaml Root node name. Also make sure that this Servicename, and Xaml Root name all match the ServiceName inside the web.config for the WCF Service Model configuration.
Finally after all this was done, I came across another simple but really good and appreciated error, ASP.Net didn’t have permissions to read my XOML file from the disk. After adding the proper permissions, restarting the IIS App Pool and IIS respectively, VIOLA!!!! My XAML only workflow hosted within WCF through ASP.Net and the workflowServiceHostFactory.

No comments: