创建通用的(静态且不变的)WCF 接口





5.00/5 (5投票s)
了解如何创建支持大部分 WCF 功能的静态且不变的 WCF 接口。
引言
本文介绍了一个用于静态接口 WCF 服务的框架。
或多或少,每个创建由第三方消费的 WCF 服务的开发人员都会遇到服务更新导致客户端和服务之间兼容性中断的情况。至少,他们会更新流经服务的数据,即使不是契约本身。当 WCF 契约发生变化,或者流经该契约的数据发生变化时,通常需要重新构建客户端和服务以包含任何更改。
我希望通过此代码示例,找到一种定义 WCF 接口的方法,使其在流经内容发生变化时,或在添加新方法时,无需进行更改。我想到的是一个 WCF 契约,它定义了识别流经消息所需的最少数据——这意味着底层数据可以根据需要经常更改,而无需更改 WCF 接口本身。客户端和服务都可以独立于 WCF 契约,定义他们希望在整体数据方面保持的支持级别。
让我们从一个典型的 WCF 契约示例开始,我传入一个 Person
类,并获取一个 Address
类作为返回。
[OperationContract]
Address GetAddress(Person person);
这一切都很好,并且运行正常,但是当我想要更改服务,并通过传入 Customer
获取 Address
时会发生什么?我现在必须要么更改接口以使其接受 Customer
,要么添加一个全新的接口来支持新的调用。
[OperationContract]
Address GetAddress(Customer customer);
添加新接口是最好的选择,因为它不会破坏 WCF 契约的兼容性,但是它仍然需要客户端和服务针对这个更新的契约重新编译以支持新方法。
注意:更改成员的名称或数据类型,或删除数据成员是破坏性更改。同样,更改方法的类型、名称或参数是破坏性更改。破坏性更改将要求客户端和服务上更新契约。
所以,您可以看到,如果我想要保持与任何消费我服务的客户端的兼容性,我必须添加一个接受 Customer
对象的新的 GetAddress
方法,而不是仅仅更改现有方法。在这个示例中,这不是一个最佳解决方案,因为我不希望我的底层服务进行任何更改(包括添加新方法)时,接口也随之更改。
解决方案
解决这个问题的方法是跳出简单、明确定义的 WCF 调用的世界,进入自定义序列化、消息处理程序和其他可怕事物的灰色地带。
其实不然……实际上,如果您使用我在这里提出的框架,它相当简单。
我为这个例子提供的解决方案对我们上面看到的 WCF 契约做了一些根本性的改变,并且也需要 WCF 调用之外的一些改变。
首先,我们之前使用的接口必须去掉。取而代之的是两个新的方法,它们将是这样的:
[OperationContract(IsOneWay = true)]
void SendMessageOneWay(int topicId, int messageId, byte[] message);
[OperationContract]
byte[] SendMessageRequestResponse(int topicId, int messageId, byte[] message);
在这里您可以看到,我们已经用两个通用方法替换了所有之前的 GetAddress
方法。每个方法接受三个参数:topicId
、messageId
和 message
。
注意:topicId
和 messageId
都是整数,内部映射到枚举值。它们被创建为整数是为了支持内部枚举中值的添加或删除而不会破坏兼容性。
topicId
:正在发送消息的顶级类别。这是一种将消息组织成有意义组的方式。对于本例,我们假设我们正在使用“Address”主题。
messageId
:指定主题中的特定消息。这标识了将通过此方法发送的消息,以便将其路由到正确的处理程序(稍后会详细介绍)。消息 ID 可以表示我们想要发送的不同消息,因此在此示例中,我们假设有两个消息 ID:“GetAddressFromPerson
”和“GetAddressFromCustomer
”。
message
:通过 WCF 发送的任何消息的序列化表示。(我将在后面的协议缓冲区部分介绍这一点)
返回值
(仅限于 SendMessageRequestResponse
):响应消息的序列化表示。
对于此示例,TopicId
和 MessageId
枚举包含以下内容
public enum TopicId : int
{
Address = 1,
Person = 2
}
public enum MessageId : int
{
GetAddressFromPerson = 1,
GetAddressFromCustomer = 2,
GetAllAddresses = 3
}
关于此上下文中枚举类型的注释: 您可能会问为什么服务两端都使用枚举,但接口为 topicId
和 messageId
参数指定了 int 类型。答案再次与兼容性有关……如果我们从特定类型(例如 TopicId
)中解耦方法的接口,那么当该类型更改时,我们可以在接口上支持向后和向前兼容性。因此,如果服务收到一个它不知道其主题或消息 ID 的消息,它可以简单地丢弃它,或者抛出错误。
协议缓冲区
正如您无疑已经注意到的,“message
”参数在操作契约中(以及“SendMessageRequestResponse
”方法上的返回值)是一个字节数组。这个字节数组表示一个序列化的 .NET 对象,正在通过 WCF 发送或接收。
为了使 WCF 接口通用且可扩展,我们来回传递的对象需要被转换为某种通用格式,这种格式在添加、删除或更改内容时不会影响 WCF 契约。在此示例中,我使用已序列化为字节数组的协议缓冲区,但您可以使用几乎任何可以通过 WCF 发送的东西。
这意味着什么?首先,这意味着要使用此项目,您需要添加对 protobuf-net 的引用(我使用 NuGet 添加了引用——因为它现在已集成到 VS2012 中!)。您还需要创建 protobuf 对象以进行来回发送。例如,我的 Person
类如下所示
[ProtoContract]
public class Address
{
[ProtoMember(1)]
public string Address1 { get; set; }
[ProtoMember(2)]
public string Address2 { get; set; }
[ProtoMember(3)]
public string City { get; set; }
[ProtoMember(4)]
public string State { get; set; }
}
协议缓冲区在 CPU 和内存使用方面都非常高效,这就是我选择将其用作本示例的序列化引擎的原因。您可以在此处阅读有关 protobuf-net 的所有信息:http://code.google.com/p/protobuf-net/
使用代码
在此示例中,我包含了一些辅助类,旨在减轻过渡到此类解决方案的痛苦。请注意,这些只是契约项目中包含的少数几个类。
- IWcfContract.cs
- SerializationHelper.cs
- ServiceWrapperBase.cs
- ClientWrapper.cs
- ClientWrapperBuilder.cs
此接口包含 WCF 服务和客户端的“SendMessageOneWay
”和“SendMessageRequestResponse
”方法。此文件还包含契约的回调版本。请注意,其中包含的契约的名称和命名空间已明确设置,以便 WCF 将它们视为可互换的(IWcfContract
和 IWcfContractWithCallback
)。
这是一个静态类,它有两个方法,允许对象的序列化或反序列化。
这个抽象类旨在由作为 WCF 服务代理的类继承。它实现了 WCF 方法“SendMessageOneWay
”和“SendMessageRequestResponse
”,并提供了注册主题和消息 ID 处理程序的支持。
这个类接受 WCF 服务的引用,并封装了“SendMessageOneWay
”和“SendMessageRequestResponse
”方法,并以更友好的方式将其作为“Send
”方法公开。
这个类是一个静态工厂类,用于创建上述的 ClientWrapper
类。
设置服务
要设置服务,您将像任何 WCF 服务一样创建一个类,通过使用 ServiceBehavior
属性进行修饰,如下所示
[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single)]
public class MyService
{…}
接下来,让你的类实现 ServiceWrapperBase
,如下所示:
[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single)]
public class MyService : ServiceWrapperBase<TopicId, MessageId>
{
public MyService(bool isDuplex)
: base(isDuplex)
{ ... }
}
注意:TopicId
和 MessageId
类型应该是基础类型为“int”的枚举类型(请参阅我上面关于此的注释)。
现在您已经设置了一个实现 ServiceWrapperBase
的类,是时候使用以下代码将其作为 WCF 服务进行宿主了
MyService service = new MyService();
ServiceHost serviceHost = new ServiceHost(service);
serviceHost.Open();
请记住:您需要添加对 System.ServiceModel
的引用才能使 ServiceHost
类可用。
完成此操作后,例如将代码放入控制台应用程序中,您将拥有一个自托管的 WCF 服务。
注册消息处理程序
将代码放置好以托管服务后,我们来注册一些消息处理程序!注册消息处理程序是 ServiceWrapperBase
类将传入消息路由到相应方法的方式。例如,如果我想要接收一个 TopicId
为 Address
,MessageId
为 GetAddressFromCustomer
,返回值为 Person
的消息,我将按如下方式注册处理程序:
首先,我将创建一个处理消息的方法
private Address HandleGetAddressFromCustomer(Customer requestMessage)
{…}
然后我将处理程序方法注册到 ServiceWrapperBase
RegisterHandler<Customer, Address>(TopicId.Address, MessageId.GetAddressFromCustomer, HandleGetAddressFromCustomer);
此时,任何以上述 TopicId
和 MessageId
进入 WCF 服务的请求都将路由到 HandleGetAddressFromCustomer
方法,在该方法中,我将根据传入的 Customer
数据进行处理,并返回一个 Address
对象。
调用服务
调用服务非常简单…您需要一个 . 您只需调用 ClientWrapperBuilder 类上的相应 GetClientWrapper 工厂方法。ChannelFactory
来创建 IWcfContract
的实例,我将留给读者作为练习。
您可以通过以下方式创建 ClientWrapper
类的新实例:
clientWrapper = ClientWrapperBuilder.GetDuplexClientWrapper<TopicId, MessageId, IWcfContractWithCallback>("ProtobufSpikeCb");
// or
clientWrapper = ClientWrapperBuilder.GetSimplexClientWrapper<TopicId, MessageId, IWcfContract>("ProtobufSpike");
请注意,我使用三种类型实例化了 ClientWrapper
类:TopicId
、MessageId
和 IWcfContract
(或者双工的 IWcfContractWithCallback
)。TopicId
和 MessageId
的作用与它们在服务端的相同,而 IWcfContract
则指定了您传入的服务的类型。
注意:传入 ClientWrapperBuilder
的 IWcfContract
类型在 ClientWrapperBuilder
类中受到约束,只接受实现 IWcfContract
或 IWcfContractWithCallback
的类型。
一旦拥有 ClientWrapper
的实例,就可以这样调用它:
Address response = clientWrapper.Send<Address, Customer>(TopicId.Address, MessageId.GetAddressFromCustomer, new Customer() { Id = “123” });
注意:上述示例用于调用 ClientWrapper
发送请求/响应类型的消息。如果您想发送单向消息,只需像这样调用 ClientWrapper
clientWrapper.Send<OneWayMessage>(TopicId.Other, MessageId.OneWay, message);\
双工 - 使用回调
以下是如何注册回调(在客户端)以及如何调用回调(在服务上)的示例。
if (clientWrapper.IsDuplex)
clientWrapper.RegisterCallbackHandler<string>(TopicId.Other, MessageId.PrintData, PrintData);
如上所示,调用
ClientWrapper
以注册回调处理程序(在此示例中,回调处理程序接受一个参数,即 string
)。回调的注册方法与服务处理程序的注册方法相同。
从服务侧调用回调非常简单,只需在服务中执行以下操作即可,该服务应实现
ServiceWrapperBase
类(使您可以访问 isDuplex
和 Callback
成员)。
if (isDuplex)
Callback<string>(TopicId.Other, MessageId.PrintData, "Hello there! This is a callback!", OperationContext.Current);
其他评论
虽然这里描述的通用接口违背了 WCF 服务版本控制和兼容性方案的目的,但它在我正在开发的软件中有其应用之地,并且优于我研究过的一些替代方案。这里真正的优势在于我获得了一个稳定且不变的接口,以及使用 WCF 的好处。例如安全性、可靠传输,以及我希望从 WCF 堆栈中利用的除契约之外的任何其他功能。
值得关注的点
额外福利!您可以使用此代码创建一个 WCF 服务,该服务具有在运行时动态更改的功能集。您可以在方法可用时为其注册处理程序,然后在不希望它们可用时取消注册——所有这些都无需停止应用程序,当然也无需重新编译任何内容。
历史
2012年11月14日 - 第一次修订。
2012年12月11日 - 第二次修订:添加了支持双工 WCF 通道的功能,以及用于客户端的基于证书的安全性。使用更好的创建方法重构了客户端包装器,简化了整体代码。