前面章节写到,我们通过 Router 将用户的请求基于 method 和 URL 分发到了对应的 Controller 上,那 Controller 负责做什么?
简单的说 Controller 负责解析用户的输入,处理后返回相应的结果,例如
- 在 RESTful接口中,Controller 接受用户的参数,从数据库中查找内容返回给用户或者将用户的请求更新到数据库中。
- 在 HTML 页面请求中,Controller 根据用户访问不同的 URL,渲染不同的模板得到 HTML 返回给用户。
- 在代理服务器中,Controller 将用户的请求转发到其他服务器上,并将其他服务器的处理结果返回给用户。
框架推荐 Controller 层主要对用户的请求参数进行处理(校验、转换),然后调用对应的 Service 方法处理业务,得到业务结果后封装并返回:
- 获取用户通过 HTTP 传递过来的请求参数。
- 校验、组装参数。
- 调用 Service 进行业务处理,必要时处理转换 Service 的返回结果,让它适应用户的需求。
- 通过 HTTP 将结果响应给用户。
Controller是一个实现了特定方法的普通类,使用时根据类全名称+方法名进行调用;类文件建议放在Controllers目录下(也可根据实际需要放在项目下的其他目录中)。
Controller的方法必须遵循如下规则:
- 方法需要定义为一个返回Task的异步方法
- 方法有且只有一个ControllerContext类型的参数
定义时建议继承BaseController基类,通过继承BaseController类,可以在Controller中方便的获取App全局对象和Service服务实例。
假设我们实现一个用户登录的接口服务,需要首先实现如下Controller类
public class LoginController : BaseController
{
public async Task LoginByAccount(ControllerContext cc)
{
// TODO
await cc.JsonAsync("登录成功");
}
}
然后,将上面编写好的Controller在路由定义文件中进行配置
<?xml version="1.0" encoding="utf-8" ?>
<routers>
<router path="/login" method="POST" handler="CodeM.FastApi.Controllers.LoginController.LoginByAccount" />
</routers>
至此,我们已经完成了一个接口服务的开发,当系统正常运行时,我们可以通过POST方法访问/login路由进行用户的登录请求。
注:定义的Controller类在有HTTP请求发生时,会实例化一个对象对请求进行响应;而示例中的Controller继承于BaseController
,会有如下的this属性和方法:
this.App
:当前应用App对象的实例,通过它我们可以拿到框架提供的全局对象和方法。
this.Service(bool singleton = true)
:该方法会返回一个和当前Controller同名的Service对象的实例,singleton参数用于指定是否使用单例模式;如:CodeM.FastApi.Controller.LoginController中调用该方法,系统会找到CodeM.FastApi.Service.LoginService对象,然后实例化后返回;要注意命名空间前缀的一致性。
this.Service(string serviceName, bool singleton = true)
:该方法和上面的方法相似,都是获取Service对象实例的方法,该方法可以指定要获取Service对象的名称;如:CodeM.FastApi.Controller.LoginController中调用this.Service("User", true)
方法,系统会找到CodeM.FastApi.Service.UserService对象,然后实例化后返回;要注意命名空间前缀的一致性。
Restful风格的Controller编写方法是通过在定义类中实现指定名称的标准方法,达到对特定操作对象进行增删改查的目的。 假设我们要实现一套对Person人员进行操作的Restful接口:
public PersonController : BaseController
{
public async Task Create(ControllerContext cc)
{
// TODO
await cc.JsonAsync("创建Person。");
}
public async Task Delete(ControllerContext cc)
{
// TODO
await cc.JsonAsync("删除Person。");
}
public async Task Update(ControllerContext cc)
{
// TODO
await cc.JsonAsync("修改Person。");
}
public async Task List(ControllerContext cc)
{
// TODO
await cc.JsonAsync("查询Person列表。");
}
public async Task Detail(ControllerContext cc)
{
// TODO
await cc.JsonAsync("查询Person详情。");
}
}
然后,将上面编写好的Controller在路由定义文件中进行配置
<?xml version="1.0" encoding="utf-8" ?>
<routers>
<router path="/person" resource="CodeM.FastApi.Controllers.PersonController" />
</routers>
通过以上的代码和配置,我们已经成功为Person对象实现了增、删、改、查的接口服务:
POST /person
// 新建Person对象
DELETE /person/{id}
// 删除Person对象
PUT /person/{id}
// 修改Person对象
GET /person
// 查询Person对象列表
GET /person/{id}
// 查询Person对象详情
由于 Controller 基本上是业务开发中唯一和 HTTP 协议打交道的地方,在继续往下了解之前,我们首先简单的看一下 HTTP 协议是怎样的。
如果我们发起一个 HTTP 请求来访问前面例子中提到的 Controller:
curl -X POST http://localhost:5000/person --data '{"name":"张三", "age": 18}' --header 'Content-Type:application/json; charset=UTF-8'
通过 curl 发出的 HTTP 请求的内容就会是下面这样的:
POST /person HTTP/1.1 Host: localhost:5000 Content-Type: application/json; charset=UTF-8
{"name": "张三", "age": 18}
请求的第一行包含了三个信息,我们比较常用的是前面两个:
- method:这个请求中 method 的值是
POST
。 - path:值为
/person
,如果用户的请求中包含 query,也会在这里出现
从第二行开始直到遇到的第一个空行位置,都是请求的 Headers 部分,这一部分中有许多常用的属性,包括这里看到的 Host,Content-Type,还有 Cookie
,User-Agent
等等。在这个请求中有两个头:
Host
:我们在浏览器发起请求的时候,域名会用来通过 DNS 解析找到服务的 IP 地址,但是浏览器也会将域名和端口号放在 Host 头中一并发送给服务端。Content-Type
:当我们的请求有 body 的时候,都会有 Content-Type 来标明我们的请求体是什么格式的。
之后的内容全部都是请求的 body,当请求是 POST, PUT, DELETE 等方法的时候,可以带上请求体,服务端会根据 Content-Type 来解析请求体。
在服务端处理完这个请求后,会发送一个 HTTP 响应给客户端
HTTP/1.1 200 OK Content-Type: application/json; charset=utf-8 Content-Length: 42 Date: Tue, 19 Apr 2022 01:40:12 GMT Connection: keep-alive
{"code":0,"data":"创建Person。","error":null}
第一行中也包含了三段,其中我们常用的主要是响应状态码,这个例子中它的值是 200,它的含义是请求已经被成功接收并返回。
和请求一样,从第二行开始到下一个空行之间都是响应头,这里的 Content-Type,表示这个响应的格式是 JSON;Content-Length,表示响应内容长度为 42 个字节。
最后剩下的部分就是这次响应真正的内容。
从上面的 HTTP 请求示例中可以看到,有好多地方可以放用户的请求数据,框架通过在 Controller 上绑定的 ControllerContext 实例,提供了许多便捷方法和属性获取用户通过 HTTP 请求发送过来的参数。
在 URL 中 ?
后面的部分是一个 Query String,这一部分经常用于 GET 类型的请求中传递参数。例如 GET /person?pageindex=1&pagesize=50
中 pageindex=1&pagesize=50
就是用户传递过来的参数。我们可以通过 ControllerContext.QueryParams
拿到解析过后的这个参数体 。
public PersonController : BaseController
{
public async Task List(ControllerContext cc)
{
string pageindex = cc.QueryParams["pageindex"];
string pagesize = cc.QueryParams["pagesize"];
// TODO
await cc.JsonAsync("查询Person列表。");
}
}
当 Query String 中的 key 重复时,使用上面的方法只取 key 第一次出现时的值,后面再出现的都会被忽略。GET /person?name=张三&name=李四
拿到的值是 张三
。
Count:返回Query参数的个数,同名参数不重复计数。
bool ContainsKey(string key):判断是否包含指定key的Query参数。
string Get(string key, string defaultValue):返回指定key的Query参数值,有多个同名key参数时,返回第一个;如果不存在,则返回defaultValue。
string Get(int index, string defaultValue):返回指定index的Query参数值;如果不存在,则返回defaultValue。
StringValues AllValues(string key):返回指定key的Query参数值,有多个同名key参数时,返回所有同名参数值;如果不存在,则返回null。
StringValues AllValues(int index):返回指定index的Query参数值;指定索引的参数包含多个值时,返回所有参数值; 如果不存在,则返回null。
有时候我们的系统会设计成让用户传递相同的 key,例如 DELETE /person?id=1&id=2&id=3
。针对此类情况,框架在QueryParams
对象上实现了AllValues方法,通过这个方法,可以获取到指定key的所有参数值,它不会丢弃任何一个重复的数据,而是将他们都放到一个集合中:
// DELETE /person?id=1&id=2&id=3
public class PersonController : BaseController
{
public async Task Delete(ControllerContext cc)
{
StringValues ids = cc.QueryParams.AllValues("id");
await cc.JsonAsync(string.Format("删除了{0}个Person。", ids.Count));
}
}
在 Router 中,我们介绍了 Router 上也可以申明路由参数,这些参数都可以通过 ControllerContext
对象上RouteParams
属性 获取到。
// DELETE /person/{id}
// DELETE /person/1
public class PersonController : BaseController
{
public async Task Delete(ControllerContext cc)
{
string id = cc.RouteParams["id"];
await cc.JsonAsync(string.Format("删除的Person是{0}。", id));
}
}
虽然我们可以通过 URL 传递参数,但是还是有诸多限制:
-
浏览器中会对 URL 的长度有所限制,如果需要传递的参数过多就会无法传递。
-
服务端经常会将访问的完整 URL 记录到日志文件中,有一些敏感数据通过 URL 传递会不安全。
当请求的ContentType为application/x-www-form-urlencoded
,表示请求是通过Key/Value的表单形式进行参数传递,这些参数可以通过ControllerContext
对象上PostForms
属性 获取到。
// POST /person
public class PersonController : BaseController
{
public async Task Create(ControllerContext cc)
{
string name = cc.PostForms["name"];
int age = int.Parse(cc.PostForms["age"]);
// TODO 保存人员信息到数据库
await cc.JsonAsync("创建Person成功。");
}
}
提交数据除了采用上面介绍的Form表单传参形式外,最常用的就是使用Json格式进行参数传递;相较于Form的Key/Value格式,Json能够提供更丰富的数据类型,支持更复杂的数据结构;当用户使用Json格式进行参数传递时,可以通过ControllerContext
对象上PostJson
属性 获取到。
上面新建Person的Controller假如使用Json格式传递参数的话,代码将如下面所示:
// POST /person
public class PersonController : BaseController
{
public async Task Create(ControllerContext cc)
{
string name = cc.PostJson.name;
int age = cc.PostJson.age;
// TODO 保存人员信息到数据库
await cc.JsonAsync("创建Person成功。");
}
}
当面对需要进行文件上传的业务场景时,请使用Asp.netcore系统功能,从Request.Form.Files获取上传文件的列表进行操作,通过Request.Form.Files对象可以获取上传文件的数量和具体内容。
// POST /person/head
public class PersonController : BaseController
{
public async Task UploadHead(ControllerContext cc)
{
if (cc.Request.Form != null &&
cc.Request.Form.Files != null)
{
if (cc.Request.Form.Files.Count == 1)
{
// TODO 更新用户头像
await cc.JsonAsync("更新用户头像成功。");
}
}
await cc.JsonAsync(-1, "修改用户头像失败。");
}
}
上传文件的大小限制默认最大5M,可以通过在appsettings.json中配置FileUpload相关属性进行修改。
{
"FileUpload": {
"MaxBodySize": 5242880 // 单位:字节
}
}
除了从 URL 和请求 body 上获取参数之外,还有许多参数是通过请求 header 传递的。 控制器方法中的唯一参数ControllerContext
对象上提供了Headers
属性可以获取到header的所有信息。
// POST /person/head
public class PersonController : BaseController
{
public async Task Create(ControllerContext cc)
{
string token = cc.Headers.Get("token", null);
if (token == null)
{
await cc.JsonAsync(-1, "缺少请求签名。");
return;
}
// TODO
await cc.JsonAsync("创建Person成功。");
}
}
HTTP 请求都是无状态的,但是我们的 Web 应用通常都需要知道发起请求的人是谁。为了解决这个问题,HTTP 协议设计了一个特殊的请求头:Cookie。服务端可以通过响应头(set-cookie)将少量数据响应给客户端,浏览器会遵循协议将数据保存,并在下次请求同一个服务的时候带上(浏览器也会遵循协议,只在访问符合 Cookie 指定规则的网站时带上对应的 Cookie 来保证安全性)。
通过 ControllerContext.Cookies
,我们可以在 Controller 中便捷、安全的设置和读取 Cookie。
// POST /login
public class LoginController : BaseController
{
public async Task Login(ControllerContext cc)
{
string token = Guid.NewGuid().ToString("N");
// TODO
CookieOptionsExt coe = new CookieOptionsExt();
coe.Encrypt = true;
coe.HttpOnly = true;
cc.Cookies.Set("token", token, coe);
await cc.JsonAsync("登录成功。");
}
}
Cookie 虽然在 HTTP 中只是一个头,但是通过 foo=bar;foo1=bar1;
的格式可以设置多个键值对。
Cookie 在 Web 应用中经常承担了传递客户端身份信息的作用,因此有许多安全相关的配置,不可忽视,Config配置 章节中详细介绍了 Cookie 的安全相关的配置项,可以深入阅读了解。
通过 Cookie,我们可以给每一个用户设置一个 Session,用来存储用户身份相关的信息,这份信息会加密后存储在 Cookie 中,实现跨请求的用户身份保持。
框架给我们提供了 ControllerContext.Session
来访问或者修改当前用户 Session 。
// POST /login
public class LoginController : BaseController
{
public async Task Login(ControllerContext cc)
{
string user = cc.PostJson.user;
cc.Session.SetString("username", user);
// TODO
await cc.JsonAsync("登录成功。");
}
}
Session 的使用方法非常直观,直接读取或者修改指定key值就可以了,如果要删除key,直接将它Remove:
// POST /logout
public class LoginController : BaseController
{
public async Task Logout(ControllerContext cc)
{
cc.Session.Remove("username");
// TODO
await cc.JsonAsync("注销登录成功。");
}
}
如果想正常使用Session,必须首先在配置文件中开启Session;否则,使用时将收到异常信息。
{
"Session": {
"Enable": true
}
}
除此之外,Session 还有许多其他配置选项,在使用之前请详细阅读Config配置 Session相关的章节。
我们并不想在 Controller 中实现太多业务逻辑,所以提供了一个 Service 层进行业务逻辑的封装,这不仅能提高代码的复用性,同时可以让我们的业务逻辑更好测试。
在 Controller 中可以调用任何一个 Service 上的任何方法,框架在BaseController基类上定义了如下方法:
serviceName
必须是Service定义类名去除Service后缀的部分,且Service定义类必须和Controller定义类命名空间保持父级的一致性。如下:
// Controller 定义
namespace MyProgram.Controllers
{
// POST /person
public class PersonController : BaseController
{
public async Task Create(ControllerContext cc)
{
string name = cc.PostJson.name;
int age = cc.PostJson.age;
Service("Person").Save(name, age);
// 当Service类名和Controller类型前缀一致时,可省略serviceName参数
// 如本例中的PersonService和PersonController
// Service().Save(name, age);
await cc.JsonAsync("创建Person成功。");
}
}
}
// Service 定义
namespace MyProgram.Services // 命名空间父级一致,都是MyProgram,可以是多级,一致即可
{
public class PersonService : BaseService
{
public bool Save(string name, int age)
{
// TODO
return true;
}
}
}
通过singleton
参数可以控制是否使用单例模式调用Service对象;默认为true,使用单例模式。
Service 更多的具体写法,请查看 Service 章节。
当业务逻辑完成之后,Controller 的最后一个职责就是将业务逻辑的处理结果通过 HTTP 响应发送给用户。
HTTP 设计了非常多的状态码 ,每一个状态码都代表了一个特定的含义,通过设置正确的状态码,可以让响应更符合语义。
框架在ControllerContext
对象上提供了一个State
属性便捷的进行状态码的设置
// POST /person
public class PersonController : BaseController
{
public async Task Create(ControllerContext cc)
{
// TODO
cc.State = 200; // 200是成功状态码,是所有Http请求成功后的正常返回,通常可以省略不设置。
await cc.JsonAsync("创建Person成功。");
}
}
绝大多数的数据都是通过 body 发送给请求方的,和请求中的 body 一样,在响应中发送的 body,也需要有配套的 Content-Type 告知客户端如何对数据进行解析。
- 作为一个 RESTful 的 API 接口 controller,我们通常会返回 Content-Type 为
application/json
格式的 body,内容是一个 JSON 字符串。
框架在ControllerContext
对象上提供了两个方法设置Json内容,使用框架方法将返回统一格式的Json字符串,具体格式如下:
{
code: 0, // 返回码,0代表成功,-1代表失败;也可以自定义其他错误码。
data: null, // 返回数据,根据不同接口逻辑,可以返回列表、对象等任意类型数据;也可以是null。
error: null // 当返回码不为0时,显示失败的文字描述或错误信息。
}
使用该方法设置返回JSON内容时,只能对返回数据或错误信息一项内容进行赋值。
当_data为null或者非Exception类型的数据时,code码将设置为0,_data作为返回数据使用。
// POST /person
public class PersonController : BaseController
{
public async Task Detail(ControllerContext cc)
{
// TODO
object detailObj = new
{
name = "wangxm",
age = 18
};
await cc.JsonAsync(detailObj);
}
}
// 返回结果
{
"code":0,
"data":{
"name":"wangxm",
"age":18
},
"error":null
}
当_data是一个Exception类型的对象时,code码将设置为-1,_data作为错误信息使用
// POST /person
public class PersonController : BaseController
{
public async Task Detail(ControllerContext cc)
{
// TODO
await cc.JsonAsync(new Exception("查询Person详情异常。"));
}
}
// 返回结果
{
"code":-1,
"data":null,
"error":"查询Person详情异常。"
}
通过该方法,可以更灵活的设置错误返回码;并同时设置返回数据和错误信息。
// POST /login
public class LoginController : BaseController
{
public async Task Login(ControllerContext cc)
{
// TODO
await cc.JsonAsync(1001, null, "用户名或密码错误。");
}
}
除了以上框架提供的JSON返回方法,你可以随时使用ASP.NETCORE自带的Response对象自定义返回的数据格式内容,使用方法遵循ASP.NETCORE相关说明。