在傳統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<T>對象。
不使用代理生成工具的理由
不可變對象與數據契約
不可變對象較少出錯,這一點如今 已被廣泛認可了。除非調用數據契約類的代碼需要直接編輯某個屬性,否則該屬性就應該被標記為只讀,以避 免發生錯誤。
這裡是一個僅限於只讀顯示的類的示例。
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<T>。
不使用代理生成工具可以解決這個問題,但是 也隨之產生了一些新問題。尤其因為你不能對集合類使用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 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(http://zsr.codeplex.com)上的源代碼。