此篇是写给新手的Demo,用于参考和借鉴,用于发散思路。老鸟可以忽略了。

自己经常有这种情况,遇到一个新东西或难题,在了解和解决之前总是说“等搞定了一定要写篇文章记录下来”,但是当掌握了之后,就感觉好简单呀不值得写下来了。其实这篇也一样,决定写下来是想在春节前最后再干一件正经事儿,不能天天回去打Dota了!

目录:

  1. 请求响应的设计

  2. 请求的Content-Type和模型绑定

  3. 自定义ApiResult和ApiControllerBase

  4. 权限验证

  5. 模型生成

  6. 文档生成

一、请求响应的设计


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实践相关推荐

  1. 我学习 Java 的历程和体会(写给新手看,欢迎老司机批评和建议,持续更新中)

    我学习 Java 的历程和体会(写给新手看,欢迎老司机批评和建议,持续更新中) 最初写这篇文章的时候,是在今年的 9 月中旬.今天,我想再写写这将近两个多月以来的感受. 在今年的 10 月我来到北京求 ...

  2. 一份针对于新手的多线程实践--进阶篇

    前言 在上文<一份针对于新手的多线程实践>留下了一个问题: 这只是多线程其中的一个用法,相信看到这里的朋友应该多它的理解更进一步了. 再给大家留个阅后练习,场景也是类似的: 在 Redis ...

  3. c语言程序结构设计的心得,写给新手 选择结构程序设计总结

    写给新手 选择结构程序设计总结 选择结构程序设计总结  2010-11-2 一:C语言有两种选择结构: (1):if语句,用来实现两个分支的选择结构: (2):switch语句,用来实现多分支的选择结 ...

  4. ASP .NET之动态WebApi实践

    动态WebApi实践 框架名称 SunLight.DynamicWebApi 创建一个普通的WebApi 引入Swagger Api界面(可选) 2.1 为什么要引入? 2.2 如何引入 2.2.1 ...

  5. 给小孩普及计算机知识,神武4端游写给新手不懂孩子的玩家普及型知识

    神武4端游写给新手不懂孩子的玩家普及型知识 作者:老魔头 关键字: 首先准备养孩子的玩家要注意的是,孩子是一个不可以首发的宝宝!再就是现在的子女,一共有3个技能格,是三个空白的技能格可以打书,在技能打 ...

  6. 写给新手站长的一封信,有效的做网站排名

    写给新手站长的一封信 如何让网站以低成本排到靠前的位置 (本文只讲干货,不玩套路) 一家新网站想要排在百度及各大搜索引擎靠前的位置是非常难的事情,他们的核心算法主要考虑以下两点. 第一点网站的域名的历 ...

  7. 水果超市-用JavaWeb写的新手练手项目

    @用JavaWeb写的新手练手项目水果超市 简介 好久没有专门的去写一个适合新手练习的JavaWeb项目,今天写一个水果超市JavaWeb项目,一个特别简单的,适合新手练习的JavaWeb项目. 目录 ...

  8. c语言写扫雷新手详解

    c语言写扫雷新手详解 一.用到的知识点 1.分支语句 2.循环语句 3.二维数组 4.最好分块,使代码的功能更加独立,思维逻辑更加清楚,此程序我写了:test.c用来存放我的主函数,game.h用来定 ...

  9. 写给新手程序员的一封信

    首先,欢迎来到程序员的世界.在这个世界上,不是有很多人想创造软件并解决问题.你是一名hacker,属于那些愿意做一些有挑战性的事情的人. "当你不创造东西时,你只会根据自己的感觉而不是能力去 ...

最新文章

  1. mysql的profile_Mysql分析-profile详解
  2. mfc edit 超出行数时出现滚动条_千金难买“老鸭头”,是A股唯一可以获利200%的战法,一旦出现,不要犹豫满仓干,后市必定爆涨...
  3. ci github 通知_初探CI,Github调戏Action手记——自动构建并发布
  4. python的os模块批量获取目标路径下的文件名
  5. Struts2_中文问题
  6. SpringBoot中访问静态资源
  7. 什么是集群(cluster)
  8. JAVA对接支付宝支付(超详细,一看就懂)
  9. 第三周PLECS仿真实验
  10. JDBC批量插入数据优化,使用addBatch和executeBatch
  11. CentOS7与CentOS6区别及特点
  12. 面向对象7:项目二的总结
  13. MATLAB工具箱下载地址总汇
  14. c语言病毒恶搞代码大全,恶搞病毒代码案例分析
  15. 系统运维数据存储知识-系统数据误删除恢复
  16. HTML表格中输数字进行计算,excel表格如何自动计算输入数字
  17. 征途LINUX服务端脚本,征途【改版教程】-版本内脚本文件-转载于-喜欢玩网游单机站...
  18. 常见的数值积分方法 (欧拉、中值、龙格-库塔,【常用于IMU中】)
  19. 10——Filter过滤器
  20. php网页播放器源码免费,基于Flowplayer打造一款免费的WEB视频播放器附源码

热门文章

  1. linux shell 嵌套expect 与服务器交互脚本
  2. flash文件制作笔记
  3. ssl certificate 验证
  4. 深入理解Magento-第十章-数据操作数据收集器
  5. 微软所谓的无人工介入的自动的机器翻译系统
  6. 手把手教你学Dapr - 3. 使用Dapr运行第一个.Net程序
  7. Windows 11 小技巧- WSL开启Linux桌面应用
  8. C# 使用 Index 和 Range 简化集合操作
  9. ASP.NETCore小技巧:使用测试用户中间件
  10. ASP.NET Core Blazor WebAssembly 之 .NET JavaScript互调