不使用代码生成工具而共享WCF代码

2013 年 7 月 23 日

在传统 WCF 开发时遇到的一个主要问题是代码重用。无论你的服务端类设计得再怎么好,一旦经过代理(proxy)生成工具的处理,你就只能得到简单的 DTO(数据传输对象)。本文将说明如何绕过代理生成工具,而使得你的客户端和服务端能够共享代码。

为了论述方便,我们在下面的例子中将使用这个服务接口。

复制代码
[ServiceContract(Namespace = "https://zsr.codeplex.com/services/")]
public interface IInformationService
{
[OperationContract]
Task<zombietypesummarycollection> ListZombieTypes();
[OperationContract]
Task<zombietypedetails> GetZombieTypeDetails(int zombieTypeKey);
[OperationContract]
Task<int> LogIncident(SessionToken session, ZombieSighting sighting);
}

为了支持.NET 4.5 中的 async/await 关键字,每个方法会返回一个 Task 或 Task对象。

不使用代理生成工具的理由

不可变对象与数据契约

不可变对象较少出错,这一点如今已被广泛认可了。除非调用数据契约类的代码需要直接编辑某个属性,否则该属性就应该被标记为只读,以避免发生错误。

这里是一个仅限于只读显示的类的示例。

复制代码
using System;
using System.Runtime.Serialization;
namespace Zombie.Services.Definitions
{
[DataContract(Namespace = "https://zsr.codeplex.com/services/")]
public class ZombieTypeSummary
{
public ZombieTypeSummary(string zombieTypeName, int zombieTypeKey, string
briefDescription = null, Uri thumbnailImage = null)
{
ZombieTypeName = zombieTypeName;
ZombieTypeKey = zombieTypeKey;
BriefDescription = null;
ThumbnailImage = thumbnailImage;
}
[Obsolete("This is only used by the DataContractSerializer", true)]
public ZombieTypeSummary() { }
[DataMember]
public string ZombieTypeName { get; private set; }
[DataMember]
public int ZombieTypeKey { get; private set; }
[DataMember]
public string BriefDescription { get; private set; }
[DataMember]
public Uri ThumbnailImage { get; private set; }
}
}

在以上代码中你会注意到一件奇怪的事,它有一个被标记为过期的公共构造函数(译注:这避免了该构造函数被任何客户端代码所直接调用)。即使在反序列化对象时 WCF 并不真正调用这个构造函数,但它必须存在。我们只需再添加一些特性,使得 WCF 知道哪些字段需要被传递就可以了。

如果我们看一下生成的代理服务,它看上去会和我们之前编写的服务端代码略有相似。

