本文 代碼 已發布到 GitHub https://github.com/whuanle/CZGL.IKonwWebApi
跟同事合作前后端分離項目,自己對 WebApi 的很多知識不夠全,雖說不必要學全棧,可是也要了解基礎知識,才能合理設計接口、API,方便與前端交接。
晚上回到宿舍后,對 WebApi 的知識查漏補缺,主要補充了 WebAPi 的一些方法、特性等如何與前端契合,如何利用工具測試 API 、Axios 請求接口。
本文主要寫 WebApi 前端請求數據到 API 、后端返回處理結果,不涉及登錄、跨域請求、前端 UI 等。(難一點我不會了。。。看張隊的公眾號,篇篇都看不懂。。。)
前提:會一點點 VUE、會一點 Axios、會一點點 Asp.net Core。
工具:Visual Studio 2019(或者其它版本) + Visual Studio Code + Swagger +Postman
由於 Visual Studio 2019 寫 ASP.NET Core 頁面時,沒有 Vue 的智能提示,所以需要使用 VSCode 來寫前端頁面。
一. 微軟WebApi
特性 | 綁定源 |
---|---|
[FromBody] | 請求正文 |
[FromForm] | 請求正文中的表單數據 |
[FromHeader] | 請求標頭 |
[FromQuery] | 請求查詢字符串參數 |
[FromRoute] | 當前請求中的路由數據 |
[FromServices] | 作為操作參數插入的請求服務 |
來一張 Postman 的圖片:
HTTP 請求中,會攜帶很多參數,這些參數可以在前端設置,例如表單、Header、文件、Cookie、Session、Token等。
那么,上面的表格正是用來從 HTTP 請求中獲取數據的 “方法”
或者說 “手段”
。HttpContext 等對象不在本文討論范圍。
Microsoft.AspNetCore.Mvc
命名空間提供很多用於配置Web API 控制器的行為和操作方法的屬性:
特性 | 說明 |
---|---|
[Route] | 指定控制器或操作的 URL 模式。 |
[Bind] | 指定要包含的前綴和屬性,以進行模型綁定。 |
[Consumes] | 指定某個操作接受的數據類型。 |
[Produces] | 指定某個操作返回的數據類型。 |
[HttpGet] | 標識支持 HTTP GET 方法的操作。 |
[HttpPost] | 標識支持 HTTP POST 方法的操作。 |
... ... ... | ... ... ... |
WebApi 應用
首先創建一個 Asp.Net Core MVC 應用,然后在 Controllers 目錄添加一個 API 控制器 DefaultController.cs
。(這里不創建 WebApi 而是 創建 MVC,通過 MVC 創建 API 控制器)。
創建后默認代碼:
[Route("api/[controller]")]
[ApiController]
public class DefaultController : ControllerBase
{
}
1. 安裝 Swagger
在 Nuget 中搜索 Swashbuckle.AspNetCore
,或打開 程序包管理器控制台 -> 程序包管理器控制台
,輸入以下命令進行安裝
Install-Package Swashbuckle.AspNetCore -Version 5.0.0-rc2
打開 Startup
文件,添加引用
using Microsoft.OpenApi.Models;
上面是微軟文檔的安裝方法,結果筆者測試。如果使用 Nuget
搜索,出現 Swashbuckle.AspNetCore 4.0.1
及以上,應當是引用
using Swashbuckle.AspNetCore.Swagger;
在 ConfigureServices
中添加服務,雙引號文字內容隨便改。
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "My API", Version = "v1" });
});
報錯,則使用
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new Info { Title = "My API", Version = "v1" });
});
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
添加中間件
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
// 添加下面的內容
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
});
訪問 /swagger
可以訪問到 Swagger 的 UI 界面。
為了便於查看輸出和固定端口,打開 Progarm,cs
,修改內容
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseUrls("https://*:5123")
.UseStartup<Startup>();
不要使用 IIS 托管運行。
注意:本文全部使用 [HttpPost] ;全局使用 JsonResult 作為返回類型。
二. 數據綁定與獲取
1,默認不加
直接寫 action
,不使用特性,對於簡單類型是 Query,對於復雜類型是 Json。
微軟官方文檔這樣說明:
默認情況下,模型綁定以鍵值對的形式從 HTTP 請求中的以下源中獲取數據:
表單域
請求正文(對於具有 [ApiController] 屬性的控制器。)
路由數據
查詢字符串參數
上傳的文件
對於每個目標參數或屬性,將按此列表中指示的順序掃描源。 有幾個例外情況:
路由數據和查詢字符串值僅用於簡單類型。
上傳的文件僅綁定到實現 IFormFile 或 IEnumerable<IFormFile> 的目標類型。
也就是說,對於 WebApi 來說,如果默認不加任何注解,簡單類型默認為 Query ,復雜類型為 Json。
如何是 MVC ,復雜類型則回產生多種情況,根據順序進行區配。簡單類型依然是 Query 。
這里先說 Query ,對於復雜類型(模型類等),在后面說明。
Query:
[HttpPost("aaa")]
public async Task<JsonResult> AAA(int? a, int? b) {
if (a == null || b == null)
return new JsonResult(new { code = 0, result = "aaaaaaaa" });
return new JsonResult(new { code = 2000, result = a + "|" + b });
}
打開 https://localhost:5123/swagger/index.html 查看 UI 界面
也就是說,創建一個 action
,什么都不加,默認是 query
。
通過 Postman 提交數據、測試接口
對於 Query 的 action 來說, axios 的寫法
postaaa: function () {
axios.post('/api/default/aaa?a=111&b=222'
)
.then(res => {
console.log(res.data)
console.log(res.data.code)
console.log(res.data.result)
})
.catch(err => {
console.error(err);
})
}
在網上查找資料時,發現有人說通過 params 添加數據也可以,不過筆者測試,貌似不行。
講道理,別人可以,為啥我不行。。。
axios 代碼:
postaaa: function () {
axios.post('/api/default/aaa', {
params: {
a: 123,
b: 234
}
}
)
.then(res => {
console.log(res.data)
console.log(res.data.code)
console.log(res.data.result)
})
.catch(err => {
console.error(err);
})
}
包括下面的,都試過了,不行。
axios.post('/api/default/aaa', {
a:1234,
b:1122
}
axios.post('/api/default/aaa', {
data:{
a:1234,
b:1122
}
}
把 [HttpPost]
改成 [HttpGet]
,則可以使用
axios.post('/api/default/aaa', {
params: {
a: 123,
b: 234
}
}
... ...
提示:
... ...
.then(res => {
console.log(res.data)
console.log(res.data.code)
console.log(res.data.result)
})
.catch(err => {
console.error(err);
})
.then
當請求成功時觸發,請求失敗時觸發 catch
。res
是請求成功后返回的信息,res.data
是請求成功后服務器返回的信息。即是 action
處理數據后返回的信息。
在瀏覽器,按下 F12 打開控制台,點擊 Console ,每次請求后,這里會打印請求結果和數據。
2, [FromBody]
官方文檔解釋:請求正文。[FromBody] 針對復雜類型參數進行推斷。 [FromBody] 不適用於具有特殊含義的任何復雜的內置類型,如 IFormCollection 和 CancellationToken。 綁定源推理代碼將忽略這些特殊類型。
算了,看得一頭霧水,手動實際試試。
剛剛開始的時候,我這樣使用:
public async Task<JsonResult> BBB([FromBody]int? a, [FromBody]int? b)
結果編譯時就報錯,提示只能使用一個 [FromBody],於是改成
[HttpPost("bbb")]
public async Task<JsonResult> BBB([FromBody]int? a, int? b) {
if (a == null || b == null)
return new JsonResult(new { code = 0, result = "aaaaaaaa" });
return new JsonResult(new { code = 2000, result = a + "|" + b });
}
打開 Swagger UI 界面,刷新一下
從圖片中發現,只有 b,沒有 a,而且右上角有下拉框,說明了加 [FromBody] 是 json 上傳。
那么說明 [FromBody] 修飾得應當是對象,而不是 字段。
修改程序如下:
// 增加一個類型
public class AppJson
{
public int? a { get; set; }
public int? b { get; set; }
}
[HttpPost("bbb")]
public async Task<JsonResult> BBB([FromBody]AppJson ss) {
if (ss.a == null || ss.b == null)
return new JsonResult(new { code = 0, result = "aaaaaaaa" });
return new JsonResult(new { code = 2000, result = ss.a + "|" + ss.b });
}
再看看微軟的文檔:[FromBody] 針對復雜類型參數進行推斷。
,這下可理解了。。。
即是不應該對 int、string 等類型使用 [FromBody] ,而應該使用一個 復雜類型
。
而且,一個 action 中,應該只能使用一個 [FromBody] 。
打開 Swagger 界面(有修改需要刷新下界面,下面不再贅述)。
這樣才是我們要的結果嘛,前端提交的是 Json 對象。
用 Postman 測試下
證實了猜想,嘿嘿,嘿嘿嘿。
前端提交的是 Json 對象,遵循 Json 的格式規范,那么 [FromBody] 把它轉為 Object 對象。
前端 axios 寫法:
methods: {
postaaa: function () {
axios.post('/api/default/bbb', {
"a": 4444,
"b": 5555
})
.then(res => {
console.log(res.data)
console.log(res.data.code)
console.log(res.data.result)
})
.catch(err => {
console.error(err);
})
}
}
3, [FromForm]
[HttpPost("ccc")]
public async Task<JsonResult> CCC([FromForm]int? a, [FromForm]int? b) {
if (a == null || b == null)
return new JsonResult(new { code = 0, result = "aaaaaaaa" });
return new JsonResult(new { code = 200, result = a + "|" + b });
}
當然,這樣寫也行,多個字段或者對象都可以
[HttpPost("ccc")]
public async Task<JsonResult> CCC([FromForm]AppJson ss) {
if (ss.a == null || ss.b == null)
return new JsonResult(new { code = 0, result = "aaaaaaaa" });
return new JsonResult(new { code = 200, result = ss.a + "|" + ss.b });
}
根據提示,使用 Postman 進行測試
事實上,這樣也行 ↓
form-data 和 x-www.form-urlencoded 都是鍵值形式,文件 form-data 可以用來上傳文件。具體的區別請自行查詢。
axios 寫法(把 Content-Type 字段修改成 form-data 或 x-www.form-urlencoded )
postccc: function () {
let fromData = new FormData()
fromData.append('a', 111)
fromData.append('b', 222)
axios.post('/api/default/ccc', fromData, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
})
.then(res => {
console.log(res.data)
console.log(res.data.code)
console.log(res.data.result)
})
.catch(err => {
console.error(err);
})
}
4, [FromHeader]
[FromHeader] 不以表單形式上傳,而是跟隨 Header 傳遞參數。
[HttpPost("ddd")]
public async Task<JsonResult> DDD([FromHeader]int? a, [FromHeader]int? b) {
if (a == null || b == null)
return new JsonResult(new { code = 0, result = "aaaaaaaa" });
return new JsonResult(new { code = 200, result = a + "|" + b });
}
axios 寫法
postddd: function () {
axios.post('/api/default/ddd', {}, {
headers: {
a: 123,
b: 133
}
})
.then(res => {
console.log(res.data)
console.log(res.data.code)
console.log(res.data.result)
})
.catch(err => {
console.error(err);
})
}
需要注意的是,headers 的參數,必須放在第三位。沒有要提交的表單數據,第二位就使用 {} 代替。
params 跟隨 url 一起在第一位,json 或表單數據等參數放在第二位,headers 放在第三位。
由於筆者對前端不太熟,這里有說錯,麻煩大神評論指出啦。
5, [FromQuery]
前面已經說了,Action 參數不加修飾,默認就是 [FromQuery] ,參考第一小節。
有個地方需要記住, Action 參數不加修飾。默認就是 [FromQuery] ,有時幾種參數並在一起放到 Action 里,會忽略掉,調試時忘記了,造成麻煩。
6, [FromRoute]
獲取路由規則,這個跟前端上傳的參數無關;跟 URL 可以說有關,又可以說無關。
[HttpPost("fff")]
public async Task<JsonResult> FFFxxx(int a,int b, [FromRoute]string controller, [FromRoute]string action) {
// 這里就不處理 a和 b了
return new JsonResult(new { code = 200, result = controller+"|"+action });
}
[FromRoute] 是根據路由模板獲取的,上面 API 的兩個參數和路由模板的名稱是對應的:
[FromRoute]string controller, [FromRoute]string action
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
當然,還可以加個 [FromRoute]int? id
[FromRoute] 和 [FromQuery] 區別
以此 URL 為例
https://localhost:5123/api/Default/fff?a=111&b=22
Route 會查到 controller = Default
,action = FFFxxx
。查詢到的是代碼里的真實名稱。
Query 會查詢到 a = 111
和 b = 22
那么,如果路由規則里,不在 URL 里出現呢?
[HttpPost("/ooo")]
public async Task<JsonResult> FFFooo(int a, int b, [FromRoute]string controller, [FromRoute]string action) {
// 這里就不處理 a和 b了
return new JsonResult(new { code = 200, result = controller + "|" + action });
}
那么,訪問地址變成 https://localhost:5123/ooo
通過 Postman ,測試
說明了 [FromRoute] 獲取的是代碼里的 Controller 和 Action 名稱,跟 URL 無關,根據測試結果推斷跟路由表規則也無關。
7, [FromService]
這個是與依賴注入容器有關,跟 URL 、路由等無關。
新建一個接口、一個類
public interface ITest
{
string GGG { get; }
}
public class Test : ITest
{
public string GGG { get { return DateTime.Now.ToLongDateString(); } }
}
在 ConfigureServices
中 注入
services.AddSingleton<ITest, Test>();
在 DefaultController
中,創建構造函數,然后
private readonly ITest ggg;
public DefaultController(ITest ttt) {
ggg = ttt;
}
添加一個 API
[HttpPost("ggg")]
public async Task<JsonResult> GGG([FromServices]ITest t) {
return new JsonResult(new { code = 200, result = t.GGG });
}
訪問時,什么參數都不需要加,直接訪問此 API 即可。
[FromService] 跟后端的代碼有關,跟 Controller 、Action 、URL、表單數據等無關。
小結:
特性可以幾種放在一起用,不過盡量每個 API 的參數只使用一種特性。
優先取值 Form > Route > Query
。
IFromFile 由於文件的上傳,本文就不談這個了。
關於數據綁定,更詳細的內容請參考:
https://docs.microsoft.com/zh-cn/aspnet/core/mvc/models/model-binding?view=aspnetcore-2.2
三. action 特性方法
Microsoft.AspNetCore.Mvc
命名空間提供可用於配置 Web API 控制器的行為和操作方法的屬性。
下表是針對於 Controller 或 Action 的特性.
特性 | 說明 |
---|---|
[Route] | 指定控制器或操作的 URL 模式。 |
[Bind] | 指定要包含的前綴和屬性,以進行模型綁定。 |
[Consumes] | 指定某個操作接受的數據類型。 |
[Produces] | 指定某個操作返回的數據類型。 |
[HttpGet] | 標識支持 HTTP GET 方法的操作。 |
... | ... |
下面使用這些屬性來指定 Controller 或 Action 接受的 HTTP 方法、返回的數據類型或狀態代碼。
1, [Route]
在微軟文檔中,把這個特性稱為 屬性路由
,定義:屬性路由使用一組屬性將操作直接映射到路由模板。
請教了大神,大神解釋說,ASP.NET Core 有路由規則表,路由表是全局性、唯一性的,在程序運行時,會把所有路由規則收集起來。
MVC 應用中設置路由的方法有多種,例如
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
[Route("Home/Index")]
public IActionResult Index() {
return View();
}
[Route("api/[controller]")]
[ApiController]
public class DefaultController : ControllerBase
{
}
路由是全局唯一的,可以通過不同形式使用,但是規則不能發生沖突,程序會在編譯時把路由表收集起來。
根據筆者經驗,發生沖突,應該就是在編譯階段直接報錯了。(注:筆者不敢確定)
關於路由,請參考 :
2, [Bind]
筆者知道這個是綁定模型的,但是對原理不太清楚。ASP.NET Core 自動生成的可讀寫的 Controller ,默認都是使用 [Bind] 來綁定數據。
文檔定義:用於對復雜類型的模型綁定。
有下面幾種相近的特性:
[BindRequired]
[BindNever]
[Bind]
微軟文檔提示:如果發布的表單數據是值的源,則這些屬性會影響模型綁定。
就是說,上面的特性是針對類、接口等復雜類型(下面統稱模型),對於 int、string 這些類型,可能出毛病。
[BindRequired] 、[BindNever] 只能應用於模型的屬性,如
public class TestB
{
[BindNever]
public int ID { get; set; }
[BindRequired]
public string Name { get; set; }
}
但是 [BindRequired] 、[BindNever] 不在討論范圍內,這里只說 [Bind]。
[Bind] 用於類或方法(Controller、Action),指定模型綁定中應包含的模型屬性。
在微軟官方文檔,對於[Bind] 的解釋:
[Bind]
屬性可用於防止“創建”方案中的過多發布情況 。 由於排除的屬性設置為 NULL 或默認值,而不是保持不變,因此它在編輯方案中無法很好地工作;- 因為
Bind
特性將清除未在 某個 參數中列出的字段中的任何以前存在的數據。
一臉懵逼。
下面是我的踩坑過程,不感興趣的話直接跳過吧。筆記筆記,記得當然是自己覺得要記的哈哈哈。
新建一個類
public class TestBind
{
public string A { get; set; }
public string B { get; set; }
public string C { get; set; }
public string D { get; set; }
public string E { get; set; }
public string F { get; set; }
public string G { get; set; }
}
新建 API
[HttpPost("hhh")]
public async Task<JsonResult> HHH([Bind("A,B,C")] TestBind test) {
if (ModelState.IsValid == true)
return new JsonResult(test);
return new JsonResult(new { Code = 0, Result = "驗證不通過" });
}
使用 Postman 進行,測試,發現必須使用 Json 形式,才能訪問到這個 Action ,其它方式會直接 返回 錯誤。
{
"errors": {
"": [
"A non-empty request body is required."
]
},
"title": "One or more validation errors occurred.",
"status": 400,
"traceId": "0HLO03IFQFTQU:00000007"
}
通過兩次 Postman 進行測試
經過測試,我猜想
ModelState.IsValid 跟模型里的驗證規則有關系,跟 [Bind] 沒關系(盡管用於測試的 TestB 類中沒有寫驗證規則),因此不能使用 ModelState.IsValid 驗證 [Bind] 是否符合規則。
Action 的參數:[Bind("A,B,C")] TestBind test
,剛開始的時候我以為請求的數據中必須包含 A、B、C。
測試后發現不是。。。再認真看了文檔 :因為 Bind
特性將清除未在 某個 參數中列出的字段中的任何以前存在的數據。
我修改一下:
[HttpPost("hhh")]
public async Task<JsonResult> HHH( string D, string E,[Bind("A,B,C")] TestBind test) {
if (ModelState.IsValid == true)
return new JsonResult(new { data1 = test, data2 = D, data3 = E });
return new JsonResult(new { Code = 0, Result = "驗證不通過" });
}
參數變成了 string D, string E,[Bind("A,B,C")] TestBind test
使用 Swagger 進行測試:
返回結果
{
"data1": {
"a": "string",
"b": "string",
"c": "string",
"d": "string",
"e": "string",
"f": "string",
"g": "string"
},
"data2": null,
"data3": null
}
改成
[HttpPost("hhh")]
public async Task<JsonResult> HHH([Bind("A,B,C")] TestBind test, string J, string Q) {
if (ModelState.IsValid == true)
return new JsonResult(new { data1 = test, data2 = J, data3 = Q });
return new JsonResult(new { Code = 0, Result = "驗證不通過" });
}
返回結果
{
"data1": {
"a": "string",
"b": "string",
"c": "string",
"d": "string",
"e": "string",
"f": "string",
"g": "string"
},
"data2": null,
"data3": null
}
文檔中對 [Bind] 描述最多的是:防止過多發布。
通過上面的測試,首先肯定的是一個 Action 里,有多個參數 如
[Bind("A,B,C")] TestBind test, string D, string E string J, string Q
。
注意,下面的結論是錯的!
那么 D、E 因為於 除了 Test, J、Q就會無效,通過百度,[Bind] 修飾的 Action ,前端請求的數據只有 Test 里面的數據有效,其它 Query等形式一並上傳的數據都會失效,防止黑客在提交數據時摻雜其它特殊參數。應該就是這樣理解吧。
上面是一開始我的結論,直到多次測試,我發現是錯的。
可是有一個地方不明白,
[Bind("A,B,C")]
[Bind("A,B,C,D,E,F,G")]
這兩者的區別是是什么。還是沒搞清楚。
突然想到 Query,當字段沒有使用特性修飾時,默認為 Query 。
最終踩坑測試代碼
模型類
public class TestBind
{
public string A { get; set; }
public string B { get; set; }
public string C { get; set; }
public string D { get; set; }
public string E { get; set; }
public string F { get; set; }
public string G { get; set; }
}
Action
[HttpPost("hhh")]
public async Task<JsonResult> HHH( string A, string B, string E, string F, string G, [Bind("A,B,C,D")] TestBind test, string C, string D, string J, string Q) {
if (ModelState.IsValid == true)
return new JsonResult(new
{
data1 = test,
dataA = A,
dataB = B,
dataC = C,
dataD = D,
dataE = E,
dataF = F,
dataG = G,
dataJ = J,
dataQ = Q
});
return new JsonResult(new { Code = 0, Result = "驗證不通過" });
}
Swagger 測試
Postman 測試
{
"data1": {
"a": "111",
"b": "111",
"c": "111",
"d": "111",
"e": "111",
"f": "111",
"g": "111"
},
"dataA": "222",
"dataB": "222",
"dataC": "222",
"dataD": "222",
"dataE": "222",
"dataF": "222",
"dataG": "222",
"dataJ": "222",
"dataQ": "222"
}
再在 Swagger 或 Postman ,換着法子嘗試各種不同組合的輸入。
我懵逼了。試了半天試不出什么。
實在不理解 [Bind] 里,“防止過多發布” 是什么意思
[Bind("A,B,C")]
[Bind("A,B,C,D,E,F,G")]
這兩者的區別是是什么。還是沒搞清楚。算了,不踩了。
我再到 stackoverflow 提問題,地址 https://stackoverflow.com/questions/56884876/asp-net-core-bind-how-to-use-it/56885153#56885153
獲得一個回答:
What's the difference between [Bind("A,B,C")] and [Bind("A,B,C,D,E,F,G")]?
The former tells the model binder to include only the properties of TestBind named A, B and C. The latter tells the model binder to include those same properties plus D, E, F and G.
Are you testing by posting data for all properties of your model? You should notice that the values you post for the excluded properties are not bound.
算了,嘿嘿,測試不出來,放棄。
3, [Consumes]、[Produces]
[Consumes("application/json")]
[Produces("application/json")]
[Produces("application/xml")]
[Produces("text/html")]
... ...
目前只了解到 [Consumes]、[Produces] 是篩選器,用來表示 Controller 或 Action 所能接受的數據類型。大概就是像下面這樣使用:
[Consumes("application/json")]
[Produces("application/json")]
public class DefaultTestController : ControllerBase
{
}
但是如何實際應用呢?我找了很久,都沒有找到什么結果。在 stackoverflow 找到一個回答:
https://stackoverflow.com/questions/41462509/adding-the-produces-filter-globally-in-asp-net-core
4, [HttpGet]、[HttpPost]、[HttpDelete]、[HttpPut]
修飾 Action ,用來標識這個 Action 能夠通過什么方式訪問、訪問名稱。
例如:
[Route("api/[controller]")]
[ApiController]
public class DefaultController : ControllerBase
{
[HttpPost("aaa")]
public async Task<JsonResult> AAA(int? a, int? b) {
if (a == null | b == null)
return new JsonResult(new { code = 0, result = "aaaaaaaa" });
return new JsonResult(new { code = 200, result = a + "|" + b });
}
}
訪問地址 https://localhost:5123/api/Default/aaa
使用時,會受到 Controller 和 Action 路由的影響。
但 本身亦可控制路由。以上面的控制器為例
[HttpPost("aaa")] //相對路徑
訪問地址 xxx:xxx/api/Default/aaa
[HttpPost("/aaa")] //絕對路徑
訪問地址 xxx:xxx/aaa
四,返回類型
1, 查詢備忘表
Microsoft.AspNetCore.Mvc
命名空間中,包含控制 MVC 的各種操作方法和類型,筆者從命名空間中抽出與 MVC 或 API 返回類型有關的類型,生成表格:
類型 | 描述 |
---|---|
AcceptedAtActionResult | An ActionResult that returns a Accepted (202) response with a Location header. |
AcceptedAtRouteResult | An ActionResult that returns a Accepted (202) response with a Location header. |
AcceptedResult | An ActionResult that returns an Accepted (202) response with a Location header. |
AcceptVerbsAttribute | Specifies what HTTP methods an action supports. |
ActionResult | A default implementation of IActionResult. |
ActionResult | A type that wraps either an TValue instance or an ActionResult. |
BadRequestObjectResult | An ObjectResult that when executed will produce a Bad Request (400) response. |
BadRequestResult | A StatusCodeResult that when executed will produce a Bad Request (400) response. |
ChallengeResult | An ActionResult that on execution invokes AuthenticationManager.ChallengeAsync. |
ConflictObjectResult | An ObjectResult that when executed will produce a Conflict (409) response. |
ConflictResult | A StatusCodeResult that when executed will produce a Conflict (409) response. |
ContentResult | |
CreatedAtActionResult | An ActionResult that returns a Created (201) response with a Location header. |
CreatedAtRouteResult | An ActionResult that returns a Created (201) response with a Location header. |
CreatedResult | An ActionResult that returns a Created (201) response with a Location header. |
EmptyResult | Represents an ActionResult that when executed will do nothing. |
FileContentResult | Represents an ActionResult that when executed will write a binary file to the response. |
FileResult | Represents an ActionResult that when executed will write a file as the response. |
FileStreamResult | Represents an ActionResult that when executed will write a file from a stream to the response. |
ForbidResult | An ActionResult that on execution invokes AuthenticationManager.ForbidAsync. |
JsonResult | An action result which formats the given object as JSON. |
LocalRedirectResult | An ActionResult that returns a Found (302), Moved Permanently (301), Temporary Redirect (307), or Permanent Redirect (308) response with a Location header to the supplied local URL. |
NotFoundObjectResult | An ObjectResult that when executed will produce a Not Found (404) response. |
NotFoundResult | Represents an StatusCodeResult that when executed will produce a Not Found (404) response. |
OkObjectResult | An ObjectResult that when executed performs content negotiation, formats the entity body, and will produce a Status200OK response if negotiation and formatting succeed. |
OkResult | An StatusCodeResult that when executed will produce an empty Status200OK response. |
PartialViewResult | Represents an ActionResult that renders a partial view to the response. |
PhysicalFileResult | A FileResult on execution will write a file from disk to the response using mechanisms provided by the host. |
RedirectResult | An ActionResult that returns a Found (302), Moved Permanently (301), Temporary Redirect (307), or Permanent Redirect (308) response with a Location header to the supplied URL. |
RedirectToActionResult | An ActionResult that returns a Found (302), Moved Permanently (301), Temporary Redirect (307), or Permanent Redirect (308) response with a Location header. Targets a controller action. |
RedirectToPageResult | An ActionResult that returns a Found (302) or Moved Permanently (301) response with a Location header. Targets a registered route. |
RedirectToRouteResult | An ActionResult that returns a Found (302), Moved Permanently (301), Temporary Redirect (307), or Permanent Redirect (308) response with a Location header. Targets a registered route. |
SignInResult | An ActionResult that on execution invokes AuthenticationManager.SignInAsync. |
SignOutResult | An ActionResult that on execution invokes AuthenticationManager.SignOutAsync. |
StatusCodeResult | Represents an ActionResult that when executed will produce an HTTP response with the given response status code. |
UnauthorizedObjectResult | An ObjectResult that when executed will produce a Unauthorized (401) response. |
UnauthorizedResult | Represents an UnauthorizedResult that when executed will produce an Unauthorized (401) response. |
UnprocessableEntityObjectResult | An ObjectResult that when executed will produce a Unprocessable Entity (422) response. |
UnprocessableEntityResult | A StatusCodeResult that when executed will produce a Unprocessable Entity (422) response. |
UnsupportedMediaTypeResult | A StatusCodeResult that when executed will produce a UnsupportedMediaType (415) response. |
ViewComponentResult | An IActionResult which renders a view component to the response. |
ViewResult | Represents an ActionResult that renders a view to the response. |
VirtualFileResult | A FileResult that on execution writes the file specified using a virtual path to the response using mechanisms provided by the host. |
留着寫 WebApi 時查詢備忘嘿嘿。
那些類型主要繼承的兩個接口:
類型 | 描述 |
---|---|
IActionResult | Defines a contract that represents the result of an action method. |
IViewComponentResult | Result type of a ViewComponent. |
注意的是,上面有些是抽象類,例如 FileResult,而 FileStreamResult 實現了 FileResult 。有些類是繼承關系。
2, 返回的數據類型
- 特定類型
- IActionResult 類型
- ActionResult 類型
Action 的 return ,返回的數據類型必定是上面三種。
3, 直接返回基元或復雜數據類型
[HttpGet]
public IEnumerable<Product> Get() {
return _repository.GetProducts();
}
4, IActionResult 類型
響應狀態碼、Json、重定向、URL 跳轉等,屬於 IActionResult。
MVC 的 Controller 與 API 的 Controller 有很多相同的地方,亦有很多不同的地方。
API 的 Controller 繼承 ControllerBase
MVC 的 Controller 繼承 Controller而 Controller 繼承
Controller : ControllerBase, IActionFilter, IFilterMetadata, IAsyncActionFilter, IDisposable
API 里的 Controller 是最原始的。
API 里的 返回類型需要實例化, new 一下; MVC 里的返回類型,“不需要實例化”。
當然,有些例如 FileResult 是抽象類,不能被實例化。
API:
[HttpGet("returnaaa")]
public async Task<IActionResult> ReturnAAA() {
return new ViewResult();
return new JsonResult(new { code="test"});
return new RedirectToActionResult("DefaultController","ReturnAAA","");
return new NoContentResult("666");
return new NotFoundResult();
...
}
MVC
public async Task<IActionResult> Test() {
return View();
return Json(new { code = "test" });
return RedirectToAction("DefaultController", "ReturnAAA", "");
return NoContent("666");
return NotFound();
...
}
MVC 中,Action 默認是 [HttpGet],不加也可以被訪問到;
而 API 的Action,不加 [Httpxxx],則默認不能被訪問到。
跟同事合作前后端分離項目,自己對 WebApi 的很多知識不夠全,雖說不必要學全棧,可是也要了解基礎知識,才能合理設計接口、API,方便與前端交接。
晚上回到宿舍后,對 WebApi 的知識查漏補缺,主要補充了 WebAPi 的一些方法、特性等如何與前端契合,如何利用工具測試 API 、Axios 請求接口。
本文主要寫 WebApi 前端請求數據到 API 、后端返回處理結果,不涉及登錄、跨域請求、前端 UI 等。(難一點我不會了。。。看張隊的公眾號,篇篇都看不懂。。。)
前提:會一點點 VUE、會一點 Axios、會一點點 Asp.net Core。
工具:Visual Studio 2019(或者其它版本) + Visual Studio Code + Swagger +Postman
由於 Visual Studio 2019 寫 ASP.NET Core 頁面時,沒有 Vue 的智能提示,所以需要使用 VSCode 來寫前端頁面。
一. 微軟WebApi
特性 | 綁定源 |
---|---|
[FromBody] | 請求正文 |
[FromForm] | 請求正文中的表單數據 |
[FromHeader] | 請求標頭 |
[FromQuery] | 請求查詢字符串參數 |
[FromRoute] | 當前請求中的路由數據 |
[FromServices] | 作為操作參數插入的請求服務 |
來一張 Postman 的圖片:
HTTP 請求中,會攜帶很多參數,這些參數可以在前端設置,例如表單、Header、文件、Cookie、Session、Token等。
那么,上面的表格正是用來從 HTTP 請求中獲取數據的 “方法”
或者說 “手段”
。HttpContext 等對象不在本文討論范圍。
Microsoft.AspNetCore.Mvc
命名空間提供很多用於配置Web API 控制器的行為和操作方法的屬性:
特性 | 說明 |
---|---|
[Route] | 指定控制器或操作的 URL 模式。 |
[Bind] | 指定要包含的前綴和屬性,以進行模型綁定。 |
[Consumes] | 指定某個操作接受的數據類型。 |
[Produces] | 指定某個操作返回的數據類型。 |
[HttpGet] | 標識支持 HTTP GET 方法的操作。 |
[HttpPost] | 標識支持 HTTP POST 方法的操作。 |
... ... ... | ... ... ... |
WebApi 應用
首先創建一個 Asp.Net Core MVC 應用,然后在 Controllers 目錄添加一個 API 控制器 DefaultController.cs
。(這里不創建 WebApi 而是 創建 MVC,通過 MVC 創建 API 控制器)。
創建后默認代碼:
[Route("api/[controller]")]
[ApiController]
public class DefaultController : ControllerBase
{
}
1. 安裝 Swagger
在 Nuget 中搜索 Swashbuckle.AspNetCore
,或打開 程序包管理器控制台 -> 程序包管理器控制台
,輸入以下命令進行安裝
Install-Package Swashbuckle.AspNetCore -Version 5.0.0-rc2
打開 Startup
文件,添加引用
using Microsoft.OpenApi.Models;
上面是微軟文檔的安裝方法,結果筆者測試。如果使用 Nuget
搜索,出現 Swashbuckle.AspNetCore 4.0.1
及以上,應當是引用
using Swashbuckle.AspNetCore.Swagger;
在 ConfigureServices
中添加服務,雙引號文字內容隨便改。
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "My API", Version = "v1" });
});
報錯,則使用
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new Info { Title = "My API", Version = "v1" });
});
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
添加中間件
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
// 添加下面的內容
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
});
訪問 /swagger
可以訪問到 Swagger 的 UI 界面。
為了便於查看輸出和固定端口,打開 Progarm,cs
,修改內容
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseUrls("https://*:5123")
.UseStartup<Startup>();
不要使用 IIS 托管運行。
注意:本文全部使用 [HttpPost] ;全局使用 JsonResult 作為返回類型。
二. 數據綁定與獲取
1,默認不加
直接寫 action
,不使用特性,對於簡單類型是 Query,對於復雜類型是 Json。
微軟官方文檔這樣說明:
默認情況下,模型綁定以鍵值對的形式從 HTTP 請求中的以下源中獲取數據:
表單域
請求正文(對於具有 [ApiController] 屬性的控制器。)
路由數據
查詢字符串參數
上傳的文件
對於每個目標參數或屬性,將按此列表中指示的順序掃描源。 有幾個例外情況:
路由數據和查詢字符串值僅用於簡單類型。
上傳的文件僅綁定到實現 IFormFile 或 IEnumerable<IFormFile> 的目標類型。
也就是說,對於 WebApi 來說,如果默認不加任何注解,簡單類型默認為 Query ,復雜類型為 Json。
如何是 MVC ,復雜類型則回產生多種情況,根據順序進行區配。簡單類型依然是 Query 。
這里先說 Query ,對於復雜類型(模型類等),在后面說明。
Query:
[HttpPost("aaa")]
public async Task<JsonResult> AAA(int? a, int? b) {
if (a == null || b == null)
return new JsonResult(new { code = 0, result = "aaaaaaaa" });
return new JsonResult(new { code = 2000, result = a + "|" + b });
}
打開 https://localhost:5123/swagger/index.html 查看 UI 界面
也就是說,創建一個 action
,什么都不加,默認是 query
。
通過 Postman 提交數據、測試接口
對於 Query 的 action 來說, axios 的寫法
postaaa: function () {
axios.post('/api/default/aaa?a=111&b=222'
)
.then(res => {
console.log(res.data)
console.log(res.data.code)
console.log(res.data.result)
})
.catch(err => {
console.error(err);
})
}
在網上查找資料時,發現有人說通過 params 添加數據也可以,不過筆者測試,貌似不行。
講道理,別人可以,為啥我不行。。。
axios 代碼:
postaaa: function () {
axios.post('/api/default/aaa', {
params: {
a: 123,
b: 234
}
}
)
.then(res => {
console.log(res.data)
console.log(res.data.code)
console.log(res.data.result)
})
.catch(err => {
console.error(err);
})
}
包括下面的,都試過了,不行。
axios.post('/api/default/aaa', {
a:1234,
b:1122
}
axios.post('/api/default/aaa', {
data:{
a:1234,
b:1122
}
}
把 [HttpPost]
改成 [HttpGet]
,則可以使用
axios.post('/api/default/aaa', {
params: {
a: 123,
b: 234
}
}
... ...
提示:
... ...
.then(res => {
console.log(res.data)
console.log(res.data.code)
console.log(res.data.result)
})
.catch(err => {
console.error(err);
})
.then
當請求成功時觸發,請求失敗時觸發 catch
。res
是請求成功后返回的信息,res.data
是請求成功后服務器返回的信息。即是 action
處理數據后返回的信息。
在瀏覽器,按下 F12 打開控制台,點擊 Console ,每次請求后,這里會打印請求結果和數據。
2, [FromBody]
官方文檔解釋:請求正文。[FromBody] 針對復雜類型參數進行推斷。 [FromBody] 不適用於具有特殊含義的任何復雜的內置類型,如 IFormCollection 和 CancellationToken。 綁定源推理代碼將忽略這些特殊類型。
算了,看得一頭霧水,手動實際試試。
剛剛開始的時候,我這樣使用:
public async Task<JsonResult> BBB([FromBody]int? a, [FromBody]int? b)
結果編譯時就報錯,提示只能使用一個 [FromBody],於是改成
[HttpPost("bbb")]
public async Task<JsonResult> BBB([FromBody]int? a, int? b) {
if (a == null || b == null)
return new JsonResult(new { code = 0, result = "aaaaaaaa" });
return new JsonResult(new { code = 2000, result = a + "|" + b });
}
打開 Swagger UI 界面,刷新一下
從圖片中發現,只有 b,沒有 a,而且右上角有下拉框,說明了加 [FromBody] 是 json 上傳。
那么說明 [FromBody] 修飾得應當是對象,而不是 字段。
修改程序如下:
// 增加一個類型
public class AppJson
{
public int? a { get; set; }
public int? b { get; set; }
}
[HttpPost("bbb")]
public async Task<JsonResult> BBB([FromBody]AppJson ss) {
if (ss.a == null || ss.b == null)
return new JsonResult(new { code = 0, result = "aaaaaaaa" });
return new JsonResult(new { code = 2000, result = ss.a + "|" + ss.b });
}
再看看微軟的文檔:[FromBody] 針對復雜類型參數進行推斷。
,這下可理解了。。。
即是不應該對 int、string 等類型使用 [FromBody] ,而應該使用一個 復雜類型
。
而且,一個 action 中,應該只能使用一個 [FromBody] 。
打開 Swagger 界面(有修改需要刷新下界面,下面不再贅述)。
這樣才是我們要的結果嘛,前端提交的是 Json 對象。
用 Postman 測試下
證實了猜想,嘿嘿,嘿嘿嘿。
前端提交的是 Json 對象,遵循 Json 的格式規范,那么 [FromBody] 把它轉為 Object 對象。
前端 axios 寫法:
methods: {
postaaa: function () {
axios.post('/api/default/bbb', {
"a": 4444,
"b": 5555
})
.then(res => {
console.log(res.data)
console.log(res.data.code)
console.log(res.data.result)
})
.catch(err => {
console.error(err);
})
}
}
3, [FromForm]
[HttpPost("ccc")]
public async Task<JsonResult> CCC([FromForm]int? a, [FromForm]int? b) {
if (a == null || b == null)
return new JsonResult(new { code = 0, result = "aaaaaaaa" });
return new JsonResult(new { code = 200, result = a + "|" + b });
}
當然,這樣寫也行,多個字段或者對象都可以
[HttpPost("ccc")]
public async Task<JsonResult> CCC([FromForm]AppJson ss) {
if (ss.a == null || ss.b == null)
return new JsonResult(new { code = 0, result = "aaaaaaaa" });
return new JsonResult(new { code = 200, result = ss.a + "|" + ss.b });
}
根據提示,使用 Postman 進行測試
事實上,這樣也行 ↓
form-data 和 x-www.form-urlencoded 都是鍵值形式,文件 form-data 可以用來上傳文件。具體的區別請自行查詢。
axios 寫法(把 Content-Type 字段修改成 form-data 或 x-www.form-urlencoded )
postccc: function () {
let fromData = new FormData()
fromData.append('a', 111)
fromData.append('b', 222)
axios.post('/api/default/ccc', fromData, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
})
.then(res => {
console.log(res.data)
console.log(res.data.code)
console.log(res.data.result)
})
.catch(err => {
console.error(err);
})
}
4, [FromHeader]
[FromHeader] 不以表單形式上傳,而是跟隨 Header 傳遞參數。
[HttpPost("ddd")]
public async Task<JsonResult> DDD([FromHeader]int? a, [FromHeader]int? b) {
if (a == null || b == null)
return new JsonResult(new { code = 0, result = "aaaaaaaa" });
return new JsonResult(new { code = 200, result = a + "|" + b });
}
axios 寫法
postddd: function () {
axios.post('/api/default/ddd', {}, {
headers: {
a: 123,
b: 133
}
})
.then(res => {
console.log(res.data)
console.log(res.data.code)
console.log(res.data.result)
})
.catch(err => {
console.error(err);
})
}
需要注意的是,headers 的參數,必須放在第三位。沒有要提交的表單數據,第二位就使用 {} 代替。
params 跟隨 url 一起在第一位,json 或表單數據等參數放在第二位,headers 放在第三位。
由於筆者對前端不太熟,這里有說錯,麻煩大神評論指出啦。
5, [FromQuery]
前面已經說了,Action 參數不加修飾,默認就是 [FromQuery] ,參考第一小節。
有個地方需要記住, Action 參數不加修飾。默認就是 [FromQuery] ,有時幾種參數並在一起放到 Action 里,會忽略掉,調試時忘記了,造成麻煩。
6, [FromRoute]
獲取路由規則,這個跟前端上傳的參數無關;跟 URL 可以說有關,又可以說無關。
[HttpPost("fff")]
public async Task<JsonResult> FFFxxx(int a,int b, [FromRoute]string controller, [FromRoute]string action) {
// 這里就不處理 a和 b了
return new JsonResult(new { code = 200, result = controller+"|"+action });
}
[FromRoute] 是根據路由模板獲取的,上面 API 的兩個參數和路由模板的名稱是對應的:
[FromRoute]string controller, [FromRoute]string action
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
當然,還可以加個 [FromRoute]int? id
[FromRoute] 和 [FromQuery] 區別
以此 URL 為例
https://localhost:5123/api/Default/fff?a=111&b=22
Route 會查到 controller = Default
,action = FFFxxx
。查詢到的是代碼里的真實名稱。
Query 會查詢到 a = 111
和 b = 22
那么,如果路由規則里,不在 URL 里出現呢?
[HttpPost("/ooo")]
public async Task<JsonResult> FFFooo(int a, int b, [FromRoute]string controller, [FromRoute]string action) {
// 這里就不處理 a和 b了
return new JsonResult(new { code = 200, result = controller + "|" + action });
}
那么,訪問地址變成 https://localhost:5123/ooo
通過 Postman ,測試
說明了 [FromRoute] 獲取的是代碼里的 Controller 和 Action 名稱,跟 URL 無關,根據測試結果推斷跟路由表規則也無關。
7, [FromService]
這個是與依賴注入容器有關,跟 URL 、路由等無關。
新建一個接口、一個類
public interface ITest
{
string GGG { get; }
}
public class Test : ITest
{
public string GGG { get { return DateTime.Now.ToLongDateString(); } }
}
在 ConfigureServices
中 注入
services.AddSingleton<ITest, Test>();
在 DefaultController
中,創建構造函數,然后
private readonly ITest ggg;
public DefaultController(ITest ttt) {
ggg = ttt;
}
添加一個 API
[HttpPost("ggg")]
public async Task<JsonResult> GGG([FromServices]ITest t) {
return new JsonResult(new { code = 200, result = t.GGG });
}
訪問時,什么參數都不需要加,直接訪問此 API 即可。
[FromService] 跟后端的代碼有關,跟 Controller 、Action 、URL、表單數據等無關。
小結:
特性可以幾種放在一起用,不過盡量每個 API 的參數只使用一種特性。
優先取值 Form > Route > Query
。
IFromFile 由於文件的上傳,本文就不談這個了。
關於數據綁定,更詳細的內容請參考:
https://docs.microsoft.com/zh-cn/aspnet/core/mvc/models/model-binding?view=aspnetcore-2.2
三. action 特性方法
Microsoft.AspNetCore.Mvc
命名空間提供可用於配置 Web API 控制器的行為和操作方法的屬性。
下表是針對於 Controller 或 Action 的特性.
特性 | 說明 |
---|---|
[Route] | 指定控制器或操作的 URL 模式。 |
[Bind] | 指定要包含的前綴和屬性,以進行模型綁定。 |
[Consumes] | 指定某個操作接受的數據類型。 |
[Produces] | 指定某個操作返回的數據類型。 |
[HttpGet] | 標識支持 HTTP GET 方法的操作。 |
... | ... |
下面使用這些屬性來指定 Controller 或 Action 接受的 HTTP 方法、返回的數據類型或狀態代碼。
1, [Route]
在微軟文檔中,把這個特性稱為 屬性路由
,定義:屬性路由使用一組屬性將操作直接映射到路由模板。
請教了大神,大神解釋說,ASP.NET Core 有路由規則表,路由表是全局性、唯一性的,在程序運行時,會把所有路由規則收集起來。
MVC 應用中設置路由的方法有多種,例如
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
[Route("Home/Index")]
public IActionResult Index() {
return View();
}
[Route("api/[controller]")]
[ApiController]
public class DefaultController : ControllerBase
{
}
路由是全局唯一的,可以通過不同形式使用,但是規則不能發生沖突,程序會在編譯時把路由表收集起來。
根據筆者經驗,發生沖突,應該就是在編譯階段直接報錯了。(注:筆者不敢確定)
關於路由,請參考 :
2, [Bind]
筆者知道這個是綁定模型的,但是對原理不太清楚。ASP.NET Core 自動生成的可讀寫的 Controller ,默認都是使用 [Bind] 來綁定數據。
文檔定義:用於對復雜類型的模型綁定。
有下面幾種相近的特性:
[BindRequired]
[BindNever]
[Bind]
微軟文檔提示:如果發布的表單數據是值的源,則這些屬性會影響模型綁定。
就是說,上面的特性是針對類、接口等復雜類型(下面統稱模型),對於 int、string 這些類型,可能出毛病。
[BindRequired] 、[BindNever] 只能應用於模型的屬性,如
public class TestB
{
[BindNever]
public int ID { get; set; }
[BindRequired]
public string Name { get; set; }
}
但是 [BindRequired] 、[BindNever] 不在討論范圍內,這里只說 [Bind]。
[Bind] 用於類或方法(Controller、Action),指定模型綁定中應包含的模型屬性。
在微軟官方文檔,對於[Bind] 的解釋:
[Bind]
屬性可用於防止“創建”方案中的過多發布情況 。 由於排除的屬性設置為 NULL 或默認值,而不是保持不變,因此它在編輯方案中無法很好地工作;- 因為
Bind
特性將清除未在 某個 參數中列出的字段中的任何以前存在的數據。
一臉懵逼。
下面是我的踩坑過程,不感興趣的話直接跳過吧。筆記筆記,記得當然是自己覺得要記的哈哈哈。
新建一個類
public class TestBind
{
public string A { get; set; }
public string B { get; set; }
public string C { get; set; }
public string D { get; set; }
public string E { get; set; }
public string F { get; set; }
public string G { get; set; }
}
新建 API
[HttpPost("hhh")]
public async Task<JsonResult> HHH([Bind("A,B,C")] TestBind test) {
if (ModelState.IsValid == true)
return new JsonResult(test);
return new JsonResult(new { Code = 0, Result = "驗證不通過" });
}
使用 Postman 進行,測試,發現必須使用 Json 形式,才能訪問到這個 Action ,其它方式會直接 返回 錯誤。
{
"errors": {
"": [
"A non-empty request body is required."
]
},
"title": "One or more validation errors occurred.",
"status": 400,
"traceId": "0HLO03IFQFTQU:00000007"
}
通過兩次 Postman 進行測試
經過測試,我猜想
ModelState.IsValid 跟模型里的驗證規則有關系,跟 [Bind] 沒關系(盡管用於測試的 TestB 類中沒有寫驗證規則),因此不能使用 ModelState.IsValid 驗證 [Bind] 是否符合規則。
Action 的參數:[Bind("A,B,C")] TestBind test
,剛開始的時候我以為請求的數據中必須包含 A、B、C。
測試后發現不是。。。再認真看了文檔 :因為 Bind
特性將清除未在 某個 參數中列出的字段中的任何以前存在的數據。
我修改一下:
[HttpPost("hhh")]
public async Task<JsonResult> HHH( string D, string E,[Bind("A,B,C")] TestBind test) {
if (ModelState.IsValid == true)
return new JsonResult(new { data1 = test, data2 = D, data3 = E });
return new JsonResult(new { Code = 0, Result = "驗證不通過" });
}
參數變成了 string D, string E,[Bind("A,B,C")] TestBind test
使用 Swagger 進行測試:
返回結果
{
"data1": {
"a": "string",
"b": "string",
"c": "string",
"d": "string",
"e": "string",
"f": "string",
"g": "string"
},
"data2": null,
"data3": null
}
改成
[HttpPost("hhh")]
public async Task<JsonResult> HHH([Bind("A,B,C")] TestBind test, string J, string Q) {
if (ModelState.IsValid == true)
return new JsonResult(new { data1 = test, data2 = J, data3 = Q });
return new JsonResult(new { Code = 0, Result = "驗證不通過" });
}
返回結果
{
"data1": {
"a": "string",
"b": "string",
"c": "string",
"d": "string",
"e": "string",
"f": "string",
"g": "string"
},
"data2": null,
"data3": null
}
文檔中對 [Bind] 描述最多的是:防止過多發布。
通過上面的測試,首先肯定的是一個 Action 里,有多個參數 如
[Bind("A,B,C")] TestBind test, string D, string E string J, string Q
。
注意,下面的結論是錯的!
那么 D、E 因為於 除了 Test, J、Q就會無效,通過百度,[Bind] 修飾的 Action ,前端請求的數據只有 Test 里面的數據有效,其它 Query等形式一並上傳的數據都會失效,防止黑客在提交數據時摻雜其它特殊參數。應該就是這樣理解吧。
上面是一開始我的結論,直到多次測試,我發現是錯的。
可是有一個地方不明白,
[Bind("A,B,C")]
[Bind("A,B,C,D,E,F,G")]
這兩者的區別是是什么。還是沒搞清楚。
突然想到 Query,當字段沒有使用特性修飾時,默認為 Query 。
最終踩坑測試代碼
模型類
public class TestBind
{
public string A { get; set; }
public string B { get; set; }
public string C { get; set; }
public string D { get; set; }
public string E { get; set; }
public string F { get; set; }
public string G { get; set; }
}
Action
[HttpPost("hhh")]
public async Task<JsonResult> HHH( string A, string B, string E, string F, string G, [Bind("A,B,C,D")] TestBind test, string C, string D, string J, string Q) {
if (ModelState.IsValid == true)
return new JsonResult(new
{
data1 = test,
dataA = A,
dataB = B,
dataC = C,
dataD = D,
dataE = E,
dataF = F,
dataG = G,
dataJ = J,
dataQ = Q
});
return new JsonResult(new { Code = 0, Result = "驗證不通過" });
}
Swagger 測試
Postman 測試
{
"data1": {
"a": "111",
"b": "111",
"c": "111",
"d": "111",
"e": "111",
"f": "111",
"g": "111"
},
"dataA": "222",
"dataB": "222",
"dataC": "222",
"dataD": "222",
"dataE": "222",
"dataF": "222",
"dataG": "222",
"dataJ": "222",
"dataQ": "222"
}
再在 Swagger 或 Postman ,換着法子嘗試各種不同組合的輸入。
我懵逼了。試了半天試不出什么。
實在不理解 [Bind] 里,“防止過多發布” 是什么意思
[Bind("A,B,C")]
[Bind("A,B,C,D,E,F,G")]
這兩者的區別是是什么。還是沒搞清楚。算了,不踩了。
我再到 stackoverflow 提問題,地址 https://stackoverflow.com/questions/56884876/asp-net-core-bind-how-to-use-it/56885153#56885153
獲得一個回答:
What's the difference between [Bind("A,B,C")] and [Bind("A,B,C,D,E,F,G")]?
The former tells the model binder to include only the properties of TestBind named A, B and C. The latter tells the model binder to include those same properties plus D, E, F and G.
Are you testing by posting data for all properties of your model? You should notice that the values you post for the excluded properties are not bound.
算了,嘿嘿,測試不出來,放棄。
3, [Consumes]、[Produces]
[Consumes("application/json")]
[Produces("application/json")]
[Produces("application/xml")]
[Produces("text/html")]
... ...
目前只了解到 [Consumes]、[Produces] 是篩選器,用來表示 Controller 或 Action 所能接受的數據類型。大概就是像下面這樣使用:
[Consumes("application/json")]
[Produces("application/json")]
public class DefaultTestController : ControllerBase
{
}
但是如何實際應用呢?我找了很久,都沒有找到什么結果。在 stackoverflow 找到一個回答:
https://stackoverflow.com/questions/41462509/adding-the-produces-filter-globally-in-asp-net-core
4, [HttpGet]、[HttpPost]、[HttpDelete]、[HttpPut]
修飾 Action ,用來標識這個 Action 能夠通過什么方式訪問、訪問名稱。
例如:
[Route("api/[controller]")]
[ApiController]
public class DefaultController : ControllerBase
{
[HttpPost("aaa")]
public async Task<JsonResult> AAA(int? a, int? b) {
if (a == null | b == null)
return new JsonResult(new { code = 0, result = "aaaaaaaa" });
return new JsonResult(new { code = 200, result = a + "|" + b });
}
}
訪問地址 https://localhost:5123/api/Default/aaa
使用時,會受到 Controller 和 Action 路由的影響。
但 本身亦可控制路由。以上面的控制器為例
[HttpPost("aaa")] //相對路徑
訪問地址 xxx:xxx/api/Default/aaa
[HttpPost("/aaa")] //絕對路徑
訪問地址 xxx:xxx/aaa
四,返回類型
1, 查詢備忘表
Microsoft.AspNetCore.Mvc
命名空間中,包含控制 MVC 的各種操作方法和類型,筆者從命名空間中抽出與 MVC 或 API 返回類型有關的類型,生成表格:
類型 | 描述 |
---|---|
AcceptedAtActionResult | An ActionResult that returns a Accepted (202) response with a Location header. |
AcceptedAtRouteResult | An ActionResult that returns a Accepted (202) response with a Location header. |
AcceptedResult | An ActionResult that returns an Accepted (202) response with a Location header. |
AcceptVerbsAttribute | Specifies what HTTP methods an action supports. |
ActionResult | A default implementation of IActionResult. |
ActionResult | A type that wraps either an TValue instance or an ActionResult. |
BadRequestObjectResult | An ObjectResult that when executed will produce a Bad Request (400) response. |
BadRequestResult | A StatusCodeResult that when executed will produce a Bad Request (400) response. |
ChallengeResult | An ActionResult that on execution invokes AuthenticationManager.ChallengeAsync. |
ConflictObjectResult | An ObjectResult that when executed will produce a Conflict (409) response. |
ConflictResult | A StatusCodeResult that when executed will produce a Conflict (409) response. |
ContentResult | |
CreatedAtActionResult | An ActionResult that returns a Created (201) response with a Location header. |
CreatedAtRouteResult | An ActionResult that returns a Created (201) response with a Location header. |
CreatedResult | An ActionResult that returns a Created (201) response with a Location header. |
EmptyResult | Represents an ActionResult that when executed will do nothing. |
FileContentResult | Represents an ActionResult that when executed will write a binary file to the response. |
FileResult | Represents an ActionResult that when executed will write a file as the response. |
FileStreamResult | Represents an ActionResult that when executed will write a file from a stream to the response. |
ForbidResult | An ActionResult that on execution invokes AuthenticationManager.ForbidAsync. |
JsonResult | An action result which formats the given object as JSON. |
LocalRedirectResult | An ActionResult that returns a Found (302), Moved Permanently (301), Temporary Redirect (307), or Permanent Redirect (308) response with a Location header to the supplied local URL. |
NotFoundObjectResult | An ObjectResult that when executed will produce a Not Found (404) response. |
NotFoundResult | Represents an StatusCodeResult that when executed will produce a Not Found (404) response. |
OkObjectResult | An ObjectResult that when executed performs content negotiation, formats the entity body, and will produce a Status200OK response if negotiation and formatting succeed. |
OkResult | An StatusCodeResult that when executed will produce an empty Status200OK response. |
PartialViewResult | Represents an ActionResult that renders a partial view to the response. |
PhysicalFileResult | A FileResult on execution will write a file from disk to the response using mechanisms provided by the host. |
RedirectResult | An ActionResult that returns a Found (302), Moved Permanently (301), Temporary Redirect (307), or Permanent Redirect (308) response with a Location header to the supplied URL. |
RedirectToActionResult | An ActionResult that returns a Found (302), Moved Permanently (301), Temporary Redirect (307), or Permanent Redirect (308) response with a Location header. Targets a controller action. |
RedirectToPageResult | An ActionResult that returns a Found (302) or Moved Permanently (301) response with a Location header. Targets a registered route. |
RedirectToRouteResult | An ActionResult that returns a Found (302), Moved Permanently (301), Temporary Redirect (307), or Permanent Redirect (308) response with a Location header. Targets a registered route. |
SignInResult | An ActionResult that on execution invokes AuthenticationManager.SignInAsync. |
SignOutResult | An ActionResult that on execution invokes AuthenticationManager.SignOutAsync. |
StatusCodeResult | Represents an ActionResult that when executed will produce an HTTP response with the given response status code. |
UnauthorizedObjectResult | An ObjectResult that when executed will produce a Unauthorized (401) response. |
UnauthorizedResult | Represents an UnauthorizedResult that when executed will produce an Unauthorized (401) response. |
UnprocessableEntityObjectResult | An ObjectResult that when executed will produce a Unprocessable Entity (422) response. |
UnprocessableEntityResult | A StatusCodeResult that when executed will produce a Unprocessable Entity (422) response. |
UnsupportedMediaTypeResult | A StatusCodeResult that when executed will produce a UnsupportedMediaType (415) response. |
ViewComponentResult | An IActionResult which renders a view component to the response. |
ViewResult | Represents an ActionResult that renders a view to the response. |
VirtualFileResult | A FileResult that on execution writes the file specified using a virtual path to the response using mechanisms provided by the host. |
留着寫 WebApi 時查詢備忘嘿嘿。
那些類型主要繼承的兩個接口:
類型 | 描述 |
---|---|
IActionResult | Defines a contract that represents the result of an action method. |
IViewComponentResult | Result type of a ViewComponent. |
注意的是,上面有些是抽象類,例如 FileResult,而 FileStreamResult 實現了 FileResult 。有些類是繼承關系。
2, 返回的數據類型
- 特定類型
- IActionResult 類型
- ActionResult 類型
Action 的 return ,返回的數據類型必定是上面三種。
3, 直接返回基元或復雜數據類型
[HttpGet]
public IEnumerable<Product> Get() {
return _repository.GetProducts();
}
4, IActionResult 類型
響應狀態碼、Json、重定向、URL 跳轉等,屬於 IActionResult。
MVC 的 Controller 與 API 的 Controller 有很多相同的地方,亦有很多不同的地方。
API 的 Controller 繼承 ControllerBase
MVC 的 Controller 繼承 Controller而 Controller 繼承
Controller : ControllerBase, IActionFilter, IFilterMetadata, IAsyncActionFilter, IDisposable
API 里的 Controller 是最原始的。
API 里的 返回類型需要實例化, new 一下; MVC 里的返回類型,“不需要實例化”。
當然,有些例如 FileResult 是抽象類,不能被實例化。
API:
[HttpGet("returnaaa")]
public async Task<IActionResult> ReturnAAA() {
return new ViewResult();
return new JsonResult(new { code="test"});
return new RedirectToActionResult("DefaultController","ReturnAAA","");
return new NoContentResult("666");
return new NotFoundResult();
...
}
MVC
public async Task<IActionResult> Test() {
return View();
return Json(new { code = "test" });
return RedirectToAction("DefaultController", "ReturnAAA", "");
return NoContent("666");
return NotFound();
...
}
MVC 中,Action 默認是 [HttpGet],不加也可以被訪問到;
而 API 的Action,不加 [Httpxxx],則默認不能被訪問到。