使用 WCF 4 中的基于内容的路由和 XSLT 2.0 中的转换实现轻量级 ESB
本文旨在尝试使用 WCF 4 中的基于内容的路由和 XSLT 2.0 进行消息映射转换,来实现一个轻量级的企业服务总线 (ESB)。
引言
在本文中,我尝试阐述我的思路,即利用 WCF 4 的基于内容的路由功能和 XSLT 2.0 的消息转换能力来实现一个轻量级的 ESB(企业服务总线)。在这里,我们将处理多个契约、多个路由服务、多个输入消息以及与输入消息对应的多个输出消息。最后,我们还将探讨如何进一步优化此实现,利用 WCF 4 的 WS-Discovery 协议支持功能以及新兴的 Windows Server AppFabric,在涉及 Microsoft 技术ルの企业 SOA 实现场景中托管和管理 WCF 服务。
背景
本文需要您具备 WCF、C# 和 .NET 4.0 的先验知识。了解 ESB Toolkit 2.0、BizTalk Server 2009 和 Windows Server AppFabric 将会是加分项,但并非必需。了解 SOA 也是有益的。
企业 SOA 模型包含五个水平层和四个垂直层,总共九层。下面将展示各层及其涉及的技术:
生产者
- 第 01 层:运营系统 - 这些是企业中运行的离散应用程序和运营系统,形成数据、知识和交易历史的孤岛。
- 第 02 层:业务组件 - 这些是与业务中特定主题领域相关的实现集合。ADO.NET 实体框架、定制组件(可能参与 COM+ 事务)等可能会实现此层。
- 第 03 层:企业服务 - 这些是松散耦合、自治的、粗粒度的服务,公开底层业务组件的功能。WCF 数据服务、WCF 服务、ASP.NET Web 服务、RESTful 服务等构成了这一层。WCF 是此处使用的技术。
消费者
- 第 04 层:业务工作流 - 此层与业务规则协同实现业务编排。工作流使用 Windows Workflow Foundation 4 实现,该版本符合 BPEL 标准。Microsoft 有符合 BPEL4WS 标准的 XLANGs。WRE(工作流规则引擎)可用于实现业务规则存储。
- 第 05 层:客户端应用程序 - 此层包含各种客户端应用程序。Silverlight 4.0 是此处的领先技术。
通用层
- 第 06 层:企业服务总线 - ESB 提供了一系列新功能,专注于构建健壮、互联、面向服务的应用程序,这些应用程序包含基于行程的服务调用,用于轻量级服务组合、端点和映射的动态解析、Web 服务和 WS-* 集成、故障管理和报告,以及与第三方 SOA 治理解决方案的集成。Neudesic ESB 是此层的产品。该产品已获得 Microsoft 的认可。
- 第 07 层:服务管理和监控 - 此层主要负责管理和监控服务。Windows Server AppFabric 1.0 是此处的领先技术。
- 第 08 层:信息架构(服务元数据、注册表、BI) - 此层将服务与 UDDI 注册表(UDDI 3.0)集成,并实现数据架构等。SSIS(SQL Server Integration Services)是此处使用的另一项技术。
- 第 09 层:SOA 治理 - 此层用于 SOA 治理。在此处实现与服务相关的策略以及其他治理相关方面。AmberPoint Inc. 的 BizTalk Nano Agent 是 Microsoft 认可的 SOA 治理产品。WCF 4 还支持 WS-Policy 和 WS-PolicyAttachment。
Using the Code
以下是代码实现中所做的工作:
- 公开了两个产品服务。
- 公开了两个客户服务。
- 公开了一个路由服务,用于根据客户消息内容选择调用两个客户服务中的一个。
- 公开了另一个路由服务,用于根据产品消息内容选择调用两个产品服务中的一个。
- 两个 XSLT 转换:一个用于将输入的客户消息转换为输出的客户详细信息消息,另一个用于将输入的产品消息转换为输出的成品消息。
- 客户和产品的输入和输出消息的服务契约和相应的数据契约。
- 实现了客户和产品契约的服务。
- 用于实现路由服务和其他独立服务的配置文件。
从上述步骤可以清楚地看出,我们将实现基于内容的路由与消息转换相结合。
需要执行以下步骤:
- 创建一个空白的 Visual Studio 解决方案,并将其命名为 *LightWeightESB.sln*。
- 添加一个名为 *CustomerProductContract* 的类库。
- 添加一个名为 *ICustomer.cs* 的代码文件,其内容如下:
- 接下来,添加一个名为 *IProduct.cs* 的代码文件,其中包含以下代码:
- 向项目中添加对 *System.ServiceModel.dll*、*System.ServiceModel.Description.dll*、*System.ServiceModel.Web.dll* 和 *System.Runtime.Serialization.dll* 的引用。
- 添加一个名为 *TransformUtility.cs* 的代码文件,其中包含以下代码:
- 现在我们需要添加 *.xslt* 文件。首先,我们添加 *MappingMapToCustomerDetails.xslt* 文件。右键单击项目,选择“添加”->“新项”选项来添加 XSLT 文件。将以下内容复制到其中:
- 接下来,我们以类似的方式添加 *.xslt* 文件,名为 *MappingMapToFinishedProducts.xslt*,内容如下:
- 现在,编译并构建解决方案。
- 接下来,向解决方案中添加一个 WCF 服务应用程序,并命名为 *ExclusiveProductService*。
- 在 *ExclusiveProductService.svc* 文件中包含以下内容:
- 在代码隐藏文件中,*ExclusiveProductService.svc.cs* 文件包含以下代码:
- 在相应的 *web.config* 文件中,放入以下代码:
- 接下来,向解决方案中添加另一个名为 *GeneralProductService* 的 WCF 服务应用程序。
- 接下来,在 *GeneralProductService.svc* 文件中,我们添加以下代码:
- 在代码隐藏文件中,*GeneralProductService.svc.cs* 文件包含以下代码:
- 相应的 *web.config* 文件应包含以下内容:
- 现在向解决方案中添加另一个名为 *OrdinaryCustomerService* 的 WCF 服务应用程序。
- 相应的 *OrdinaryCustomerService.svc* 文件应包含以下代码:
- 而代码隐藏文件 *OrdinaryCustomerService.svc.cs* 应包含以下代码:
- 相应的 *web.config* 文件应包含以下代码:
- 接下来,向解决方案中添加另一个名为 *PremiumCustomerService* 的 WCF 服务应用程序。
- 相应的 *PremiumCustomerService.svc* 文件应包含以下代码:
- 而代码隐藏文件 *PremiumCustomerService.svc.cs* 文件应包含:
- 现在,相应的 *web.config* 文件应包含以下代码:
- 现在我们将添加路由服务。首先,我们将添加一个名为 *ProductService* 的 WCF 服务应用程序。该服务将根据
ProductCategory
属性的内容,将产品消息路由到 *GeneralProductService* 或 *ExsclusiveProductService* 中的一个。 - ProductService.svc 文件应包含以下代码:
- 毋庸置疑,现在我们需要为项目添加对 *System.ServiceModel.Routing.dll* 的引用。
- 此路由服务的 *web.config* 文件应包含以下代码:
- 接下来,我们向解决方案中添加另一个 WCF 服务应用程序,命名为 *CustomerService*。
- CustomerService.svc 文件应包含以下代码:
- 此路由服务的相应 *web.config* 文件应包含以下代码:
- 现在我们需要考虑托管。我们可以将所有服务部署到 Windows Server AppFabric。实际上,任何部署到 IIS 7.0 的服务都会被 AppFabric 在管理工具中捕获。我们只需要确保在部署前预装了 .NET 4 的应用程序服务器扩展。
- 我们通过创建与物理目录相对应的虚拟目录来部署所有服务,例如 *CustomerService*、*ExclusiveProductService*、*GeneralProductService*、*OrdinaryCustomerService*、*PremiumCustomerService* 和 *ProductService*。
- 现在我们需要添加客户端项目。向解决方案中添加一个 Windows Forms 应用程序,并命名为 *ProtocolBridgingClient*。
- app.config 文件无需包含除以下内容之外的任何内容:
- 将 *Form1.cs* 重命名为 *ProtocolBridgingForm.cs*,此 Windows 窗体的代码应包含以下内容:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel;
using System.ServiceModel.Description;
using System.ServiceModel.Web;
using System.Runtime.Serialization;
namespace CustomerProductContract
{
[ServiceContract]
public interface ICustomer
{
[OperationContract]
CustomerDetail GetCustomerDetails(Customer cust);
}
[DataContract]
public class Customer
{
[DataMember]
public string CustomerID { get; set; }
[DataMember]
public string CustomerName { get; set; }
[DataMember]
public string CustomerCreditRating { get; set; }
}
[DataContract]
public class CustomerDetail
{
[DataMember]
public string CustomerID { get; set; }
[DataMember]
public string CustomerFirstName { get; set; }
[DataMember]
public string CustomerMiddleName { get; set; }
[DataMember]
public string CustomerLastName { get; set; }
[DataMember]
public string CustomerCreditRating { get; set; }
}
}
Customer
类对应输入消息,CustomerDetail
类对应输出消息。使用 XSLT 2.0 实现的消息映射将输入客户消息映射到输出客户详细信息消息。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel;
using System.ServiceModel.Description;
using System.ServiceModel.Web;
using System.Runtime.Serialization;
namespace CustomerProductContract
{
[ServiceContract]
public interface IProduct
{
[OperationContract]
FinishedProduct GetProductDetails(Product prod);
}
[DataContract]
public class Product
{
[DataMember]
public string ProductName { get; set; }
[DataMember]
public string ProductCategory { get; set; }
}
[DataContract]
public class FinishedProduct
{
[DataMember]
public string MfgDate { get; set; }
[DataMember]
public string ExpDate { get; set; }
[DataMember]
public string ProductName { get; set; }
[DataMember]
public string BatchID { get; set; }
[DataMember]
public string ProductCategory { get; set; }
}
}
在这里,Product
类对应输入消息,FinishedProduct
类对应输出消息。输入和输出消息之间的映射使用 XSLT 2.0 完成。因此,我们看到轻量级服务总线接收多个输入消息。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml;
using System.Xml.Xsl;
using System.Xml.XPath;
using System.Xml.Serialization;
using System.IO;
namespace CustomerProductContract
{
public static class TransformUtility
{
private static XmlSerializer GetSerializer(Type type)
{
return new XmlSerializer(type);
}
public static string ToXml<T>(T obj)
{
var xs = GetSerializer(typeof(T));
var sb = new StringBuilder();
using (var swriter = new StringWriter(sb))
{
xs.Serialize(swriter, obj);
swriter.Close();
}
return sb.ToString();
}
public static T FromXml<T>(string xml)
{
T axb;
var xs = GetSerializer(typeof(T));
using (var reader = new StringReader(xml))
{
axb = (T)xs.Deserialize(reader);
reader.Close();
}
return axb;
}
public static string TransformXml(TextReader reader, bool isCustomer)
{
//Change this path suitably as per location of your solution folder
string xslPath = isCustomer ? @"F:\Works\ProtocolBridge\Customer" +
@"Contract\MappingMapToCustomerDetails.xslt" :
@"F:\Works\ProtocolBridge\CustomerContract" +
@"\MappingMapToFinishedProducts.xslt";
XPathDocument xpathDoc = new XPathDocument(reader);
XslCompiledTransform xsl = new XslCompiledTransform();
xsl.Load(xslPath);
StringBuilder sb = new StringBuilder();
TextWriter tw = new StringWriter(sb);
xsl.Transform(xpathDoc, null, tw);
return sb.ToString();
}
}
}
现在,这需要一些解释。ToXml<>()
方法将 .NET 对象序列化为其等效的 XML 字符串,而 FromXml<>()
方法将 XML 字符串反序列化为等效的 .NET 对象。这里使用泛型是因为我们可以使用上述方法序列化和反序列化任何类型。用 DataContract
特性修饰类会自动使其可序列化。TransformXml()
方法接受等效于 XML 消息的输入 TextReader
,并使用 XslCompiledTransform
进行转换,利用包含转换的 *.xslt* 文件。
<stylesheet version="2.0" exclude-result-prefixes="xs fn"
xmlns:fn="http://www.w3.org/2005/xpath-functions"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<output method="xml" indent="yes" encoding="UTF-8"/>
<template match="/">
<customerdetail >
<attribute name="xsi:noNamespaceSchemaLocation"
namespace="http://www.w3.org/2001/XMLSchema-instance"/>
<for-each select="Customer">
<customerid>
<value-of select="CustomerID"/>
</customerid>
</for-each>
<for-each select="Customer">
<customermiddlename>
<value-of select="CustomerName"/>
</customermiddlename>
</for-each>
</customerdetail>
</template>
</stylesheet>
在此,我们通过首先将 Customer
对象序列化为 XML 字符串,然后应用转换,最后反序列化结果 XML 字符串到 CustomerDetail
对象,来提供 Customer
和 CustomerDetail
对象之间的映射。因此,我们实现了输入消息到输出消息的转换,这是 ESB 的主要功能之一。
<stylesheet version="2.0" exclude-result-prefixes="xs fn"
xmlns:fn="http://www.w3.org/2005/xpath-functions"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<output method="xml" indent="yes" encoding="UTF-8"/>
<template match="/">
<finishedproduct>
<attribute name="xsi:noNamespaceSchemaLocation"
namespace="http://www.w3.org/2001/XMLSchema-instance"/>
<for-each select="Product">
<productname>
<value-of select="ProductName"/>
</productname>
</for-each>
<for-each select="Product">
<productcategory >
<value-of select="ProductCategory"/>
</productcategory>
</for-each>
</finishedproduct>
</template>
</stylesheet>
在这里,我们通过首先将 Product
对象序列化为 XML 字符串,然后应用转换,最后反序列化结果 XML 字符串到 FinishedProduct
对象,来提供 Product
和 FinishedProduct
对象之间的映射。在以上两种情况下,我们都只进行了简单的转换,以展示 ESB 中转换的概念,而不是深入研究 XSLT 的复杂性。
<%@ServiceHost Language="C#"
Debug="true"
Service="ExclusiveProductService.ExclusiveProductService"
CodeBehind="ExclusiveProductService.svc.cs"%>
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.ServiceModel.Web;
using System.Text;
using System.IO;
namespace ExclusiveProductService
{
// NOTE: You can use the "Rename" command
// on the "Refactor" menu to change the class name
// "Service1" in code, svc and config file together.
public class ExclusiveProductService : CustomerProductContract.IProduct
{
public CustomerProductContract.FinishedProduct
GetProductDetails(CustomerProductContract.Product prod)
{
string prodXml =
CustomerProductContract.TransformUtility.ToXml
<CustomerProductContract.Product>(prod);
StringReader reader = new StringReader(prodXml);
string finProdXml =
CustomerProductContract.TransformUtility.TransformXml(reader, false);
CustomerProductContract.FinishedProduct finProd =
CustomerProductContract.TransformUtility.FromXml
<CustomerProductContract.FinishedProduct>(finProdXml);
return finProd;
}
}
}
在这里,实现了 IProduct
接口。我们首先获取输入 Product
对象的 XML 等效物,然后将其放入 reader 对象中,随后调用 TransformXml
方法。在获得反序列化的输出对象后,我们将其返回。
<configuration>
<system.web>
<compilation debug="true" targetFramework="4.0"/>
</system.web>
<system.serviceModel>
<bindings>
<basicHttpBinding>
<binding name="BasicHttpBinding_ICustomerProduct">
<security mode="None"/>
</binding>
</basicHttpBinding>
</bindings>
<services>
<service name="ExclusiveProductService">
<endpoint address="ExclusiveProductService.svc"
contract="CustomerProductContract.IProduct"
bindingConfiguration="BasicHttpBinding_ICustomerProduct"
binding="basicHttpBinding"/>
<endpoint address="mex"
contract="IMetadataExchange"
binding="mexHttpBinding"/>
<host>
<baseAddresses>
<add baseAddress="https:///ExclusiveProductService/"/>
</baseAddresses>
</host>
</service>
</services>
<behaviors>
<serviceBehaviors>
<behavior>
<!-- To avoid disclosing metadata information, set the value
below to false and remove the metadata endpoint above before deployment -->
<serviceMetadata httpGetEnabled="true"/>
<!-- To receive exception details in faults for debugging purposes,
set the value below to true. Set to false before deployment
to avoid disclosing exception information -->
<serviceDebug includeExceptionDetailInfaults="false"/>
</behavior>
</serviceBehaviors>
</behaviors>
<serviceHostingEnvironment multipleSiteBindingsEnabled="true"/>
</system.serviceModel>
<system.webServer>
<modules runAllManagedModulesForAllRequests="true"/>
</system.webServer>
</configuration>
<%@ServiceHost Language="C#" Debug="true"
Service="GeneralProductService.GeneralProductService"
CodeBehind="GeneralProductService.svc.cs"%>
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.ServiceModel.Web;
using System.Text;
using System.Xml;
using System.IO;
namespace GeneralProductService
{
// NOTE: You can use the "Rename" command
// on the "Refactor" menu to change the class name
// "Service1" in code, svc and config file together.
public class GeneralProductService : CustomerProductContract.IProduct
{
public CustomerProductContract.FinishedProduct
GetProductDetails(CustomerProductContract.Product prod)
{
string prodXml =
CustomerProductContract.TransformUtility.ToXml
<CustomerProductContract.Product>(prod);
StringReader reader = new StringReader(prodXml);
string finProdXml =
CustomerProductContract.TransformUtility.TransformXml(reader, false);
CustomerProductContract.FinishedProduct finProd =
CustomerProductContract.TransformUtility.FromXml
<CustomerProductContract.FinishedProduct>(finProdXml);
return finProd;
}
}
}
在这里,我们实现了 IProduct
接口。首先,我们通过输入获取 Product
对象,并将其转换为等效的 XML 字符串。然后,我们将该 XML 转换为输出 XML,反序列化后得到 FinishedProduct
对象,该对象将进入输出消息。
<configuration>
<system.web>
<compilation debug="true" targetFramework="4.0"/>
</system.web>
<system.serviceModel>
<bindings>
<basicHttpBinding>
<binding name="BasicHttpBinding_ICustomerProduct"/>
<security mode="None"/>
</binding>
</basicHttpBinding>
</bindings>
<services>
<service name="GeneralProductService"/>
<endpoint address="GeneralProductService.svc"
contract="CustomerProductContract.IProduct"
bindingConfiguration="BasicHttpBinding_ICustomerProduct"
binding="basicHttpBinding"/>
<endpoint address="mex" contract="IMetadataExchange"
binding="mexHttpBinding"/>
<host>
<baseAddresses>
<add baseAddress="https:///GeneralProductService/"/>
</baseAddresses>
</host>
</service>
</services>
<behaviors>
<serviceBehaviors>
<behavior>
<!-- To avoid disclosing metadata information, set the value
below to false and remove the metadata endpoint above before deployment -->
<serviceMetadata httpGetEnabled="true"/>
<!-- To receive exception details in faults for debugging purposes,
set the value below to true. Set to false before deployment
to avoid disclosing exception information -->
<serviceDebug includeExceptionDetailInFaults="false"/>
</behavior>
</serviceBehaviors>
</behaviors>
<serviceHostingEnvironment multipleSiteBindingsEnabled="true"/>
</system.serviceModel>
<system.webServer>
<modules runAllManagedModulesForAllRequests="true"/>
</system.webServer>
</configuration>
<%@ ServiceHost Language="C#" Debug="true"
Service="OrdinaryCustomerService.OrdinaryCustomerService"
CodeBehind="OrdinaryCustomerService.svc.cs" %>
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.ServiceModel.Web;
using System.Text;
using System.IO;
namespace OrdinaryCustomerService
{
// NOTE: You can use the "Rename" command on the "Refactor"
// menu to change the class name "Service1"
// in code, svc and config file together.
public class OrdinaryCustomerService : CustomerProductContract.ICustomer
{
public CustomerProductContract.CustomerDetail
GetCustomerDetails(CustomerProductContract.Customer cust)
{
string custXml = CustomerProductContract.TransformUtility.ToXml
<CustomerProductContract.Customer>(cust);
StringReader reader = new StringReader(custXml);
string detCustXml =
CustomerProductContract.TransformUtility.TransformXml(reader, true);
CustomerProductContract.CustomerDetail detCust =
CustomerProductContract.TransformUtility.FromXml
<CustomerProductContract.CustomerDetail>(detCustXml);
return detCust;
}
}
}
在这里,实现了 ICustomer
契约。GetCustomerDetails
方法接受一个 Customer
对象,获取其 XML 等效字符串,并将其转换为输出的 CustomerDetail
对象后返回。
<configuration>
<system.web>
<compilation debug="true" targetFramework="4.0"/>
</system.web>
<system.serviceModel>
<bindings>
<basicHttpBinding>
<binding name="BasicHttpBinding_ICustomer">
<security mode="None"/>
</binding>
</basicHttpBinding>
</bindings>
<services>
<service name="OrdinaryCustomerService">
<endpoint address="OrdinaryCustomerService.svc"
contract="CustomerProductContract.ICustomer"
bindingConfiguration="BasicHttpBinding_ICustomer"
binding="basicHttpBinding"/>
<endpoint address="mex"
contract="IMetadataExchange" binding="mexHttpBinding"/>
<host>
<baseAddresses>
<add baseAddress="https:///OrdinaryCustomerService/"/>
</baseAddresses>
</host>
</service>
</services>
<behaviors>
<serviceBehaviors>
<behavior>
<!-- To avoid disclosing metadata information, set the value below
to false and remove the metadata endpoint above before deployment -->
<serviceMetadata httpGetEnabled="true" />
<!-- To receive exception details in faults for debugging purposes,
set the value below to true. Set to false before
deployment to avoid disclosing exception information -->
<serviceDebug includeExceptionDetailInFaults="false"/>
</behavior>
</serviceBehaviors>
</behaviors>
<serviceHostingEnvironment multipleSiteBindingsEnabled="true"/>
</system.serviceModel>
<system.webServer>
<modules runAllManagedModulesForAllRequests="true"/>
</system.webServer>
</configuration>
<%@ServiceHost Language="C#" Debug="true"
Service="PremiumCustomerService.PremiumCustomerService"
CodeBehind="PremiumCustomerService.svc.cs"%>
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.ServiceModel.Web;
using System.Text;
using System.IO;
namespace PremiumCustomerService
{
// NOTE: You can use the "Rename" command on
// the "Refactor" menu to change the class name
// "Service1" in code, svc and config file together.
public class PremiumCustomerService : CustomerProductContract.ICustomer
{
public CustomerProductContract.CustomerDetail
GetCustomerDetails(CustomerProductContract.Customer cust)
{
string custXml = CustomerProductContract.TransformUtility.ToXml
<CustomerProductContract.Customer>(cust);
StringReader reader = new StringReader(custXml);
string detCustXml =
CustomerProductContract.TransformUtility.TransformXml(reader, true);
CustomerProductContract.CustomerDetail detCust =
CustomerProductContract.TransformUtility.FromXml
<CustomerProductContract.CustomerDetail>(detCustXml);
return detCust;
}
}
}
<configuration>
<system.web>
<compilation debug="true" targetFramework="4.0"/>
</system.web>
<system.serviceModel>
<bindings>
<basicHttpBinding>
<binding name="BasicHttpBinding_ICustomer">
<security mode="None"/>
</binding>
</basicHttpBinding>
</bindings>
<services>
<service name="PremiumCustomerService">
<endpoint address="PremiumCustomerService.svc"
contract="CustomerProductContract.ICustomer"
bindingConfiguration="BasicHttpBinding_ICustomer"
binding="basicHttpBinding"/>
<endpoint address="mex"
contract="IMetadataExchange"
binding="mexHttpBinding"/>
<host>
<baseAddresses>
<add baseAddress="https:///PremiumCustomerService/"/>
</baseAddresses>
</host>
</service>
</services>
<behaviors>
<serviceBehaviors>
<behavior>
<!-- To avoid disclosing metadata information, set the value
below to false and remove the metadata endpoint
above before deployment -->
<serviceMetadata httpGetEnabled="true" />
<!-- To receive exception details in faults for debugging purposes,
set the value below to true. Set to false before deployment
to avoid disclosing exception information -->
<serviceDebug includeExceptionDetailInFaults="false"/>
</behavior>
</serviceBehaviors>
</behaviors>
<serviceHostingEnvironment multipleSiteBindingsEnabled="true"/>
</system.serviceModel>
<system.webServer>
<modules runAllManagedModulesForAllRequests="true"/>
</system.webServer>
</configuration>
<%@ ServiceHost Language="C#" Debug="true"
Service="System.ServiceModel.Routing.RoutingService,System.ServiceModel.Routing,
version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" %>
这里我们使用了 *System.ServiceModel.Routing.dll* 中的 System.ServiceModel.Routing.RoutingService
作为服务。
<configuration>
<system.web>
<compilation debug="true" targetFramework="4.0"/>
</system.web>
<system.serviceModel>
<bindings>
<basicHttpBinding>
<binding name="BasicHttpBinding_ICustomerProduct">
<security mode="None"/>
</binding>
</basicHttpBinding>
</bindings>
<client>
<endpoint name="ProductServiceLibrary_GeneralProductService"
address="https:///GeneralProductService/GeneralProductService.svc"
contract="*"
bindingConfiguration="BasicHttpBinding_ICustomerProduct"
binding="basicHttpBinding"/>
<endpoint name="ProductServiceLibrary_ExclusiveProductService"
address="https:///ExclusiveProductService/ExclusiveProductService.svc"
contract="*" bindingConfiguration="BasicHttpBinding_ICustomerProduct"
binding="basicHttpBinding"/>
<endpoint address="mex" contract="IMetadataExchange"
binding="mexHttpBinding"/>
</client>
<services>
<service name="System.ServiceModel.Routing.RoutingService"
behaviorConfiguration="RoutingServiceBehavior"/>
<host>
<baseAddresses>
<add baseAddress="https:///ProductService/"/>
</baseAddresses>
</host>
<endpoint name="RoutingServiceEndpoint"
contract="System.ServiceModel.Routing.IRequestReplyRouter"
bindingConfiguration="BasicHttpBinding_ICustomerProduct"
binding="basicHttpBinding"/>
<endpoint name="udpDiscovery" kind="udpDiscoveryEndpoint"/>
<endpoint address="mex" contract="IMetadataExchange"
binding="mexHttpBinding"/>
</service>
</services>
<behaviors>
<serviceBehaviors>
<behavior name="RoutingServiceBehavior">
<!-- To avoid disclosing metadata information, set the value below
to false and remove the metadata endpoint above before deployment -->
<serviceMetadata httpgetenabled="true" />
<!-- To receive exception details in faults for debugging purposes,
set the value below to true. Set to false before deployment
to avoid disclosing exception information -->
<serviceDebug includeExceptionDetailInFaults="true"/>
<routing routeOnHeadersOnly="False"
filterTableName="routingRules">
<serviceDiscovery/>
</behavior>
<behavior>
<serviceMetadata httpGetEnabled="true"/>
<serviceDebug includeExceptionDetailInFaults="false"/>
</behavior>
</serviceBehaviors>
</behaviors>
<routing>
<namespaceTable>
<add namespace="http://schemas.datacontract.org/2004/07/CustomerProductContract"
prefix="pp"/>
</namespaceTable>
<filters>
<filter name="GeneralProductFilter"
filterData="//pp:ProductCategory = 'General'"
filterType="XPath"/>
<filter name="ExclusiveProductFilter"
filterData="//pp:ProductCategory = 'Exclusive'"
filterType="XPath"/>
</filters/>
<filterTables>
<filterTable name="routingRules">
<add priority="0"
endpointName="ProductServiceLibrary_GeneralProductService"
filterName="GeneralProductFilter"/>
<add priority="0"
endpointName="ProductServiceLibrary_ExclusiveProductService"
filterName="ExclusiveProductFilter"/>
</filterTable>
</filterTables>
<backupLists>
<backupList name="ProductBackupList">
<add endpointName="ProductServiceLibrary_GeneralProductService"/>
</backupList>
</backupLists>
</routing>
</system.serviceModel>
<system.webServer>
<modules runAllManagedModulesForAllRequests="true"/>
</system.webServer>
</configuration>
在这里,在 serviceBehaviors
部分,我们为 routing 元素指定了 filterTableName
属性。我们还在此处启用了 serviceDiscovery
。在 routing 部分,我们为在 XPath 表达式中使用基于内容的路由添加了一个命名空间前缀。我们在 filters
部分添加了一个过滤器。我们将 filterType
指定为 XPath
,并在 filterData
中使用先前声明的命名空间前缀指定实际的 XPath 表达式。过滤器只是用 DataContract
特性装饰的类的公共自动属性的值。我们还声明了一个 backupLists
部分用于故障的软恢复。
<%@ServiceHost Language="C#" Debug="true"
Service="System.ServiceModel.Routing.RoutingService,System.ServiceModel.Routing,
version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"%>
可以看到,每个接口都需要创建一个路由服务。
<configuration>
<system.web>
<compilation debug="true" targetframework="4.0"/>
</system.web>
<system.servicemodel>
<bindings>
<basicHttpBinding>
<binding name="BasicHttpBinding_ICustomer">
<security mode="None"/>
</binding>
</basicHttpBinding>
</bindings>
<client>
<endpoint name="CustomerServiceLibrary_PremiumCustomerService"
address="https:///PremiumCustomerService/PremiumCustomerService.svc"
contract="*" bindingConfiguration="BasicHttpBinding_ICustomer"
binding="basicHttpBinding"/>
<endpoint name="CustomerServiceLibrary_OrdinaryCustomerService"
address="https:///OrdinaryCustomerService/OrdinaryCustomerService.svc"
contract="*" bindingConfiguration="BasicHttpBinding_ICustomer"
binding="basicHttpBinding"/>
<endpoint address="mex"
contract="IMetadataExchange" binding="mexHttpBinding"/>
</client>
<services>
<service name="System.ServiceModel.Routing.RoutingService"
behaviorConfiguration="RoutingServiceBehavior">
<host>
<baseAddresses>
<add baseAddress="https:///CustomerService/"/>
</baseAddresses>
</host>
<endpoint name="RoutingServiceEndpoint"
contract="System.ServiceModel.Routing.IRequestReplyRouter"
bindingConfiguration="BasicHttpBinding_ICustomer"
binding="basicHttpBinding"/>
<endpoint name="udpDiscovery" kind="udpDiscoveryEndpoint"/>
<endpoint address="mex"
contract="IMetadataExchange" binding="mexHttpBinding"/>
</service>
</services>
<behaviors>
<serviceBehaviors>
<behavior name="RoutingServiceBehavior">
<!-- To avoid disclosing metadata information, set the value below
to false and remove the metadata endpoint above before deployment -->
<serviceMetadata httpGetEnabled="true" />
<!-- To receive exception details in faults for debugging purposes,
set the value below to true. Set to false before deployment
to avoid disclosing exception information -->
<serviceDebug includeExceptionDetailInFaults="true"/>
<routing routeOnHeadersOnly="False"
filterTableName="routingRules"/>
<serviceDiscovery/>
</behavior>
<behavior>
<serviceMetadata httpGetEnabled="true"/>
<serviceDebug includeExceptionDetailInFaults="false"/>
</behavior>
</serviceBehaviors>
</behaviors>
<routing>
<namespaceTable>
<add namespace="http://schemas.datacontract.org/2004/07/CustomerProductContract"
prefix="cc"/>
</namespaceTable>
<filters>
<filter name="PremiumCustomerFilter"
filterData="//cc:CustomerCreditRating = 'Good'"
filterType="XPath"/>
<filter name="OrdinaryCustomerFilter"
filterData="//cc:CustomerCreditRating = 'Bad'"
filterType="XPath"/>
</filters>
<filterTables>
<filterTable name="routingRules">
<add priority="0"
endpointName="CustomerServiceLibrary_PremiumCustomerService"
filterName="PremiumCustomerFilter"/>
<add priority="0"
endpointName="CustomerServiceLibrary_OrdinaryCustomerService"
filterName="OrdinaryCustomerFilter"/>
</filterTable>
</filterTables>
<backupLists>
<backupList name="CustomerBackupList">
<add endpointName="CustomerServiceLibrary_OrdinaryCustomerService" />
</backupList>
</backupLists>
</routing>
</system.serviceModel>
<system.webserver>
<modules runAllManagedModulesForAllRequests="true"/>
</system.webServer>
</configuration>
这里,我们也使用 routing
部分声明了路由逻辑,在其中实现了 filterTable
和 filters
,filters
包含针对 Customer
类 CustomerCreditRating
公共自动属性的 XPath 表达式,用于 filterData
。
<configuration>
<system.serviceModel>
<client />
</system.serviceModel>
</configuration>
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.ServiceModel;
using System.ServiceModel.Description;
using System.ServiceModel.Discovery;
namespace ProtocolBridgingClient
{
public partial class ProtocolBridgingForm : Form
{
public ProtocolBridgingForm()
{
InitializeComponent();
}
private void ProtocolBridgingForm_Load(object sender, EventArgs e)
{
try
{
BasicHttpBinding binding = new BasicHttpBinding();
EndpointAddress custAddress = new EndpointAddress(
"https:///CustomerService/CustomerService.svc");
CustomerProductContract.ICustomer custProxy =
ChannelFactory<CustomerProductContract.ICustomer>.CreateChannel(
binding, custAddress);
CustomerProductContract.Customer cust1 =
new CustomerProductContract.Customer
{
CustomerID = "ar0045855",
CustomerName = "Ambar Ray",
CustomerCreditRating = "Good"
};
CustomerProductContract.Customer cust2 =
new CustomerProductContract.Customer
{
CustomerID = "am0046067",
CustomerName = "Abhijit Mahato",
CustomerCreditRating = "Bad"
};
EndpointAddress prodAddress = new EndpointAddress(
"https:///ProductService/ProductService.svc");
CustomerProductContract.IProduct prodProxy =
ChannelFactory<CustomerProductContract.IProduct>.CreateChannel(
binding, prodAddress);
CustomerProductContract.Product prod1 =
new CustomerProductContract.Product { ProductName = "LUX",
ProductCategory = "General" };
CustomerProductContract.Product prod2 =
new CustomerProductContract.Product { ProductName = "VIM",
ProductCategory = "Exclusive" };
CustomerProductContract.CustomerDetail custDet1 =
custProxy.GetCustomerDetails(cust1);
CustomerProductContract.CustomerDetail custDet2 =
custProxy.GetCustomerDetails(cust2);
CustomerProductContract.FinishedProduct finProd1 =
prodProxy.GetProductDetails(prod1);
CustomerProductContract.FinishedProduct finProd2 =
prodProxy.GetProductDetails(prod2);
string res1 = custDet1.CustomerID + " " +
custDet1.CustomerFirstName + " " +
custDet1.CustomerMiddleName +
" " + custDet1.CustomerLastName +
" " + custDet1.CustomerCreditRating;
string res2 = custDet2.CustomerID + " " +
custDet2.CustomerFirstName +
" " + custDet2.CustomerMiddleName + " " +
custDet2.CustomerLastName + " " +
custDet2.CustomerCreditRating;
string res3 = finProd1.BatchID + " " + finProd1.MfgDate + " " +
finProd1.ExpDate + " " + finProd1.ProductName +
" " + finProd1.ProductName;
string res4 = finProd2.BatchID + " " + finProd2.MfgDate +
" " + finProd2.ExpDate + " " +
finProd2.ProductName + " " + finProd2.ProductName;
txtCustomer.Text = res1 + "\n" + res2 +
"\n" + res3 + "\n" + res4;
}
catch (Exception ex)
{
txtCustomer.Text = (ex.InnerException != null) ?
ex.InnerException.Message + "\t" +
ex.Message : ex.Message;
}
}
private void ProtocolBridgingForm_FormClosed(object sender,
FormClosedEventArgs e)
{
GC.Collect();
}
}
}
在这里,我们使用 ChannelFactory<>.CreateChannel()
方法创建 *CustomerService*(路由服务)和 *ProductService*(路由服务)的代理类,将 BasicHttpBinding
和 EndpointAddress
对象作为参数传递。EndpointAddress
对象的创建需要路由服务的硬编码 URI。最后,我们调用代理方法,这些方法会调用服务。
结论
最后,我们所做的是,使用一个路由服务,根据消息内容将客户消息路由到两个客户相关服务中的一个,同样,我们使用另一个路由服务根据消息内容将产品消息路由到两个产品相关服务中的一个。此外,在获得消息后,我们使用 XSLT 转换来转换消息,然后将输出消息发送回。这样,我们就实现了路由和转换。
关注点
在客户端代码中,我们已经为两个路由服务硬编码了路由服务的 URI。这可以通过利用 WCF 4 的 WS-Discovery 协议支持功能来避免。我们可以说 WS-Discovery 是一种基于 UDP 的多播消息交换。消息从服务接收端点信息,并将其用作发现信息。客户端使用发现信息来发现网络上可用的服务。之前,我们通过在路由服务的 *web.config* 文件中添加一个额外的端点来启用服务发现支持,例如:
...
<endpoint name="udpDiscovery" kind="udpDiscoveryEndpoint"/>
...
这样,我们就使该服务成为一个动态服务。现在,在客户端,我们需要添加对 *System.ServiceModel.Discovery.dll* 的引用。在客户端,我们可以添加如下代码:
...
DiscoveryClient discoverclient =
new DiscoveryClient(new UdpDiscoveryEndpoint());
FindResponse response = discoverclient.Find(
new FindCriteria(typeof(CustomerProductContract.ICustomer)));
EndpointAddress address = response.Endpoints[0].Address;
...
此外,我们还可以使用 UDDI 3.0 API 将 WCF 服务与 UDDI 3.0 注册表集成,以便能够进行服务终结点的运行时确定(动态路由)。我在本文中将其保留,因为它需要进行大量增强才能真正作为 Microsoft 平台上的低成本、轻量级 ESB 替代方案。我预计未来会出现一个功能更完善的自定义轻量级 ESB 解决方案。目前,使用 ESB Toolkit 2.0 需要使用 BizTalk Server,而 BizTalk Server 又需要使用 SQL Server。一个基于 WCF 4.0 和 XSLT 2.0 的 ESB 替代方案将提供低成本的替代方案,并带来快速的投资回报,同时还为架构师提供了使用多个数据库产品而不是仅仅依赖 SQL Server 的灵活性。
历史
- 2004 年 8 月 4 日 - 发布。