設計代理服務的最佳做法
請遵循針對 StreamJsonRpc RPC 介面所記載的一般
此外,下列指導方針適用於代理服務。
方法簽名
所有方法都應該採用 CancellationToken 參數作為其最後一個參數。 此參數通常應該 不 為選擇性參數,因此呼叫者不太可能不小心省略自變數。 即使方法的實作預期是簡單的,提供 CancellationToken 仍能讓客戶在傳送至伺服器之前取消其自己的請求。 它也允許伺服器的實作演變成更昂貴的方案,而不需要更新現有的方法,以便稍後將取消功能新增為選項。
請考慮 避免在 RPC 介面上 相同方法的多個多載。 雖然多載決議通常能夠運作(且應該撰寫測試來驗證其有效性),但它依賴 嘗試 根據每個多載的參數類型進行參數反序列化,因此在選擇多載時,通常會出現第一次擲回例外狀況。 由於我們想要將成功路徑中擲回的第一個機會例外狀況數目降到最低,因此最好只有一個具有指定名稱的方法。
參數和傳回型別
記住,透過 RPC 交換的所有參數和傳回值只是 資料。 它們全都是透過電線串行化和傳送的。 您在這些數據類型上定義的任何方法只會在數據的本機複本上運作,而且無法與產生數據的 RPC 服務進行通訊。 此串行化行為的唯一例外是 StreamJsonRpc 對 特殊類型提供特別支援。
請考慮使用 ValueTask<T>
而不是 Task<T>
作為方法的傳回類型,因為 ValueTask<T>
所需的配置較少。
使用非泛型品種時(例如,Task 和 ValueTask),它比較不重要,但 ValueTask 可能仍然比較好。
請留意 ValueTask<T>
的使用限制,如該 API 所述。 此 部落格文章 和 影片 也可以幫助你決定要使用哪種類型。
自訂數據類型
請考慮將所有數據型別都定義為不可變,這可讓您在不複製的情況下更安全地跨進程共享數據,並協助強化取用者的想法,讓他們無法在不放置另一個 RPC 的情況下變更他們收到的數據來回應查詢。
使用 ServiceJsonRpcDescriptor.Formatters.UTF8時,請將資料類型定義為 class
,而不是 struct
,以避免在使用 Newtonsoft.Json 時產生(可能重複的)封箱成本。
Boxing 不會 使用 ServiceJsonRpcDescriptor.Formatters.MessagePack 時發生,因此如果您認可到該格式器,結構可能是適合的選項。
請考慮在數據類型上實作 IEquatable<T> 和覆寫 GetHashCode() 和 Equals(Object) 方法,這可讓客戶端根據它是否等於另一次收到的數據,有效率地儲存、比較及重複使用收到的數據。
使用 DiscriminatedTypeJsonConverter<TBase> 支援使用 JSON 序列化多態類型。
收集
在 RPC 方法簽章中,使用唯讀集合介面(例如,IReadOnlyList<T>)而非具體型別(例如,List<T> 或 T[]
),這可以讓反序列化變得更高效。
避免 IEnumerable<T>。
其缺少 Count
屬性會導致程式碼效率不佳,並暗示數據可能延遲產生,這不適用於 RPC 情境。
請改用 IReadOnlyCollection<T> 來代表無序集合,或改用 IReadOnlyList<T> 來代表有序集合。
請考慮 IAsyncEnumerable<T>。 任何其他集合類型或 IEnumerable<T> 都會將整個集合在一則訊息中傳送。 使用 IAsyncEnumerable<T> 允許發送小型初始訊息,並提供接收者獲取從集合中取得所需數量的項目,以異步方式列舉項目。 深入瞭解這個新穎的模式。
觀察者模式
請考慮在介面中使用 觀察者設計模式。 這是用戶端訂閱數據的簡單方式,而不需要套用至下一節所述傳統事件模型的許多陷阱。
觀察者模式可能如下所示:
Task<IDisposable> SubscribeAsync(IObserver<YourDataType> observer);
上述使用的 IDisposable 和 IObserver<T> 類型是 StreamJsonRpc 中 特殊 類型的兩種,因此它們經過特殊封送處理,而不是僅作為純數據進行序列化。
事件
當透過 RPC 執行時,事件可能會由於多種原因而引發問題,因此我們建議使用上面描述的觀察者模式。
請記住,服務無法查看當服務與客戶端位於個別進程中時,用戶端所附加的事件處理程式數目。 JsonRpc 永遠只會附加唯一一個負責將事件傳播給客戶端的處理程式。 客戶端可能會有零個或多個處理程式連接在遠端。
大部分的 RPC 客戶端在首次連線時,尚未設定事件處理程式。 避免在客戶端於介面上調用「Subscribe*」方法表示有興趣和準備接受事件之前觸發第一個事件。
如果您的事件指出狀態變更(例如,加入集合的新項目),請考慮觸發所有過去的事件,或將目前所有數據描述為在事件引數中為新的,以便用戶端訂閱時,只需透過事件處理程式碼來協助「同步處理」。
如果用戶端可能想要對數據或通知的子集表示興趣,請考慮在上述的 “Subscribe*” 方法上接受額外的自變數,以減少轉送這些通知所需的網路流量和 CPU。
如果您也公開事件以接收變更通知,或主動勸阻用戶端與事件搭配使用,請考慮不提供傳回目前值的方法。 訂閱數據事件的用戶端,並呼叫 方法來取得目前值,以與該值的變更競爭,並遺漏變更事件,或不知道如何協調某個線程上的變更事件與在另一個線程上取得的值。 對於任何介面而言,這項考慮是一般問題,而不只是當它透過 RPC 時。
命名慣例
- 在 RPC 介面上使用
Service
後綴和簡單的I
前綴。 - 請勿針對 SDK 中的類別使用
Service
後綴。 您的連結庫或 RPC 包裝函式應該使用名稱來確切描述其用途,避免「服務」一詞。 - 請避免介面或成員名稱中的「遠端」一詞。 請記住,代理服務在本地和遠端場景中同樣適用。
版本相容性問題
我們希望任何向其他擴充功能或透過 Live Share 公開的指定仲介服務能向前和向後相容,這表示我們應假設用戶端可能比服務更舊或更新,而且其功能性應大致相當於兩個適用版本中的較低版本。
首先,讓我們來檢視重大變更的相關用詞:
二進位中斷性變更:API 變更會導致針對舊版組件編譯的其他受管理程式代碼在運行時間無法系結至新的組件。 範例包括:
- 變更現有公用成員的簽章。
- 重新命名公用成員。
- 移除公開類型。
- 將抽象成員加入至類型,或任何成員加入至介面。
但下列 不會 二進位重大變更:
- 將非抽象成員加入至類別或結構。
- 將完整的(非抽象)介面實作新增至現有的類型。
協定破壞性變更:更改某些數據類型或 RPC 方法呼叫的序列化形式,使遠端端點無法正確反序列化和處理該數據。 範例包括:
- 將必要的參數新增至 RPC 方法。
- 從先前保證為非 Null 的數據類型中移除成員。
- 新增必須在其他既有作業之前放置方法呼叫的需求。
- 在控制該成員中數據串行化名稱的欄位或屬性上加入、移除或變更屬性。
- (MessagePack):變更現有成員的 DataMemberAttribute.Order 屬性或
KeyAttribute
整數。
但下列 不會 通訊協定中斷性變更:
- 將選擇性成員加入至數據類型。
- 將成員新增至 RPC 介面。
- 將選擇性參數新增至現有的方法。
- 將代表整數或浮點數的參數類型變更為長度或精確度較大的參數類型(例如,
int
為long
或float
double
)。 - 重新命名參數。 在技術上,這會中斷使用 JSON-RPC 具名自變數的用戶端,但使用 ServiceJsonRpcDescriptor 的客戶端預設會使用位置自變數,而且不會受到參數名稱變更的影響。 這與用戶端 原始程式碼 是否使用具名自變數語法無關,參數重新命名將是 中斷來源 變更。
行為中斷變更:對代理服務的實作進行變更或新增行為,導致較舊的客戶端可能發生故障。 範例包括:
- 不再初始化過去總是被初始化的數據類型的某個成員。
- 在一種先前能順利完成的情況下拋出例外狀況。
- 傳回錯誤碼與先前傳回的錯誤碼不同。
但下列 不屬於 行為上有重大影響的變更:
- 拋出新的例外狀況類型(因為所有例外狀況都包裝在 RemoteInvocationException 中)。
需要重大變更時,可以通過註冊並提供新的服務標識來安全地進行變更。 此 Moniker 可以共用相同的名稱,但版本號碼較高。 如果沒有二進位中斷變更,原始 RPC 介面 可能會 可重複使用。 否則,請為新的服務版本定義新的介面。 為避免影響舊用戶,請繼續註冊、提供及支援舊版本。
除了將成員新增至 RPC 介面之外,我們想要避免所有這類重大變更。
將成員新增至 RPC 介面
請勿 不要 將成員新增至 RPC 用戶端回呼介面 ,因為許多用戶端可能會實作該介面,而且新增成員會導致載入這些類型但未實作新介面成員時擲回 CLR 擲回 TypeLoadException。 如果必須在 RPC 用戶端回呼目標上新增需要的成員,請定義一個新的介面(可能是從原始介面衍生而來),然後按照標準流程以遞增版本號來提供代理服務,並提供一個描述元,其指定了更新後的用戶端介面類型。
您 可以 將成員新增至定義代理服務的 RPC 介面。 這不是會破壞協定的變更,而只是對實作該服務的人的二進位相容性破壞變更,但您可能也會更新服務以實作新成員。 由於 我們的指引 是,除了代理服務本身之外,任何人都不應該實作 RPC 介面(而測試應該使用模擬架構),因此將成員新增至 RPC 介面不應中斷任何人。
這些新成員應該有 xml 檔批注,可識別哪一個服務版本會先新增該成員。 如果較新的用戶端在未實作 方法的較舊服務上呼叫 方法,該用戶端就可以攔截 RemoteMethodNotFoundException。 但該用戶端可以(且可能應該)預測失敗,並避免第一次呼叫。 將成員新增至現有服務的最佳做法包括:
- 如果這是您的服務版本發行中的第一個變更:在您新增成員並宣告新的描述元時,請提升服務標籤的次要版本號。
- 除了 舊版本之外,請更新您的服務以註冊及提供新版本
。 - 如果您有代理服務的客戶端,請更新客戶端以請求較新版號,若較新版號返回為 null,則改為請求舊版號。