Click here to Skip to main content
15,867,686 members
Articles / Web Development / IIS

MEF and Workflow Services Integration

Rate me:
Please Sign up or sign in to vote.
4.83/5 (9 votes)
1 Jan 2011CPOL6 min read 34.7K   442   26   3
Building up activities using the Managed Extensibility Framework for workflow services hosted in IIS.

Introduction

This is my first article for CodeProject and while I have wanted to do this for a while, I wanted to write something that hadn't already been written. So here we go, please be gentle, and all comments will be greatly received.

I've been reading lots about the Managed Extensibility Framework, and wondered if it would be possible to have MEF resolve my imports and exports from within a workflow service hosted in IIS. How would I register my assemblies?

After trying some things that were just plain wrong, it turned out I just needed to register a simple service behaviour that will allow me to hook into the service host and register my exports. This is then available as an extension within a workflow activity context.

The project contains an implementation and test library, a shared contracts library, a model library, and a common library that could be shared across services\modules.

First, we'll look at WorkflowServiceLibrary to see how to use it and then look under the covers at how the base classes work. WorkflowServiceLibrary doesn't contain references to TestLibrary and ImplementationLibrary, so any breakpoints set in these will require rebuilding the entire solution.

Project-Structure.jpg

Background

I had been torn between Unity and MEF for some time, reading up the pros and cons and where they are similar and differ. It seems MEF is more about discovery, and Unity requires you to specifically wire up the contracts and their implementations.

As I only develop services available internally to the company, I like the idea of being able to copy my assemblies to a remote location and just telling MEF to go load them up and make them available.

I also wanted to be able to catch any exceptions that weren't handled and provide a fault back to the client and log the error.

The extensibility available to you in WCF runtime and subsequently WF Services can make all this possible by implementing IServiceBehavior and adding them to the run time through the web configuration file.

The Demo Project

The BaseLibrary contains the abstract classes for implementing the service behavior and registers an IErrorHandler to write any unhandled exceptions to the debug console; very simple, but it shows how you can call into your own logging framework.

The other job of the interface is to provide a fault back to the client so you don't get the extremely helpful internal error, or if your service is external, you can really generalize the error the client receives while capturing the detail in your logs.

wcf-test-error-no-mef.jpg

I think what led to this entire article was the above generic error message that doesn't really point in the right direction; what I wanted was more like the dialog below:

wcf-test-error-handled.jpg

This is a result of the normal implementation not having the method implemented.

The Code

The interface we will be implementing is contained in the contracts library, and is very simple and only contains one method:

C#
public interface IOrderSubmissionService<t>
{
    IEnumerable<t> GetPendingOrders();
}

The NormalOrderSubmission class specifies an export for the interface and just throws an exception.

C#
[Export(typeof(IOrderSubmissionService<Order>))]
public class NormalOrderSubmission : IOrderSubmissionService<Order>
{
    public IEnumerable<Order> GetPendingOrders()
    {
        throw new NotImplementedException();
    }
}

TestOrderSubmission just returns a couple of Order objects with some sample OrderLine objects; the service will just add up the lines and count the orders, reporting the totals.

Our SendOrders service just calls a code activity and sends the result back to the client:

C#
public sealed class GetPendingOrders : CodeActivity<IEnumerable<Order>>
{
    [Import]
    IOrderSubmissionService<Order> orderSubmissionService;

    protected override IEnumerable<Order> Execute(CodeActivityContext context)
    {
        context.GetExtension<MefWorkflowProvider>().Container.SatisfyImportsOnce(this);
        return orderSubmissionService.GetPendingOrders();
    }
}

To make the extension available to CodeActivityContext requires just a few lines of code in our web.config in the WorkflowServiceLibrary:

XML
<system.serviceModel>
    <extensions>
      <behaviorExtensions>
        <add name="mefProviderExtension" 
           type="WorkflowServiceLibrary.Mef.WorkflowServiceBehaviorElement, 
                  WorkflowServiceLibrary"/>
      </behaviorExtensions>
    </extensions>
    <behaviors>
      <serviceBehaviors>
        <behavior>
          <mefProviderExtension /> 
          <!--<mefProviderExtension useTestServices="true"/>-->
        </behavior>
      </serviceBehaviors>
    </behaviors>
    <serviceHostingEnvironment multipleSiteBindingsEnabled="true" />
</system.serviceModel> 

To keep things simple, I've omitted the irrelevant lines, so first we add a reference to the behavior configuration element and the library it can be found in:

XML
<add name="mefProviderExtension" 
  type="WorkflowServiceLibrary.Mef.WorkflowServiceBehaviorElement, 
        WorkflowServiceLibrary"/>

and then we add the name we gave it to the behaviors element:

XML
<mefProviderExtension useTestServices="true"/>

This tag also demonstrates passing configuration parameters to extensions; by setting the parameter useTestServices, we can load a set of assemblies from a different location.

wcf-test-services.jpg

Implementing the Base Classes

In order to let the runtime know where to look for our imports, we need to implement some abstract classes. When we look at the base classes, we will see how properties can be added through the configuration file and used in our provider.

Provider

Implementing MefWorkflowProvider

C#
public abstract class MefWorkflowProvider : IDisposable
{
    /// <summary>
    /// MEF composition Container
    /// </summary>
    protected CompositionContainer container { get; set; }
    public CompositionContainer Container { get { return container; } }

    public MefWorkflowProvider(bool useTestServices = false)
    {
        RegisterServices(useTestServices);
    }

    /// <summary>
    /// Register the services in the catalog
    /// </summary>
    /// <param name="useTestServices">Should we discover
    ///     imports/exports for testomg</param>
    private void RegisterServices(bool useTestServices = false)
    {
        var aggregateCatalog = new AggregateCatalog();

        var coreCatalog = RegisterCoreCatalogs();
        var catalog = useTestServices ? RegisterTestCatalogs() : 
                      RegisterNormalCatalogs();

        // create the catalogs from the derived type
        if (coreCatalog != null)
            aggregateCatalog.Catalogs.Add(coreCatalog);
        if (catalog != null)
            aggregateCatalog.Catalogs.Add(catalog);
        
        // compose the container
        container = new CompositionContainer(aggregateCatalog);
    }

    protected abstract ComposablePartCatalog RegisterTestCatalogs();
    protected abstract ComposablePartCatalog RegisterCoreCatalogs();
    protected abstract ComposablePartCatalog RegisterNormalCatalogs();

    public void Dispose()
    {
        container.Dispose();
    }
}

There are three overrides you have to implement:

  • RegisterNormalCatalog() is only called when the useTestServices parameter is false or not included.
  • RegisterCoreCatalogs() will always be called.
  • RegisterTestCatalogs() will be called when useTestServices is true.

You can build your assembly catalogs in any of the normal MEF ways, but here I am just using them from their directory with an AssemblyCatalog. I have a remote directory catalog that I hope to do an article on that allows you to read your assembly exports from a file share, something that proved problematic when I tried it with the standard DirectoryCatalog.

Although I chose to put the assembly locations in the above class, they'd probably be better added to the web configuration file.

The Abstract Class

MefWorkflowProvider
C#
public abstract class MefWorkflowProvider : IDisposable
{
    /// <summary>
    /// MEF composition Container
    /// </summary>
    protected CompositionContainer container { get; set; }
    public CompositionContainer Container { get { return container; } }

    public MefWorkflowProvider(bool useTestServices = false)
    {
        RegisterServices(useTestServices);
    }

    /// <summary>
    /// Register the services in the catalog
    /// </summary>
    /// <param name="useTestServices">Should we
    ///    discover imports/exports for testomg</param>
    private void RegisterServices(bool useTestServices = false)
    {
        var aggregateCatalog = new AggregateCatalog();

        var coreCatalog = RegisterCoreCatalogs();
        var catalog = useTestServices ? RegisterTestCatalogs() : 
                                        RegisterNormalCatalogs();
        
        // create the catalogs from the derived type
        if (coreCatalog != null)
            aggregateCatalog.Catalogs.Add(coreCatalog);
        if (catalog != null)
            aggregateCatalog.Catalogs.Add(catalog);
        
        // compose the container
        container = new CompositionContainer(aggregateCatalog);
    }

        protected abstract ComposablePartCatalog RegisterTestCatalogs();
    protected abstract ComposablePartCatalog RegisterCoreCatalogs();
    protected abstract ComposablePartCatalog RegisterNormalCatalogs();

    public void Dispose()
    {
        container.Dispose();
    }
}

All this does is set up a container and call the abstract methods to populate the parts you defined on your derived type.

Behavior

It is our behaviour's job to initialize the provider we defined. WorkflowServiceBehavior does this by implementing the required override GetProvider().

This creates an instance of our provider and passes any parameters that it was passed from the web configuration file/element.

C#
public class WorkflowServiceBehavior : MefWorkflowProviderExtensionBehavior
{
    private bool useTestServices { get; set; }

    protected internal WorkflowServiceBehavior(bool useTestServices)
    {
        this.useTestServices = useTestServices;
    }

    protected override MefWorkflowProvider GetProvider()
    {
        return new WorkflowServiceMefProvider(useTestServices);
    }
}

The Abstract Class

MefWorkflowExtensionBehavior is where we plug the extension into the WorkflowServiceHost and hides the logic away.

C#
public abstract class MefWorkflowProviderExtensionBehavior : IServiceBehavior
{      
    /// <summary>
    /// Where the magic happens and your extension
    /// gets added to the workflow service host
    /// </summary>
    /// <param name="serviceDescription"></param>
    /// <param name="serviceHostBase"></param>
    public void ApplyDispatchBehavior(ServiceDescription serviceDescription, 
                System.ServiceModel.ServiceHostBase serviceHostBase)
    {
        WorkflowServiceHost host = serviceHostBase as WorkflowServiceHost;
        if (host != null)
        {
            // register the mef provider
            host.WorkflowExtensions.Add(GetProvider());

            // register a error handler on the channel dispatchers
            foreach (ChannelDispatcher cd in host.ChannelDispatchers)
            {
                cd.ErrorHandlers.Add(new WorkflowUnhandledException());
            }
        }
    }

    protected abstract MefWorkflowProvider GetProvider();

    public virtual void AddBindingParameters(ServiceDescription serviceDescription, 
                   ServiceHostBase serviceHostBase, 
                   Collection<ServiceEndpoint> endpoints, 
                   BindingParameterCollection bindingParameters) { }
    public virtual void Validate(ServiceDescription serviceDescription, 
                                 ServiceHostBase serviceHostBase) { }
}

The only method you need to implement from IServiceBehavior is ApplyDispatchBehavior(...) where we cast the serviceHostBase as WorkflowServiceHost and add our provider to the runtime through the WorkflowExtensions property.

We also have the opportunity to add a class implementing the IErrorHandler interface to the host's ChannelDispatcher.ErrorHandlers collection.

C#
/// <summary>
/// handles errors in the wcf pipeline and logs them to the debug console
/// </summary>
public class WorkflowUnhandledException : IErrorHandler
{
    /// <summary>
    /// Handle and\or log the error
    /// </summary>
    /// <param name="error">channel exception</param>
    /// <returns></returns>
    public bool HandleError(Exception error)
    {
        Debug.WriteLine(string.Format("A unhandled exception " + 
              "occoured during the workflow: {0}", error.Message));
        return false; 
    }
    /// <summary>
    /// Provide a fault for the client
    /// </summary>
    /// <param name="error"></param>
    /// <param name="version"></param>
    /// <param name="fault"></param>
    public void ProvideFault(Exception error, 
           System.ServiceModel.Channels.MessageVersion version, 
           ref System.ServiceModel.Channels.Message fault)
    {
        // provide a fault message for wcf to return
        fault = Message.CreateMessage(version, MessageFault.CreateFault(
          new FaultCode("Workflow"), error.Message), 
          "Unhandled Exception");
    }
}

This simple interface allows us to log, handle, and return a friendly or not so friendly message back to the client.

Configuration Element

Finally, to tie this all together, we need to be able to specify our service behavior in web.config.

As I have implemented the ability to switch to another location used for testing parts of my base class, our inherited class is extremely simple, receiving the parameters from our base element and creating our behaviour.

C#
public class WorkflowServiceBehaviorElement : 
             MefWorkflowProviderExtensionBehaviorElement
{
    protected override MefWorkflowProviderExtensionBehavior 
              CreateExtensionBehavior(bool useTestServices)
    {
        return new WorkflowServiceBehavior(useTestServices);
    }
}

MefWorkflowProviderExtensionBehaviorElement inherits from BehaviorExtensionElement and specifies the configuration file properties using ConfigurationProperty, and calls CreateExtensionBehavior to allow for further configuration.

C#
public abstract class MefWorkflowProviderExtensionBehaviorElement : 
                BehaviorExtensionElement
{
    /// <summary>
    /// parameter name in the configuration file
    /// </summary>
    private const string useTestServices = "useTestServices";

    /// <summary>
    /// Define the configuration propert
    /// </summary>
    [ConfigurationProperty(useTestServices, 
             IsRequired = false, DefaultValue = false)]
    public bool UseTestServices
    {
        get { return (bool) base[useTestServices]; }
        set { base[useTestServices] = value.ToString(); }
    }

    public override Type BehaviorType
    {
        get { return typeof (MefWorkflowProviderExtensionBehavior); }
    }

    protected override object CreateBehavior()
    {
        return CreateExtensionBehavior(this.UseTestServices);
    }

    protected abstract MefWorkflowProviderExtensionBehavior 
              CreateExtensionBehavior(bool useTestServices);
}

That finally wraps us up; to access our provider within a code activity, we only require this one line:

C#
context.GetExtension<MefWorkflowProvider>().Container.SatisfyImportsOnce(this)

All your activities, services, and children all get wired up automatically. Next time, I hope to talk about native activities and custom designers. Happy New Year to you all.

History

  • 01-01-11 - First version posted.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer NBN International
United Kingdom United Kingdom
I am Gareth, working in the book industry specializing in EDI and B2B integration. Starting out as a child bashing out programs on the old commodore 64, I have since progressed from VB6 through the .net framework and loving c#, wf, wpf and where these are heading... hoping one day to have the excuse to play with silverlight.

Comments and Discussions

 
GeneralMy vote of 5 Pin
Kanasz Robert5-Nov-12 0:45
professionalKanasz Robert5-Nov-12 0:45 
QuestionWhat is the startup project to run your example?. Pin
Member 440541928-Feb-11 7:22
Member 440541928-Feb-11 7:22 
AnswerRe: What is the startup project to run your example?. Pin
Gareth Barlow (NBNi)28-Feb-11 8:26
Gareth Barlow (NBNi)28-Feb-11 8:26 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.