65.9K
CodeProject 正在变化。 阅读更多。
Home

MEF 与工作流服务集成

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.83/5 (9投票s)

2011年1月1日

CPOL

6分钟阅读

viewsIcon

37612

downloadIcon

463

在 IIS 托管的工作流服务中使用托管可扩展性框架 (MEF) 构建活动。

引言

这是我为 CodeProject 撰写的第一个文章。虽然我很久以前就想写点东西了,但我希望写一篇别人还没有写过的。所以,请大家多多包涵,任何评论都将不胜感激。

我一直在阅读有关托管可扩展性框架 (MEF) 的大量资料,我想知道是否可以在 IIS 托管的工作流服务中让 MEF 解析我的导入和导出。我该如何注册我的程序集?

经过一些错误的尝试后,我发现我只需要注册一个简单的服务行为,这样我就可以挂钩到服务主机并注册我的导出。然后,它就可以在工作流活动上下文中作为扩展使用。

该项目包含一个实现和测试库、一个共享合同库、一个模型库和一个可以在服务/模块之间共享的通用库。

首先,我们将看看 WorkflowServiceLibrary 如何使用它,然后深入了解基类的工作原理。WorkflowServiceLibrary 不包含对 TestLibrary 和 ImplementationLibrary 的引用,因此在这些项目中设置的任何断点都需要重新构建整个解决方案。

Project-Structure.jpg

背景

我曾在一段时间内纠结于 Unity 和 MEF,阅读了它们的优缺点以及它们的相似之处和不同之处。MEF 更侧重于发现,而 Unity 需要你专门连接合同及其实现。

由于我只为公司内部开发服务,我喜欢能够将我的程序集复制到远程位置,然后告诉 MEF 去加载它们并使其可用。

我还希望能够捕获任何未处理的异常,并将错误信息返回给客户端并记录下来。

WCF 运行时(以及 WF 服务)的扩展性可以通过实现 IServiceBehavior 并通过 web 配置文件将其添加到运行时来实现所有这些功能。

演示项目

BaseLibrary 包含实现服务行为的抽象类,并注册一个 IErrorHandler 将任何未处理的异常写入调试控制台;非常简单,但它展示了如何调用您自己的日志记录框架。

该接口的另一个作用是向客户端提供一个错误信息,这样您就不会收到极其无用的内部错误信息。如果您的服务是外部的,您可以真正地对客户端收到的错误信息进行泛化处理,同时将详细信息捕获在您的日志中。

wcf-test-error-no-mef.jpg

我认为导致整篇文章的原因是上面那个通用的错误消息,它并没有真正指向正确的方向;我想要的是类似下面对话框的内容

wcf-test-error-handled.jpg

这是正常实现中未实现该方法的正常结果。

代码

我们将要实现的接口包含在合同库中,非常简单,只有一个方法

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

NormalOrderSubmission 类指定了该接口的导出,并抛出异常。

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

TestOrderSubmission 返回几个 Order 对象和一些示例 OrderLine 对象;服务将对这些行进行求和并计算订单数量,然后报告总数。

我们的 SendOrders 服务只是调用一个代码活动并将结果返回给客户端

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();
    }
}

为了使扩展能够被 CodeActivityContext 使用,我们只需要在 WorkflowServiceLibraryweb.config 文件中添加几行代码

<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> 

为了保持简单,我省略了不相关的行,所以首先我们添加对行为配置元素及其所在库的引用

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

然后我们将我们给它起的名字添加到 behaviors 元素中

<mefProviderExtension useTestServices="true"/>

这个标签还演示了如何将配置参数传递给扩展;通过设置参数 useTestServices,我们可以从不同的位置加载一组程序集。

wcf-test-services.jpg

实现基类

为了让运行时知道在哪里查找我们的导入,我们需要实现一些抽象类。当我们查看基类时,我们将看到如何通过配置文件添加属性并在我们的提供程序中使用它们。

提供商

实现 MefWorkflowProvider

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();
    }
}

有三个重写方法是您必须实现的

  • RegisterNormalCatalog() 仅在 useTestServices 参数为 false 或未包含时调用。
  • RegisterCoreCatalogs() 将始终被调用。
  • RegisterTestCatalogs() 将在 useTestServicestrue 时调用。

您可以使用任何常规的 MEF 方法来构建您的程序集目录,但我在这里只是从它们的目录中使用 AssemblyCatalog。我有一个远程目录目录,我希望以后能写一篇文章来介绍它,它允许您从文件共享读取您的程序集导出,当我尝试使用标准的 DirectoryCatalog 时,这似乎有问题。

虽然我选择将程序集位置放在上面的类中,但最好还是将它们添加到 web 配置文件中。

抽象类

MefWorkflowProvider
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();
    }
}

它所做的只是设置一个容器并调用抽象方法来填充您在派生类型上定义的组件。

行为

我们的行为负责初始化我们定义的提供程序。WorkflowServiceBehavior 通过实现必需的重写 GetProvider() 来做到这一点。

这会创建一个我们提供程序的实例,并将从 web 配置文件/元素传递给它的任何参数传递给它。

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);
    }
}

抽象类

MefWorkflowExtensionBehavior 是我们将扩展插入 WorkflowServiceHost 并隐藏逻辑的地方。

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) { }
}

您需要从 IServiceBehavior 实现的唯一方法是 ApplyDispatchBehavior(...),在这里我们将 serviceHostBase 强制转换为 WorkflowServiceHost,并通过 WorkflowExtensions 属性将我们的提供程序添加到运行时。

我们还有机会将实现 IErrorHandler 接口的类添加到主机的 ChannelDispatcher.ErrorHandlers 集合中。

/// <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");
    }
}

这个简单的接口允许我们记录、处理并向客户端返回一个友好或不友好的消息。

配置元素

最后,为了将这一切联系起来,我们需要能够在 web.config 中指定我们的服务行为。

由于我实现了切换到另一个用于测试我的基类一部分的位置的功能,我们继承的类非常简单,它从我们的基元素接收参数并创建我们的行为。

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

MefWorkflowProviderExtensionBehaviorElement 继承自 BehaviorExtensionElement,并使用 ConfigurationProperty 指定配置文件属性,并调用 CreateExtensionBehavior 以允许进一步配置。

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);
}

这一切终于完成了;要在一个代码活动中访问我们的提供程序,我们只需要这一行

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

您所有的活动、服务和子组件都会自动连接起来。下次,我希望谈谈原生活动和自定义设计器。祝大家新年快乐。

历史

  • 11-01-01 - 发布第一个版本。
© . All rights reserved.