




版權說明:本文檔由用戶提供并上傳,收益歸屬內(nèi)容提供方,若內(nèi)容存在侵權,請進行舉報或認領
文檔簡介
1、寫給新手的WebAPI實踐此篇是寫給新手的Demo,用于參考和借鑒,用于發(fā)散思路。老鳥可以忽略了。自己經(jīng)常有這種情況,遇到一個新東西或難題,在了解和解決之前總是說“等搞定了一定要寫篇文章記錄下來”,但是當掌握了之后,就感覺好簡單呀不值得寫下來了。其實這篇也一樣,決定寫下來是想在春節(jié)前最后再干一件正經(jīng)事兒,不能天天回去打Dota了!目錄:請求響應的設計請求的Content-Type和模型綁定自定義ApiResult和ApiControllerBase權限驗證模型生成文檔生成 一、請求響應的設計 RESTFul風格響亮很久了,但是我沒用過,以后也不打算用。當系統(tǒng)稍微復雜時,為了符合RESTFul要
2、吃力地創(chuàng)建一些不直觀的名詞,這不是我的風格。所以此文設計的不是RESTFul風格,是只最常用的POST和GET請求。請求部分就是調(diào)用API的參數(shù),抽象出一個接口如下: public interface IRequest ResultObject Validate(); 這里面只定義了一個Validate()方法,用于驗證請求參數(shù)的有效性,返回值是響應里的東西,下面會講到。對于請求對象,傳遞到業(yè)務邏輯層,甚至是數(shù)據(jù)訪問層都可以,因為它本身就是用來傳輸數(shù)據(jù)的,俗話叫DTO(Data Transfer Object),不過定義多層傳輸對象,然后復制來復制去也是可以的。但是有時候業(yè)務處理會需要當前登錄
3、人的信息,而這個信息我并不希望直接從接口層向下傳遞,所以這里我再抽象一個UserRequestBase,用于附加登錄人相關信息:復制代碼 public abstract class UserRequestBase : IRequest public int ApiUserID get; set; public string ApiUserName get; set; / .可以添加其他要專遞的登錄用戶相關的信息 public abstract ResultObject Validate(); 復制代碼ApiUserID和ApiUserName這樣的字段是不需要客戶端傳遞的,我們會根據(jù)登錄人信息
4、自動填充。根據(jù)實際中的經(jīng)驗,我們往往會做分頁查詢,會用到頁碼和每頁條數(shù),所為我們再定義個PageRequestBase: public abstract class PageRequestBase : UserRequestBase public int PageIndex get; set; public int PageSize get; set; 因為.net只能繼承單個父類,而且有些分頁查詢可能需要用戶信息,所以我們選擇繼承UserRequestBase。當然,還可以根據(jù)自己的實際情況抽象出更多的公用類,在這不一一枚舉。 響應的設計分為兩部分,第一個是實際響應部分,第二個會把響應包裝一
5、下,加上code和msg,用于表示調(diào)用狀態(tài)和錯誤信息(好老的方法了,大家都懂)。響應接口IResponse里什么也沒有,就是一個標記接口,不過我們也可以抽象出來兩個常用的公用類用于響應列表和分頁數(shù)據(jù):復制代碼 public class ListResponseBase<T> : IResponse public List<T> List get; set; public class PageResponseBase<T>: ListResponseBase<T> / <summary> / 頁碼數(shù) / </summary>
6、 public int PageIndex get; set; / <summary> / 總條數(shù) / </summary> public long TotalCount get; set; / <summary> / 每頁條數(shù) / </summary> public int PageSize get; set; / <summary> / 總頁數(shù) / </summary> public long PageCount get; set; 復制代碼 包裝響應的時候,有兩種情況,第一種是操作類接口,比如添加商品,這些接口是不用
7、響應對象的,只要返回是否成功就行了,第二種查詢類,這個時候必須要返回一些具體的東西了,所以響應包裝設計成兩個類:復制代碼public class ResultObject / <summary> / 等于0表示成功 / </summary> public int Code get; set; / <summary> / code不為0時,返回錯誤消息 / </summary> public string Msg get; set; public class ResultObject<TResponse> : ResultObject
8、where TResponse : IResponse public ResultObject() public ResultObject(TResponse data) Data = data; / <summary> / 返回的數(shù)據(jù) / </summary> public TResponse Data get; set; 復制代碼IRequest接口的Validate()方法返回值就是第一個ResultObject,當請求參數(shù)驗證不通過的時候,肯定是沒有數(shù)據(jù)返回了。再業(yè)務邏輯層,我選擇以包裝類作為返回類型,因為有很多錯誤都會在業(yè)務邏輯層出現(xiàn),我們的接口是需要這些錯誤
9、信息的。 二、請求的Content-Type和模型綁定 現(xiàn)在前后端分離大行其道,我們做后端的通常會返回JSON格式給前端,響應的Content-Type為application/json,前端通過一些框架可以直接作為js對象使用。但是前端請求后端的時候還有很多是以form表單形式,也就是請求的Content-Type為:application/x-www-form-urlencoded,請求體為id=23&name=loogn這樣的字符串,如果數(shù)據(jù)格式復雜了,前端不好傳,后端解析起來也麻煩。還有的直接用一個固定參數(shù)傳遞json字符串,比如json=id:23,name:'loo
10、gn',后端用formjson取出來后再反序列化。這些方法都可以,但是不夠好,最好的方法是前端也直接傳json,幸好現(xiàn)在很多web服務器都是支持請求的Content-Type為application/json的,這個時候請求的參數(shù)會以有效負荷(Payload)的形式傳遞過去,比如用jQuery的ajax來請求:復制代碼 $.ajax( type: "POST", url: "/product/editProduct", contentType: "application/json; charset=utf-8", data:
11、JSON.stringify(id:1,name:"name1"), success: function (result) console.log(result); )復制代碼 除了contentType,還要注意使用了JSON.stringify把對象轉(zhuǎn)換成了字符串。其實ajax使用的XmlHttpRequest對象只能處理字符串(json字符串呀,xml字符串呀,text純文本呀,base64呀)。這些數(shù)據(jù)到了后端之后,從請求流里讀出來就是json形式的字符串了,可直接反序列化成后端對象。然而這些考慮,.net mvc框架已經(jīng)幫我們做好了,這都要歸功于DefaultMo
12、delBinder。關于Form表單形式的請求,可以參見這位園友的文章:你從未知道如此強大的ASP.NET MVC DefaultModelBinder我這里想說的是,DefaultModelBinder足夠智能,并不需要我們自己做什么,它會根據(jù)請求的contentType的不同,用不同的方式解析請求,然后綁定到對象,遇到contentType為application/json是,就直接反序列化得到對象,遇到application/x-www-form-urlencoded就用form表單的形式綁定對象,唯一要注意的就是前端同學,不要把請求的contentType和請求的實際內(nèi)容搞錯就行了。你
13、告訴我你送過來一只貓,而實際上是一只狗,我以對待貓的方式對待狗當然就有被咬一口的危險了(肯定會報錯)。 三、自定義ApiResult和ApiControllerBase因為我不需要RESTFul風格,也不需要根據(jù)客戶端的意愿返回json或xml,所以我選擇AsyncController作為控制器的基類。AsyncController是直接繼承Controller的,而且支持異步處理,具體Controller和ApiController的區(qū)別,想了解的同學可以看這篇文章difference-between-apicontroller-and-controller-in-asp-net-mvc ,
14、或者直接閱讀源碼。Controller里的Action需要返回一個ActionResult對象,結合上面的響應包裝對象ResultObject,我決定自定義一個ApiResult作為Action的返回值,同時在這里處理jsonp調(diào)用、跨域調(diào)用、序列化的小駝峰命名和時間格式問題。復制代碼 / <summary> / api返回結果,控制jsonp、跨域、小駝峰命名和時間格式問題 / </summary> public class ApiResult : ActionResult / <summary> / 返回數(shù)據(jù) / </summary> pub
15、lic ResultObject ResultData get; set; / <summary> / 返回數(shù)據(jù)編碼,默認utf8 / </summary> public Encoding ContentEncoding get; set; / <summary> / 是否接受Get請求,默認允許 / </summary> public JsonRequestBehavior JsonRequestBehavior get; set; / <summary> / 是否允許跨域請求 / </summary> public b
16、ool AllowCrossDomain get; set; / <summary> / jsonp回調(diào)參數(shù)名 / </summary> public string JsonpCallbackName = "callback" public ApiResult() : this(null) public ApiResult(ResultObject resultData) this.ResultData = resultData; ContentEncoding = Encoding.UTF8; JsonRequestBehavior = JsonR
17、equestBehavior.AllowGet; AllowCrossDomain = true; public override void ExecuteResult(ControllerContext context) var response = context.HttpContext.Response; var request = context.HttpContext.Request; response.ContentEncoding = ContentEncoding; response.ContentType = "text/plain" if (Result
18、Data != null) string buffer; if (JsonRequestBehavior = JsonRequestBehavior.DenyGet) && string.Equals(context.HttpContext.Request.HttpMethod, "GET") buffer = "該接口不允許Get請求" else var jsonpCallback = requestJsonpCallbackName; if (string.IsNullOrWhiteSpace(jsonpCallback) /如果可以
19、跨域,寫入響應頭 if (AllowCrossDomain) WriteAllowAccessOrigin(context); response.ContentType = "application/json" buffer = JsonConvert.SerializeObject(ResultData, JsonSetting.Settings); else /jsonp if (AllowCrossDomain) /這個判斷可能非必須 response.ContentType = "text/javascript" buffer = string.
20、Format("0(1);", jsonpCallback, JsonConvert.SerializeObject(ResultData, JsonSetting.Settings); else buffer = "該接口不允許跨域請求" try response.Write(buffer); catch (Exception exp) response.Write(exp.Message); else response.Write("ApiResult.Data為null"); response.End(); / <summ
21、ary> / 寫入跨域請求頭 / </summary> / <param name="context"></param> private void WriteAllowAccessOrigin(ControllerContext context) var origin = context.HttpContext.Request.Headers"Origin" if (true) /可以維護一個允許跨域的域名集合,類判斷是否可以跨域 context.HttpContext.Response.Headers.Add(
22、"Access-Control-Allow-Origin", origin ? "*"); 復制代碼里面都是一些常規(guī)的邏輯,不做說明了,其中的JsonSetting就是設置序列化的小駝峰和日期格式的:復制代碼 public class JsonSetting public static JsonSerializerSettings Settings = new JsonSerializerSettings ContractResolver = new CamelCasePropertyNamesContractResolver(), DateFormat
23、String = "yyyy-MM-dd HH:mm:ss", ; 復制代碼這個時候有個問題,如果一個時間的字段需要"yyyy-MM-dd"這種格式怎么辦呢?這個時候要定義一個JsonConverter的子類,來實現(xiàn)自定義日期格式:復制代碼 / <summary> / 日期格式化器 / </summary> public class CustomDateConverter : DateTimeConverterBase private IsoDateTimeConverter dtConverter = new IsoDateTi
24、meConverter ; public CustomDateConverter(string format) dtConverter.DateTimeFormat = format; public CustomDateConverter() : this("yyyy-MM-dd") public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) return dtConverter.ReadJson(re
25、ader, objectType, existingValue, serializer); public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) dtConverter.WriteJson(writer, value, serializer); 復制代碼在需要的響應屬性上加上 JsonConverter(typeof(CustomDateConverter) 或 JsonConverter(typeof(CustomDateConverter),"yyyy年
26、MM月dd日") 即可。ApiResult定義好了,再定義一個控制器基類,目的是便于處理ApiResult:復制代碼 / <summary> / API控制器基類 / </summary> public class ApiControllerBase : AsyncController public ApiResult Api<TRequest>(TRequest request, Func<TRequest, ResultObject> handle) try var requestBase = request as IRequest
27、; if (requestBase != null) /處理需要登錄用戶的請求 var userRequest = request as UserRequestBase; if (userRequest != null) var loginUser = LoginUser.GetUser(); if (loginUser != null) userRequest.ApiUserID = loginUser.UserID; userRequest.ApiUserName = loginUser.UserName; var validResult = requestBase.Validate();
28、 if (validResult != null) return new ApiResult(validResult); var result = handle(request); /處理請求 return new ApiResult(result); catch (Exception exp) /異常日志: return new ApiResult ResultData = new ResultObject Code = 1, Msg = "系統(tǒng)異常:" + exp.Message ; public ApiResult Api(Func<ResultObject&g
29、t; handle) try var result = handle();/處理請求 return new ApiResult(result); catch (Exception exp) /異常日志 return new ApiResult ResultData = new ResultObject Code = 1, Msg = "系統(tǒng)異常:" + exp.Message ; / <summary> / 異步api / </summary> / <typeparam name="TRequest"></typ
30、eparam> / <param name="request"></param> / <param name="handle"></param> / <returns></returns> public Task<ApiResult> ApiAsync<TRequest, TResponse>(TRequest request, Func<TRequest, Task<TResponse>> handle) where TResp
31、onse : ResultObject return handle(request).ContinueWith(x => return Api() => x.Result); ); 復制代碼最常用的應該就是第一個Api<TRequest>方法,里面處理了請求參數(shù)的驗證,把用戶信息賦給需要的請求對象,異常記錄等。第二個方法是對沒有請求參數(shù)的api調(diào)用處理。第三個方法是異步處理,可以對異步IO處理做一些優(yōu)化,比如你提供的這個接口是調(diào)用的另一個網(wǎng)絡接口的情況。 四、權限驗證 關于這個問題,我在一篇文章中貼了一些代碼,其實只要是知道怎么回事之后,自己可以想怎么玩就怎么玩了,下面
32、講的的沒有涉及角色的權限。根據(jù)以往經(jīng)驗,我們可以把資源(也就是一個接口)的權限分為三個等級(標紅的第二點很重要,會大大簡化后臺權限管理的工作):1,公開和訪問2,登錄用戶可訪問3,有權限的登錄用戶可訪問所以我們?nèi)绱嗽O計驗證的過濾器:復制代碼 public class AuthFilterAttribute : ActionFilterAttribute / <summary> / 匿名可訪問 / </summary> public bool AllowAnonymous get; set; / <summary> / 登錄用戶就可以訪問 / </sum
33、mary> public bool OnlyLogin get; set; / <summary> / 使用的資源權限名,比如多個接口可以使用同一個資源的權限,默認是/ControllerName/ActionName / </summary> public string PowerName get; set; public sealed override void OnActionExecuting(ActionExecutingContext filterContext) /跨域時,客戶端會用OPTIONS請求來探測服務器 if (filterContext.
34、HttpContext.Request.HttpMethod = "OPTIONS") var origin = filterContext.HttpContext.Request.Headers"Origin" if (true) /可以維護一個允許跨域的域名集合,類判斷是否可以跨域 filterContext.HttpContext.Response.Headers.Add("Access-Control-Allow-Origin", origin ? "*"); filterContext.Result =
35、new EmptyResult(); return; if (AllowAnonymous) return; var user = LoginUser.GetUser(); if (user = null) filterContext.Result = new ApiResult ResultData = new ResultObject Code = -1, Msg = "未登錄" , JsonRequestBehavior = JsonRequestBehavior.AllowGet ; return; if (OnlyLogin) return; var url =
36、PowerName; if (string.IsNullOrEmpty(url) url = "/" + filterContext.ActionDescriptor.ControllerDescriptor.ControllerName + "/" + filterContext.ActionDescriptor.ActionName; var hasPower = true; /可以根據(jù) user和url等信息判斷是否有權限 if (!hasPower) filterContext.Result = new ApiResult ResultData
37、= new ResultObject Code = -2, Msg = "無權限" , JsonRequestBehavior = JsonRequestBehavior.AllowGet ; 復制代碼AllowAnonymous屬性和OnlyLogin屬性的功能已經(jīng)說過了,匿名訪問就是公開的,一個系統(tǒng)總會需要這樣的接口,登錄可訪問一般針對安全性比較低,比如字典數(shù)據(jù)的獲取,只要登錄了,就可以訪問,在權限管理里也不用配置了。PowerName的屬性是出于什么考慮呢?有些時候,兩個接口的權限級別是綁定在一起的,比如一個商品的添加和修改接口,可以設置成同一個資源權限,所以都可以設
38、置成/product/edit,這樣我們在權限管理里,只要維護/product/edit,而不需要分別維護/product/add和/product/update了(例子可能不太恰當,因為很多時候添加和修改本來就是一個接口,但是這個情況的確存在,設置PowerName也是為了簡化后臺的權限管理)。對于跨域的情況,上面代碼也有注釋,客戶端會用OPTIONS動作來探測服務器,除了上述代碼,在web.config也需要配置一下:復制代碼 <system.webServer> <httpProtocol> <customHeaders> <!-<add
39、name="Access-Control-Allow-Origin" value="*" />-> <add name="Access-Control-Allow-Headers" value="Origin, X-Requested-With, Content-Type, Accept,apiToken" /> </customHeaders> </httpProtocol> </system.webServer>復制代碼配置中注釋掉的一行,我故意留著,
40、就是因為要和代碼里有個對應的地方,在配置中只能配置為“*” 和特定域名,我們要更靈活,所以在程序里控制,可以允許一個域名列表。 LoginUser的邏輯和上面的連接里的代碼差不多,不再貼了,下載里也有,apiToken從cookie和http頭部都可以取得,這樣不管是同域名網(wǎng)頁,跨域,app都是可以調(diào)用接口的。 五、模型生成以前的模型生產(chǎn)器很多,現(xiàn)在使用T4模板的也不少,而且VS里自帶T4模板。但是我不太喜歡用T4(主要是沒有智能提示)。我感覺Razor引擎就挺好呀,完全可以用來生成模型。自己寫的一個ORM新加了兩個方法,來獲取數(shù)據(jù)庫表的元數(shù)據(jù),目前支持MSSql和MySql,稍微寫點代碼就可
41、以生成模型了,下面是cshtml的內(nèi)容,截圖是為了展示代碼高亮效果,哈哈(完整代碼在最下方有下載)所以有時候,自己動動手還是挺好的。其實所有web語言都可以生成,jsp,php,nodejs,和動態(tài)生成頁面返回給客戶端是一樣的,這個只不過是寫到文件里。 六、文檔生成這里自然說的是API文檔,和上面那個生成模型不太一樣,雖說生成基本上都是:模板+數(shù)據(jù)=結果,但是這個生成在獲取數(shù)據(jù)的時候有點難點,先看效果圖:api文檔自動生成的重要性想必大家都知道了,如果還是手動寫word或excel,工作量大不說,是很難保持一致性的。 1. webapi 自帶一個Help Page 有興趣可以了解。 2. Sw
42、agger 是個生成api的框架,很強大,也支持接口測試,但是.net下的swagger好像只能使用在webapi中,一般的mvc不行,有興趣的也可以了解。下面主要說一下本輪子的實現(xiàn)。從一個類型得到一個該類型的對象圖,在不嚴謹?shù)那闆r下,還是比容易實現(xiàn)的,主要用反射和遞歸就可以了。上面截圖中的C#類:復制代碼public class GetProductRequest : IRequest / <summary> / 商品編號 / </summary> public int? ProductID get; set; public ResultObject Validate
43、() if (ProductID = null | ProductID.Value <= 0) return new ResultObject Code = 1, Msg = "商品編號有誤" ; return null; public class GetProductResponse : IResponse / <summary> / 編號 / </summary> public int? ID get; set; / <summary> / 商品名稱 / </summary> public string Name g
44、et; set; / <summary> / 顏色集合 / </summary> public List<string> Colors get; set; public List<ProductTag> TagList get; set; public class ProductTag / <summary> / 標簽編號 / </summary> public int ID get; set; / <summary> / 標簽名稱 / </summary> public string TagNam
45、e get; set; 復制代碼 轉(zhuǎn)換成JSON字符串:復制代碼 "data": "id": 0, "name": "str", "colors": "str" , "tagList": "id": 0, "tagName": "str" , "code": 0, "msg": "str"復制代碼 這樣我們就顯示了對象的結構,但是如果加上注釋呢? 如何顯示成下面的結果呢?這也是本輪子的特色,還是以json的格式展示中文說明。復制代碼 "data": "id": "編號", "name": "商品名稱", "colors": "顏色集合" , "tagList": "id": "標簽編號", "tagName&q
溫馨提示
- 1. 本站所有資源如無特殊說明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請下載最新的WinRAR軟件解壓。
- 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請聯(lián)系上傳者。文件的所有權益歸上傳用戶所有。
- 3. 本站RAR壓縮包中若帶圖紙,網(wǎng)頁內(nèi)容里面會有圖紙預覽,若沒有圖紙預覽就沒有圖紙。
- 4. 未經(jīng)權益所有人同意不得將文件中的內(nèi)容挪作商業(yè)或盈利用途。
- 5. 人人文庫網(wǎng)僅提供信息存儲空間,僅對用戶上傳內(nèi)容的表現(xiàn)方式做保護處理,對用戶上傳分享的文檔內(nèi)容本身不做任何修改或編輯,并不能對任何下載內(nèi)容負責。
- 6. 下載文件中如有侵權或不適當內(nèi)容,請與我們聯(lián)系,我們立即糾正。
- 7. 本站不保證下載資源的準確性、安全性和完整性, 同時也不承擔用戶因使用這些下載資源對自己和他人造成任何形式的傷害或損失。
最新文檔
- 公司借款合同書集錦
- 勞動合同法第四條
- 國際貿(mào)易貨物買賣合同
- 交通安全統(tǒng)籌服務合同
- 醫(yī)院聘用醫(yī)師協(xié)議書
- 2025年漯河貨運資格證考試答案
- 借貸擔保合同協(xié)議5篇
- 農(nóng)場整體出租合同范本
- 買賣礦居間合同范本
- 農(nóng)村豬種出售合同范本
- 供應商開發(fā)流程及質(zhì)量要求
- 2024年技術監(jiān)督質(zhì)檢職業(yè)技能考試-電力技術監(jiān)督上崗員(中國華能)筆試歷年真題薈萃含答案
- 反假幣測試附有答案
- 怎樣調(diào)動員工積極性
- 2024年內(nèi)科護理學(第七版)期末考試復習題庫(含答案)
- 【上市公司的財務風險的分析和防范:以三只松鼠為例10000字(論文)】
- 急診科培訓急診科與其他科室的協(xié)作與溝通
- JCT414-2017 硅藻土的標準
- 肌肉注射評分標準
- 鋼結構主要技術標準和要求
- 臘八粥 第一課時自學導學單
評論
0/150
提交評論