写给新手的WebAPI实践_第1页
写给新手的WebAPI实践_第2页
写给新手的WebAPI实践_第3页
写给新手的WebAPI实践_第4页
写给新手的WebAPI实践_第5页
已阅读5页,还剩14页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

1、写给新手的WebAPI实践此篇是写给新手的Demo,用于参考和借鉴,用于发散思路。老鸟可以忽略了。自己经常有这种情况,遇到一个新东西或难题,在了解和解决之前总是说“等搞定了一定要写篇文章记录下来”,但是当掌握了之后,就感觉好简单呀不值得写下来了。其实这篇也一样,决定写下来是想在春节前最后再干一件正经事儿,不能天天回去打Dota了!目录:请求响应的设计请求的Content-Type和模型绑定自定义ApiResult和ApiControllerBase权限验证模型生成文档生成 一、请求响应的设计 RESTFul风格响亮很久了,但是我没用过,以后也不打算用。当系统稍微复杂时,为了符合RESTFul要

2、吃力地创建一些不直观的名词,这不是我的风格。所以此文设计的不是RESTFul风格,是只最常用的POST和GET请求。请求部分就是调用API的参数,抽象出一个接口如下: public interface IRequest ResultObject Validate(); 这里面只定义了一个Validate()方法,用于验证请求参数的有效性,返回值是响应里的东西,下面会讲到。对于请求对象,传递到业务逻辑层,甚至是数据访问层都可以,因为它本身就是用来传输数据的,俗话叫DTO(Data Transfer Object),不过定义多层传输对象,然后复制来复制去也是可以的。但是有时候业务处理会需要当前登录

3、人的信息,而这个信息我并不希望直接从接口层向下传递,所以这里我再抽象一个UserRequestBase,用于附加登录人相关信息:复制代码 public abstract class UserRequestBase : IRequest public int ApiUserID get; set; public string ApiUserName get; set; / .可以添加其他要专递的登录用户相关的信息 public abstract ResultObject Validate(); 复制代码ApiUserID和ApiUserName这样的字段是不需要客户端传递的,我们会根据登录人信息

4、自动填充。根据实际中的经验,我们往往会做分页查询,会用到页码和每页条数,所为我们再定义个PageRequestBase: public abstract class PageRequestBase : UserRequestBase public int PageIndex get; set; public int PageSize get; set; 因为.net只能继承单个父类,而且有些分页查询可能需要用户信息,所以我们选择继承UserRequestBase。当然,还可以根据自己的实际情况抽象出更多的公用类,在这不一一枚举。 响应的设计分为两部分,第一个是实际响应部分,第二个会把响应包装一

5、下,加上code和msg,用于表示调用状态和错误信息(好老的方法了,大家都懂)。响应接口IResponse里什么也没有,就是一个标记接口,不过我们也可以抽象出来两个常用的公用类用于响应列表和分页数据:复制代码 public class ListResponseBase<T> : IResponse public List<T> List get; set; public class PageResponseBase<T>: ListResponseBase<T> / <summary> / 页码数 / </summary>

6、 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; 复制代码 包装响应的时候,有两种情况,第一种是操作类接口,比如添加商品,这些接口是不用

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> / 返回的数据 / </summary> public TResponse Data get; set; 复制代码IRequest接口的Validate()方法返回值就是第一个ResultObject,当请求参数验证不通过的时候,肯定是没有数据返回了。再业务逻辑层,我选择以包装类作为返回类型,因为有很多错误都会在业务逻辑层出现,我们的接口是需要这些错误

9、信息的。 二、请求的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:'loo

10、gn',后端用formjson取出来后再反序列化。这些方法都可以,但是不够好,最好的方法是前端也直接传json,幸好现在很多web服务器都是支持请求的Content-Type为application/json的,这个时候请求的参数会以有效负荷(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把对象转换成了字符串。其实ajax使用的XmlHttpRequest对象只能处理字符串(json字符串呀,xml字符串呀,text纯文本呀,base64呀)。这些数据到了后端之后,从请求流里读出来就是json形式的字符串了,可直接反序列化成后端对象。然而这些考虑,.net mvc框架已经帮我们做好了,这都要归功于DefaultMo

12、delBinder。关于Form表单形式的请求,可以参见这位园友的文章:你从未知道如此强大的ASP.NET MVC DefaultModelBinder我这里想说的是,DefaultModelBinder足够智能,并不需要我们自己做什么,它会根据请求的contentType的不同,用不同的方式解析请求,然后绑定到对象,遇到contentType为application/json是,就直接反序列化得到对象,遇到application/x-www-form-urlencoded就用form表单的形式绑定对象,唯一要注意的就是前端同学,不要把请求的contentType和请求的实际内容搞错就行了。你

