溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

寫給新手的WebAPI實(shí)踐

發(fā)布時(shí)間:2020-10-12 21:15:20 來源:網(wǎng)絡(luò) 閱讀:1557 作者:26度出太陽 欄目:網(wǎng)絡(luò)安全

此篇是寫給新手的Demo,用于參考和借鑒,用于發(fā)散思路。老鳥可以忽略了。

自己經(jīng)常有這種情況,遇到一個(gè)新東西或難題,在了解和解決之前總是說“等搞定了一定要寫篇文章記錄下來”,但是當(dāng)掌握了之后,就感覺好簡單呀不值得寫下來了。其實(shí)這篇也一樣,決定寫下來是想在春節(jié)前最后再干一件正經(jīng)事兒!

目錄:

  1. 請(qǐng)求響應(yīng)的設(shè)計(jì)

  2. 請(qǐng)求的Content-Type和模型綁定

  3. 自定義ApiResult和ApiControllerBase

  4. 權(quán)限驗(yàn)證

  5. 模型生成

  6. 文檔生成

 

一、請(qǐng)求響應(yīng)的設(shè)計(jì)


 RESTFul風(fēng)格響亮很久了,但是我沒用過,以后也不打算用。當(dāng)系統(tǒng)稍微復(fù)雜時(shí),為了符合RESTFul要吃力地創(chuàng)建一些不直觀的名詞,這不是我的風(fēng)格。所以此文設(shè)計(jì)的不是RESTFul風(fēng)格,是最常用的POST和GET請(qǐng)求。

請(qǐng)求部分就是調(diào)用API的參數(shù),抽象出一個(gè)接口如下:

    public interface IRequest
    {
        ResultObject Validate();
    }

這里面只定義了一個(gè)Validate()方法,用于驗(yàn)證請(qǐng)求參數(shù)的有效性,返回值是響應(yīng)里的東西,下面會(huì)講到。

對(duì)于請(qǐng)求對(duì)象,傳遞到業(yè)務(wù)邏輯層,甚至是數(shù)據(jù)訪問層都可以,因?yàn)樗旧砭褪怯脕韨鬏敂?shù)據(jù)的,俗話叫DTO(Data Transfer Object),不過定義多層傳輸對(duì)象,然后復(fù)制來復(fù)制去也是可以的~。但是有時(shí)候業(yè)務(wù)處理會(huì)需要當(dāng)前登錄人的信息,而這個(gè)信息我并不希望直接從接口層向下傳遞,所以這里我再抽象一個(gè)UserRequestBase,用于附加登錄人相關(guān)信息:

寫給新手的WebAPI實(shí)踐

    public abstract class UserRequestBase : IRequest
    {        public int ApiUserID { get; set; }        public string ApiUserName { get; set; }        // ......可以添加其他要專遞的登錄用戶相關(guān)的信息

        public abstract ResultObject Validate();
    }

寫給新手的WebAPI實(shí)踐

ApiUserID和ApiUserName這樣的字段是不需要客戶端傳遞的,我們會(huì)根據(jù)登錄人信息自動(dòng)填充。

根據(jù)實(shí)際中的經(jīng)驗(yàn),我們往往會(huì)做分頁查詢,會(huì)用到頁碼和每頁條數(shù),所為我們?cè)俣x個(gè)PageRequestBase:

    public abstract class PageRequestBase : UserRequestBase
    {        public int PageIndex { get; set; }        public int PageSize { get; set; }
    }

因?yàn)?net只能繼承單個(gè)父類,而且有些分頁查詢可能需要用戶信息,所以我們選擇繼承UserRequestBase。

當(dāng)然,還可以根據(jù)自己的實(shí)際情況抽象出更多的公用類,在這不一一枚舉。

 

響應(yīng)的設(shè)計(jì)分為兩部分,第一個(gè)是實(shí)際響應(yīng)部分,第二個(gè)會(huì)把響應(yīng)包裝一下,加上code和msg,用于表示調(diào)用狀態(tài)和錯(cuò)誤信息(好老的方法了,大家都懂)。

響應(yīng)接口IResponse里什么也沒有,就是一個(gè)標(biāo)記接口,不過我們也可以抽象出來兩個(gè)常用的公用類用于響應(yīng)列表和分頁數(shù)據(jù):

