前面章节例子代码中我们已经写过一些简单的控制器了,这篇笔记我们继续学习ASP.NET Core中的控制器。本篇笔记主要介绍WebAPI控制器的编写和HTTP请求响应的基本处理方法,有关MVC开发模式中控制器的其它用法将在MVC章节专门介绍。
在实际开发中,对于WebAPI项目和MVC项目定义控制器的写法有一些区别。WebAPI的控制器通常继承ControllerBase
并标注[ApiController]
注解,而MVC的控制器通常继承Controller
类。下面例子我们定义了一个典型的用于WebAPI项目的控制器。
using DemoWebAPI.Model;
using Microsoft.AspNetCore.Mvc;
namespace DemoWebAPI.Controllers;
[ApiController]
[Route("api/Demo")]
public class DemoController : ControllerBase
{
[HttpGet]
[Route("DemoAction")]
public ActionResult<ApiResult> DemoAction()
{
return ApiResult.Success();
}
}
例子中我们定义了DemoController
类,它标注了[ApiController]
注解,并继承了ControllerBase
抽象类。控制器方法中,返回了一个ActionResult<ApiResult>
类型的对象。
[ApiController]
注解是ASP.NET Core中提供的专用于WebAPI的注解,它赋予控制器类一些额外行为使得控制器的表现更符合WebAPI环境下的预期。总而言之,对于WebAPI工程中的Controller我们应该添加这个注解。
此外,Controller定义中,我们继承了ControllerBase
这个类,这个基类提供了很多基础功能,控制器类通常应该直接或间接的继承该类。不继承任何类的控制器也是可以正常运行的的,但它会缺少很多方便的功能,我们一般不这样写。
控制器类和控制器方法上的[Route("")]
注解用于标注路由匹配路径,[HttpGet]
注解用于标注该控制器方法用于响应GET请求,控制器方法上它们也可以合并简写为[HttpGet("")]
的形式,前一篇笔记中我们已经介绍过了这部分内容。
控制器方法也被称为“Action”,这里我们定义了一个名为DemoAction()
的控制器方法,它返回ActionResult<ApiResult>
类型的对象。实际上,控制器方法可以返回任意对象,ASP.NET Core会自动将其序列化为JSON。但实际开发中,我们推荐控制器返回ActionResult
类型,ActionResult
有很多常用的派生类,比如StatusCodeResult
、JsonResult
、FileStreamResult
等,我们能很方便的基于这些类构建我们需要的响应方式。
ApiResult
是我们自定义的一个类,其中包含了一些通用的JSON返回信息,下面是ApiResult
类内部的具体实现供参考。
namespace DemoWebAPI.Model;
public class ApiResult
{
public string Code { get; set; }
public string? Message { get; set; }
public object? Data { get; set; }
public static ApiResult Success()
{
return new ApiResult() { Code = "0", Message = "ok", Data = null };
}
// ...其它方法
}
这里还要注意的是,我们可以发现Controller方法声明的返回值是ActionResult<ApiResult>
,但我们实际方法体内的返回值是ApiResult
,这是因为ActionResult<>
重载了隐式转换操作符,让ActionResult<>
的泛型类型能隐式转换为ActionResult<>
。操作符重载是个很有争议的特性,它能让代码变得简洁同时也让代码变得让人费解,不过这里ASP.NET Core就是这么设计的,我们了解其用法即可。
Request
和Response
是ControllerBase
基类中的两个属性,分别代表HTTP请求和HTTP响应,前者包含了请求中的所有数据,后者则可以用于设置各种响应信息。不过我们实际开发中一般不会直接操作这两个属性,请求数据我们一般声明在控制器方法上并由ASP.NET Core框架从参数中注入,响应则使用各种ActionResult
的派生类来实现,如果这些满足不了我们的需求,才会考虑使用Request
和Response
属性。
下面例子代码中,我们直接操作ControllerBase
基类提供的Request
属性从请求信息中读取所有的请求头和QueryString。
IHeaderDictionary headers = Request.Headers;
foreach (var header in headers)
{
Console.WriteLine($"{header.Key} {header.Value}");
}
QueryString queryString = Request.QueryString;
Console.WriteLine(queryString.Value);
读取GET参数非常简单,我们直接在控制器方法中指定参数即可,下面例子我们在方法中指定id
参数,此时会读取URL中的同名GET参数。例如我们请求/api/Demo/DemoAction?id=3
,此时控制器方法会读取到id
参数为3
。
using DemoWebAPI.Model;
using Microsoft.AspNetCore.Mvc;
namespace DemoWebAPI.Controllers;
[ApiController]
[Route("api/Demo")]
public class DemoController : ControllerBase
{
[HttpGet]
[Route("DemoAction")]
public ActionResult<ApiResult> DemoAction(int id)
{
return ApiResult.Success();
}
}
如果我们的请求参数名和方法参数名不同,我们可以使用[FromQuery]
注解显式指定GET参数名,下面例子中,我们可以请求/api/Demo/DemoAction?uid=3
,此时控制器方法会读取到id
参数为3
。
public ActionResult<ApiResult> DemoAction([FromQuery(Name = "uid")] int id) {}
如果我们希望参数不存在时读取一个默认值,我们直接指定方法默认值即可。
public ActionResult<ApiResult> DemoAction(int id = 1) {}
如果我们希望参数是可选的,我们直接指定可空类型即可。
public ActionResult<ApiResult> DemoAction(int? id) {}
不过这里要注意如果我们没有指定为可空类型,int
类型的id
参数不存在时它默认会被赋值为0
。
路径参数前面路由章节我们已经介绍过了,在路由设置中我们需要使用大括号{}
声明将路径绑定到一个参数上,控制器方法中我们使用同名的方法参数接收即可。
[HttpGet]
[Route("DemoAction/{id:int}")]
public ActionResult<ApiResult> DemoAction(int id) {}
如果占位符参数名和方法参数名不一致,我们也可以在方法参数上显式的使用[FromRoute]
注解指定对应的占位符参数名,下面是一个例子。
[HttpGet]
[Route("DemoAction/{uid:int}")]
public ActionResult<ApiResult> DemoAction([FromRoute(Name = "uid")] int id) {}
实际开发中很少出现这种情况,偶尔可能在参数有多个来源(路径参数、GET参数等同时使用)时用到。
表单请求包括form-data
和x-www-form-urlencoded
两种格式,ASP.NET Core对这两种格式进行了统一处理。下面例子代码中,我们将表单封装为了一个FormReq
类型,Controller方法中我们需要使用[FromForm]
注解标注参数。
public record FormReq(string Username, string Password);
[HttpPost]
[Route("DemoAction")]
public ActionResult<ApiResult> DemoAction([FromForm] FormReq formReq) {}
注意我们必须使用[FromForm]
注解,否则表单参数不会被正确解析,因为ASP.NET Core默认解析的是JSON请求。此外,表单键值对的键要和FormReq
中的字段对应,不过ASP.NET Core默认会忽略大小写。
文件上传最常见的实现方式就是采用form-data
格式的表单,ASP.NET Core中我们可以使用IFormFile
类型接收表单文件上传。下面例子代码中,我们接收一个表单,它包含文件上传字段file
和普通文本字段Username
,我们在控制器方法中使用[FromForm]
注解标注请求表单对象。
using DemoWebAPI.Model;
using Microsoft.AspNetCore.Mvc;
namespace DemoWebAPI.Controllers;
public record FormReq(IFormFile file, string Username);
[ApiController]
[Route("api/Demo")]
public class DemoController : ControllerBase
{
[HttpPost]
[Route("DemoAction")]
public ActionResult<ApiResult> DemoAction([FromForm] FormReq formReq)
{
// 保存文件
using FileStream fileStream = new FileStream(Path.Combine($"E:/{formReq.file.FileName}"), FileMode.OpenOrCreate);
formReq.file.CopyTo(fileStream);
// 输出Username字段
Console.WriteLine(formReq.Username);
return ApiResult.Success();
}
}
代码中,我们将上传的文件名保存在了E
盘下,然后在控制台输出了表单中的文本字段。
读取JSON数据非常简单,Controller默认就会处理JSON数据,我们将JSON对应封装的对象作为方法参数即可。
using DemoWebAPI.Model;
using Microsoft.AspNetCore.Mvc;
namespace DemoWebAPI.Controllers;
public record JsonReq(string Username, string Password);
[ApiController]
[Route("api/Demo")]
public class DemoController : ControllerBase
{
[HttpPost]
[Route("DemoAction")]
public ActionResult<ApiResult> DemoAction(JsonReq jsonReq)
{
// 输出请求数据
Console.WriteLine(jsonReq);
return ApiResult.Success();
}
}
类似[FromQuery]
、[FromRoute]
、[FromForm]
等控制器方法参数注解,来自请求体的请求也可以使用[FromBody]
注解明确指定该参数取自HTTP请求体,通常来说如果只有一个参数的POST或PUT控制器方法是可以省略该参数的,但它在混用多个来源的参数时可能用到。
[HttpPost]
[Route("DemoAction")]
public ActionResult<ApiResult> DemoAction([FromBody] JsonReq jsonReq)
{
// ...
}
此外,和表单类似,此处Json字段的键默认也是忽略大小写的,即使我们传如下的奇怪JSON内容,服务端也能正常解析。
{
"usernAme": "tom",
"Password": "123"
}
忽略大小写在有些时候可能造成问题,我们可以手动设置让JSON反序列化时大小写敏感。
builder.Services.AddControllers()
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.PropertyNameCaseInsensitive = false;
});
此时,我们传入的JSON必须符合camelCase才能正确解析(即首字母小写的驼峰命名,JSON序列化器默认使用camelCase)。
读取请求头时,我们可以使用[FromHeader]
注解,默认读取和方法名同名的Header,如果不同名,注解中我们可以指定Name
参数作为Header名。
[HttpGet]
[Route("DemoAction")]
public ActionResult<ApiResult> DemoAction([FromHeader(Name = "Token")] string token) {}
读取Header的另一种方法是从HttpContext.Request.Headers
字典中读取。
[HttpGet]
[Route("DemoAction")]
public ActionResult<ApiResult> DemoAction()
{
string userAgent = HttpContext.Request.Headers["User-Agent"];
// 输出User-Agent Header值
Console.WriteLine(userAgent);
return ApiResult.Success();
}
实际开发中,我们服务端肯定要对客户端提交的数据进行各种验证,ASP.NET Core中我们可以使用System.ComponentModel.DataAnnotations
的验证注解声明式实现。下面代码中,我们在请求的模型中使用了验证注解,它标注了两个字段不能为空。
using DemoWebAPI.Model;
using Microsoft.AspNetCore.Mvc;
using System.ComponentModel.DataAnnotations;
namespace DemoWebAPI.Controllers;
public record JsonReq(
[Required(ErrorMessage ="用户名不能为空")]
string Username,
[Required(ErrorMessage ="密码不能为空")]
string Password
);
[ApiController]
[Route("api/Demo")]
public class DemoController : ControllerBase
{
[HttpPost]
[Route("DemoAction")]
public ActionResult<ApiResult> DemoAction(JsonReq jsonReq)
{
// 输出请求数据
Console.WriteLine(jsonReq);
return ApiResult.Success();
}
}
一旦标注了模型字段验证注解,ASP.NET Core的ApiController就会自动对模型进行验证,如果验证失败,默认会返回一个特殊的错误JSON响应。如果希望能自定义这个错误响应,可以在Program.cs
中配置ApiBehaviorOptions
,下面是一个例子。
builder.Services.Configure<ApiBehaviorOptions>(options =>
{
options.InvalidModelStateResponseFactory = context =>
{
var firstError = context.ModelState
.Where(kv => kv.Value.Errors.Count > 0)
.Select(kv => kv.Value.Errors.FirstOrDefault()?.ErrorMessage)
.FirstOrDefault();
return new ObjectResult(ApiResult.Failure("400", firstError ?? "请求参数异常"));
};
});
代码中,我们配置了当数据验证异常发生时,将取出所有错误中的第一个错误并构造响应体返回给用户。
有关System.ComponentModel.DataAnnotations
模型字段验证注解可以参考官方文档,这里就不把所有的注解逐一列出了。
返回JSON非常简单,前面介绍过ActionResult
的用法,我们在方法体中直接返回对象即可,对象会被自动序列化为JSON数据,字段风格默认使用camelCase。我个人喜欢给JSON封装一个通用返回体,比如{"code": "0", "message": "ok", "data": {}}
的格式,一些严格遵循REST风格的团队则可能不喜欢这样做,我们实际开发中根据项目要求或个人喜好返回统一的数据格式即可。
using DemoWebAPI.Model;
using Microsoft.AspNetCore.Mvc;
namespace DemoWebAPI.Controllers;
[ApiController]
[Route("api/Demo")]
public class DemoController : ControllerBase
{
[HttpGet]
[Route("DemoAction")]
public ActionResult<ApiResult> DemoAction()
{
Student s = new Student(1L, "汤姆", 18);
return ApiResult.Success(s);
}
}
File()
是ControllerBase
中提供的一个方法,这个名字起的很不好因为它和System.IO.File
冲突,使用后者时我们只能使用全限定名System.IO.File
了。File()
方法用于返回文件下载响应,它的返回值是FileStreamResult
,该类是ActionResult
的派生类。
using Microsoft.AspNetCore.Mvc;
namespace DemoWebAPI.Controllers;
[ApiController]
[Route("api/Demo")]
public class DemoController : ControllerBase
{
[HttpGet]
[Route("DemoAction")]
public ActionResult DemoAction()
{
FileStream fileStream = new FileStream(Path.Combine("E:/極楽浄土.m4a"), FileMode.Open);
return File(fileStream, "audio/mp4");
}
}
设置响应头非常简单,我们直接在Response.Headers
字典中添加新的值或覆盖其中的值即可。
Response.Headers["Token"] = "abc123";
如果要直接返回一个HTTP状态码,我们可以使用StatusCodeResult
,下面是一个例子。
[HttpGet]
[Route("DemoAction")]
public ActionResult DemoAction()
{
return new StatusCodeResult(500);
}
上面代码手动返回了一个HTTP 500
状态,除了直接使用StatusCodeResult
,我们也可以使用ControllerBase
中提供的Ok()
、NotFound()
、NoContent()
等方法返回特定的响应状态码。
ASP.NET Core的控制器支持内容协商(Content Negotiation)机制,简单来说就是根据HTTP请求头中的Accept
字段自动判断返回数据类型,例如Accept
请求头给出了application/xml
就返回XML响应,如果给出了application/json
就返回JSON响应。
不过这里注意WebAPI工程默认没有配置XML的序列化器,我们可以添加以下代码将XML的序列化器配置上。
builder.Services.AddControllers().AddXmlSerializerFormatters();
控制器方法例子如下。
[HttpGet]
[Route("DemoAction")]
public ActionResult<ApiResult> DemoAction()
{
return ApiResult.Success();
}
此时如果我们的请求头Accept
字段为application/xml
,就会生成类似如下的响应。
<ApiResult xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Code>0</Code>
<Message>ok</Message>
</ApiResult>
请求头Accept
字段为application/json
则会生成类似如下的响应。
{
"code": "0",
"message": "ok",
"data": null
}
下面例子代码中,我们在控制器里实现了Cookie的设置、读取和删除。
using DemoWebAPI.Model;
using Microsoft.AspNetCore.Mvc;
namespace DemoWebAPI.Controllers;
[ApiController]
[Route("api/Cookie")]
public class CookieController : ControllerBase
{
[HttpGet("WriteCookie")]
public ActionResult<ApiResult> WriteCookie()
{
Response.Cookies.Append("UserPreferences", "Language=English", new CookieOptions
{
Expires = DateTimeOffset.Now.AddDays(7),
HttpOnly = true,
SameSite = SameSiteMode.Lax
});
return ApiResult.Success();
}
[HttpGet("ReadCookie")]
public ActionResult<ApiResult> ReadCookie()
{
string? cookieValue = Request.Cookies["UserPreferences"];
return ApiResult.Success(cookieValue);
}
[HttpGet("DeleteCookie")]
public ActionResult<ApiResult> DeleteCookie()
{
Response.Cookies.Delete("UserPreferences");
return ApiResult.Success();
}
}
存取Cookie需要操作Request
和Response
属性,我们可以在Swagger页面分别执行上述代码的控制器方法查看Cookie的存取效果。