在傳統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)上的源代碼。