寫給新手的WebAPI實(shí)踐

    public class ListResponseBase<T> : IResponse
    {        public List<T> List { get; set; }
    }    public class PageResponseBase<T>: ListResponseBase<T>
    {        /// <summary>
        /// 頁碼數(shù)        /// </summary>
        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; }
    }

寫給新手的WebAPI實(shí)踐

 包裝響應(yīng)的時(shí)候,有兩種情況,第一種是操作類接口,比如添加商品,這些接口是不用響應(yīng)對(duì)象的,只要返回是否成功就行了,第二種查詢類,這個(gè)時(shí)候必須要返回一些具體的東西了,所以響應(yīng)包裝設(shè)計(jì)成兩個(gè)類:

寫給新手的WebAPI實(shí)踐

public class ResultObject
    {        /// <summary>
        /// 等于0表示成功        /// </summary>
        public int Code { get; set; }        /// <summary>
        /// code不為0時(shí),返回錯(cuò)誤消息        /// </summary>
        public string Msg { get; set; }
    }    public class ResultObject<TResponse> : ResultObject where TResponse : IResponse
    {        public ResultObject()
        {
        }        public ResultObject(TResponse data)
        {
            Data = data;
        }        /// <summary>
        /// 返回的數(shù)據(jù)        /// </summary>
        public TResponse Data { get; set; }

    }

寫給新手的WebAPI實(shí)踐

IRequest接口的Validate()方法返回值就是第一個(gè)ResultObject,當(dāng)請(qǐng)求參數(shù)驗(yàn)證不通過的時(shí)候,肯定是沒有數(shù)據(jù)返回了。

在業(yè)務(wù)邏輯層,我選擇以包裝類作為返回類型,因?yàn)橛泻芏噱e(cuò)誤都會(huì)在業(yè)務(wù)邏輯層出現(xiàn),我們的接口是需要這些錯(cuò)誤信息的。

 

二、請(qǐng)求的Content-Type和模型綁定


 現(xiàn)在前后端分離大行其道,我們做后端的通常會(huì)返回JSON格式給前端,響應(yīng)的Content-Type為application/json,前端通過一些框架可以直接作為js對(duì)象使用。但是前端請(qǐng)求后端的時(shí)候還有很多是以form表單形式,也就是請(qǐng)求的Content-Type為:application/x-www-form-urlencoded,請(qǐng)求體為id=23&name=loogn這樣的字符串,如果數(shù)據(jù)格式復(fù)雜了,前端不好傳,后端解析起來也麻煩。還有的直接用一個(gè)固定參數(shù)傳遞json字符串,比如json={id:23,name:'loogn'},后端用form[‘json’]取出來后再反序列化。這些方法都可以,但是不夠好,最好的方法是前端也直接傳json,幸好現(xiàn)在很多web服務(wù)器都是支持請(qǐng)求的Content-Type為application/json的,這個(gè)時(shí)候請(qǐng)求的參數(shù)會(huì)以有效負(fù)荷(Payload)的形式傳遞過去,比如用jQuery的ajax來請(qǐng)求:

寫給新手的WebAPI實(shí)踐

    $.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);
        }
    })

寫給新手的WebAPI實(shí)踐

 除了contentType,還要注意使用了JSON.stringify把對(duì)象轉(zhuǎn)換成了字符串。其實(shí)ajax使用的XmlHttpRequest對(duì)象只能處理字符串(json字符串呀,xml字符串呀,text純文本呀,base64呀)。這些數(shù)據(jù)到了后端之后,從請(qǐng)求流里讀出來就是json形式的字符串了,可直接反序列化成后端對(duì)象。

然而這些考慮,.net mvc框架已經(jīng)幫我們做好了,這都要?dú)w功于DefaultModelBinder。

關(guān)于Form表單形式的請(qǐng)求,可以參見這位園友的文章:你從未知道如此強(qiáng)大的ASP.NET MVC DefaultModelBinder

