ASP.NET Web API基础(01)---初识 ASP.NET Web API
1.1 什么是Web API
WebApi是一个很广泛的概念,ASP.NET Web API是一个在.NET框架上构建Web API的框架。用于轻松构建可以由多种客户端(包括浏览器和移动设备)访问的 HTTP 服务。它是一种RestFul风格的开发接口的技术,它比WebService更省流量,比WCF更简单。
Web API在ASP.NET完整框架中地位如下图,与SignalR一起同为构建Service的框架。Web API负责构建http常规服务,而SingalR主要负责的是构建实时服务,例如股票,聊天室,在线游戏等实时性要求比较高的服务。
- 支持RestFul风格的的请求,即可以通过 Get、Post、Put、Delete请求CRUD操作,不需要暴露更多的API。
- 方法的返回值可以直接返回对象,专注于数据。
- 独立于IIS部署(selfhost、winform、控制台)。
- 方法可以直接声明为async。
- 支持MVC大部分功能,但和MVC在路由、Filter、ModelBinder等方面都有一些区别.(这里指.Net FrameWork)。
- MVC主要用来构建网站,所以既关心数据又关心页面,而WebApi主要用来构建接口,所以只关心数据。
- WebApi支持Self Host,即独立于IIS部署,MVC不支持。(.Net Core 下的MVC是支持的)
- WebApi通过请求方式来区分请求哪个接口,MVC则需要通过Action的名字来确定。
- WebApi位于System.Web.Http命名空间下,而MVC位于System.Web.Mvc命名空间下,所以二者在路由、Filter、ModelBinder等方面都有一些区别。
- WebApi更轻量更适合构建App的接口服务。
在VS中创建 Asp.net Web Application 项目,项目模板中选择Web API。、
新生成的WebAPI项目和典型的MVC项目一样,包含主要的Models,Views,Controllers等文件夹和Global.asax文件
Controllers里面有一个ValuesController,是自动生成的一个最简单的Web API Controller。
正如我们前面所说,里面引用的是System.Web.Http命名空间。
默认情况下,模板自带了两个路由规则,分别对应于WebAPI和普通MVC的Web请求,默认的WebAPI路由规则如下
routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
可以看到,默认路由使用的固定的api作为Uri的先导,按照微软官方的说法,用于区分普通Web请求和WebService的请求路径:
默认的路由规则只指向了Controller,没有指向具体的Action,因为默认情况下,对于Controller中的Action的匹配是和Action的方法名相关联的:具体来说,如果使用上面的路由规则,对应下面的Controller:
public class ContactController : ApiController { static List<Contact> contacts = new List<Contact>() { new Contact{Name="张三", Id="1", Address="邯郸", Email="", Phone="43665434"}, new Contact{Name="李四", Id="102", Address="邯郸", Email="", Phone="13454565"} };
public IEnumerable<Contact> Get() { return contacts; }
public Contact Get(string id) { return contacts.FirstOrDefault(n => n.Id == id); }
public bool Post(Contact contact) { contacts.Add(contact); return true; }
public bool Put(Contact contact) { var ct = contacts.FirstOrDefault(n => n.Id == contact.Id); if (ct != null) { ct.Name = contact.Name; ct.Email = contact.Email; ct.Phone = contact.Phone; ct.Address = contact.Address; return true; } return false; }
public bool Delete(string id) { return contacts.Remove(contacts.FirstOrDefault(n => n.Id == id)); } } |
则,会有下面的对应关系:
URL | HttpMethod | 对应的Action名 |
/api/Contact | GET | Get() |
/apiContact/1 | GET | Get(string id) |
/api/Contact | POST | Post() |
/api/Contact/1 | DELETE | Delete(string id) |
/api/Contact | DELETE | Delete() |
/api/Contact | PUT | Put() |
客户端代码:
<script src="Scripts/jquery-3.3.1.min.js"></script> <script> //查询全部 function getAll() { $.ajax({ url: "api/contact/", type: "get", success: function (result) { $('tbody').html(""); $(result).each(function (index, ele) { var li = "<tr><td>" + ele.Id + "</td><td>" + ele.Name + "</td><td>" + ele.Phone + "</td><td>" + ele.Address + "</td><td><button onclick='getItem(" + ele.Id + ")'>修改</button><button onclick='deleteItem(" + ele.Id+")'>删除</button></td></tr>" $('tbody').append($(li)); }); } }) } //删除 function deleteItem(id) { $.ajax({ url: "api/contact/" + id, type: "delete", success: function () { getAll(); } }) } //查询单个 function getItem(id) { $.ajax({ url: "api/contact/" + id, type: "get", success: function (data) { $('#Id').val(data.Id); $('#Name').val(data.Name); $('#Phone').val(data.Phone); $('#Address').val(data.Address); }, fail: function (xhr, textStatus, err) { alert('Error: ' + err); } }) }
$(function () { getAll(); //添加 $('#addItem').click(function () { var id = $('#Id').val(); var name = $('#Name').val(); var phone = $('#Phone').val(); var address = $('#Address').val(); $.ajax({ url: "api/Contact/", type: "post", dataType: "json", data: { "Id": id, "Name": name, "Phone": phone, "Address": address }, success: function () { getAll(); } }) }) //修改 $('#EditItem').click(function () { var id = $('#Id').val(); var name = $('#Name').val(); var phone = $('#Phone').val(); var address = $('#Address').val(); $.ajax({ url: "api/Contact/", type: "put", dataType: "json", data: { "Id": id, "Name": name, "Phone": phone, "Address": address }, success: function () { getAll(); } }) }) }) </script> |
这样就实现了最基本的CRUD操作。
问题1:我想按照用户名称(UserName)进行查询,怎么办? 答:第一步:在UserController类中加一个方法名称叫:GetUserByName,如下所示: public UserModel GetUserByName(string userName) { return contacts.Find((m) => { return m.UserName.Equals(userName); }); } 第二步:在客户端index.cshtml中调用 如果URL是: url: "api/User/zhang",将会报错:Bad Request 原因是他会自动调用我们的Get(string id) 这个方法,类型转换出错 解决办法: 改变URL为: url: "api/User/?userName=zhang", |
1.3 Restful 简介
网络应用程序,分为前端和后端两个部分。当前的发展趋势,就是前端设备层出不穷(手机、平板、桌面电脑、其他专用设备......),因此,必须有一种统一的机制,方便不同的前端设备与后端进行通信。这导致API构架的流行,甚至出现"API First"的设计思想。RESTful API是目前比较成熟的一套互联网应用程序的API设计理论。在此,我们将介绍RESTful API的设计细节,探讨如何设计一套合理、好用的API。
1.3.1 什么是REST
REST全称是Representational State Transfer,中文意思是表述(通常译为表征)性状态转移。 它首次出现在2000年Roy Fielding的博士论文中,Roy Fielding是HTTP规范的主要编写者之一。 他在论文中提到:"我这篇文章的写作目的,就是想在符合架构原理的前提下,理解和评估以网络为基础的应用软件的架构设计,得到一个功能强、性能好、适宜通信的架构。REST指的是一组架构约束条件和原则。" 如果一个架构符合REST的约束条件和原则,我们就称它为RESTful架构。
REST本身并没有创造新的技术、组件或服务,而隐藏在RESTful背后的理念就是使用Web的现有特征和能力, 更好地使用现有Web标准中的一些准则和约束。虽然REST本身受Web技术的影响很深, 但是理论上REST架构风格并不是绑定在HTTP上,只不过目前HTTP是唯一与REST相关的实例。 所以我们这里描述的REST也是通过HTTP实现的REST。
1.3.2 Restful API 设计
RESTful架构应该遵循统一接口原则,统一接口包含了一组受限的预定义的操作,不论什么样的资源,都是通过使用相同的接口进行资源的访问。接口应该使用标准的HTTP方法如GET,PUT和POST,并遵循这些方法的语义。
1、协议
API与用户的通信协议,总是使用HTTPs协议。
2、域名
应该尽量将API部署在专用域名之下。
例如:https://api.example.com
如果确定API很简单,不会有进一步扩展,可以考虑放在主域名下。
3、版本(Versioning)
应该将API的版本号放入URL。
例如:https://api.example.com/v1/
另一种做法是,将版本号放在HTTP头信息中,但不如放入URL方便和直观。Github采用这种做法。
4、路径(Endpoint)
路径又称"终点"(endpoint),表示API的具体网址。
在RESTful架构中,每个网址代表一种资源(resource),所以网址中不能有动词,只能有名词,而且所用的名词往往与数据库的表格名对应。一般来说,数据库中的表都是同种记录的"集合"(collection),所以API中的名词也应该使用复数。
举例来说,有一个API提供动物园(zoo)的信息,还包括各种动物和雇员的信息,则它的路径应该设计成下面这样。
https://api.example.com/v1/zoos
https://api.example.com/v1/animals
https://api.example.com/v1/employees
5、HTTP动词
对于资源的具体操作类型,由HTTP动词表示。常用的HTTP动词有下面五个(括号里是对应的SQL命令)。
- GET(SELECT):从服务器取出资源(一项或多项)。
- POST(CREATE):在服务器新建一个资源。
- PUT(UPDATE):在服务器更新资源(客户端提供改变后的完整资源)。
- PATCH(UPDATE):在服务器更新资源(客户端提供改变的属性)。
- DELETE(DELETE):从服务器删除资源。
还有两个不常用的HTTP动词。
- HEAD:获取资源的元数据。
- OPTIONS:获取信息,关于资源的哪些属性是客户端可以改变的。
下面是一些例子。
GET /zoos:列出所有动物园
POST /zoos:新建一个动物园
GET /zoos/ID:获取某个指定动物园的信息
PUT /zoos/ID:更新某个指定动物园的信息(提供该动物园的全部信息)
PATCH /zoos/ID:更新某个指定动物园的信息(提供该动物园的部分信息)
DELETE /zoos/ID:删除某个动物园
GET /zoos/ID/animals:列出某个指定动物园的所有动物
DELETE /zoos/ID/animals/ID:删除某个指定动物园的指定动物
6、过滤信息(Filtering)
如果记录数量很多,服务器不可能都将它们返回给用户。API应该提供参数,过滤返回结果。
下面是一些常见的参数。
?limit=10:指定返回记录的数量
?offset=10:指定返回记录的开始位置。
?page=2&per_page=100:指定第几页,以及每页的记录数。
?sortby=name&order=asc:指定返回结果按照哪个属性排序,以及排序顺序。
?animal_type_id=1:指定筛选条件
参数的设计允许存在冗余,即允许API路径和URL参数偶尔有重复。比如,GET /zoo/ID/animals 与 GET /animals?zoo_id=ID 的含义是相同的。
7、状态码(Status Codes)
服务器向用户返回的状态码和提示信息,常见的有以下一些(方括号中是该状态码对应的HTTP动词)。
200 OK - [GET]:服务器成功返回用户请求的数据,该操作是幂等的(Idempotent)。
201 CREATED - [POST/PUT/PATCH]:用户新建或修改数据成功。
202 Accepted - [*]:表示一个请求已经进入后台排队(异步任务)
204 NO CONTENT - [DELETE]:用户删除数据成功。
400 INVALID REQUEST - [POST/PUT/PATCH]:用户发出的请求有错误,服务器没有进行新建或修改数据的操作,该操作是幂等的。
401 Unauthorized - [*]:表示用户没有权限(令牌、用户名、密码错误)。
403 Forbidden - [*] 表示用户得到授权(与401错误相对),但是访问是被禁止的。
404 NOT FOUND - [*]:用户发出的请求针对的是不存在的记录,服务器没有进行操作,该操作是幂等的。
406 Not Acceptable - [GET]:用户请求的格式不可得(比如用户请求JSON格式,但是只有XML格式)。
410 Gone -[GET]:用户请求的资源被永久删除,且不会再得到的。
422 Unprocesable entity - [POST/PUT/PATCH] 当创建一个对象时,发生一个验证错误。
500 INTERNAL SERVER ERROR - [*]:服务器发生错误,用户将无法判断发出的请求是否成功。
8、错误处理(Error handling)
如果状态码是4xx,就应该向用户返回出错信息。一般来说,返回的信息中将error作为键名,出错信息作为键值即可。
{
error: "Invalid API key"
}
9、返回结果
针对不同操作,服务器向用户返回的结果应该符合以下规范。
GET /collection:返回资源对象的列表(数组)
GET /collection/resource:返回单个资源对象
POST /collection:返回新生成的资源对象
PUT /collection/resource:返回完整的资源对象
PATCH /collection/resource:返回完整的资源对象
DELETE /collection/resource:返回一个空文档
10、Hypermedia API
RESTful API最好做到Hypermedia,即返回结果中提供链接,连向其他API方法,使得用户不查文档,也知道下一步应该做什么。
比如,当用户向api.example.com的根目录发出请求,会得到这样一个文档。
{"link": {
"rel": "collection https://www.example.com/zoos",
"href": "https://api.example.com/zoos",
"title": "List of zoos",
"type": "application/vnd.yourformat+json"
}}
上面代码表示,文档中有一个link属性,用户读取这个属性就知道下一步该调用什么API了。rel表示这个API与当前网址的关系(collection关系,并给出该collection的网址),href表示API的路径,title表示API的标题,type表示返回类型。
Hypermedia API的设计被称为HATEOAS。Github的API就是这种设计,访问api.github.com会得到一个所有可用API的网址列表。
{
"current_user_url": "https://api.github.com/user",
"authorizations_url": "https://api.github.com/authorizations",
// ...
}
从上面可以看到,如果想获取当前用户的信息,应该去访问api.github.com/user,然后就得到了下面结果。
{
"message": "Requires authentication",
"documentation_url": "https://developer.github.com/v3"
}
上面代码表示,服务器给出了提示信息,以及文档的网址。
11、其他
(1)API的身份认证应该使用OAuth 2.0框架。
(2)服务器返回的数据格式,应该尽量使用JSON,避免使用XML。
问题1:什么是RestFul风格的接口? (1).调用者不再根据方法名称区分请求方法,而是通过请求方式进行区分,将所有的操作都抽象成对资源的增删改查。 即:新增用Post请求,查询用get请求,修改用put请求,删除用delete请求。 (2).请求的路径中将不在出现方法名称 (3).用Http的返回状态码表示服务器端的处理结果,eg:找不到用404、没有权限返回201,不在需要自己约定状态码。 (4).有利于系统优化,浏览器可以自动缓存Get请求。 问题2:RestFul风格的弊端 理论性太强,很容易把初学者搞晕,比如 如何通过状态码区分是"账号错误"还是"密码错误"? 比如"登录"属于什么操作?再比如 一个控制器里有两个方法 GetM1() 和 GetM2(), 两个方法中都没有参数值,这种情况如何通过请求方式来区分呢?再比如 下面的GetStudets(string str) 和 GetTeachers(string str) 方法,参数名都是str,所以无法区分(可以把其中一个参数名改为str2,就不冲突了,或者通过别的路由规则声明一下,或者给标注个非Get请求的标记,比如[HttpPost]总之很麻烦) |
1.4 生成API 帮助文档
1.4.1 生成API文档
通过观察,发现WebApi项目中Area文件夹下有一个HelpPage文件夹,如下图,该文件夹就是WebApi自带的生成Api的方式,如果该文件夹没了,可以通过Nuget安装:Microsoft.AspNet.WebApi.HelpPage ,你就会发现下图这一坨代码又回来了。
使用:http://localhost:2131/Help/Index , 即可访问生成的Api目录,如下图:
你会发现方法名的注释和参数的注释均不显示,这对使用者而言,相当不放方便了。
改进支持参数的注释
(1). 选中项目,右键属性,填写生成xml文件的路径,如下图: bin\api.xml
(2). 找到 Areas/HelpPage/App_Start 目录下的HelpPageConfig.cs 文件,Register 方法,添加一行代码:
config.SetDocumentationProvider(new XmlDocumentationProvider(AppDomain.CurrentDomain.BaseDirectory + "bin\\api.xml"));
(3).再次访问 http://localhost:2131/Help/Index ,发现无论是方法名还是参数名,均有描述了
上述通过改进,已经生成比较完善的Api文档了,但美中不足的是不能直接测试,当然可以整合别的控件使其支持,但比较麻烦,不如使用下面的SwashBuckle生成SwaggerUI形式的Api文档。
1.4.2 借助SwaggerUI生成API文档
1. 通过Nuget安装 Swashbuckle (版本:5.6.0)程序集,会发现在 App_Start 文件夹下生成一个 SwaggerConfig.cs 配置文件,用于配置 SwaggerUI 相关展示行为的,如下图:
2. 选中项目,右键属性,勾上xml文档文件,注意:这里默认是什么就保留什么,不要在自己改了 。如下图: bin\05-WebApiExtend.xml
3. 在SwaggerConfig.cs文件中 搜索 c.IncludeXmlComments(GetXmlCommentsPath()); ,在这句话的下面新增一句代码:
c.IncludeXmlComments(AppDomain.CurrentDomain.BaseDirectory + "bin\\05-WebApiExtend.xml");
4. 访问:http://localhost:2182/swagger/ui/index ,即可查看生成Api文档。