RAML用户应遵循的C#与Web API代码生成模式
在過去幾年間,REST規范的各種語言正在逐漸流行起來,例如RAML、Swagger以及API Blueprint。但這些語言的主要范疇在于客戶端工具,主要用于生成JavaScript或TypeScript文件、模擬對象(mock),以及對應的客戶端單元測試。
與此同時,傳統的.NET后端開發者往往擁有C#與SQL方面的經驗,而對于如何暴露REST服務的各種細節缺乏興趣。他們更樂于通過在controller的方法中添加一些路由特性(attribute)的方式完成任務,而在數據存儲與服務端之間的通信方面發揮他們的核心競爭力。
這種方式通常會造成出現不計其數的信息傳達錯誤,雖然這種錯誤并不太嚴重。一旦UI開發者與服務端開發者對于如何暴露某個REST終結點產生了分歧,就必須有人去更新他的代碼。通常來說,這種更新只是一個較小的變更,但如果不斷重復這一過程,則會造成開發者生產力的極大下降。
為了克服這一問題,UI開發者可以尋求規規范語言的幫助,例如RAML,以生成他們所需的Web API代碼。而服務的開發者可專注于如何連接這些代碼,而不是為路由特性和HTTP謂詞生成各種模擬對象。
本文并不打算討論如何使用RAML,而是強調RAML,或你所選擇的規范語言需要為你生成怎樣的代碼。
C#代碼生成的概念
C# 2.0在設計時就考慮到了代碼生成的問題。如今代碼生成器的使用已經變得非常普遍,甚至包括Visual Studio本身。代碼生成器可創建部分類(partial class)。一個部分類中包括組成整個類所需的部分代碼,但未必是全部的代碼。這就允許你將類的定義分散在多個文件中,其中部分代碼是自動生成的,而另一部分則是手寫的。這種分離性能夠防止代碼生成器刪除開發者手寫的代碼。
不幸的是,這種方式還不完善。部分類允許你添加新的方法,但不能夠修改現有方法的行為。因為這一點,我們不得不等待2008年所發布的C# 3,其中引入了部分方法的概念。
從表面上看,部分方法與抽象方法非常相似,但這種比喻是錯誤的。抽象方法必須在某處實現,否則會使代碼無法編譯。而部分方法更類似于C++中的空的宏,如果未實現某個部分方法,則編譯器會直接取消對該方法的調用,就像這行調用代碼從不存在一樣。
部分方法的使用有一些嚴格的需求。由于編譯器可能會取消該方法,因此不可返回任何類型,也不可以使用任何“out”參數。不過,你可以在部分方法中使用“ref”參數以返回某個值。由于在使用ref參數時必須在調用該部分方法之前為其賦值,那么即使該部分方法被刪除,編譯器仍然能夠滿足明確賦值的規則。
由于以上限制的存在,我們的實現將很大程度上依賴于ref參數。
Controller的模式
所有的REST終結點都必須包含在某個controller類中,以下代碼是一個簡單的示例:
[GeneratedCode("My Tool", "1.0.0.0")] [RoutePrefix("api/customer")] public partial class CustomerController : ApiController {//methods go here }以這種方式創建的Web API controller通常包含兩種特性,通過中括號表示。GeneratedCode表示這個類是由某個工具生成的,因此開發者不應直接修改它。RoutePrefix這個可選的特性將用于基于特性的路由,我們稍后將展開討論。
同步方法的模式
以下代碼展示了一種可用于你的Web API代碼生成的基本模式。
[Route("search")] [HttpGet] public List<Customer> QueryCustomers(int zipCode, string lastName = "") {Tuple<List<Customer>> result = null;QueryCustomers(zipCode, lastName, ref result);if (result == null)throw new NotImplementedException("RAML defined method wasn’t implemented");elsereturn result.Item1; } partial void QueryCustomers(int zipCode, string lastName, ref Tuple<List<Customer>> result);請注意一點,該部分方法將返回結果封裝為一個元組(Tuple)對象,這樣就使你能夠區分“該方法未實現”以及“該方法返回null”中null的不同意義。
我們之后將始終遵循這一模式以確保代碼可編譯。雖然對某個由RAML定義的REST方法進行變更可能會破壞構建,但如果僅僅是添加一個新方法,則不應當產生破壞性的后果。
每個REST方法應當至少包含兩個特性。route特性用于確定URL的形式,如果該類具有一個RoutePrefix特性,則方法中的route特性將附加在RoutePrefix特性之后。你可以通過Mike Wasson所撰寫的文章“Attribute Routing in ASP.NET Web API 2”學習這一主題的更多內容。
另一個特性則表現了該方法對應的謂詞。從技術上說,謂詞是可以從方法的名稱中推斷出來的,但一個明確的特性可使代碼的閱讀者更方便地快速理解其內容。而且由于這部分代碼是自動生成的,因此不會造成額外的冗長感。
你可能還需要代碼生成器為方法添加Authorize特性,表示該方法只能夠由已登錄的用戶進行訪問。
而無返回類型的REST方法看起來稍有一些不同之處:
[Route("update")] [HttpPost] public void UpdateCustomer(Customer customer) {bool wasExecuted = false;UpdateCustomer(customer, ref wasExecuted);if (!wasExecuted)throw new NotImplementedException("RAML defined method wasn’t implemented"); } partial void UpdateCustomer(Customer customer, ref bool wasExecuted);在這一模式中,方法的實現者需要將wasExecuted改為true。
異步方法的模式
現如今,只要任何一個方法提供了異步的調用方式,那么同步的調用方式往往是受人鄙視的。雖然在延遲性方面表現得稍慢一些,但異步代碼對于負載很大的服務器來說能夠提供更好的吞吐性,從而提升整體的用戶體驗。
[Route("search")] [HttpGet] public async Task<List<Customer>> QueryCustomersAsync(intzipCode, string lastName = "") {Task<List<Customer>> resultTask = null;QueryCustomersAsync(zipCode, lastName, ref resultTask);if (resultTask == null)throw new NotImplementedException("RAML defined method wasn’t implemented");elsereturn await resultTask; } partial void QueryCustomersAsync(int zipCode, string lastName, ref Task<List<Customer>> resultTask);你需要注意的第一個不同之處在于返回類型被封裝在一個Task中,這允許框架以異步方式等待該方法的完成。
為了獲取Task對象其中的實際內容,你需要使用“await”關鍵字。即使該方法未返回任何值,該關鍵字也允許你等待該Task的完成。在異步上下文中絕對不要直接讀取Task.Result中的內容,因為這可能會造成死鎖。
而對于不返回值的REST方法,其模式也稍有不同。
[Route("update")] [HttpPost] public async Task UpdateCustomerAsync(Customer customer) {Task resultTask = null;UpdateCustomerAsync(customer, ref resultTask);if (resultTask == null)throw new NotImplementedException("RAML defined method wasn’t implemented");elseawait resultTask; } partial void UpdateCustomerAsync(Customer customer, ref Task resultTask);支持客戶端斷開連接的情況
如果用戶撤消了某個請求,或是斷開了連接,那么取消一個運行時間較長的操作是很有益處的。為了在異步代碼中實現這一點,你需要通過ClientDisconnectedToken偵聽撤消操作。以下代碼展示了一個使用該對象的示例:
[Route("search")] [HttpGet] public async Task<List<Customer>> QueryCustomersCancellableAsync(int zipCode, string lastName = "", CancellationToken cancellationToken = default(CancellationToken)) {Task<List<Customer>> resultTask = null;QueryCustomersCancellableAsync(zipCode, lastName, cancellationToken, ref resultTask);if (resultTask == null)throw new NotImplementedException("RAML defined method wasn’t implemented");elsereturn await resultTask; } partial void QueryCustomersCancellableAsync(int zipCode, string lastName, CancellationToken cancellationToken, ref Task<List<Customer>> resultTask);注意,cancellationtoken在REST方法中被標記為可選的,即使框架會確保為其提供一個值,因此該參數可出現在任意可選參數之后的位置上。
Controller基類
Web API controller的標準基類是ApiController,但服務的開發者可能會選擇覆蓋這個基類,為了能夠進行覆蓋,所生成的代碼必須忽略這個基類。這就需要服務的開發者必須指定一個基類,否則該API就將變得不可見。
作為一個臨時方案,代碼生成器可以指定一個由服務開發者所命名的自定義基類,該基類需要繼承自ApiController,并包含任何共享的功能。
Model
當需要使用一些復雜對象時,代碼生成器將試圖生成這些類。雖然你可以使用一些純粹的對象,但更好的方式是為其添加DataContract和DataMember這些特性的標注。這將允許服務開發者為其添加一些不需要暴露給客戶端的額外屬性(property),只要不將某個屬性標注為DataMember即可。
[DataContract] public partial class Customer {[DataMember]public int CustomerKey { get; set; }[DataMember]public string CustomerName { get; set; } }Model的校驗
為了對model進行校驗,可以對需要檢查的屬性添加適當的特性。常見的校驗特性包括Required、MaxLength、MinLength、Phone以及EmailAddress。在DataAnnotations命名空間中包含了內置的校驗特性的列表。
一旦定義了model校驗邏輯之后,你還需要強制實施他們,可以在REST方法的開頭添加這兩行代碼以實現該操作。
if (!ModelState.IsValid)throw new HttpResponseException(HttpStatusCode.BadRequest);進階應用
一旦你實現了基本的代碼生成器之后,可以進一步探索可移除樣板代碼的場合。舉例來說,你可以在生成的代碼中加入對username的解析操作,將結果傳遞至部分方法中。比方說:
[Route("search")] [HttpGet] public List<Customer> QueryCustomers(int zipCode, string lastName = "") {var user = [application specific logic]Tuple<List<Customer>> result = null;QueryCustomers(zipCode, lastName, ref result, user);if (result == null)throw new NotImplementedException();elsereturn result.Item1; } partial void QueryCustomers(int zipCode, string lastName, ref Tuple<List<Customer>> result, User user);另一種移除樣板代碼的方式是使用一個數據上下文或其他資源,并傳遞給部分方法。你也可以選擇通過日志記錄的條目捕獲請求與響應的信息。基本上,只要是公式化的或是重復性的操作,都可以按照這種方式進行簡化。
付諸實踐
RAML與C#的代碼生成功能結合使用可極大地減少前端與后端開發團隊之間的摩擦。實現這種方式的秘密取決于一個可靠的設計步驟,即JavaScript與C#的開發工作在開始具體編碼之前需要對RAML達成一致。實現了這一點之后,當開發流程中出現各種不可避免的變更時,雙方應當能夠心平氣和地接受對共享的RAML進行更改。
如果你希望發布與RAML、代碼生成、或是其他.NET方面的文章,歡迎你通過jonathan@infoq.com聯系Jonathan Allen。如果你正在尋找某種將RAML轉換至C#的代碼生成器,可以參考一下Mulesoft Lab發布的RAML Tools for .NET。
關于作者
Jonathan Allen的第一份工作是在上世紀90年代后期為某個診所開發的MIS項目,該項目從早期的Access與Excel逐漸發展為一個企業級解決方案。之后,他又為財政部門編寫自動交易系統達五年之久,在那之后,他決定轉而進行高端用戶界面的開發工作。在空余時間,他的興趣是學習15世紀至17世紀的西方武術史并撰寫相關文章。
原文地址:http://www.infoq.com/cn/articles/webapr-for-raml
.NET社區新聞,深度好文,微信中搜索dotNET跨平臺或掃描二維碼關注
總結
以上是生活随笔為你收集整理的RAML用户应遵循的C#与Web API代码生成模式的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: asp.net MVC 应用程序的生命周
- 下一篇: 教你实践ASP.NET Core Aut