我這里想說的是,DefaultModelBinder足夠智能,并不需要我們自己做什么,它會(huì)根據(jù)請(qǐng)求的contentType的不同,用不同的方式解析請(qǐng)求,然后綁定到對(duì)象,遇到contentType為application/json時(shí),就直接反序列化得到對(duì)象,遇到application/x-www-form-urlencoded就用form表單的形式綁定對(duì)象,唯一要注意的就是前端同學(xué),不要把請(qǐng)求的contentType和請(qǐng)求的實(shí)際內(nèi)容搞錯(cuò)就行了。你告訴我你送過來一只貓,而實(shí)際上是一只狗,我以對(duì)待貓的方式對(duì)待狗當(dāng)然就有被咬一口的危險(xiǎn)了(肯定會(huì)報(bào)錯(cuò))。

 

三、自定義ApiResult和ApiControllerBase


因?yàn)槲也恍枰猂ESTFul風(fēng)格,也不需要根據(jù)客戶端的意愿返回json或xml,所以我選擇AsyncController作為控制器的基類。AsyncController是直接繼承Controller的,而且支持異步處理,具體Controller和ApiController的區(qū)別,想了解的同學(xué)可以看這篇文章difference-between-apicontroller-and-controller-in-asp-net-mvc ,或者直接閱讀源碼。

Controller里的Action需要返回一個(gè)ActionResult對(duì)象,結(jié)合上面的響應(yīng)包裝對(duì)象ResultObject,我決定自定義一個(gè)ApiResult作為Action的返回值,同時(shí)在這里處理jsonp調(diào)用、跨域調(diào)用、序列化的小駝峰命名和時(shí)間格式問題。

寫給新手的WebAPI實(shí)踐

    /// <summary>
    /// api返回結(jié)果,控制jsonp、跨域、小駝峰命名和時(shí)間格式問題
    /// </summary>    public class ApiResult : ActionResult
    {        /// <summary>
        /// 返回?cái)?shù)據(jù)
        /// </summary>        public ResultObject ResultData { get; set; }        /// <summary>
        /// 返回?cái)?shù)據(jù)編碼,默認(rèn)utf8
        /// </summary>        public Encoding ContentEncoding { get; set; }        /// <summary>
        /// 是否接受Get請(qǐng)求,默認(rèn)允許
        /// </summary>        public JsonRequestBehavior JsonRequestBehavior { get; set; }        /// <summary>
        /// 是否允許跨域請(qǐng)求
        /// </summary>        public bool 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 = JsonRequestBehavior.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 (ResultData != null)
            {
                string buffer;                if ((JsonRequestBehavior == JsonRequestBehavior.DenyGet) && string.Equals(context.HttpContext.Request.HttpMethod, "GET"))
                {
                    buffer = "該接口不允許Get請(qǐng)求";
                }                else
                {                    var jsonpCallback = request[JsonpCallbackName];                    if (string.IsNullOrWhiteSpace(jsonpCallback))
                    {                        //如果可以跨域,寫入響應(yīng)頭
                        if (AllowCrossDomain)
                        {
                            WriteAllowAccessOrigin(context);
                        }
                        response.ContentType = "application/json";
                        buffer = JsonConvert.SerializeObject(ResultData, JsonSetting.Settings);
                    }                    else
                    {                        //jsonp
                        if (AllowCrossDomain) //這個(gè)判斷可能非必須                        {
                            response.ContentType = "text/javascript";
                            buffer = string.Format("{0}({1});", jsonpCallback, JsonConvert.SerializeObject(ResultData, JsonSetting.Settings));
                        }                        else
                        {
                            buffer = "該接口不允許跨域請(qǐng)求";
                        }
                    }
                }                try
                {
                    response.Write(buffer);
                }                catch (Exception exp)
                {
                    response.Write(exp.Message);
                }
            }            else
            {
                response.Write("ApiResult.Data為null");
            }
            response.End();
        }        /// <summary>
        /// 寫入跨域請(qǐng)求頭
        /// </summary>
        /// <param name="context"></param>
        private void WriteAllowAccessOrigin(ControllerContext context)
        {            var origin = context.HttpContext.Request.Headers["Origin"];            if (true) //可以維護(hù)一個(gè)允許跨域的域名集合,類判斷是否可以跨域            {
                context.HttpContext.Response.Headers.Add("Access-Control-Allow-Origin", origin ?? "*");
            }
        }
    }

寫給新手的WebAPI實(shí)踐

里面都是一些常規(guī)的邏輯,不做說明了,其中的JsonSetting就是設(shè)置序列化的小駝峰和日期格式的:

寫給新手的WebAPI實(shí)踐

    public class JsonSetting
    {
        public static JsonSerializerSettings Settings = new JsonSerializerSettings
        {
            ContractResolver = new CamelCasePropertyNamesContractResolver(),
            DateFormatString = "yyyy-MM-dd HH:mm:ss",
        };
    }

寫給新手的WebAPI實(shí)踐

這個(gè)時(shí)候有個(gè)問題,如果一個(gè)時(shí)間字段需要"yyyy-MM-dd"這種格式怎么辦呢?這個(gè)時(shí)候要定義一個(gè)JsonConverter的子類,來實(shí)現(xiàn)自定義日期格式:

寫給新手的WebAPI實(shí)踐

    /// <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);
        }
    }

寫給新手的WebAPI實(shí)踐

在需要的響應(yīng)屬性上加上 [JsonConverter(typeof(CustomDateConverter))] 或  [JsonConverter(typeof(CustomDateConverter),"yyyy年MM月dd日")] 即可。

ApiResult定義好了,再定義一個(gè)控制器基類,目的是便于處理ApiResult:

寫給新手的WebAPI實(shí)踐

    /// <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)
                {                    //處理需要登錄用戶的請(qǐng)求
                    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); //處理請(qǐng)求
                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> handle)
        {            try
            {                
                var result = handle();//處理請(qǐng)求
                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"></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);
            });
        }
    }

寫給新手的WebAPI實(shí)踐

最常用的應(yīng)該就是第一個(gè)Api<TRequest>方法,里面處理了請(qǐng)求參數(shù)的驗(yàn)證,把用戶信息賦給需要的請(qǐng)求對(duì)象,異常記錄等。第二個(gè)方法是對(duì)沒有請(qǐng)求參數(shù)的api調(diào)用處理。第三個(gè)方法是異步處理,可以對(duì)異步IO處理做一些優(yōu)化,比如你提供的這個(gè)接口是調(diào)用的另一個(gè)網(wǎng)絡(luò)接口的情況。

 

四、權(quán)限驗(yàn)證


 關(guān)于這個(gè)問題,我在一篇文章中貼了一些代碼,其實(shí)只要是知道怎么回事之后,自己可以想怎么玩就怎么玩了,下面講的沒有涉及角色的權(quán)限。

根據(jù)以往經(jīng)驗(yàn),我們可以把資源(也就是一個(gè)接口)的權(quán)限分為三個(gè)等級(jí)(標(biāo)紅的第二點(diǎn)很重要,會(huì)大大簡化后臺(tái)權(quán)限管理的工作):

1,公開可訪問

2,登錄用戶可訪問

3,有權(quán)限的登錄用戶可訪問

所以我們?nèi)绱嗽O(shè)計(jì)驗(yàn)證的過濾器:

寫給新手的WebAPI實(shí)踐

    public class AuthFilterAttribute : ActionFilterAttribute
    {        /// <summary>
        /// 匿名可訪問        /// </summary>
        public bool AllowAnonymous { get; set; }        /// <summary>
        /// 登錄用戶就可以訪問        /// </summary>
        public bool OnlyLogin { get; set; }        
        /// <summary>
        /// 使用的資源權(quán)限名,比如多個(gè)接口可以使用同一個(gè)資源的權(quán)限,默認(rèn)是/ControllerName/ActionName        /// </summary>
        public string PowerName { get; set; }        public sealed override void OnActionExecuting(ActionExecutingContext filterContext)
        {            //跨域時(shí),客戶端會(huì)用OPTIONS請(qǐng)求來探測服務(wù)器
            if (filterContext.HttpContext.Request.HttpMethod == "OPTIONS")
            {                var origin = filterContext.HttpContext.Request.Headers["Origin"];                if (true) //可以維護(hù)一個(gè)允許跨域的域名集合,類判斷是否可以跨域                {
                    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; //可以根據(jù) user和url等信息判斷是否有權(quán)限
            if (!hasPower)
            {
                filterContext.Result = new ApiResult
                {
                    ResultData = new ResultObject { Code = -2, Msg = "無權(quán)限" },
                    JsonRequestBehavior = JsonRequestBehavior.AllowGet
                };
            }
        }
        
    }

寫給新手的WebAPI實(shí)踐

AllowAnonymous屬性和OnlyLogin屬性的功能已經(jīng)說過了,匿名訪問就是公開的,一個(gè)系統(tǒng)總會(huì)需要這樣的接口,登錄可訪問一般針對(duì)安全性比較低,比如字典數(shù)據(jù)的獲取,只要登錄了,就可以訪問,在權(quán)限管理里也不用配置了。

PowerName的屬性是出于什么考慮呢?有些時(shí)候,兩個(gè)接口的權(quán)限級(jí)別是綁定在一起的,比如一個(gè)商品的添加和修改接口,可以設(shè)置成同一個(gè)資源權(quán)限,所以都可以設(shè)置成/product/edit,這樣我們?cè)跈?quán)限管理里,只要維護(hù)/product/edit,而不需要分別維護(hù)/product/add和/product/update了(例子可能不太恰當(dāng),因?yàn)楹芏鄷r(shí)候添加和修改本來就是一個(gè)接口,但是這個(gè)情況的確存在,設(shè)置PowerName也是為了簡化后臺(tái)的權(quán)限管理)。

對(duì)于跨域的情況,上面代碼也有注釋,客戶端會(huì)用OPTIONS動(dòng)作來探測服務(wù)器,除了上述代碼,在web.config也需要配置一下:

寫給新手的WebAPI實(shí)踐

  <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>

寫給新手的WebAPI實(shí)踐

配置中注釋掉的一行,我故意留著,就是因?yàn)橐痛a里有個(gè)對(duì)應(yīng)的地方,在配置中只能配置為“*” 或特定域名,我們要更靈活,所以在程序里控制,可以允許一個(gè)域名集合。

 LoginUser的邏輯和上面的連接里的代碼差不多,不再貼了,下載里也有,apiToken從cookie和http頭部都可以取得,這樣不管是同域名網(wǎng)頁,跨域,app都是可以調(diào)用接口的。

 

五、模型生成


以前的模型生產(chǎn)器很多,現(xiàn)在使用T4模板的也不少,而且VS里自帶T4模板。但是我不太喜歡用T4(主要是沒有智能提示)。我感覺Razor引擎就挺好呀,完全可以用來生成模型。自己寫的一個(gè)ORM新加了兩個(gè)方法,來獲取數(shù)據(jù)庫表的元數(shù)據(jù),目前支持MSSql和MySql,稍微寫點(diǎn)代碼就可以生成模型了,下面是cshtml的內(nèi)容,截圖是為了展示代碼高亮效果,哈哈(完整代碼在最下方有下載)

寫給新手的WebAPI實(shí)踐

所以有時(shí)候,自己動(dòng)動(dòng)手還是挺好的。其實(shí)所有web語言都可以生成,jsp,php,nodejs,和動(dòng)態(tài)生成頁面返回給客戶端是一樣的,這個(gè)只不過是寫到文件里。

 

六、文檔生成


這里自然說的是API文檔,和上面那個(gè)生成模型不太一樣,雖說生成基本上都是:模板+數(shù)據(jù)=結(jié)果,但是這個(gè)生成在獲取數(shù)據(jù)的時(shí)候有點(diǎn)困難,先看效果圖:

寫給新手的WebAPI實(shí)踐

api文檔自動(dòng)生成的重要性想必大家都知道了,如果還是手動(dòng)寫word或excel,工作量大不說,是很難保持一致性的。

   1. asp.net webapi 自帶一個(gè)Help Page 有興趣可以了解。

   2. Swagger 是個(gè)生成api的框架,很強(qiáng)大,也支持接口測試,但是.net下的swagger好像只能使用在webapi中,一般的mvc不行,有興趣的也可以了解。

下面主要說一下本輪子的實(shí)現(xiàn)。從一個(gè)類型得到一個(gè)該類型的對(duì)象圖,在不嚴(yán)謹(jǐn)?shù)那闆r下,還是比容易實(shí)現(xiàn)的,主要用反射和遞歸就可以了。

上面截圖中的C#類:

寫給新手的WebAPI實(shí)踐

public class GetProductRequest : IRequest
    {        /// <summary>
        /// 商品編號(hào)        /// </summary>
        public int? ProductID { get; set; }        public ResultObject Validate()
        {            if (ProductID == null || ProductID.Value <= 0)
            {                return new ResultObject { Code = 1, Msg = "商品編號(hào)有誤" };
            }            return null;
        }
    }    public class GetProductResponse : IResponse
    {        /// <summary>
        /// 編號(hào)        /// </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>
        /// 標(biāo)簽編號(hào)        /// </summary>
        public int ID { get; set; }        /// <summary>
        /// 標(biāo)簽名稱        /// </summary>
        public string TagName { get; set; }
    }

寫給新手的WebAPI實(shí)踐

 轉(zhuǎn)換成JSON字符串:

寫給新手的WebAPI實(shí)踐

{  "data": {    "id": 0,    "name": "str",    "colors": [      "str"
    ],    "tagList": [
      {        "id": 0,        "tagName": "str"
      }
    ]
  },  "code": 0,  "msg": "str"}

寫給新手的WebAPI實(shí)踐

 這樣我們就顯示了對(duì)象的結(jié)構(gòu),但是如果加上注釋呢? 如何顯示成下面的結(jié)果呢?這也是本輪子的特色,還是以json的格式展示中文說明。

寫給新手的WebAPI實(shí)踐

{  "data": {    "id": "編號(hào)",    "name": "商品名稱",    "colors": [      "顏色集合"
    ],    "tagList": [
      {        "id": "標(biāo)簽編號(hào)",        "tagName": "標(biāo)簽名稱"
      }
    ]
  },  "code": "等于0表示成功",  "msg": "code不為0時(shí),返回錯(cuò)誤消息"}

寫給新手的WebAPI實(shí)踐

 思考一下,一個(gè)什么樣的對(duì)象才能被序列化成上面顯示的JSON字符串呢?

沿著這個(gè)思路,我打算在生成對(duì)象圖的時(shí)候再生成一個(gè)對(duì)象B,對(duì)象B用字典表示,而且末端的值填充成為對(duì)象圖對(duì)應(yīng)屬性的Summary。

比如 一個(gè)C#類:

寫給新手的WebAPI實(shí)踐

    public class A
    {        /// <summary>
        /// 編號(hào)
        /// </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; }
        }
    }

寫給新手的WebAPI實(shí)踐

 在構(gòu)建A的對(duì)象圖的同時(shí)會(huì)像執(zhí)行如下代碼一樣構(gòu)建另一個(gè)對(duì)象B:

    Dictionary<string, object> dict = new Dictionary<string, object>();
    dict.Add("ID", "編號(hào)");
    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 });

 ObjectGenerator的代碼如下:

