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.
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.
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:
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:
public interface IOrderSubmissionService<t>
{
IEnumerable<t> GetPendingOrders();
}
The NormalOrderSubmission
class specifies an export for the interface and just throws an exception.
[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:
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:
<system.serviceModel>
<extensions>
<behaviorExtensions>
<add name="mefProviderExtension"
type="WorkflowServiceLibrary.Mef.WorkflowServiceBehaviorElement,
WorkflowServiceLibrary"/>
</behaviorExtensions>
</extensions>
<behaviors>
<serviceBehaviors>
<behavior>
<mefProviderExtension />
</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:
<add name="mefProviderExtension"
type="WorkflowServiceLibrary.Mef.WorkflowServiceBehaviorElement,
WorkflowServiceLibrary"/>
and then we add the name we gave it to the behaviors element:
<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.
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
public abstract class MefWorkflowProvider : IDisposable
{
protected CompositionContainer container { get; set; }
public CompositionContainer Container { get { return container; } }
public MefWorkflowProvider(bool useTestServices = false)
{
RegisterServices(useTestServices);
}
private void RegisterServices(bool useTestServices = false)
{
var aggregateCatalog = new AggregateCatalog();
var coreCatalog = RegisterCoreCatalogs();
var catalog = useTestServices ? RegisterTestCatalogs() :
RegisterNormalCatalogs();
if (coreCatalog != null)
aggregateCatalog.Catalogs.Add(coreCatalog);
if (catalog != null)
aggregateCatalog.Catalogs.Add(catalog);
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
public abstract class MefWorkflowProvider : IDisposable
{
protected CompositionContainer container { get; set; }
public CompositionContainer Container { get { return container; } }
public MefWorkflowProvider(bool useTestServices = false)
{
RegisterServices(useTestServices);
}
private void RegisterServices(bool useTestServices = false)
{
var aggregateCatalog = new AggregateCatalog();
var coreCatalog = RegisterCoreCatalogs();
var catalog = useTestServices ? RegisterTestCatalogs() :
RegisterNormalCatalogs();
if (coreCatalog != null)
aggregateCatalog.Catalogs.Add(coreCatalog);
if (catalog != null)
aggregateCatalog.Catalogs.Add(catalog);
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.
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.
public abstract class MefWorkflowProviderExtensionBehavior : IServiceBehavior
{
public void ApplyDispatchBehavior(ServiceDescription serviceDescription,
System.ServiceModel.ServiceHostBase serviceHostBase)
{
WorkflowServiceHost host = serviceHostBase as WorkflowServiceHost;
if (host != null)
{
host.WorkflowExtensions.Add(GetProvider());
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.
public class WorkflowUnhandledException : IErrorHandler
{
public bool HandleError(Exception error)
{
Debug.WriteLine(string.Format("A unhandled exception " +
"occoured during the workflow: {0}", error.Message));
return false;
}
public void ProvideFault(Exception error,
System.ServiceModel.Channels.MessageVersion version,
ref System.ServiceModel.Channels.Message fault)
{
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.
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.
public abstract class MefWorkflowProviderExtensionBehaviorElement :
BehaviorExtensionElement
{
private const string useTestServices = "useTestServices";
[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:
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.