WCF 服务端临时多态






4.80/5 (2投票s)
在 WCF 服务开发中模拟临时多态(运算符重载)的方法。
引言
WCF ServiceContracts 的设计必须符合 WSDL 的约束,这排除了 ad hoc 多态的概念,即运算符重载。通过使用 OperationContractAttribute,可以通过映射重载的 OperationContracts 实现(隐式实现)到一个唯一的名称,该名称随后出现在 WSDL 的 <operation/> 元素中,从而隐藏服务端的此问题。虽然这为服务端重载实现提供了一个(也许)可接受的方案,但将重载引入客户端还需要额外的工作。可以为每个服务合同开发一个专用代理,该代理再次利用 OperationContractAttribute 来处理名称映射。1 然而,这种技术增加了开发复杂性并扩大了部署范围。
如果可以选择为可以使用 DataContractSerializer 的环境实现 WCF 服务,那么通过利用 KnownTypes 可以出现另一种选择。可以设计具有抽象基类的 DataContract 层级结构,这些层级结构用于参数化 OperationContracts。然后,序列化器可以透明地在网络上传输 DataContract 参数类的子类,WCF 在操作分派时会具体化(materialize)这些子类。这通过允许单个 OperationContract 具有多种实际参数化形式,有效地提供了重载的一种形式。
尽管这在 ServiceContract 管理中很有用,但如果没有进一步的基础设施支持,OperationContract 的实现将迅速变得难以管理。每个 OperationContract 都必须以某种方式单独处理每种可能的参数特化。本文介绍了一种桥模式(Bridge Pattern)的实现,该模式干净地解决了这个问题,并实现了 OperationContract 实现与生成其结果的代码之间的完全分离。其副作用是提供了一种非常干净的服务实现模式,可以通过常规的子类化技术进行增强。ServiceContract 实现类变得主要为样板代码,并且服务实现可能在运行时通过 IOC 注射。
背景
桥模式可以看作是一种多重分派(multiple dispatch)形式。该模式中的四个主要实体是:
- 抽象(Abstraction),它定义了抽象的、外部暴露的接口,并维护一个指向实际实现的引用,通常在 Implementor 类型的成员变量中。
- 精炼抽象(RefinedAbstraction),它实现了抽象。
- 实现者(Implementor),它定义了实现的接口。
- 具体实现者(ConcreteImplementor),它实现了实现者。
在操作上,当精炼抽象被构造时,它必须用具体实现者的特定实例初始化抽象的实现者变量。随后对精炼抽象接口实现的调用通过抽象委托到具体实现者中的适当实现。
通过在 .NET 中使用反射,在 WCF 服务实现的上下文中,抽象的本质可以得到极大的泛化。通过采用具体实现者的命名和参数签名约定,可以消除显式定义 Implementor 接口的需要。这里使用的约定是,具体实现者中有效的委托方法必须匹配服务实现的 OperationContract 的名称和签名。然后,抽象委托过程只需要最少的 WCF 接口知识。
在实现 WCF ServiceContracts 时,可以使用接口的隐式或显式实现。隐式实现的形式是服务类中具有正确签名的公共方法。显式实现的形式是服务类中具有正确签名的私有方法,并且操作名称前加上 ServiceContract 名称限定。虽然下面的委托器处理这两种情况,但精炼抽象和具体实现者之间的关注点分离(Separation of Concerns)的完全实现只能通过显式实现来实现。此外,显式实现允许具有相同名称和参数结构的 OperationContracts 在必要时拥有不同的实现。
合同
本文实现的 WCF 服务采取状态化的加/减计算器的形式,其参数可以是整数或整数的字符串表示。这些参数类型由 MathArgument
DataContract 层级结构描述。
namespace Samples.CalculatorService.Contract
{
[DataContract(IsReference = true)]
[KnownType(typeof(IntArgument))]
[KnownType(typeof(StringArgument))]
public abstract class MathArgument
{
}
[DataContract(IsReference = true)]
public class IntArgument : MathArgument
{
[DataMember]
public int Value;
}
[DataContract(IsReference = true)]
public class StringArgument : MathArgument
{
[DataMember]
public string Value;
}
}
该服务支持两个具有不同接口名称但 OperationContracts 相同的合同。namespace Samples.CalculatorService.Contract
{
[ServiceContract(Namespace = "http://Samples/CalculatorService/",
SessionMode = SessionMode.Required)]
public interface ICalculatorServiceB
{
[OperationContract(IsOneWay = false)]
int Add(MathArgument number);
[OperationContract(IsOneWay = false)]
int Subtract(MathArgument number);
}
:
[ServiceContract(Namespace = "http://Samples/CalculatorService/",
SessionMode = SessionMode.Required)]
public interface ICalculatorServiceA
{
[OperationContract(IsOneWay = false)]
int Add(MathArgument number);
[OperationContract(IsOneWay = false)]
int Subtract(MathArgument number);
}
}
抽象
抽象由 ServiceBase
类实现。ServiceBase
的构造函数参数初始化具体实现者实例 _implementor
。ServiceBase
的泛型类型参数是 ServiceBase
的子类。如果子类没有 ServiceBehaviorAttribute,所有后续的分派尝试都会失败,因为调用者无法确定为有效的 WCF 操作分派点。如果子类有 ServiceBehaviorAttribute,则构造函数通过分析服务类上的每个 ServiceContract 来填充 _callSites
,其中包含所有有效分派点的名称列表。
ServiceBase
的单个受保护方法 MethodDispatcher
执行精炼抽象和具体实现者之间的委托。由于通过反射验证委托调用的开销,MethodDispatcher
会缓存每个成功解析的委托以供将来使用。
namespace Samples.CalculatorService.Infrastructure.Service
{
public class ServiceBase<t> where T : class
{
private readonly HashSet _callSites = new HashSet();
// Contains names of all viable callSites (dispatch points from WCF)
// in the service class
private readonly object _implementor;
// the class to which the calls will be dispatched, i.e. the ConcreteImplementor
private readonly Dictionary _methodCache =
new Dictionary(new DispatchKeyComparer());
// dynamically built cache of dispatchable call information
public ServiceBase(object implementor)
{
_implementor = implementor;
Array.ForEach(FindCallSites(), s => _callSites.Add(s)); // load all possible dispatchable methods names
}
:
protected virtual object MethodDispatcher(MethodBase methodBase, params object[] parameters)
{
// Note by the time we get here, WCF will have deserialized abstract DataContract parameters
// into concrete specializations.
DispatchEntry delegatedCall;
var key = new DispatchKey(methodBase.Name, parameters);
// if entry has not been cached before, do so now.
if (!_methodCache.TryGetValue(key, out delegatedCall))
{
if (!_callSites.Contains(methodBase.Name)) // is call from a legal point of origin?
{
throw new ServiceMethodDispatchException(
ServiceMethodDispatchException.IllegalCallerMessageFormatter(methodBase));
}
// Get specifications of delegation
delegatedCall = LoadDelegationDetails(methodBase, key);
// Save for next call at same target
// The cache can ultimately contain many DispatchKeys with the same callSite (MethodName) name and different ParameterTypes
_methodCache.Add(key, delegatedCall);
}
// delegate call
return delegatedCall.DelegationTarget.Invoke(delegatedCall.DelegationInstance, parameters);
}
:
}
}
缓存的键是复合的,由 OperationContract 名称和相关的参数类型的有序数组组成。LoadDelgationDetails
对调用者(精炼抽象)和具体实现者执行反射分析,以确定正确的委托。精炼抽象
精炼抽象是两个 WCF 服务类CalculatorService
和 CalculatorExtender
。CalculatorService
完全实现服务合同(一个方法隐式实现),将实现委托给 MethodDispatcher
,并适当转换返回值。namespace Samples.CalculatorService
{
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerSession)]
public partial class CalculatorService : ServiceBase, ICalculatorService, ICalculatorServiceA
{
public CalculatorService(object implementor) : base(implementor)
{
}
public CalculatorService() : base(new CalculatorImplementation())
{
}
// Implicit implementation of ICalculatorService.Subtract
public int Subtract(MathArgument number)
{
return (int)MethodDispatcher(MethodBase.GetCurrentMethod(),
new object[] {number});
}
int ICalculatorService.Add(MathArgument number)
{
return (int)MethodDispatcher(MethodBase.GetCurrentMethod(),
new object[] {number});
}
int ICalculatorServiceA.Add(MathArgument number)
{
return (int)MethodDispatcher(MethodBase.GetCurrentMethod(),
new object[] { number });
}
int ICalculatorServiceA.Subtract(MathArgument number)
{
return (int)MethodDispatcher(MethodBase.GetCurrentMethod(),
new object[] { number });
}
}
}
然而,CalculatorExtender
要简单得多,因为它只需继承自 CalculatorService
并提供不同的具体实现者实现。WCF 接口实现不必重新实现。namespace Samples.CalculatorService
{
/// <summary>
/// Customization sample
/// Overrides implementations in CalculatorService
/// </summary>
public class CalculatorExtender : CalculatorService
{
public CalculatorExtender() : base( new ExtenderImplementation())
{
}
}
}
具体实现者
具体实现者CalculatorImplementation
和 ExtenderImplementation
提供了计算服务值的实现。CalculatorImplementation
为 WCF 接口中显示的 OperationContract 的每种可能的参数化具体类型都提供了一个函数。在这种情况下,对于 WCF 可以反序列化的每种具体的 MathArgument
都有一个函数。其中两个函数被标记为
ExplicitMethodDispatchAttribute
。此属性的出现会指示分派器在处理指定的 OperationContracts(在本例中为 ICalculatorServiceA
)的调用时,将分派到被装饰的方法。虽然这会将实现者与 ServiceContract(而不是服务类)耦合,但它提供了一种为两个接口提供不同实现的方式。如果删除了 ExplicitMethodDispatchAttributes
,则相应的 OperationContracts 将被委托给相同的实现函数(名称不带“A”后缀的那个)。namespace Samples.CalculatorService
{
public partial class CalculatorImplementation
{
protected int total { get; set; }
protected virtual int Add(IntArgument number)
{
this.total += number.Value;
return this.total;
}
protected virtual int Add(StringArgument number)
{
this.total += Convert.ToInt32(number.Value);
return this.total;
}
protected virtual int Subtract(IntArgument number)
{
this.total -= number.Value;
return this.total;
}
[ExplicitMethodDispatch(typeof(ICalculatorServiceA), "Add")]
protected virtual int AddA(StringArgument number)
{
this.total += 3 * Convert.ToInt32(number.Value);
return total;
}
[ExplicitMethodDispatch(typeof(ICalculatorServiceA), "Subtract")]
protected virtual int SubtractA(StringArgument number)
{
this.total -= 3 * Convert.ToInt32(number.Value);
return this.total;
}
protected virtual int Subtract(StringArgument number)
{
return total -= Convert.ToInt32(number.Value);
}
}
}
与服务类类似,ExtenderImplementation
仅继承自 CalculatorImplementation
并覆盖其部分函数。请注意,其中一个覆盖是一个被 ExplicitMethodDispatchAttribute
装饰的方法;属性不必重复即可发生正确的委托。namespace Samples.CalculatorService
{
public class ExtenderImplementation : CalculatorImplementation
{
protected override int Add(IntArgument number)
{
total += number.Value * 2;
return total;
}
protected override int SubtractA(StringArgument number)
{
this.total -= Convert.ToInt32(number.Value);
return this.total;
}
}
}
还应注意到,具体实现者会执行“有趣的数学”。在某些情况下,输入值会被缩放,以在验证分派路径的单元测试中提供易于识别的结果。异常
如果动态解析委托的调用失败,MethodDispatcher
将在运行时抛出 ServiceMethodDispatchException
,其中包含两条消息之一。两者都表明服务开发中存在问题,而不是执行中。UndispatchableCall
消息表明分派器找不到符合传入 WCF 调用参数化的具体实现者方法。例如,如果参数的 DataContract 层级结构增加了新的子类,但没有提供相应的实现支持,就会发生这种情况。IllegalCall
消息表示尝试从定义 OperationContract 实现的地点以外的其他位置进行调用。使用MethodDispatcher
的 WCF 接口必须用ServiceContractAttribute
标记才能使验证通过。
使用代码
提供的示例解决方案包含三个项目(VS2012)。CalculatorService 包含上述所有类,并为 nettcp 上的 CalulatorExtender 服务提供 WCF 控制台主机。CalculatorClient 为该服务提供了一个 nettcp 客户端。要作为 WCF 服务运行,请启动 CalulatorService(例如,Debug/Start New Instance)。主机控制台窗口打开后,可以类似地启动 Calculator Client。
ServiceImplTests 包含针对服务类基础设施的 xUnit 测试。所有这些都是 POCO,不利用 WCF 传输。
关注点
- 在使用显式接口实现时,所有 WCF 服务类本质上都是样板代码。这意味着代码工件的创建可以轻松地委托给代码生成过程,从而减少开发工作量。这些甚至可以打包在独立的程序集中。
- 精炼抽象和具体实现者中通过子类化进行的自定义,可以更容易地演进代码,减少自定义中的重复代码。
- 具体实现者与抽象的绑定可以通过依赖注入(Dependency Injection)来完成,使得委托在运行时可以被替换,而无需重新实现精炼抽象。
替代方案
IDispatchOperationSelector
WCF 的操作调用分派管道提供了一个可选的拦截点,在该点可以像本文中包含的流程一样,将调用重定向到备用实现。所需功能必须封装在 EndPointBehavior 中,并附加到每个服务 EndPoint 的 EndPointDispatcher。Lowy 在他的书中2 举了一个例子。将分派封装在 EndPointBehavior 中将完全消除此处所示的
ServiceBase
类。抽象实现将驻留在 EndPointBehavior 中,并且管道中的一次调用分派将被消除。然后,注入具体实现者的过程也将大不相同。
可赋值参数
桥模式本身允许在委托调用之前进行参数强制转换的可能性。虽然此实现基于精确的参数类型匹配,但此约束可以放宽,以允许匹配发生在传入参数可以分配给具体实现者的参数时,即 OperationContract 可以有一个类型为 IEnumerable<T> 的参数,并匹配一个类型为 List<T> 的具体实现者参数。
要在
IDispatchOperationSelector
实现中实现类似的宽松,可能还需要另一个实现IParameterInspector
的 EndpointBehavior。
参考文献
1Lowy, Juval, Programming WCF Services, Third Edition: Mastering WCF and the Azure
AppFabric Service Bus. Sabastopol, CA, USA: O'Reilley Media, Inc., August, 2010, pp. 83-85.
2同上,第 798-800 页。