寫給新手的WebAPI實(shí)踐

    public class ObjectGenerator
    {        public static string GetSummary(PropertyInfo prop, Dictionary<string, string> summaryDict)
        {            if (summaryDict == null || summaryDict.Count == 0) return string.Empty;            var objType = prop.DeclaringType;            var propName = prop.Name;            var key = "P:" + objType.Namespace + "." + GetPrettyName(objType) + objType.Name + "." + propName;            if (summaryDict.ContainsKey(key))
            {                return summaryDict[key];
            }            else
            {                return "";
            }
        }        private static string GetPrettyName(Type objType, string namespaceStr = "")
        {
            if (objType.DeclaringType != null)
            {
                return GetPrettyName(objType.DeclaringType, objType.DeclaringType.Name + "." + namespaceStr);
            }
            else
            {
                return namespaceStr;
            }
        }        public static Tuple<object, object> GetObjectMapDict(Type type, PropertyInfo typeProp, Dictionary<string, string> summaryDict, HashSet<string> ignoreProps = null)
        {            if (typeProp != null)
            {                var p = typeProp;
            }            // 
            if (type.IsPrimitive || type == typeof(decimal))
            {                var v1 = Convert.ChangeType(0, type);                var v2 = v1.ToString();                if (typeProp != null)
                {
                    v2 = GetSummary(typeProp, summaryDict);
                }                return new Tuple<object, object>(v1, v2);
            }            else if (type == typeof(string))
            {                var v1 = "str";                var v2 = v1.ToString();                if (typeProp != null)
                {
                    v2 = GetSummary(typeProp, summaryDict);
                }                return new Tuple<object, object>(v1, v2);
            }            else if (type == typeof(DateTime))
            {                var v1 = DateTime.Now;                var v2 = v1.ToString("yyyy-MM-dd HH:mm:ss");                if (typeProp != null)
                {
                    v2 = GetSummary(typeProp, summaryDict);
                }                return new Tuple<object, object>(v1, v2);

            }            else if (type.IsArray)
            {                var eleType = type.GetElementType();                var arr = Array.CreateInstance(eleType, 1);                var list = new List<object>();                var ele_tuple = GetObjectMapDict(eleType, typeProp, summaryDict, ignoreProps);
                arr.SetValue(ele_tuple.Item1, 0);
                list.Add(ele_tuple.Item2);                return new Tuple<object, object>(arr, list);

            }            else if (type.Name.Equals("List`1"))
            {                var list = (IList)Activator.CreateInstance(type);                var list1 = new List<object>();                var eleType = type.GetGenericArguments()[0];                var ele_tuple = GetObjectMapDict(eleType, typeProp, summaryDict, ignoreProps);
                list.Add(ele_tuple.Item1);
                list1.Add(ele_tuple.Item2);                return new Tuple<object, object>(list, list1);
            }            else if (type.Name.Equals("Dictionary`2"))
            {                var dict = (IDictionary)Activator.CreateInstance(type);                var dict1 = new Dictionary<string, object>();                var keyType = type.GetGenericArguments()[0];                var valueType = type.GetGenericArguments()[1];                var key = GetObjectMapDict(keyType, null, summaryDict, ignoreProps);                var value = GetObjectMapDict(valueType, null, summaryDict, ignoreProps);
                dict.Add(key.Item1, value.Item1);
                dict1.Add(key.Item2.ToString(), value.Item2);                return new Tuple<object, object>(dict, dict1);
            }            else if (type.IsClass)
            {                var props = type.GetProperties(BindingFlags.Public | BindingFlags.Instance);                try
                {                    var obj = Activator.CreateInstance(type);                    var dict = new Dictionary<string, object>();                    foreach (var prop in props)
                    {                        if (ignoreProps != null && ignoreProps.Contains(prop.Name))
                        {                            continue;
                        }                        var pType = DealNullable(prop.PropertyType);                        var val = GetObjectMapDict(pType, prop, summaryDict, ignoreProps);
                        dict.Add(prop.Name, val.Item2);                        var setter = prop.GetSetMethod(true);                        if (setter != null)
                        {
                            prop.SetValue(obj, val.Item1, null);
                        }

                    }                    return new Tuple<object, object>(obj, dict);
                }                catch (Exception e)
                {                    return null;
                }
            }            else
            {                try
                {                    var obj = Activator.CreateInstance(type);                    return new Tuple<object, object>(obj, obj);
                }                catch (Exception e)
                {                    return null;
                }
            }
        }        private static Type DealNullable(Type type)
        {            if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>))
            {                return type.GetGenericArguments()[0];
            }            return type;
        }
    }

寫給新手的WebAPI實(shí)踐

 這段代碼是很不完善的,但是目前夠用了,不夠用可以再改嘛,javascript數(shù)據(jù)類型本來也不多,接口定義當(dāng)然也是越簡單越好了。可巧的是webapi的 help page里也有個(gè)同名同功的ObjectGenerator,它的實(shí)現(xiàn)是比較完善的,但是只返回了對(duì)象圖,我開始還打算要在它上面按照我的思路修改一下呢,嘗試之后就作罷了,改動(dòng)太多了,而且對(duì)我來說,上面代碼夠用了。

 上面的summaryDict可以從外部讀取注釋文件獲取,要讀取哪些項(xiàng)目的注釋都需要設(shè)置一下:

寫給新手的WebAPI實(shí)踐

讀取的代碼也很簡單,因?yàn)槲抑魂P(guān)注屬性的注釋,所以我只讀取屬性的:

寫給新手的WebAPI實(shí)踐

        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;
        }

寫給新手的WebAPI實(shí)踐

 


Demo并不完整,沒有真正讀取數(shù)據(jù)庫,有興趣的同學(xué)可以下載下來玩玩。


向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請(qǐng)聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI