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;

public class User
{[Required]public  string Name { get; set; }public string Code { get; set; }
}

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

希望本文对您有所帮助。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/310740.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

你需要了解的 HTTP Status Code

你需要了解的 HTTP Status CodeIntro现在前后端分离的开发模式越来越流行&#xff0c;后端负责开发对应的 API&#xff0c;前端只需要 关注前端页面的数据展示和前端逻辑即可。对于前后端分离这种开发模式&#xff0c;我个人还是比较喜欢的&#xff0c;因为这样可以让更专业的人…

LeetCode 24两两交换链表中的节点-中等

给定一个链表&#xff0c;两两交换其中相邻的节点&#xff0c;并返回交换后的链表。 你不能只是单纯的改变节点内部的值&#xff0c;而是需要实际的进行节点交换。 输入&#xff1a;head [1,2,3,4] 输出&#xff1a;[2,1,4,3] 示例 2&#xff1a; 输入&#xff1a;head []…

2021中考高考成绩查询,2021中考

2021年浙江东阳中考查分入口暂未公布&#xff01;如有最新信息&#xff0c;中考网会第一时间发布&#xff0c;请中考生和家长及时关注中考网中考考试时间频道&#xff01; 编辑推荐&#xff1a; 2021年浙江省中考查分时间及入口汇总 2021年全国各省市中考查分时间及入2021-06-1…

今天网站都变成灰色了,这其中是怎么实现的?

“ 阅读本文大概需要 7 分钟。 ”今天是 2020 年 4 月 4 日&#xff0c;星期六&#xff0c;清明节。我们的国家经历了非常惨痛的时刻&#xff0c;很多英雄在救助他人的路上倒下&#xff0c;更有很多烈士英雄保卫人民的安危遇难&#xff0c;今天全国下降半旗&#xff0c;北京时间…

深圳市公务员考试计算机专业素养,深圳市考职位分析_公务员考试专业对照表...

2020深圳市公务员招录1069人公告已发布&#xff0c;报名时间&#xff1a;11月13日-19日16:00&#xff0c;报名入口&#xff1a;深圳市考试院专栏(http://hrss.sz.gov.cn/szksy/)或深圳市人事考试考生服务系统(以下简称考生服务系统&#xff0c;https://hrsstext.sz.gov.cn/ess/…

LeetCode 61旋转链表-中等

给你一个链表的头节点 head &#xff0c;旋转链表&#xff0c;将链表每个节点向右移动 k 个位置。 输入&#xff1a;head [1,2,3,4,5], k 2 输出&#xff1a;[4,5,1,2,3] 输入&#xff1a;head [0,1,2], k 4 输出&#xff1a;[2,0,1] 提示&#xff1a; 链表中节点的数目在…

科个普:进程、线程、并发、并行

一、进程刘大胖打开电脑&#xff0c;想写点东西&#xff0c;于是打开WPS&#xff0c;突然又想和女朋友(反正我不信)聊聊天&#xff0c;就又打开了微信PC端&#xff0c;这时操作系统就会为这两个程序生成两个进程&#xff0c;如图&#xff1a;二、线程每个进程至少包含一个线程&…

LeetCode 19删除链表的倒数第N个节点-中等

给你一个链表&#xff0c;删除链表的倒数第 n 个结点&#xff0c;并且返回链表的头结点。 进阶&#xff1a;你能尝试使用一趟扫描实现吗&#xff1f; 输入&#xff1a;head [1,2,3,4,5], n 2 输出&#xff1a;[1,2,3,5] 示例 2&#xff1a; 输入&#xff1a;head [1], n …

EFCore查询语句生成流程、让EFCore支持批量Update/Delete/MergeInto

引子之前发现了一款叫 EFCore.BulkExtensions 的 nuget 包。里面提供了大量的 BulkInsertOrUpdateOrDelete 和 BatchUpdate 的拓展&#xff0c;可以很方便的解决批量更新和删除的问题&#xff0c;不用让 EFCore 一条一条的删除和更新。其中几个比较有用的函数签名是Task<int…

html程序国庆节祝福,2018国庆节祝福祖国的话