13、告诉我你送过来一只猫,而实际上是一只狗,我以对待猫的方式对待狗当然就有被咬一口的危险了(肯定会报错)。 三、自定义ApiResult和ApiControllerBase因为我不需要RESTFul风格,也不需要根据客户端的意愿返回json或xml,所以我选择AsyncController作为控制器的基类。AsyncController是直接继承Controller的,而且支持异步处理,具体Controller和ApiController的区别,想了解的同学可以看这篇文章difference-between-apicontroller-and-controller-in-asp-net-mvc ,

14、或者直接阅读源码。Controller里的Action需要返回一个ActionResult对象,结合上面的响应包装对象ResultObject,我决定自定义一个ApiResult作为Action的返回值,同时在这里处理jsonp调用、跨域调用、序列化的小驼峰命名和时间格式问题。复制代码 / <summary> / api返回结果,控制jsonp、跨域、小驼峰命名和时间格式问题 / </summary> public class ApiResult : ActionResult / <summary> / 返回数据 / </summary> pub

15、lic ResultObject ResultData get; set; / <summary> / 返回数据编码,默认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回调参数名 / </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 ? "*"); 复制代码里面都是一些常规的逻辑,不做说明了,其中的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的子类,来实现自定义日期格式:复制代码 / <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 = "系统异常:" + 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 = "系统异常:" + 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>方法,里面处理了请求参数的验证,把用户信息赋给需要的请求对象,异常记录等。第二个方法是对没有请求参数的api调用处理。第三个方法是异步处理,可以对异步IO处理做一些优化,比如你提供的这个接口是调用的另一个网络接口的情况。 四、权限验证 关于这个问题,我在一篇文章中贴了一些代码,其实只要是知道怎么回事之后,自己可以想怎么玩就怎么玩了,下面

32、讲的的没有涉及角色的权限。根据以往经验,我们可以把资源(也就是一个接口)的权限分为三个等级(标红的第二点很重要,会大大简化后台权限管理的工作):1,公开和访问2,登录用户可访问3,有权限的登录用户可访问所以我们如此设计验证的过滤器:复制代码 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; /可以根据 user和url等信息判断是否有权限 if (!hasPower) filterContext.Result = new ApiResult ResultData

37、= new ResultObject Code = -2, Msg = "无权限" , JsonRequestBehavior = JsonRequestBehavior.AllowGet ; 复制代码AllowAnonymous属性和OnlyLogin属性的功能已经说过了,匿名访问就是公开的,一个系统总会需要这样的接口,登录可访问一般针对安全性比较低,比如字典数据的获取,只要登录了,就可以访问,在权限管理里也不用配置了。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头部都可以取得,这样不管是同域名网页,跨域,app都是可以调用接口的。 五、模型生成以前的模型生产器很多,现在使用T4模板的也不少,而且VS里自带T4模板。但是我不太喜欢用T4(主要是没有智能提示)。我感觉Razor引擎就挺好呀,完全可以用来生成模型。自己写的一个ORM新加了两个方法,来获取数据库表的元数据,目前支持MSSql和MySql,稍微写点代码就可

41、以生成模型了,下面是cshtml的内容,截图是为了展示代码高亮效果,哈哈(完整代码在最下方有下载)所以有时候,自己动动手还是挺好的。其实所有web语言都可以生成,jsp,php,nodejs,和动态生成页面返回给客户端是一样的,这个只不过是写到文件里。 六、文档生成这里自然说的是API文档,和上面那个生成模型不太一样,虽说生成基本上都是:模板+数据=结果,但是这个生成在获取数据的时候有点难点,先看效果图:api文档自动生成的重要性想必大家都知道了,如果还是手动写word或excel,工作量大不说,是很难保持一致性的。 1. webapi 自带一个Help Page 有兴趣可以了解。 2. Sw

42、agger 是个生成api的框架,很强大,也支持接口测试,但是.net下的swagger好像只能使用在webapi中,一般的mvc不行,有兴趣的也可以了解。下面主要说一下本轮子的实现。从一个类型得到一个该类型的对象图,在不严谨的情况下,还是比容易实现的,主要用反射和递归就可以了。上面截图中的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; 复制代码 转换成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. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论