复制代码
[DebuggerStepThroughAttribute()]
[GeneratedCodeAttribute("System.Runtime.Serialization", "4.0.0.0")]
[DataContractAttribute(Name = "ZombieTypeSummary", Namespace =
"https://zsr.codeplex.com/services/")]
[SerializableAttribute()]
[KnownTypeAttribute(typeof(ZombieTypeDetails))]
public partial class ZombieTypeSummary : object, IExtensibleDataObject,
INotifyPropertyChanged
{
[NonSerializedAttribute()]
private ExtensionDataObject extensionDataField;
[OptionalFieldAttribute()]
private string BriefDescriptionField;
[OptionalFieldAttribute()]
private Uri ThumbnailImageField;
[OptionalFieldAttribute()]
private int ZombieTypeKeyField;
[OptionalFieldAttribute()]
private string ZombieTypeNameField;
[BrowsableAttribute(false)]
public ExtensionDataObject ExtensionData
{
get { return this.extensionDataField; }
set { this.extensionDataField = value; }
}
[DataMemberAttribute()]
public string BriefDescription
{
get { return this.BriefDescriptionField; }
set
{
if ((object.ReferenceEquals(this.BriefDescriptionField, value) != true))
{
this.BriefDescriptionField = value;
this.RaisePropertyChanged("BriefDescription");
}
}
}
[DataMemberAttribute()]
public Uri ThumbnailImage
{
get { return this.ThumbnailImageField; }
set
{
if ((object.ReferenceEquals(this.ThumbnailImageField, value) != true))
{
this.ThumbnailImageField = value;
this.RaisePropertyChanged("ThumbnailImage");
}
}
}
[DataMemberAttribute()]
public int ZombieTypeKey
{
get { return this.ZombieTypeKeyField; }
set
{
if ((this.ZombieTypeKeyField.Equals(value) != true))
{
this.ZombieTypeKeyField = value;
this.RaisePropertyChanged("ZombieTypeKey");
}
}
}
[DataMemberAttribute()]
public string ZombieTypeName
{
get { return this.ZombieTypeNameField; }
set
{
if ((object.ReferenceEquals(this.ZombieTypeNameField, value) != true))
{
this.ZombieTypeNameField = value;
this.RaisePropertyChanged("ZombieTypeName");
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected void RaisePropertyChanged(string propertyName)
{
PropertyChangedEventHandler propertyChanged = this.PropertyChanged;
if ((propertyChanged != null))
{
propertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}

补充:性能与 PropertyChangedEventArgs

假设我们所操作的属性是可变的,那么创建 PropertyChangedEventArgs 的实例就将成为一个性能问题。单独一个实例创建的开销其实是非常小的,构造这些实例的字符串已经由外部传入对象,因此你只需为每个事件做一次内存分配就可以了。

问题就出在 “每个事件”上。如果有大量事件产生,你将会制造不必要的内存压力和更频繁的垃圾回收周期。并且如果事件引起了其它对象被分配,你就混杂地制造了很多短生命周期和长生命周期的对象。通常情况下这不是问题,但在对性能敏感的应用程序中它就可能成为问题了。因此,你需要像以下方法那样缓存事件参数对象:

复制代码
static readonly IReadOnlyDictionary s_EventArgs =
Helpers.BuildEventArgsDictionary(typeof(ZombieSighting));
void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
OnPropertyChanged(s_EventArgs[propertyName]);
}
public DateTimeOffset SightingDateTime
{
get { return m_SightingDateTime; }
set
{
if (m_SightingDateTime == value)
return;
m_SightingDateTime = value;
OnPropertyChanged();
}
}

令人惊讶的是,代理生成工具并不会自动创建事件参数的缓存。其实它甚至不需要在 Dictionary 中查找对象,只需像这样生成静态字段就可以了:

复制代码
static readonly PropertyChangedEventArgs s_SightingDateTime = new
PropertyChangedEventArgs("SightingDateTime");

验证,计算属性及类似代码

使用传统的代理服务时,往往倾向于通过复制和粘贴共享验证方法、计算属性及类似代码,这样很容易导致错误,尤其是在基础代码也在不断地进行修改时。可以通过 partial 类将它们放到独立的文件当中,并共享其中部分文件。这可以减少它的错误机率,但是这种方法仍然有一些局限性。

一个设计良好的代码生成器(比如 ADO.NET Entity Framework)会创建“XxxChanging” 和 “XxxChanged”等 partial 方法,允许开发者在属性的 setter 方法中注入附加的逻辑。遗憾的是代理生成工具并没有这么做,这迫使开发者不得不把属性更改的事件监听传入构造函数和 OnDeserialized 方法中。

另一个问题是客户端和服务端不能共享声明性的验证方法。由于所有的属性都是由代理生成工具创建的,没有地方可以加入适当的特性声明(attribute)。

集合

如同每一个 WCF 开发者会告诉你的一样,代理生成工具会完全忽视集合的类型。客户端虽然可以在数组、list 和 observable 集合中进行选择,但所有特定类型信息都会丢失。事实上,对 WCF 代理生成工具来说,所有的集合都可以暴露为 IList

不使用代理生成工具可以解决这个问题,但是也随之产生了一些新问题。尤其因为你不能对集合类使用 DataContract 特性,意味着集合不能有任何属性被序列化,这是一个相当不幸的设计决策,因为 SOAP 是基于 XML 的,而使用 XML 的特性和属性是非常适合于表达集合概念的。

如果你能够从集合的子项中推算出集合的所有属性,你就能够凭空生成它们。否则,你必须把这个类分离为普通类和集合类。

代码生成

在开发过程中,有许多可以避免的 bug 产生自代码生成工具本身。它要求代理被生成的时候服务端处于运行状态,而这一步骤是难以集成到通常的构建过程中的。开发者不得不选择手动进行更新,而这一任务经常被忽视。虽然它不大会在生产环境中产生问题,但会浪费开发者的时间去查找服务调用突然间不能正常工作的原因。

实现无代理的 WCF

由于基本的设计模式如此简单,简单到令人质疑代理生成工具存在的理由。(代理生成也并非全无用处,在调用非 WCF 的服务时还是需要它的)。如你所见,你只需创建一个 ClientBase 的子类,传递你打算实现的接口,并暴露 Channel 属性。建议加入构造函数,不过它是可选的。

复制代码
using System;
using System.ServiceModel;
using System.ServiceModel.Channels;
namespace Zombie.Services.Definitions
{
public class InformationClient : ClientBase
{
public new IInformationService Channel
{
get { return base.Channel; }
}
public InformationClient()
{
}
public InformationClient(string endpointConfigurationName) :
base(endpointConfigurationName)
{
}
public InformationClient(string endpointConfigurationName, string remoteAddress) :
base(endpointConfigurationName, remoteAddress)
{
}
public InformationClient(string endpointConfigurationName, EndpointAddress <br></br>remoteAddress) :
base(endpointConfigurationName, remoteAddress)
{
}
public InformationClient(Binding binding, EndpointAddress remoteAddress) :
base(binding, remoteAddress)
{
}
}
}

支持依赖注入

这个模式带来的一个好的副作用是,为了单元测试而让它支持依赖注入是很方便的。为此,我们首先需要一个接受这个服务接口的构造函数,然后重写或屏蔽由 ClientBase 暴露的某些方法。

复制代码
private IInformationService m_MockSerivce;
public InformationClient(IInformationService mockService)
: base(new BasicHttpBinding(), new EndpointAddress("http://fakeAddress.com"))
{
m_MockSerivce = mockService;
}
public new IInformationService Channel
{
get { return m_MockSerivce ?? base.Channel; }
}
protected override IInformationService CreateChannel()
{
return m_MockSerivce ?? base.CreateChannel();
}
public new void Open()
{
if (m_MockSerivce == null)
base.Open();
}

机敏的读者会注意到这并非最整洁的 API,并且遗留了某些缺陷。例如,一个 QA 开发者可以将其转换为基类,并直接调用真正的 Open 方法。只要这是大家都知道的一个局限性,就不大会出错。并且只要使用伪地址,它就不会有机会去实际连接到真实的服务器。

部分代码共享的选项

在.NET 服务端和.NET 或 WinRT 客户端共享代码的默认选项是共享程序集引用。但有时候你只想在服务端和客户端共享某个类的一部分,有两种方法可以实现:

选项 1 是使用关联文件,配合使用条件编译指令,它的优点是所有的生成代码都在一起,但结果可能相当混乱。

选项 2 也使用关联文件,但这次你将使用一个包含在多个文件中的 partial 类,其中一个文件将被共享,而其余文件仅包含用在客户端或服务端的代码。

考虑 Silverlight

这个模式可以使用在 Silverlight 中,但是还有些额外的考虑。首先,WCF 的 Silverlight 版本要求所有的服务方法用老式的 IAsyncResult 方式编写。

复制代码
[ServiceContract(Namespace = "https://zsr.codeplex.com/services/")]
public interface IInformationService
{
[OperationContractAttribute(AsyncPattern = true)]
IAsyncResult BeginListZombieTypes(AsyncCallback callback, object asyncState);
ZombieTypeSummaryCollection EndListZombieTypes(IAsyncResult result);
[OperationContractAttribute(AsyncPattern = true)]
IAsyncResult BeginGetZombieTypeDetails(int zombieTypeKey, AsyncCallback callback
, object asyncState);
ZombieTypeDetails EndGetZombieTypeDetails(IAsyncResult result);
[OperationContractAttribute(AsyncPattern = true)]
IAsyncResult BeginLogIncident(SessionToken session, ZombieSighting sighting,
AsyncCallback callback, object asyncState);
int EndLogIncident(IAsyncResult result);
}

为了使用新的 async/await 方式,你需要使用 FromAsync 函数将接口重新封装为 Task。

复制代码
public static class InformationService
{
public static Task ListZombieTypes(this IInformationService client)
{
return Task.Factory.FromAsync(client.BeginListZombieTypes(null, null),
client.EndListZombieTypes);
}
public static Task GetZombieTypeDetails(this IInformationService client,
int zombieTypeKey)
{
return Task.Factory.FromAsync(client.BeginGetZombieTypeDetails(zombieTypeKey,
null, null), client.EndGetZombieTypeDetails);
}
public static Task LogIncident(this IInformationService client, SessionToken
session, ZombieSighting sighting)
{
return Task.Factory.FromAsync(client.BeginLogIncident(session, sighting, null,
null), client.EndLogIncident);
}
}

关于“僵尸标准参考”项目

为了展示.NET 平台上和各种技术实现的不同,我们正在创建一个参考应用程序。这不仅仅是个传统的 hello world 应用,我们决定打造的是“僵尸标准参考”项目。这包含了一系列应用,如报告僵尸的目击情况,管理库存(例如对抗僵尸毒的疫苗),以及调查队派遣等等。这使得我们有机会观察一个真实世界中的应用程序的数据库、移动应用、定位修正及一些其它的常见的实用功能。

在每篇文章发表后,我们会持续更新 CodePlex 上的源代码。

关于作者

Jonathan Allen从 2006 年起就一直为 InfoQ 撰写新闻报道,现在他是.NET 栏目的首席编辑。如果你对为 InfoQ 编写新闻或技术指导方面的文章感兴趣,请用 email(jonathan@infoq.com)和他联系。

查看英文原文: Sharing Code in WCF without Code Generation


感谢杨赛对本文的审校。

给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢 迎大家通过新浪微博( @InfoQ )或者腾讯微博( @InfoQ )关注我们,并与我们的编辑和其他读者朋友交流。

2013 年 7 月 23 日 15:042793
用户头像

发布了 428 篇内容, 共 148.0 次阅读, 收获喜欢 20 次。

关注

评论

发布
暂无评论
发现更多内容

Android Development最佳实践

teoking

这16道Redis最常见面试问题,你能回答上来几个?

火羊哥

Java

机器学习基石第四节 学习笔记

半亩房顶

Machine Learning

JVM系列:通过一个例子分析JIT的汇编代码

简爱W

信创舆情一线--抖音、微信读书被判侵害用户个人信息权益

统小信uos

机器学习基石第三节 学习笔记

半亩房顶

Machine Learning

秒杀系统

俊俊哥

秒杀

我收集的 3 个企业经营“失败”案例

泰稳@极客邦科技

Go: 并发访问 Map — Part III

陈思敏捷

go golang 并发 map sync

Java七种排序算法以及实现

狸猫换太子

Java 排序算法 实现

【面试必问】Spring中的事务管理详解

只喝纯牛奶

格一格你的情欲念

王进行

【写作群星榜】7.24~7.31 写作平台优秀作者 & 文章排名

InfoQ写作平台

写作平台 排行榜

机器学习基石第二节 学习笔记

半亩房顶

Machine Learning

机器学习基石第五节 学习笔记

半亩房顶

Machine Learning

数据结构与算法之排序

shirley

排序算法

如何进行需求梳理及埋点方案设计

易观大数据

dubbo-go 中使用 sentinel

apache/dubbo-go

golang dubbo sentinel

JVM参数手册

Rayjun

JVM GC

ARTS打卡Week 09

teoking

一年多远程工作经验,说说真实的感受

盛安德软件

架构师训练营第九周学习总结

张明森

webRTC框架下的视频主动丢帧

fumingwang

音视频 WebRTC

机器学习基石第一节 学习笔记

半亩房顶

Machine Learning

零代码可视化开发平台iVX是什么?

代码制造者

编程语言 可视化 零代码 iVX

职场求生攻略答疑篇之 2 —— 无所适从的向上沟通

臧萌

别在网上乱找代码了,找了一段代码突然爆了!!!

导导

Java

新生必备清单:不想成为虚度青春的“小透明”,手机应该怎样选?

脑极体

30岁的二三事

大唐小生

总结 个人感悟

密码朋克的社会实验(三):比特币发明了什么

腾讯安全云鼎实验室

比特币 区块链 密码学

最牛逼的Java框架,没有之一

我是苞谷

不使用代码生成工具而共享WCF代码-InfoQ