Wednesday 15 February 2012

Host Workflows stored in database as WCF services in ASP.Net MVC

Today I am going to show you how to host WF4 workflows in WCF which are stored in database. We'll be using ASP.Net MVC as the solution project.

On internet, there are several examples available on how to host compiled workflows as WCF service but my requirement was to get the workflow from database and expose as WCF end points. 
After a lot of research and hacking, I was able to create a small sample project that does the work. My entire code goes in global.asax.cs file.


When you open global.asax.cs file and find an entry where mvc register the routes to the Routecollection. This routing  helps IIS to redirect the incoming request with the route specified. As I already told my workflows are stored in database in table Workflows so now I'm going to get them from database and add to Routecollection as ServiceRoute.

    foreach(var wf in GetAllWorkflows())
            {
                routes.Add(new ServiceRoute(wf.Name, new TestWorkflowServiceHostFactory(wf.Name), typeof(Workflow1)));
            }
GetAllWorkflows() is the method which returns a list of workflows from database. I store them as a list of custom class of workflows. I'm not explaining here how to get the data from database for the sake of simplicity but you can use ADO.Net classes or Ado.Net Entity data model or some other mode.

Here are some interesting bits to understand about the code. The ServiceRoute class enables the creation of service routes over Http for WCF services with support for extension-less base addresses. What does it mean. It means when an incoming request is received by IIS, it route the request using this Url. And how does it do that...Well, when the ServiceRoute constructor is called, it adds the corresponding route prefix (url pattern) and a (hidden) route handler to the ASP.NET RouteCollection, and cache the corresponding route prefix, service host factory and service type info into an internal hash table for future service activation. The  three arguments used here are defined below: - 

  1. RoutePrefix -> WCF Endpoint will be exposed with this name.
  2. TestWorkflowServiceHostFactory -> Inherited from WorkflowServiceHostFactory. Factory that provides instances of WorkflowServiceHost in managed hosting environments (e.g. IIS) where the host instance is created dynamically in response to incoming messages. It means a serviceHostFactory that is used by IIS to create a new instance of service host everytime a new request is received. This class accepts workflow name as string which is the name of workflow stored in database and is further used to expose the endpoints. (e.g. If the workflow name is GetMyName then the endpoints exposed would be something like http://localhost:1997/GetMyName)
  3. ServiceType -> Used to get the type of service. Since we are creating workflows from database so we can't have types for workflows. So what do we pass here? Now this is a bit hacky. I added a workflow.xaml class in project and refer it here (I tried passing with typeof(object) but it didn't work somehow...however didn't get enough time to explore the reason).


Now we come to the implementation of WorkflowServiceHostFactory. TestWorkflowServiceHostFactory is inherited by WorkflowServiceHostFactory class. This class declare two constants to set/get temp files to stores the workflow/'workflow service' definitions.
        private const string TempXamlFile = "temp.xaml";
        private const string TempXamlService = "temp.xamlx";


The constructor holds the name of workflow passed at the time of registering routes.


   public TriboldWorkflowServiceHostFactory(string routePrefix)
        {
            XamlRoutePrefix = routePrefix;
            ...
        }


Override CreateServiceHost method to create the host and return to IIS. This is the main method that does the magic.

public override ServiceHostBase CreateServiceHost(string constructorString, Uri[] baseAddresses)
        {
            ...

            //WorkflowService service = LoadWFService(XamlRoutePrefix);
            //var host = new WorkflowServiceHost(service, baseAddresses);


              Activity activity1 = LoadActivity(XamlRoutePrefix);
              var host = new WorkflowServiceHost(activity1, baseAddresses);
              ...



            host.Description.Behaviors.Add(new AspNetCompatibilityRequirementsAttribute() { RequirementsMode = AspNetCompatibilityRequirementsMode.Required });
            host.Description.Behaviors.Add(new ServiceMetadataBehavior() { HttpGetEnabled = true });
            return host;

}

The LoadActivity method get the workflowDefinition from DB and create a temp.xaml file. Create the file stream and writes the workflow definition into the stream. (I was getting 'File in use' error while loading xaml file from stream so I closed the stream and again open in read mode to load the .xaml file) ActivityXamlServices.Load method uses this stream to load and return Activity object

        ...
  var stream = new FileStream(Path.Combine(tempPath, TempXamlFile), FileMode.Create);
        byte[] bytes = Encoding.ASCII.GetBytes(workflowDefinition);
        stream.Write(bytes, 0, Encoding.ASCII.GetByteCount(workflowDefinition));
        stream.Flush();
        stream.Close();

        stream = File.OpenRead(Path.Combine(tempPath, TempXamlFile));
        var service = ActivityXamlServices.Load(stream);
        stream.Flush();
        stream.Close();
        return service as Activity;

Now, we've got the activity, so we use WorkflowServiceHost class and pass this activity with baseAddress thus creating the host and return to the IIS.

And that's all, We've done the job. In the same manner we can load Workflow service. The only difference is we need to pass workflowservice object in WorkflowServiceHost as mentioned above (commented code)




Here's the complete code...

public class TestWorkflowServiceHostFactory : WorkflowServiceHostFactory
    {
        private const string TempXamlFile = "temp.xaml";
        private const string TempXamlService = "temp.xamlx";

        public string XamlRoutePrefix { get; set; }
        
        public TriboldWorkflowServiceHostFactory(string routePrefix)
        {
            XamlRoutePrefix = routePrefix;
        }

        public override ServiceHostBase CreateServiceHost(string constructorString, Uri[] baseAddresses)
        {
            var type = GetObjectOrThrowNotFound(() => Type.GetType(constructorString));
            var activity = GetObjectOrThrowNotFound(() => Activator.CreateInstance(type) as Activity);

            Activity activity1 = LoadActivity(XamlRoutePrefix);
            var host = new WorkflowServiceHost(activity1, baseAddresses);

            //WorkflowService service = LoadWFService(XamlRoutePrefix);
            //var host = new WorkflowServiceHost(service, baseAddresses);

            host.Description.Behaviors.Add(new AspNetCompatibilityRequirementsAttribute() { RequirementsMode = AspNetCompatibilityRequirementsMode.Required });
            host.Description.Behaviors.Add(new ServiceMetadataBehavior() { HttpGetEnabled = true });

            return host;
        }

        private Activity LoadActivity(string searchString)
        {
            var input = new Dictionary<string, object>();
            input["@WorkflowName"] = searchString;
            DataTable dt = MvcApplication.FetchData.Fetch(input, "GetWorkflowList");
            string workflowDefinition = dt.Rows[0][1].ToString();
            string tempPath = Path.GetTempPath();

            //if file already exists then delete it
            if (File.Exists(Path.Combine(tempPath, TempXamlFile)))
                File.Delete(Path.Combine(tempPath, TempXamlFile));

            var stream = new FileStream(Path.Combine(tempPath, TempXamlFile), FileMode.Create);
            byte[] bytes = Encoding.ASCII.GetBytes(workflowDefinition);
            stream.Write(bytes, 0, Encoding.ASCII.GetByteCount(workflowDefinition));
            stream.Flush();
            stream.Close();

            stream = File.OpenRead(Path.Combine(tempPath, TempXamlFile));
            var service = ActivityXamlServices.Load(stream);
            stream.Flush();
            stream.Close();
            return service as Activity;
        }

        private WorkflowService LoadWFService(string searchString)
        {
            var input = new Dictionary<string, object>();
            input["@WorkflowName"] = searchString;
            DataTable dt = MvcApplication.FetchData.Fetch(input, "GetWorkflowList");
            string workflowDefinition = dt.Rows[0][1].ToString();
            string tempPath = Path.GetTempPath();

            //if file already exists then delete it
            if (File.Exists(Path.Combine(tempPath, TempXamlService)))
                File.Delete(Path.Combine(tempPath, TempXamlService));

            var stream = new FileStream(Path.Combine(tempPath, TempXamlService), FileMode.Create);
            byte[] bytes = Encoding.ASCII.GetBytes(workflowDefinition);
            stream.Write(bytes, 0, Encoding.ASCII.GetByteCount(workflowDefinition));
            stream.Flush(); 
            stream.Close();

            stream = File.OpenRead(Path.Combine(tempPath, TempXamlService));
            var service = XamlServices.Load(stream);
            stream.Flush();
            stream.Close();
            return service as WorkflowService;
        }



 public static void RegisterRoutes(RouteCollection routes)
        {
            foreach(var wf in GetAllWorkflows())
            {
                routes.Add(new ServiceRoute(wf.Name, new TestWorkflowServiceHostFactory(wf.Name), typeof(Workflow1)));
            }
        }

1 comment:

  1. Hi, Do you have a solution project for this that I can download?

    ReplyDelete