2018国庆节即将来袭~那么2018国庆节祝福祖国的话有哪些呢&#xff1f;今天语录大全网小编就为大家整理了一篇10.1国庆节祝福祖国的话语&#xff0c;分享给大家&#xff0c;在这里小编祝大家国庆节快乐1、【祖国是东方的明珠&#xff0c;是亚洲腾飞的巨龙&#xff0c;是地平线上…

【翻译】.NET 5 Preview2发布

在4月2日&#xff0c;发布了.NET 5.0 Preview2&#xff0c;这次发布对一些功能和性能做了相关的改进&#xff0c;同时后面也会实施5.0版本更多的功能&#xff0c;其中一些功能目前也dotnet/designs在.NET 5 Preview1中可以看到.NET 5里程碑中已经完成的建设任务&#xff0c;当然…

LeetCode 142环形链表||-中等

给定一个链表&#xff0c;返回链表开始入环的第一个节点。 如果链表无环&#xff0c;则返回 null。 为了表示给定链表中的环&#xff0c;我们使用整数 pos 来表示链表尾连接到链表中的位置&#xff08;索引从 0 开始&#xff09;。 如果 pos 是 -1&#xff0c;则在该链表中没有…

.NET 5.0 Preview 2发布解析

2020年4月2日微软.NET 团队的项目经理 Richard 在博客上 发布了.NET 5 Preview 2&#xff1a;https://devblogs.microsoft.com/dotnet/announcing-net-5-0-preview-2/ &#xff0c;3月16号&#xff0c;Scott Hunter 在博客中发布了.NET 5 Preview 1 第一个预览版发布。https://…

DotNet SSL TLS证书问题分析排障

问题说明前几天运维同事反馈开发同事代码在Windows 2008 R2 Datacenter服务器上跑会出现无法正常建立SSL/TLS连接的情况&#xff0c;在自己的电脑上跑是OK的&#xff0c;代码也没有变动过。于是我问他改了服务器上什么配置没有&#xff0c;他说改了注册表也不行。接过这个坑&am…

LeetCode 92反转链表||-中等

给你单链表的头指针 head 和两个整数 left 和 right &#xff0c;其中 left < right 。请你反转从位置 left 到位置 right 的链表节点&#xff0c;返回 反转后的链表 。 输入&#xff1a;head [1,2,3,4,5], left 2, right 4 输出&#xff1a;[1,4,3,2,5] 示例 2&#xf…

oppo手机html文件,OPPO手机怎么传输数据包括文档和图片,有哪些传输方式

在工作和学习中&#xff0c;我们每天都需要传输大量的数据包括文档和图片等&#xff0c;对于大文件和大量文件的传输&#xff0c;既要保证传输的质量还要操作简单&#xff0c;省时省力&#xff0c;这也就成了一个令人头疼的问题。不同的传输方式有着不同的特点&#xff0c;那么…

如何让Docker镜像飞起来

前言Docker用起来非常爽&#xff0c;尤其是用于DevOps实践时。但是&#xff0c;当你在国内或者本地拉取镜像时&#xff0c;经常会碰到各种“便秘”——要么镜像拉取缓慢&#xff0c;要么时断时连&#xff0c;要么连接超时&#xff01;当我们的镜像又比较大时&#xff08;比如某…

C++ class类 实现搜索二叉树(BST)

代码如下: #include <iostream> using namespace std;class BSTNode {private:double key;BSTNode *lchild;BSTNode *rchild;BSTNode *parent;friend class BSTree;public:BSTNode(double k 0.0, BSTNode *l nullptr, BSTNode *r nullptr, BSTNode *p nullptr): key…

Nuget多项目批量打包上传服务器的简明教程

本篇不会介绍Nuget是什么&#xff0c;如何打包上传Nuget包&#xff0c;怎么搭建私有Nuget服务器。这些问题园子里都有相应的文章分享&#xff0c;这里不做过多阐述。另外本文假设你已经下载了Nuget.exe&#xff0c;并且已经设置好了环境变量。什么&#xff1f;你还不会&#xf…

C++ struct结构体 实现搜索二叉树(BST)

代码如下: #include <iostream> using namespace std;struct BSTNode {double v 0.0;BSTNode *lc nullptr;BSTNode *rc nullptr;BSTNode *par nullptr; };void inorder_tree(BSTNode *t) {if (t ! nullptr) {inorder_tree(t->lc);cout << t->v <<…