写给新手的WebAPI实践.docx
《写给新手的WebAPI实践.docx》由会员分享,可在线阅读,更多相关《写给新手的WebAPI实践.docx(27页珍藏版)》请在冰豆网上搜索。
写给新手的WebAPI实践
写给新手的WebAPI实践
此篇是写给新手的Demo,用于参考和借鉴,用于发散思路。
老鸟可以忽略了。
自己经常有这种情况,遇到一个新东西或难题,在了解和解决之前总是说“等搞定了一定要写篇文章记录下来”,但是当掌握了之后,就感觉好简单呀不值得写下来了。
其实这篇也一样,决定写下来是想在春节前最后再干一件正经事儿,不能天天回去打Dota了!
目录:
请求响应的设计
请求的Content-Type和模型绑定
自定义ApiResult和ApiControllerBase
权限验证
模型生成
文档生成
一、请求响应的设计
RESTFul风格响亮很久了,但是我没用过,以后也不打算用。
当系统稍微复杂时,为了符合RESTFul要吃力地创建一些不直观的名词,这不是我的风格。
所以此文设计的不是RESTFul风格,是只最常用的POST和GET请求。
请求部分就是调用API的参数,抽象出一个接口如下:
publicinterfaceIRequest
{
ResultObjectValidate();
}
这里面只定义了一个Validate()方法,用于验证请求参数的有效性,返回值是响应里的东西,下面会讲到。
对于请求对象,传递到业务逻辑层,甚至是数据访问层都可以,因为它本身就是用来传输数据的,俗话叫DTO(DataTransferObject),不过定义多层传输对象,然后复制来复制去也是可以的~。
但是有时候业务处理会需要当前登录人的信息,而这个信息我并不希望直接从接口层向下传递,所以这里我再抽象一个UserRequestBase,用于附加登录人相关信息:
复制代码
publicabstractclassUserRequestBase:
IRequest
{
publicintApiUserID{get;set;}
publicstringApiUserName{get;set;}
//......可以添加其他要专递的登录用户相关的信息
publicabstractResultObjectValidate();
}
复制代码
ApiUserID和ApiUserName这样的字段是不需要客户端传递的,我们会根据登录人信息自动填充。
根据实际中的经验,我们往往会做分页查询,会用到页码和每页条数,所为我们再定义个PageRequestBase:
publicabstractclassPageRequestBase:
UserRequestBase
{
publicintPageIndex{get;set;}
publicintPageSize{get;set;}
}
因为.net只能继承单个父类,而且有些分页查询可能需要用户信息,所以我们选择继承UserRequestBase。
当然,还可以根据自己的实际情况抽象出更多的公用类,在这不一一枚举。
响应的设计分为两部分,第一个是实际响应部分,第二个会把响应包装一下,加上code和msg,用于表示调用状态和错误信息(好老的方法了,大家都懂)。
响应接口IResponse里什么也没有,就是一个标记接口,不过我们也可以抽象出来两个常用的公用类用于响应列表和分页数据:
复制代码
publicclassListResponseBase:
IResponse
{
publicListList{get;set;}
}
publicclassPageResponseBase:
ListResponseBase
{
///
///页码数
///
publicintPageIndex{get;set;}
///
///总条数
///
publiclongTotalCount{get;set;}
///
///每页条数
///
publicintPageSize{get;set;}
///
///总页数
///
publiclongPageCount{get;set;}
}
复制代码
包装响应的时候,有两种情况,第一种是操作类接口,比如添加商品,这些接口是不用响应对象的,只要返回是否成功就行了,第二种查询类,这个时候必须要返回一些具体的东西了,所以响应包装设计成两个类:
复制代码
publicclassResultObject
{
///
///等于0表示成功
///
publicintCode{get;set;}
///
///code不为0时,返回错误消息
///
publicstringMsg{get;set;}
}
publicclassResultObject:
ResultObjectwhereTResponse:
IResponse
{
publicResultObject()
{
}
publicResultObject(TResponsedata)
{
Data=data;
}
///
///返回的数据
///
publicTResponseData{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形式的字符串了,可直接反序列化成后端对象。
然而这些考虑,.netmvc框架已经帮我们做好了,这都要归功于DefaultModelBinder。
关于Form表单形式的请求,可以参见这位园友的文章:
你从未知道如此强大的ASP.NETMVCDefaultModelBinder
我这里想说的是,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调用、跨域调用、序列化的小驼峰命名和时间格式问题。
复制代码
///
///api返回结果,控制jsonp、跨域、小驼峰命名和时间格式问题
///
publicclassApiResult:
ActionResult
{
///
///返回数据
///
publicResultObjectResultData{get;set;}
///
///返回数据编码,默认utf8
///
publicEncodingContentEncoding{get;set;}
///
///是否接受Get请求,默认允许
///
publicJsonRequestBehaviorJsonRequestBehavior{get;set;}
///
///是否允许跨域请求
///
publicboolAllowCrossDomain{get;set;}
///
///jsonp回调参数名
///
publicstringJsonpCallbackName="callback";
publicApiResult():
this(null)
{
}
publicApiResult(ResultObjectresultData)
{
this.ResultData=resultData;
ContentEncoding=Encoding.UTF8;
JsonRequestBehavior=JsonRequestBehavior.AllowGet;
AllowCrossDomain=true;
}
publicoverridevoidExecuteResult(ControllerContextcontext)
{
varresponse=context.HttpContext.Response;
varrequest=context.HttpContext.Request;
response.ContentEncoding=ContentEncoding;
response.ContentType="text/plain";
if(ResultData!
=null)
{
stringbuffer;
if((JsonRequestBehavior==JsonRequestBehavior.DenyGet)&&string.Equals(context.HttpContext.Request.HttpMethod,"GET"))
{
buffer="该接口不允许Get请求";
}
else
{
varjsonpCallback=request[JsonpCallbackName];
if(string.IsNullOrWhiteSpace(jsonpCallback))
{
//如果可以跨域,写入响应头
if(AllowCrossDomain)
{
WriteAllowAccessOrigin(context);
}
response.ContentType="application/json";
buffer=JsonConvert.SerializeObject(ResultData,JsonSetting.Settings);
}
else
{
//jsonp
if(AllowCrossDomain)//这个判断可能非必须
{
response.ContentType="text/javascript";
buffer=string.Format("{0}({1});",jsonpCallback,JsonConvert.SerializeObject(ResultData,JsonSetting.Settings));
}
else
{
buffer="该接口不允许跨域请求";
}
}
}
try
{
response.Write(buffer);
}
catch(Exceptionexp)
{
response.Write(exp.Message);
}
}
else
{
response.Write("ApiResult.Data为null");
}
response.End();
}
///
///写入跨域请求头
///
///
privatevoidWriteAllowAccessOrigin(ControllerContextcontext)
{
varorigin=context.HttpContext.Request.Headers["Origin"];
if(true)//可以维护一个允许跨域的域名集合,类判断是否可以跨域
{
context.HttpContext.Response.Headers.Add("Access-Control-Allow-Origin",origin?
?
"*");
}
}
}
复制代码
里面都是一些常规的逻辑,不做说明了,其中的JsonSetting就是设置序列化的小驼峰和日期格式的:
复制代码
publicclassJsonSetting
{
publicstaticJsonSerializerSettingsSettings=newJsonSerializerSettings
{
ContractResolver=newCamelCasePropertyNamesContractResolver(),
DateFormatString="yyyy-MM-ddHH:
mm:
ss",
};
}
复制代码
这个时候有个问题,如果一个时间的字段需要"yyyy-MM-dd"这种格式怎么办呢?
这个时候要定义一个JsonConverter的子类,来实现自定义日期格式:
复制代码
///
///日期格式化器
///
publicclassCustomDateConverter:
DateTimeConverterBase
{
privateIsoDateTimeConverterdtConverter=newIsoDateTimeConverter{};
publicCustomDateConverter(stringformat)
{
dtConverter.DateTimeFormat=format;
}
publicCustomDateConverter():
this("yyyy-MM-dd"){}
publicoverrideobjectReadJson(JsonReaderreader,TypeobjectType,objectexistingValue,JsonSerializerserializer)
{
returndtConverter.ReadJson(reader,objectType,existingValue,serializer);
}
publicoverridevoidWriteJson(JsonWriterwriter,objectvalue,JsonSerializerserializer)
{
dtConverter.WriteJson(writer,value,serializer);
}
}
复制代码
在需要的响应属性上加上[JsonConverter(typeof(CustomDateConverter))]或[JsonConverter(typeof(CustomDateConverter),"yyyy年MM月dd日")]即可。
ApiResult定义好了,再定义一个控制器基类,目的是便于处理ApiResult:
复制代码
///
///API控制器基类
///
publicclassApiControllerBase:
AsyncController
{
publicApiResultApi(TRequestrequest,Funchandle)
{
try
{
varrequestBase=requestasIRequest;
if(requestBase!
=null)
{
//处理需要登录用户的请求
varuserRequest=requestasUserRequestBase;
if(userRequest!
=null)
{
varloginUser=LoginUser.GetUser();
if(loginUser!
=null)
{
userRequest.ApiUserID=loginUser.UserID;
userRequest.ApiUserName=loginUser.UserName;
}
}
varvalidResult=requestBase.Validate();
if(validResult!
=null)
{
returnnewApiResult(validResult);
}
}
varresult=handle(request);//处理请求
returnnewApiResult(result);
}
catch(Exceptionexp)
{
//异常日志:
returnnewApiResult{ResultData=newResultObject{Code=1,Msg="系统异常:
"+exp.Message}};
}
}
publicApiResultApi(Funchandle)
{
try
{
varresult=handle();//处理请求
returnnewApiResult(result);
}
catch(Exceptionexp)
{
//异常日志
returnnewApiResult{ResultData=newResultObject{Code=1,Msg="系统异常:
"+exp.Message}};
}
}
///
///异步api
///
///
///
///
///
publicTaskApiAsync(TRequestrequest,Func>handle)whereTResponse:
ResultObject
{
returnhandle(request).ContinueWith(x=>
{
returnApi(()=>x.Result);
});
}
}
复制代码
最常用的应该就是第一个Api方法,里面处理了请求参数的验证,把用户信息赋给需要的请求对象,异常记录等。
第二个方法是对没有请求参数的api调用处理。
第三个方法是异步处理,可以对异步IO处理做一些优化,比如你提供的这个接口是调用的另一个网络接口的情况。
四、权限验证
关于这个问题,我在一篇文章中贴了一些代码,其实只要是知道怎么回事之后,自己可以想怎么玩就怎么玩了,下面讲的的没有涉及角色的权限。
根据以往经验,我们可以把资源(也就是一个接口)的权限分为三个等级(标红的第二点很重要,会大大简化后台权限管理的工作):
1,公开和访问
2,登录用户可访问
3,有权限的登录用户可访问
所以我们如此设计验证的过滤器:
复制代码
publicclassAuthFilterAttribute:
ActionFilterAttribute
{
///
///匿名可访问
///