程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> 關於.NET >> 不使用代碼生成工具而共享WCF代碼

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

編輯:關於.NET

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

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved