现在实际开发中用webapi来实现Restful接口开发很多,我们项目组前一段时间也在用这东西,发现大家用的还是不那么顺畅,所以这里写一个Demo给大家讲解一下,我的出发点不是如何实现,而是为什么?
首先我们来看看我么的code吧:
control:
public class Users { public int UserID { set; get; } public string UserName { set; get; } public string UserEmail { set; get; } } public class ValuesController : ApiController { private static List<Users> _userList; static ValuesController() { _userList = new List<Users> { new Users {UserID = 1, UserName = "zzl", UserEmail = "[email protected]"}, new Users {UserID = 2, UserName = "Spiderman", UserEmail = "[email protected]"}, new Users {UserID = 3, UserName = "Batman", UserEmail = "[email protected]"} }; } /// <summary> /// User Data List /// </summary> /// <summary> /// 得到列表对象 /// </summary> /// <returns></returns> public IEnumerable<Users> Get() { return _userList; } /// <summary> /// 得到一个实体,根据主键 /// </summary> /// <param name="id"></param> /// <returns></returns> public Users Get(int id) { return _userList.FirstOrDefault();// (i => i.UserID == id); } /// <summary> /// 添加 /// </summary> /// <param name="form">表单对象,它是唯一的</param> /// <returns></returns> public Users Post([FromBody] Users entity) { entity.UserID = _userList.Max(x => x.UserID) + 1; _userList.Add(entity); return entity; } /// <summary> /// 更新 /// </summary> /// <param name="id">主键</param> /// <param name="form">表单对象,它是唯一的</param> /// <returns></returns> public Users Put(int id, [FromBody]Users entity) { var user = _userList.FirstOrDefault(i => i.UserID == id); if (user != null) { user.UserName = entity.UserName; user.UserEmail = entity.UserEmail; } else { _userList.Add(entity); } return user; } /// <summary> /// 删除 /// </summary> /// <param name="id">主键</param> /// <returns></returns> public void Delete(int id) { //_userList.Remove(_userList.FirstOrDefault(i => i.UserID == id)); _userList.Remove(_userList.FirstOrDefault()); } public string Options() { return null; // HTTP 200 response with empty body } }
HTML:
<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <title>web api test</title> </head> <body> <script type="text/javascript" src="js/jquery-1.7.1.js"></script> <script type="text/javascript"> function add() { $.ajax({ url: "http://localhost:6221/api/values/", type: "POST", data: { "UserID": 4, "UserName": "test", "UserEmail": "[email protected]" }, success: function (data) { alert(JSON.stringify(data)); } }); } //更新 function update(id) { $.ajax({ url: "http://localhost:6221/api/values?id=" + id, type: "Put", data: { "UserID": 1, "UserName": "moditest", "UserEmail": "[email protected]" }, success: function (data) { alert(JSON.stringify(data)); } }); } function deletes(id) { $.ajax({ url: "http://localhost:6221/api/values/1", type: "DELETE", success: function (data) { alert(data); } }); } function users() { $.getJSON("http://localhost:6221/api/values", function (data) { alert(JSON.stringify(data)); }); } function user() { $.getJSON("http://localhost:6221/api/values/1", function (data) { alert(JSON.stringify(data)); }); } </script> <fieldset> <legend>测试Web Api </legend> <a href="javascript:add()">添加(post)</a> <a href="javascript:update(1)">更新(put)</a> <a href="javascript:deletes(1)">删除(delete)</a> <a href="javascript:users()">列表(Gets)</a> <a href="javascript:user()">实体(Get)</a> </fieldset> </body> </html>
WebAPI的配置:
<system.webServer> <validation validateIntegratedModeConfiguration="false" /> <httpProtocol> <customHeaders> <add name="Access-Control-Allow-Origin" value="*" /> <add name="Access-Control-Allow-Headers" value="Content-Type" /> <add name="Access-Control-Allow-Methods" value="GET, POST, PUT, DELETE, OPTIONS" /> </customHeaders> </httpProtocol>
首先说明一下,配置中的httpProtocol和control中的Options都是在跨域的时候才需要的。
问题1.
Get,Post,Put,Delete,Options 这几个方法 ,服务器端是怎么来定位的, 或者说服务器是如何确定是调用哪个Action?
其实我以前的文章 Asp.net web Api源码分析-HttpActionDescriptor的创建 中有提到,这里简单回忆一下:
首先我们客户端的请求Url中都是 http://localhost:6221/api/values/ 打头,这里的values就是我们的Control,这样我们就可以很容易找到这个control下面的方法。主要的类是ApiControllerActionSelector,在它里面有一个子类ActionSelectorCacheItem, 其构造函数就负责初始化control里面的ReflectedHttpActionDescriptor,
MethodInfo[] allMethods = _controllerDescriptor.ControllerType.GetMethods(BindingFlags.Instance | BindingFlags.Public);
MethodInfo[] validMethods = Array.FindAll(allMethods, IsValidActionMethod);
_actionDescriptors = new ReflectedHttpActionDescriptor[validMethods.Length];
for (int i = 0; i < validMethods.Length; i++)
{
MethodInfo method = validMethods[i];
ReflectedHttpActionDescriptor actionDescriptor = new ReflectedHttpActionDescriptor(_controllerDescriptor, method);
_actionDescriptors[i] = actionDescriptor;
HttpActionBinding actionBinding = actionDescriptor.ActionBinding;
// Building an action parameter name mapping to compare against the URI parameters coming from the request. Here we only take into account required parameters that are simple types and come from URI.
_actionParameterNames.Add(
actionDescriptor,
actionBinding.ParameterBindings
.Where(binding => !binding.Descriptor.IsOptional && TypeHelper.IsSimpleUnderlyingType(binding.Descriptor.ParameterType) && binding.WillReadUri())
.Select(binding => binding.Descriptor.Prefix ?? binding.Descriptor.ParameterName).ToArray());
}
_actionNameMapping = _actionDescriptors.ToLookup(actionDesc => actionDesc.ActionName, StringComparer.OrdinalIgnoreCase);
int len = _cacheListVerbKinds.Length;
_cacheListVerbs = new ReflectedHttpActionDescriptor[len][];
for (int i = 0; i < len; i++)
{
_cacheListVerbs[i] = FindActionsForVerbWorker(_cacheListVerbKinds[i]);
}
这里的validMethods 就是我们定义6个方法(2个Get,Post,Put,Delete,Options),在ReflectedHttpActionDescriptor里面的InitializeProperties 的实现如下:
private void InitializeProperties(MethodInfo methodInfo)
{
_methodInfo = methodInfo;
_returnType = GetReturnType(methodInfo);
_actionExecutor = new Lazy<ActionExecutor>(() => InitializeActionExecutor(_methodInfo));
_attrCached = _methodInfo.GetCustomAttributes(inherit: true);
CacheAttrsIActionMethodSelector = _attrCached.OfType<IActionMethodSelector>().ToArray();
_actionName = GetActionName(_methodInfo, _attrCached);
_supportedHttpMethods = GetSupportedHttpMethods(_methodInfo, _attrCached);
}
private static Collection<HttpMethod> GetSupportedHttpMethods(MethodInfo methodInfo, object[] actionAttributes) { Collection<HttpMethod> supportedHttpMethods = new Collection<HttpMethod>(); ICollection<IActionHttpMethodProvider> httpMethodProviders = TypeHelper.OfType<IActionHttpMethodProvider>(actionAttributes); if (httpMethodProviders.Count > 0) { // Get HttpMethod from attributes foreach (IActionHttpMethodProvider httpMethodSelector in httpMethodProviders) { foreach (HttpMethod httpMethod in httpMethodSelector.HttpMethods) { supportedHttpMethods.Add(httpMethod); } } } else { // Get HttpMethod from method name convention for (int i = 0; i < _supportedHttpMethodsByConvention.Length; i++) { if (methodInfo.Name.StartsWith(_supportedHttpMethodsByConvention[i].Method, StringComparison.OrdinalIgnoreCase)) { supportedHttpMethods.Add(_supportedHttpMethodsByConvention[i]); break; } } } if (supportedHttpMethods.Count == 0) { // Use POST as the default HttpMethod supportedHttpMethods.Add(HttpMethod.Post); } return supportedHttpMethods; } private static readonly HttpMethod[] _supportedHttpMethodsByConvention = { HttpMethod.Get, HttpMethod.Post, HttpMethod.Put, HttpMethod.Delete, HttpMethod.Head, HttpMethod.Options, new HttpMethod("PATCH") };
GetSupportedHttpMethods判断当前action支持的请求类型,首先读取HttpMethod attributes,如果没有我们就读取action的name(Get,Post,Put,Delete,Options),所以put 方法支持put httpmethod。实在没有httpmethod就添加默认的post。
现在我们来看看_cacheListVerbs里面放的是什么东西?
private readonly HttpMethod[] _cacheListVerbKinds = new HttpMethod[] { HttpMethod.Get, HttpMethod.Put, HttpMethod.Post };
private ReflectedHttpActionDescriptor[] FindActionsForVerbWorker(HttpMethod verb)
{
List<ReflectedHttpActionDescriptor> listMethods = new List<ReflectedHttpActionDescriptor>();
foreach (ReflectedHttpActionDescriptor descriptor in _actionDescriptors)
{
if (descriptor.SupportedHttpMethods.Contains(verb))
{
listMethods.Add(descriptor);
}
}
return listMethods.ToArray();
}
到这里么知道_cacheListVerbs里面放的就是Get,Put,Post对应的action,方便后面通过http request type来查找action。
现在action list已经准备好了,然后确定该调用哪个了?在ActionSelectorCacheItem类里面有SelectAction。主要逻辑如下:
string actionName;
bool useActionName = controllerContext.RouteData.Values.TryGetValue(ActionRouteKey, out actionName);
ReflectedHttpActionDescriptor[] actionsFoundByHttpMethods;
HttpMethod incomingMethod = controllerContext.Request.Method;
// First get an initial candidate list.
if (useActionName)
{
.......................................
}
else
{
// No {action} parameter, infer it from the verb.
actionsFoundByHttpMethods = FindActionsForVerb(incomingMethod);
}
// Throws HttpResponseException with MethodNotAllowed status because no action matches the Http Method
if (actionsFoundByHttpMethods.Length == 0)
{
throw new HttpResponseException(controllerContext.Request.CreateErrorResponse(
HttpStatusCode.MethodNotAllowed,
Error.Format(SRResources.ApiControllerActionSelector_HttpMethodNotSupported, incomingMethod)));
}
// Make sure the action parameter matches the route and query parameters. Overload resolution logic is applied when needed.
IEnumerable<ReflectedHttpActionDescriptor> actionsFoundByParams = FindActionUsingRouteAndQueryParameters(controllerContext, actionsFoundByHttpMethods, useActionName);
首先从路由里面获取actionname,restful请求地址都不含有actionname, 那么就从请求type里面获取action了,即这里的FindActionsForVerb方法,该方法首先从_cacheListVerbs里面找,如果没有找到再在当前control的所有action里面找,比如Delete,Options在_cacheListVerbs是没有的。 如果通过FindActionsForVerb找到的action是多个,那么久需要通过FindActionUsingRouteAndQueryParameters方法来过滤了,该方法首先读取route和query参数,查看是否满足action需要的参数。
如这里的get action,如果请求地址是http://localhost:6221/api/values 这个,那么Get(int id)肯定要被过滤掉,因为它需要参数id,但是这里没有参数id,所以只能返回Get() 了。如果地址http://localhost:6221/api/values/1的话,那么这里的2个action都满足条件 ,我们就取参数多的那个action。
if (actionsFound.Count() > 1)
{
// select the results that match the most number of required parameters
actionsFound = actionsFound
.GroupBy(descriptor => _actionParameterNames[descriptor].Length)
.OrderByDescending(g => g.Key)
.First();
}
到这里大家就应该知道后台是如何获取action的了吧。一句话,把Request.Method作为actionname。
2.浏览器跨域问题。
其实网上已经有很多说明:
在网上找的这张图,并不是所有的跨域请求 都有Options预请求,简单跨域是不需要。
一个简单的请求应该满足如下要求:
1.请求方法为GET,POST 这里是否包含HEAD我不怎么清楚,没测试过,还有HEAD我实际也没有用到
2.请求方法中没有设置请求头(Accept, Accept-Language, Content-Language, Content-Type除外)如果设置了Content-Type头,其值为application/x-www-form-urlencoded, multipart/form-data或 text/plain
常用的复杂请求是:发送PUT
、DELETE
等HTTP动作,或者发送Content-Type: application/json
的内容,来看看预请求的请求头和返回头:
Access-Control-Allow-Origin
(必含)- 不可省略,否则请求按失败处理。该项控制数据的可见范围,如果希望数据对任何人都可见,可以填写“*”。Access-Control-Allow-Methods
(必含) – 这是对预请求当中Access-Control-Request-Method
的回复,这一回复将是一个以逗号分隔的列表。尽管客户端或许只请求某一方法,但服务端仍然可以返回所有允许的方法,以便客户端将其缓存。Access-Control-Allow-Headers
(当预请求中包含Access-Control-Request-Headers
时必须包含) – 这是对预请求当中Access-Control-Request-Headers
的回复,和上面一样是以逗号分隔的列表,可以返回所有支持的头部。Access-Control-Allow-Credentials(可选) – 该项标志着请求当中是否包含cookies信息,只有一个可选值:
true
(必为小写)。如果不包含cookies,请略去该项,而不是填写false
。这一项与XmlHttpRequest2
对象当中的withCredentials
属性应保持一致,即withCredentials
为true
时该项也为true
;withCredentials
为false
时,省略该项不写。反之则导致请求失败。Access-Control-Max-Age
(可选) – 以秒为单位的缓存时间。预请求的的发送并非免费午餐,允许时应当尽可能缓存。
一旦预回应如期而至,所请求的权限也都已满足,则实际请求开始发送。
Credentials
在跨域请求中,默认情况下,HTTP Authentication信息,Cookie头以及用户的SSL证书无论在预检请求中或是在实际请求都是不会被发送的。但是,通过设置XMLHttpRequest的credentials为true,就会启用认证信息机制。
虽然简单请求还是不需要发送预检请求,但是此时判断请求是否成功需要额外判断Access-Control-Allow-Credentials,如果Access-Control-Allow-Credentials为false,请求失败。
十分需要注意的的一点就是此时Access-Control-Allow-Origin不能为通配符"*"(真是便宜了一帮偷懒的程序员),如果Access-Control-Allow-Origin是通配符"*"的话,仍将认为请求失败
即便是失败的请求,如果返回头中有Set-Cookie的头,浏览器还是会照常设置Cookie。
有不当之处欢迎拍砖。