写给新手的WebAPI实践
此篇是寫給新手的Demo,用于參考和借鑒,用于發散思路。老鳥可以忽略了。
自己經常有這種情況,遇到一個新東西或難題,在了解和解決之前總是說“等搞定了一定要寫篇文章記錄下來”,但是當掌握了之后,就感覺好簡單呀不值得寫下來了。其實這篇也一樣,決定寫下來是想在春節前最后再干一件正經事兒,不能天天回去打Dota了!
目錄:
請求響應的設計
請求的Content-Type和模型綁定
自定義ApiResult和ApiControllerBase
權限驗證
模型生成
文檔生成
?
一、請求響應的設計
?RESTFul風格響亮很久了,但是我沒用過,以后也不打算用。當系統稍微復雜時,為了符合RESTFul要吃力地創建一些不直觀的名詞,這不是我的風格。所以此文設計的不是RESTFul風格,是只最常用的POST和GET請求。
請求部分就是調用API的參數,抽象出一個接口如下:
public interface IRequest{ResultObject Validate();}這里面只定義了一個Validate()方法,用于驗證請求參數的有效性,返回值是響應里的東西,下面會講到。
對于請求對象,傳遞到業務邏輯層,甚至是數據訪問層都可以,因為它本身就是用來傳輸數據的,俗話叫DTO(Data Transfer Object),不過定義多層傳輸對象,然后復制來復制去也是可以的~。但是有時候業務處理會需要當前登錄人的信息,而這個信息我并不希望直接從接口層向下傳遞,所以這里我再抽象一個UserRequestBase,用于附加登錄人相關信息:
public abstract class UserRequestBase : IRequest{ public int ApiUserID { get; set; }public string ApiUserName { get; set; }
// ......可以添加其他要專遞的登錄用戶相關的信息public abstract ResultObject Validate();}
ApiUserID和ApiUserName這樣的字段是不需要客戶端傳遞的,我們會根據登錄人信息自動填充。
根據實際中的經驗,我們往往會做分頁查詢,會用到頁碼和每頁條數,所為我們再定義個PageRequestBase:
public abstract class PageRequestBase : UserRequestBase{ public int PageIndex { get; set; }public int PageSize { get; set; }}
因為.net只能繼承單個父類,而且有些分頁查詢可能需要用戶信息,所以我們選擇繼承UserRequestBase。
當然,還可以根據自己的實際情況抽象出更多的公用類,在這不一一枚舉。
?
響應的設計分為兩部分,第一個是實際響應部分,第二個會把響應包裝一下,加上code和msg,用于表示調用狀態和錯誤信息(好老的方法了,大家都懂)。
響應接口IResponse里什么也沒有,就是一個標記接口,不過我們也可以抽象出來兩個常用的公用類用于響應列表和分頁數據:
public class ListResponseBase<T> : IResponse{ public List<T> List { get; set; }} public class PageResponseBase<T>: ListResponseBase<T>{ /// <summary>/// 頁碼數 ? ? ? ?/// </summary>public int PageIndex { get; set; } /// <summary>/// 總條數 ? ? ? ?/// </summary>public long TotalCount { get; set; } /// <summary>/// 每頁條數 ? ? ? ?/// </summary>public int PageSize { get; set; } /// <summary>/// 總頁數 ? ? ? ?/// </summary>public long PageCount { get; set; }}?包裝響應的時候,有兩種情況,第一種是操作類接口,比如添加商品,這些接口是不用響應對象的,只要返回是否成功就行了,第二種查詢類,這個時候必須要返回一些具體的東西了,所以響應包裝設計成兩個類:
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 where TResponse : IResponse{ public ResultObject(){} public ResultObject(TResponse data){Data = data;} /// <summary>/// 返回的數據 ? ? ? ?/// </summary>public TResponse Data { get; set; }}
IRequest接口的Validate()方法返回值就是第一個ResultObject,當請求參數驗證不通過的時候,肯定是沒有數據返回了。
再業務邏輯層,我選擇以包裝類作為返回類型,因為有很多錯誤都會在業務邏輯層出現,我們的接口是需要這些錯誤信息的。
?
二、請求的Content-Type和模型綁定
?現在前后端分離大行其道,我們做后端的通常會返回JSON格式給前端,響應的Content-Type為application/json,前端通過一些框架可以直接作為js對象使用。但是前端請求后端的時候還有很多是以form表單形式,也就是請求的Content-Type為:application/x-www-form-urlencoded,請求體為id=23&name=loogn這樣的字符串,如果數據格式復雜了,前端不好傳,后端解析起來也麻煩。還有的直接用一個固定參數傳遞json字符串,比如json={id:23,name:'loogn'},后端用form[‘json’]取出來后再反序列化。這些方法都可以,但是不夠好,最好的方法是前端也直接傳json,幸好現在很多web服務器都是支持請求的Content-Type為application/json的,這個時候請求的參數會以有效負荷(Payload)的形式傳遞過去,比如用jQuery的ajax來請求:
$.ajax({type: "POST",url: "/product/editProduct",contentType: "application/json; charset=utf-8",data: JSON.stringify({id:1,name:"name1"}),success: function (result) {console.log(result);}})
?除了contentType,還要注意使用了JSON.stringify把對象轉換成了字符串。其實ajax使用的XmlHttpRequest對象只能處理字符串(json字符串呀,xml字符串呀,text純文本呀,base64呀)。這些數據到了后端之后,從請求流里讀出來就是json形式的字符串了,可直接反序列化成后端對象。
然而這些考慮,.net mvc框架已經幫我們做好了,這都要歸功于DefaultModelBinder。
關于Form表單形式的請求,可以參見這位園友的文章:你從未知道如此強大的ASP.NET MVC DefaultModelBinder
我這里想說的是,DefaultModelBinder足夠智能,并不需要我們自己做什么,它會根據請求的contentType的不同,用不同的方式解析請求,然后綁定到對象,遇到contentType為application/json是,就直接反序列化得到對象,遇到application/x-www-form-urlencoded就用form表單的形式綁定對象,唯一要注意的就是前端同學,不要把請求的contentType和請求的實際內容搞錯就行了。你告訴我你送過來一只貓,而實際上是一只狗,我以對待貓的方式對待狗當然就有被咬一口的危險了(肯定會報錯)。
?
三、自定義ApiResult和ApiControllerBase
因為我不需要RESTFul風格,也不需要根據客戶端的意愿返回json或xml,所以我選擇AsyncController作為控制器的基類。AsyncController是直接繼承Controller的,而且支持異步處理,具體Controller和ApiController的區別,想了解的同學可以看這篇文章difference-between-apicontroller-and-controller-in-asp-net-mvc?,或者直接閱讀源碼。
Controller里的Action需要返回一個ActionResult對象,結合上面的響應包裝對象ResultObject,我決定自定義一個ApiResult作為Action的返回值,同時在這里處理jsonp調用、跨域調用、序列化的小駝峰命名和時間格式問題。
里面都是一些常規的邏輯,不做說明了,其中的JsonSetting就是設置序列化的小駝峰和日期格式的:
public class JsonSetting{public static JsonSerializerSettings Settings = new JsonSerializerSettings{ContractResolver = new CamelCasePropertyNamesContractResolver(),DateFormatString = "yyyy-MM-dd HH:mm:ss",};}這個時候有個問題,如果一個時間的字段需要"yyyy-MM-dd"這種格式怎么辦呢?這個時候要定義一個JsonConverter的子類,來實現自定義日期格式:
/// <summary>/// 日期格式化器/// </summary>public class CustomDateConverter : DateTimeConverterBase{private IsoDateTimeConverter dtConverter = new IsoDateTimeConverter { };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(reader, 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年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;
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(); 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 = "系統異常:" + exp.Message } };}}public ApiResult Api(Func<ResultObject> handle){ try{ var result = handle();//處理請求return new ApiResult(result);} catch (Exception exp){ //異常日志return new ApiResult { ResultData = new ResultObject { Code = 1, Msg = "系統異常:" + exp.Message } };}} /// <summary>/// 異步api/// </summary>/// <typeparam name="TRequest"></typeparam>/// <param name="request"></param>/// <param name="handle"></param>/// <returns></returns>public Task<ApiResult> ApiAsync<TRequest, TResponse>(TRequest request, Func<TRequest, Task<TResponse>> handle) where TResponse : ResultObject{ return handle(request).ContinueWith(x =>{ return Api(() => x.Result);});}}
最常用的應該就是第一個Api<TRequest>方法,里面處理了請求參數的驗證,把用戶信息賦給需要的請求對象,異常記錄等。第二個方法是對沒有請求參數的api調用處理。第三個方法是異步處理,可以對異步IO處理做一些優化,比如你提供的這個接口是調用的另一個網絡接口的情況。
?
四、權限驗證
?關于這個問題,我在一篇文章中貼了一些代碼,其實只要是知道怎么回事之后,自己可以想怎么玩就怎么玩了,下面講的的沒有涉及角色的權限。
根據以往經驗,我們可以把資源(也就是一個接口)的權限分為三個等級(標紅的第二點很重要,會大大簡化后臺權限管理的工作):
1,公開和訪問
2,登錄用戶可訪問
3,有權限的登錄用戶可訪問
所以我們如此設計驗證的過濾器:
public class AuthFilterAttribute : ActionFilterAttribute{ /// <summary>/// 匿名可訪問 ? ? ? ?/// </summary>public bool AllowAnonymous { get; set; } /// <summary>/// 登錄用戶就可以訪問 ? ? ? ?/// </summary>public bool OnlyLogin { get; set; } /// <summary>/// 使用的資源權限名,比如多個接口可以使用同一個資源的權限,默認是/ControllerName/ActionName ? ? ? ?/// </summary>public string PowerName { get; set; }public sealed override void OnActionExecuting(ActionExecutingContext filterContext){ //跨域時,客戶端會用OPTIONS請求來探測服務器if (filterContext.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 = 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 = PowerName;
if (string.IsNullOrEmpty(url)){url = "/" + filterContext.ActionDescriptor.ControllerDescriptor.ControllerName + "/" + filterContext.ActionDescriptor.ActionName;}
var hasPower = true; //可以根據 user和url等信息判斷是否有權限if (!hasPower){filterContext.Result = new ApiResult{ResultData = new ResultObject { Code = -2, Msg = "無權限" },JsonRequestBehavior = JsonRequestBehavior.AllowGet};}}}
AllowAnonymous屬性和OnlyLogin屬性的功能已經說過了,匿名訪問就是公開的,一個系統總會需要這樣的接口,登錄可訪問一般針對安全性比較低,比如字典數據的獲取,只要登錄了,就可以訪問,在權限管理里也不用配置了。
PowerName的屬性是出于什么考慮呢?有些時候,兩個接口的權限級別是綁定在一起的,比如一個商品的添加和修改接口,可以設置成同一個資源權限,所以都可以設置成/product/edit,這樣我們在權限管理里,只要維護/product/edit,而不需要分別維護/product/add和/product/update了(例子可能不太恰當,因為很多時候添加和修改本來就是一個接口,但是這個情況的確存在,設置PowerName也是為了簡化后臺的權限管理)。
對于跨域的情況,上面代碼也有注釋,客戶端會用OPTIONS動作來探測服務器,除了上述代碼,在web.config也需要配置一下:
<system.webServer><httpProtocol><customHeaders><!--<add 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>
配置中注釋掉的一行,我故意留著,就是因為要和代碼里有個對應的地方,在配置中只能配置為“*” 和特定域名,我們要更靈活,所以在程序里控制,可以允許一個域名列表。
?LoginUser的邏輯和上面的連接里的代碼差不多,不再貼了,下載里也有,apiToken從cookie和http頭部都可以取得,這樣不管是同域名網頁,跨域,app都是可以調用接口的。
?
五、模型生成
以前的模型生產器很多,現在使用T4模板的也不少,而且VS里自帶T4模板。但是我不太喜歡用T4(主要是沒有智能提示)。我感覺Razor引擎就挺好呀,完全可以用來生成模型。自己寫的一個ORM新加了兩個方法,來獲取數據庫表的元數據,目前支持MSSql和MySql,稍微寫點代碼就可以生成模型了,下面是cshtml的內容,截圖是為了展示代碼高亮效果,哈哈(完整代碼在最下方有下載)
所以有時候,自己動動手還是挺好的。其實所有web語言都可以生成,jsp,php,nodejs,和動態生成頁面返回給客戶端是一樣的,這個只不過是寫到文件里。
?
六、文檔生成
這里自然說的是API文檔,和上面那個生成模型不太一樣,雖說生成基本上都是:模板+數據=結果,但是這個生成在獲取數據的時候有點難點,先看效果圖:
api文檔自動生成的重要性想必大家都知道了,如果還是手動寫word或excel,工作量大不說,是很難保持一致性的。
? ?1. asp.net webapi 自帶一個Help Page?有興趣可以了解。
? ?2.?Swagger 是個生成api的框架,很強大,也支持接口測試,但是.net下的swagger好像只能使用在webapi中,一般的mvc不行,有興趣的也可以了解。
下面主要說一下本輪子的實現。從一個類型得到一個該類型的對象圖,在不嚴謹的情況下,還是比容易實現的,主要用反射和遞歸就可以了。
上面截圖中的C#類:
public class GetProductRequest : IRequest{ /// <summary>/// 商品編號 ? ? ? ?/// </summary>public int? ProductID { get; set; }public ResultObject Validate(){ 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 { get; 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 TagName { get; set; }}
?轉換成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": "標簽名稱"}]},"code": "等于0表示成功","msg": "code不為0時,返回錯誤消息" }?思考一下,一個什么樣的對象才能被序列化成上面顯示的JSON字符串呢?
沿著這個思路,我打算在生成對象圖的時候再生成一個對象B,對象B用字典表示,而且末端的值填充成為對象圖對應屬性的Summary。
比如 一個C#類:
public class A{ /// <summary>/// 編號/// </summary>public int ID { get; set; } /// <summary>/// 字符串列表/// </summary>public List<string> StrList { get; set; }public List<Sub> SubList { get; set; }public class Sub{ /// <summary>/// Sub名稱/// </summary>public int SubName { get; set; }}}?在構建A的對象圖的同時會像執行如下代碼一樣構建另一個對象B:
Dictionary<string, object> dict = new Dictionary<string, object>();dict.Add("ID", "編號");dict.Add("StrList", new List<string> { "字符串列表" });var subDict = new Dictionary<string, object>();subDict.Add("SubName", "Sub名稱");dict.Add("SubList", new List<Dictionary<string, object>> { subDict });
?這段代碼是很不完善的,但是目前夠用了,不夠用可以再改嘛,javascript數據類型本來也不多,接口定義當然也是越簡單越好了。可巧的是webapi的?help page里也有個同名同功的ObjectGenerator,它的實現是比較完善的,但是只返回了對象圖,我開始還打算要在它上面按照我的思路修改一下呢,嘗試之后就作罷了,改動太多了,而且對我來說,上面代碼夠用了。
?上面的summaryDict可以從外部讀取注釋文件獲取,要讀取哪些項目的注釋都需要設置一下:
讀取的代碼也很簡單,因為我只關注屬性的注釋,所以我只讀取屬性的:
Dictionary<string, string> getSummaryDict(){ var path = Server.MapPath("~/") + "bin\\";
var files = Directory.GetFiles(path, "*.xml");Dictionary<string, string> msDict = new Dictionary<string, string>();
foreach (var file in files){ XmlDocument xmldoc = new XmlDocument();xmldoc.Load(file);
var memberNodes = xmldoc.SelectNodes("/doc/members/member"); foreach (XmlNode item in memberNodes){
var name = item.Attributes["name"].Value;
if (name.StartsWith("P:")) //只取屬性{ var summaryNode = item
.SelectSingleNode("summary");
if (summaryNode != null){msDict[name] = summaryNode.InnerText.Trim();}}}} return msDict;}
?
平時如果打游戲,一般11點就睡了;寫博客的話,總是需要幾個凌晨后。想想真是佩服那些堅持寫博客的人!
Demo并不完整,沒有真正讀取數據庫,有興趣的同學可以下載下來玩玩。(由于上傳大小有限,我把packages文件夾刪除了)
原文地址:http://www.cnblogs.com/loogn/p/6275659.html
.NET社區新聞,深度好文,微信中搜索dotNET跨平臺或掃描二維碼關注
總結
以上是生活随笔為你收集整理的写给新手的WebAPI实践的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: C#高性能TCP服务的多种实现方式
- 下一篇: 公司技术需求备忘录