dotNET Core 3.X 使用 Web API
現在的 Web 開發大多都是前后端分離的方式,后端接口的正確使用顯得尤為重要,本文講下在 dotNET Core 3.X 下使用 Web API 。
環境
操作系統:Mac
IDE:Rider
dotNET Core:3.1
創建項目
如果是 Windows 操作系統當然是首選 VS2019 ,在 Mac 中雖然也有 VS2019 For Mac,但還是感覺 Rider 比較好用(調試和智能提示),在 Rider 中創建 Web API 項目:
3.x 和 2.x 區別
1、Program 類的 IWebHostBuilder 修改為了 IHostBuilder,這一塊的改動如果是直接使用 3.x 可以不用過于關心,如果是從 2.x 升級到 3.x,就要注意了,兩個 Program 類對比結果如下圖:
2、Startup 類的區別如下圖:
最重要的是在 3.x 中使用的是 services.AddControllers(); 來注冊服務,相比?2.x 中的 services.AddMvc() 更加輕量級,因為在 AddMvc 方法中添加了很多 Web API 不需要的功能,如下圖:
3、3.x 引入了新的 JSON API ,新的 JSON API 使用更少的內存,擁有更快的執行速度,引用 using System.Text.Json; 就可以使用,如果需要使用原來的功能,需要引入 Nuget包:Microsoft.AspNetCore.Mvc.NewtonsoftJson
另:
有關 3.x 中被刪除的程序集可以參考這里:https://github.com/dotnet/aspnetcore/issues/3755
有關 3.x 中性能提升可以參考這篇文章:https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-core-3-0/
[ApiController] 特性
在 3.x 中默認項目模板中會創建的一個名為 WeatherForecastController 的控制器,按照約束控制器類以 Controller 結尾。
可以看到在 WeatherForecastController 類的上面自動添加了 [ApiController] 特性,添加此特性后,會對 Api 功能有所加持,比如:
自動模型狀態驗證
意思是當客戶端傳遞的模型數據(輸入參數)不符合要求時,在接口方法中不需要做任何處理,接口會自動返回 400 的錯誤,看下面的例子:
1、創建 UserController 類,并將 [ApiController] 特性注釋掉;
2、添加 User 類,將 Name 屬性設置為 Required;
3、在?UserController 類中添加 AddUser 方法
[HttpPost] [Route("adduser")] public?ActionResult?AddUser(User?user) {????return?Ok(); }4、使用 Postman 調用,沒有添加任何參數,返回的結果為?200
這個結果不是我們所期望的,之前沒有 [ApiController] 特性的時候,需要在接口方法中處理,如下:
[HttpPost] [Route("adduser")] public?ActionResult?AddUser(User?user) {if?(!ModelState.IsValid){return?BadRequest((ModelState));}return?Ok(); }5、再用 Postman 調用,結果如下:
6、現在添加上 [ApiController] 特性,并將 AddUser 中的校驗邏輯去掉,再次使用 Postman,結果如下:
推斷參數綁定源
之前需要在參數上添加 [FromBody]、[FromQuery]等特性,現在可以去掉這些特性,系統會自動推斷參數的來源,比如:如果一個參數在 Route 里面定義了,會自動從先從Path 查找,沒找到會從查詢參數上查找然后進行綁定。
錯誤狀態碼詳細信息
之前的版本中,如果接口返回一個 BadRequest,是沒有內容的,只有狀態碼,如下:
加上 [ApiController] 特性后,結果如下:
基類
在 3.x 中創建控制器后,默認的基類為 ControllerBase ,該類中提供了 OK、BadRequest 等常用方法給我們使用。
在我們實際開發中,通常會自定義添加一個所有 ?Controller 類的基礎類,一些通用的功能可以放到基類中,比如,對 AutoMapper 的注入,代碼如下:
public?class?BaseController:?ControllerBase {private?readonly?IMapper?_mapper;public?BaseController(IMapper?mapper){_mapper?=?mapper;}public?IMapper?Mapper?=>?_mapper; }HTTP 方法
先看下面這張圖
按照標準的 RESTful Web API 風格,不同的請求動作需要使用相對應的方法,但實際我們最常用的是 GET 和 POST,查詢使用 GET,其他的操作都是使用 POST。
HTTP 狀態碼
正確的返回狀態碼有助于客戶端分析請求返回結果和問題排查,常用的狀態碼如下:
常見的一個問題:由于客戶端參數的問題,導致接口代碼中執行異常了,最終返回了 500,導致排查問題非常復雜,還需要還原問題場景下的數據和入參。正確的做法應該是對參數做相關校驗最終返回相應的 4XX 的狀態碼。
輸入參數
模型綁定
接口的輸入參數就是通過模型綁定將 HTTP 請求中的值映射到參數中,模型綁定有以下六種:
[FromRoute]:通過路由的 URL 中取值,可以自動推斷;
[FromQuery]:獲取 URL 地址中的參數,可以自動推斷;
[FromBody]:從HTTP Body取值,通常用于取JSON, XML,可以自動推斷;
[FromHeader]:獲取 Request Header 中的參數信息,需要指定
[FromForm]:獲取 Content-Type 為 multipart/form-data 或 application/x-www-form-urlencoded 類型的參數,需要指定
[FromServices]:獲取依賴注入的參數,依賴注入默認是使用構造函數注入,但Controller 可能會因為每個Action用到不一樣的 Service 導致很多參數,所以也可以在 Action 注入Service,需要指定。
下面實現一個使用 [FromServices] 的示例:
1、創建 IUserService 接口和 UserService 類,代碼如下:
public?interface?IUserService {string?GetUserName(string?userId); } public?class?UserService:IUserService {public?string?GetUserName(string?userId){return?$"UserName:{userId}";} }2、在 Startup 類的 ConfigureServices 方法中添加下面代碼進行注冊
services.AddScoped<IUserService,UserService>();3、添加 UserController 類,里面添加名為 GetUserName 的 Action 方法
[HttpGet]public?ActionResult<string>?GetUserName(string?userId,??[FromServices]IUserService?userService) {return?Ok($"{userService.GetUserName(userId)}"); }4、執行結果如下:
參數驗證
參數驗證是非常重要的,否則本來是 4XX 的問題就會變成 5XX 的問題,參數驗證有這么幾種:
Data Annotations
自定義 Attribute
實現 IValitableObject 接口
使用第三方的驗證庫,比如 FluentValidation
Data Annotations
1、在 User 的實體類上添加相關特性
public?class?User {[Required(ErrorMessage?=?"姓名不能為空")]public?string??Name?{?get;?set;?}[EmailAddress(ErrorMessage?=?"郵件格式不正確")]public?string??Email?{?get;?set;?} }2、調用結果如下:
有關更多的 Data Annotations 特性的使用,可以參考官方文檔:https://docs.microsoft.com/en-us/dotnet/api/system.componentmodel.dataannotations?view=netcore-3.1
IValitableObject 接口
1、將 User 類繼承 IValitableObject 接口,并實現 Validate 方法,代碼如下:
public?class?User:?IValidatableObject {[Required(ErrorMessage?=?"姓名不能為空")]public?string??Name?{?get;?set;?}[EmailAddress(ErrorMessage?=?"郵件格式不正確")]public?string??Email?{?get;?set;?}public?IEnumerable<ValidationResult>?Validate(ValidationContext?validationContext){if?(Name?==?Email){yield?return?new?ValidationResult("名稱不能和郵箱相等",new?[]{nameof(Name),nameof(Email)});}} }2、調用結果如下:
自定義 Attribute
自定義 Attribute 功能和 IValitableObject 接口類似,但可以作用于類級別也能用于屬性級別,更加靈活。
1、創建 NameNotEqualEmailAttribute 類,用來實現判斷 User 類中的名稱和郵箱不能相等
public?class?NameNotEqualEmailAttribute?:?ValidationAttribute {protected?override?ValidationResult?IsValid(object?value,?ValidationContext?validationContext){var?user?=?validationContext.ObjectInstance?as?User;if?(user.Name?==?user.Email){return?new?ValidationResult("名稱不能和郵箱相等",new?[]{nameof(User)});}return?ValidationResult.Success;} }2、在 User 類上添加此特性
[NameNotEqualEmail] public?class?User {[Required(ErrorMessage?=?"姓名不能為空")]public?string??Name?{?get;?set;?}[EmailAddress(ErrorMessage?=?"郵件格式不正確")]public?string??Email?{?get;?set;?} }3、調用結果如下:
FluentValidation
FluentValidation 就不多做介紹了,可以參見官方文檔:https://fluentvalidation.net/
ModelBinder
ModelBinder 是自定義模型綁定器,可以對入參的類型進行一些轉換,比如,參數中傳遞 001,002 這樣的字符串,在接口中使用 IEnumerable來進行接收。
1、創建 StringToListModelBinder 類,如下:
public?class?StringToListModelBinder:?IModelBinder { public?Task?BindModelAsync(ModelBindingContext?bindingContext) {if?(!bindingContext.ModelMetadata.IsEnumerableType){bindingContext.Result?=?ModelBindingResult.Failed();return?Task.CompletedTask;}var?value?=?bindingContext.ValueProvider.GetValue(bindingContext.ModelName).ToString();if?(string.IsNullOrWhiteSpace(value)){bindingContext.Result?=?ModelBindingResult.Success(null);return?Task.CompletedTask;}var?elementType?=?bindingContext.ModelType.GetTypeInfo().GenericTypeArguments[0];var?converter?=?TypeDescriptor.GetConverter(elementType);var?values?=?value.Split(new[]?{','},?StringSplitOptions.RemoveEmptyEntries).Select(x?=>?converter.ConvertFromString(x.Trim())).ToArray();var?typedValues?=?Array.CreateInstance(elementType,?values.Length);values.CopyTo(typedValues,0);bindingContext.Model?=?typedValues;bindingContext.Result?=?ModelBindingResult.Success(bindingContext.Model);return?Task.CompletedTask; }2、在 UserController 類中創建 GetUsersByIds 方法
[HttpGet("ids")] public?ActionResult<List<User>>?GetUsersByIds([ModelBinder(BinderType?=?typeof(StringToListModelBinder))]IEnumerable<string>?ids) {if?(ids?==?null){return?BadRequest();}return?Ok();}3、調用結果
返回值
返回 XML 格式
盡管使用 Web API 通常都是使用 JSON 格式,但有些時候需要返回 XML 格式,默認情況下,即使請求頭中添加了 Accept=application/xml,接口依然會返回 JSON 格式的結果,想要返回 XML 格式,修改 Startup 類的 ConfigureServices 方法即可。
services.AddControllers().AddXmlDataContractSerializerFormatters();結果如下:
錯誤信息統一返回
之前的文章中有講過使用過濾器的方式來做到結果的統一返回。這里介紹另一種方式,使用 ConfigureApiBehaviorOptions ,可以讓我們自定義錯誤信息的返回內容和格式。修改 Startup 類中的 ConfigureServices 方法
services.AddControllers().AddXmlDataContractSerializerFormatters().ConfigureApiBehaviorOptions(setup?=>{setup.InvalidModelStateResponseFactory?=?context?=>{var?details?=?new?ValidationProblemDetails(context.ModelState){Type?=?"http://api.oec2003.com/help",Title?=?"實體驗證錯誤",Status?=?StatusCodes.Status422UnprocessableEntity,Detail?=?"看詳細",Instance?=?context.HttpContext.Request.Path,};details.Extensions.Add("trachid",context.HttpContext.TraceIdentifier);return?new?UnprocessableEntityObjectResult(details){ContentTypes?=?{?"application/problem+json"?}};};});當出現驗證問題時,結果如下:
更多詳細信息可以看文檔:https://docs.microsoft.com/zh-cn/aspnet/core/web-api/handle-errors?view=aspnetcore-3.1
數據塑形
在 API 中返回結果到前端時,一般不會直接將底層的 Entity 返回,會創建相對應的 Dto,比如,用戶的 Entity 是這樣的
public?class?User {public?string??Name?{?get;?set;?}public?string??Email?{?get;?set;?}public?string??Password?{?get;?set;?} }創建 User 的 Dto 類 UserDto,如下
public?class?UserDto {public?string??Name?{?get;?set;?}public?string??Email?{?get;?set;?}}在接口的 Action 方法中使用 AutoMapper 做下轉換
[HttpGet("{userId}")] public?ActionResult<UserDto>?GetUserById(string?userId) {User?user?=?new?User(){Name?=?"oec2003",Email?=?"oec2003@qq.com",Password?=?"123456"};return?Ok(base.Mapper.Map<UserDto>(user)); }請求結果如下:
同樣的接口在前端不同的場景下需要返回不一樣的字段數據,一種方式是創建很多不同的接口,返回不同的 Dto 的結果,但這樣做非常繁瑣,可以通過 ExpandoObject 來實現按客戶端的需要進行返回結果,具體步驟如下:
1、因為獲取用戶列表的接口方法的是 List,所以先創建一個 IEnumerable 的擴展方法,該擴展方法用于根據傳進的字段參數來組裝返回的結果,代碼如下:
public?static?class?IEnumerableExtension {public?static?IEnumerable<ExpandoObject>?GetData<T>(this?IEnumerable<T>?source,?string?fields){if?(source?==?null){throw?new?ArgumentNullException(nameof(source));}var?objectList?=?new?List<ExpandoObject>(source.Count());var?propertyInfoList?=?new?List<PropertyInfo>();if?(string.IsNullOrWhiteSpace(fields)){var?propertyInfos?=?typeof(T).GetProperties(BindingFlags.Public?|BindingFlags.Instance);propertyInfoList.AddRange(propertyInfos);}else{var?fieldSplit?=?fields.Split(',');foreach?(var?field?in?fieldSplit){var?propertyName?=?field.Trim();var?propertyInfo?=?typeof(T).GetProperty(propertyName,BindingFlags.IgnoreCase?|?BindingFlags.Public?|?BindingFlags.Instance);if?(propertyInfo?==?null){throw??new?Exception($"屬性名:{propertyName}?沒有找到");}propertyInfoList.Add(propertyInfo);}}foreach?(T?t?in?source){var?obj=new?ExpandoObject();foreach?(var?propertyInfo?in?propertyInfoList){var?value?=?propertyInfo.GetValue(t);((IDictionary<string,?object>)?obj).Add(propertyInfo.Name,?value);}objectList.Add(obj);}return?objectList;} }2、創建獲取用戶列表的 Action 方法
[HttpGet] public?ActionResult?GetUsers([FromBody]string?fields) {var?userList?=new?List<User>()?{new?User(){?Name?=?"oec2003",Email?=?"oec2003@qq.com",Password?=?"123456"},new?User(){?Name?=?"oec2004",Email?=?"oec2004@qq.com",Password?=?"123456"},new?User(){?Name?=?"oec2004",Email?=?"oec2004@qq.com",Password?=?"123456"}};var?returnResult?=?base.Mapper.Map<List<UserDto>>(userList);//使用擴展方法按需獲取return?Ok(returnResult.GetData(fields)); }3、查看調用結果
返回一個屬性 Name
返回所有
最后
本文只是涉及了在 Web API 中比較常用的一些功能點,限于篇幅,每個點并沒有寫的非常深入,也較少涉及原理,但我們在學習過程中,除了實現效果外還應該深入去了解其中細節和原理。
文中示例代碼:https://github.com/oec2003/DotNetCoreThreeAPIDemo
希望本文對您有所幫助。
總結
以上是生活随笔為你收集整理的dotNET Core 3.X 使用 Web API的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 你复工了吗?啥感受?
- 下一篇: 今天网站都变成灰色了,这其中是怎么实现的