如何:服务版本控制
本主题概述了创建路由配置以将消息路由到同一服务的不同版本所需采取的基本步骤。在本示例中,消息将路由到计算器服务的两个不同版本:roundingCalc
(v1) 和 regularCalc
(v2)。这两个实现都支持相同的操作,但较早的服务 roundingCalc
在返回计算结果前会将所有计算结果舍入到最接近的整数值。客户端应用程序必须能够指示是否使用较新的 regularCalc
服务。
警告: |
---|
为了将消息路由到特定服务版本,路由服务必须能够根据消息内容确定消息目标。在下面演示的方法中,客户端将信息插入消息头中来指定版本。存在几种无需客户端传递额外数据的服务版本控制方法。例如,可以将消息路由到服务的最新版本或最兼容的版本,或者路由器可以使用标准 SOAP 信封的一部分。 |
这两个服务公开的操作为:
Add
Subtract
Multiply
Divide
由于两个服务实现可处理相同的操作,并且除了返回的数据之外基本上相同,因此客户端应用程序发送的消息中包含的基本数据不具备足够的独特性,无法确定如何路由请求。例如,由于两个服务的默认操作相同,因此不能使用操作筛选器。
可以通过多种方式解决这个问题,例如针对服务的各个版本在路由器上公开特定终结点,或者将自定义标头元素添加到消息中以指示服务版本。这些方法均可以将传入消息唯一地路由到服务的特定版本,但利用唯一的消息内容是区分不同服务版本的请求的首选方法。
在本示例中,客户端应用程序将“CalcVer”自定义标头添加到请求消息中。此标头将包含一个值,指示应将消息路由到的服务版本。值“1”指示该消息必须由 roundingCalc 服务处理,而值“2”指示必须由 regularCalc 服务处理。这样,客户端应用程序就可以直接控制将由服务的哪个版本来处理消息。由于自定义标头是消息中包含的值,因此您可以使用一个终结点来接收将两个服务版本作为目标的消息。可以在客户端应用程序中使用下面的代码,将此自定义标头添加到消息中:
messageHeadersElement.Add(MessageHeader.CreateHeader("CalcVer", "http://my.custom.namespace/", "2"));
实现服务版本控制
通过指定由服务公开的服务终结点,创建基本的路由服务配置。下面的示例将定义一个用于接收消息的服务终结点,还定义用于将消息发送到
roundingCalc
(v1) 和regularCalc
(v2) 服务的客户端终结点。<services> <service behaviorConfiguration="routingConfiguration" name="System.ServiceModel.Routing.RoutingService"> <host> <baseAddresses> <add baseAddress="https://localhost/routingservice/router" /> </baseAddresses> </host> <!--Set up the inbound endpoint for the Routing Service--> <endpoint address="calculator" binding="wsHttpBinding" name="routerEndpoint" contract="System.ServiceModel.Routing.IRequestReplyRouter" /> </service> </services> <client> <!--set up the destination endpoints--> <endpoint name="regularCalcEndpoint" address="net.tcp://localhost:9090/servicemodelsamples/service/" binding="netTcpBinding" contract="*" /> <endpoint name="roundingCalcEndpoint" address="https://localhost:8080/servicemodelsamples/service/" binding="wsHttpBinding" contract="*" /> </client>
定义用于将消息路由到目标终结点的筛选器。在本示例中,将使用 XPath 筛选器检测“CalcVer”自定义标头的值,以确定应将消息路由到哪个版本。还可使用 XPath 筛选器检测不包含“CalcVer”标头的消息。下面的示例定义所需的筛选器和命名空间表。
<!-- use the namespace table element to define a prefix for our custom namespace--> <namespaceTable> <add prefix="custom" namespace="http://my.custom.namespace/"/> </namespaceTable> <filters> <!--define the different message filters--> <!--define an xpath message filter to look for the custom header containing a value of 2--> <filter name="XPathFilterRegular" filterType="XPath" filterData="sm:header()/custom:CalcVer = '2'"/> <!--define an xpath message filter to look for the custom header containing a value of 1--> <filter name="XPathFilterRounding" filterType="XPath" filterData="sm:header()/custom:CalcVer = '1'"/> <!--define an xpath message filter to look for messages that do not contain the custom header--> <filter name="XPathFilterNoHeader" filterType="XPath" filterData="count(sm:header()/custom:CalcVer)=0"/> </filters
注意: 默认情况下,在命名空间表中定义 s12 命名空间前缀,该前缀代表命名空间“http://www.w3.org/2003/05/soap-envelope”。 定义筛选器表,该表将各个筛选器与客户端终结点相关联。如果消息包含值为 1 的“CalcVer”标头,则系统会将该消息发送到 regularCalc 服务。如果标头包含值 2,则系统将该消息发送到 roundingCalc 服务。如果没有标头,则系统将该消息路由到 regularCalc。
以下代码定义筛选器表并添加前面定义的筛选器。
<filterTables> <filterTable name="filterTable1"> <!--add the filters to the message filter table--> <!--look for the custom header = 1, and if we find it, send the message to the rounding calc endpoint--> <add filterName="XPathFilterRounding" endpointName="roundingCalcEndpoint"/> <!--look for the custom header = 2, and if we find it, send the message to the rounding calc endpoint--> <add filterName="XPathFilterRegular" endpointName="regularCalcEndpoint"/> <!--look for the absence of the custom header, and if it is not present, assume the v1 endpoint--> <add filterName="XPathFilterNoHeader" endpointName="roundingCalcEndpoint"/> </filterTable> </filterTables>
若要根据筛选器表中包含的筛选器对传入消息求值,必须使用路由行为将筛选器表与服务终结点相关联。下面的示例演示将“filterTable1”与服务终结点相关联:
<behaviors> <!--default routing service behavior definition--> <serviceBehaviors> <behavior name="routingConfiguration"> <routing filterTableName="filterTable1" /> </behavior> </serviceBehaviors> </behaviors>
示例
下面是配置文件的完整代码清单。
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright (c) Microsoft Corporation. All rights reserved -->
<configuration>
<system.serviceModel>
<services>
<service behaviorConfiguration="routingConfiguration"
name="System.ServiceModel.Routing.RoutingService">
<host>
<baseAddresses>
<add baseAddress="https://localhost/routingservice/router" />
</baseAddresses>
</host>
<!--Set up the inbound endpoint for the Routing Service-->
<endpoint address="calculator"
binding="wsHttpBinding"
name="routerEndpoint"
contract="System.ServiceModel.Routing.IRequestReplyRouter" />
</service>
</services>
<behaviors>
<!--default routing service behavior definition-->
<serviceBehaviors>
<behavior name="routingConfiguration">
<routing filterTableName="filterTable1" />
</behavior>
</serviceBehaviors>
</behaviors>
<client>
<!--set up the destination endpoints-->
<endpoint name="regularCalcEndpoint"
address="net.tcp://localhost:9090/servicemodelsamples/service/"
binding="netTcpBinding"
contract="*" />
<endpoint name="roundingCalcEndpoint"
address="https://localhost:8080/servicemodelsamples/service/"
binding="wsHttpBinding"
contract="*" />
</client>
<routing>
<!-- use the namespace table element to define a prefix for our custom namespace-->
<namespaceTable>
<add prefix="custom" namespace="http://my.custom.namespace/"/>
</namespaceTable>
<filters>
<!--define the different message filters-->
<!--define an xpath message filter to look for the
custom header containing a value of 2-->
<filter name="XPathFilterRegular" filterType="XPath"
filterData="sm:header()/custom:CalcVer = '2'"/>
<!--define an xpath message filter to look for the
custom header containing a value of 1-->
<filter name="XPathFilterRounding" filterType="XPath"
filterData="sm:header()/custom:CalcVer = '1'"/>
<!--define an xpath message filter to look for
messages that do not contain the custom header-->
<filter name="XPathFilterNoHeader" filterType="XPath"
filterData="count(sm:header()/custom:CalcVer)=0"/>
</filters>
<filterTables>
<filterTable name="filterTable1">
<!--add the filters to the message filter table-->
<!--look for the custom header = 1, and if we find it,
send the message to the rounding calc endpoint-->
<add filterName="XPathFilterRounding" endpointName="roundingCalcEndpoint"/>
<!--look for the custom header = 2, and if we find it,
send the message to the rounding calc endpoint-->
<add filterName="XPathFilterRegular" endpointName="regularCalcEndpoint"/>
<!--look for the absence of the custom header, and if
it is not present, assume the v1 endpoint-->
<add filterName="XPathFilterNoHeader" endpointName="roundingCalcEndpoint"/>
</filterTable>
</filterTables>
</routing>
</system.serviceModel>
</configuration>
下面是客户端应用程序的完整代码清单。
using System;
using System.ServiceModel;
using System.ServiceModel.Channels;
namespace Microsoft.Samples.AdvancedFilters
{
//The service contract is defined in generatedClient.cs, generated from the service by the svcutil tool.
//Client implementation code.
class Client
{
static void Main()
{
//Print out the welcome text
Console.WriteLine("This sample routes the Calculator Sample through the new WCF RoutingService");
Console.WriteLine("Wait for all the services to indicate that they've started, then press");
Console.WriteLine("<ENTER> to start the client.");
while (Console.ReadLine() != "quit")
{
//Offer the Address configuration for the client
Console.WriteLine("");
Console.WriteLine("Welcome to the Calculator Client!");
EndpointAddress epa;
//set the default address as the general router endpoint
epa = new EndpointAddress("https://localhost/routingservice/router/calculator");
//set up the CalculatorClient with the EndpointAddress, the WSHttpBinding, and the ICalculator contract
//We use the WSHttpBinding so that the outgoing has a message envelope, which we'll manipulate in a minute
CalculatorClient client = new CalculatorClient(new WSHttpBinding(), epa);
//client.Endpoint.Contract = ContractDescription.GetContract(typeof(ICalculator));
//Ask the customer if they want to add a custom header to the outgoing message.
//The Router will look for this header, and if so ignore the endpoint the message was
//received on, and instead direct the message to the RoundingCalcService.
Console.WriteLine("");
Console.WriteLine("Which calculator service should be used?");
Console.WriteLine("Enter 1 for the rounding calculator, 2 for the regular calculator.");
Console.WriteLine("[1] or [2]?");
string header = Console.ReadLine();
//get the current operationContextScope from the client's inner channel
using (OperationContextScope ocs = new OperationContextScope((client.InnerChannel)))
{
//get the outgoing message headers element (collection) from the context
MessageHeaders messageHeadersElement = OperationContext.Current.OutgoingMessageHeaders;
//if they wanted to create the header, go ahead and add it to the outgoing message
if (header != null && (header=="1" || header=="2"))
{
//create a new header "RoundingCalculator", no specific namespace, and set the value to
//the value of header.
//the Routing Service will look for this header in order to determine if the message
//should be routed to the RoundingCalculator
messageHeadersElement.Add(MessageHeader.CreateHeader("CalcVer", "http://my.custom.namespace/", header));
}
else //incorrect choice, no header added
{
Console.WriteLine("Incorrect value entered, not adding a header");
}
//call the client operations
CallClient(client);
}
//close the client to clean it up
client.Close();
Console.WriteLine();
Console.WriteLine("Press <ENTER> to run the client again or type 'quit' to quit.");
}
}
private static void CallClient(CalculatorClient client)
{
Console.WriteLine("");
Console.WriteLine("Sending!");
// Call the Add service operation.
double value1 = 100.00D;
double value2 = 15.99D;
double result = client.Add(value1, value2);
Console.WriteLine("Add({0},{1}) = {2}", value1, value2, result);
// Call the Subtract service operation.
value1 = 145.00D;
value2 = 76.54D;
result = client.Subtract(value1, value2);
Console.WriteLine("Subtract({0},{1}) = {2}", value1, value2, result);
// Call the Multiply service operation.
value1 = 9.00D;
value2 = 81.25D;
result = client.Multiply(value1, value2);
Console.WriteLine("Multiply({0},{1}) = {2}", value1, value2, result);
// Call the Divide service operation.
value1 = 22.00D;
value2 = 7.00D;
result = client.Divide(value1, value2);
Console.WriteLine("Divide({0},{1}) = {2}", value1, value2, result);
}
}
}