.Net Core Web Api 1、什么是.Net Core Api
Api
:Application Programming Interface,应用程序编程接口
前后端分离:
2、.Net Core Api 与 MVC
开发区别 返回数据格式不同,Api
返回json
格式。
MVC
:返回整个html
页面。
3、Api
项目创建 在创建项目的时候,我们选择的是Asp.net Core Web Api
的项目。
在创建项目的时候把启用OpenAPI
支持,这一个选项选中。
创建好的项目中有Controllers
文件夹,但是没有Views
文件夹,因为现在我们不需要操作视图。
因为,视图也就是我们所说的页面,都是有前端开发人员进行开发的,而我们作为服务端开发人员,不用关心页面的实现,
也就是说,我们服务端开发人员,只负责实现服务端的业务,并且向前端提供数据,前端页面应该怎样展示数据,服务端开发人员不用关心,都是有前端开发人员实现的。
当启动默认创建好的Api
程序以后,会呈现出Swagger
。
这是由于我们在创建项目的时候,选择了OpenAPI
这个选项以后所提供的。
因为Api
项目没有界面,所以提供了Swagger
访问我们所创建的Api
程序。普通用户是无法看到这个Swagger
页面的。
普通用户是通过浏览器,App
,微信小程序来访问我们的api
程序的。所以说Swagger
页面是我们开发人员,对Api
接口程序进行调试所使用的。
1 2 [ApiController ] [Route("[controller]" ) ]
1 [Route("api/[controller]" ) ]
如果发送的请求是一个get
请求,就会被Get
这个方法所处理。
当然,这里我们也可以直接在浏览器的地址栏中输入api
的地址进行访问。
4、第一个Api
程序 在这一小节中,我们重新创建一个控制器
。
注意:这里在创建控制器的时候,一定要选择的是API控制器
在UserInfoController.cs
这个控制器中添加如下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 [Route("api/[controller]" ) ] [ApiController ] public class UserInfoController : ControllerBase { [HttpGet ] public Person GetPerson () { return new Person() { Id = 1 , Name = "Test" , Password = "123" }; } }
当以Get
请求的方式,请求GetPerson
方法的时候,会返回用户的信息,这里返回的是一个对象。
这里需要在项目中创建Models
文件夹,在该文件夹中创建一个Person.cs
类,该类中的代码如下所示:
1 2 3 4 5 6 public class Person { public int Id { get ; set ; } public string ? Name { get ; set ; } public string ? Password { get ; set ; } }
下面我们在发送一个Post
请求
1 2 3 4 5 6 7 8 9 10 11 12 [HttpPost ] public ApiResult Login (Person p ) { if (p.Name =="admin" && p.Password == "123" ) { return new ApiResult { Code = 200 , Message = "登录成功" }; } else { return new ApiResult { Code = 500 , Message = "登录失败" }; } }
返回的结果是通过ApiResult
这个类来表示的。
所以在Models
目录下面创建ApiResult
这个类,该类中的代码如下所示:
1 2 3 4 5 public class ApiResult { public int Code { get ; set ; } public string Message { get ; set ; } }
启动程序进行测试。
这里会要求输入请求体。
5、什么是REST/Restful
Rest
与Restful
是一样的,只是不同的叫法。
针对Web Api
的开发风格(所谓的风格,指的是请求方式怎么使用,状态码怎样使用,路径怎样使用),有两种。
分别是面向过程(RPC
【Remote Procedure Call
】), 面向Rest
【Representational State Transfer
】
先来看一下面向过程:
1 2 3 在RPC风格的WebAPI中,我们通过“控制器/操作方法”的形式把服务器端的代码当成方法去调用。这样的接口只是把HTTP当成一个传输数据的通道,而不关心HTTP谓词的语义(`http`语义,可以理解成协议的一种规范、约定、要求,。例如,删除数据,按照`http`的语义来讲,应该发送`delete`请求,但是我们前面发送的是`get`请求,这就是说明我们定义的`api`不符合`http`协议的语义)、状态码。比如:/Persons/GetAll、/Persons/GetById?id=8、/Persons/Update、/Persons/DeleteById/8; 这种方式简单,粗暴,很多人都习惯采用这种方式来开发`Web Api`
例如:
1 2 3 4 5 [HttpGet("[action]" ) ] public string GetName (int id ) { return "zhangsan" + id; }
这里访问的地址:https://localhost:7081/api/UserInfo/GetName?id=10
下面看一下REST
REST
:按照http
的语义来使用http
协议。
http
语义,可以理解成协议的一种规范、约定、要求。例如,删除数据,按照http
的语义来讲,应该发送delete
请求,但是我们前面发送的是get
请求,这就是说明我们定义的api
不符合http
协议的语义,也不是rest
方式。还有关于状态码,如果新增成功,按照http
的语义,应该返回的是201(表示新增成功),但是前面我们写的都是200,说明我们前面写的也不符合http
语义,也不符合rest
风格。
我们只有将符合http
语义的web api
开发方式称作为rest
风格。
当然,我们不使用rest
方式,使用rpc
方式程序也不会出错。
举一个生活的例子来进一步理解语义的含义:
小轿车的作用就是拉人,但是你拉货,也没有问题,但是不符合小轿车的最初设计的用途。
所以说按照http
语义来设计Web api
的接口,我们就说所设计的web api
接口符合rest
风格。
常见的语义:
第一:域名
应该尽量将API部署在专用域名之下。
如果确定API
很简单,不会有进一步扩展,可以考虑放在主域名下。(我们默认创建的WebApi
项目符合这个规范)
1 https://example.org/api/
第二:版本
应该将API
的版本号放入URL
。
1 https://api.example.com/v1/
第三:路径
路径又称”终点”(endpoint
),表示API的具体网址。
在RESTful
架构中,每个网址代表一种资源(resource
),所以网址中不能有动词,只能有名词,而且所用的名词往往与数据库的表格名对应。一般来说,数据库中的表都是同种记录的”集合”(collection
),所以API
中的名词也应该使用复数。
举例来说,有一个API提供动物园(zoo
)的信息,还包括各种动物和雇员的信息,则它的路径应该设计成下面这样。
1 2 3 https://api.example.com/v1/zoos https://api.example.com/v1/animals https://api.example.com/v1/employees
还有如下形式
1 2 3 /users/5 // 获取编号为5的用户信息 /users/5/orders // 获取编号为5的用户的所有的订单信息
第四:HTTP
动词(谓词)
对于资源的具体操作类型,由HTTP
动词表示。
常用的HTTP
动词有下面五个(括号里是对应的SQL
命令)。
1 2 3 4 5 GET(SELECT):从服务器取出资源(一项或多项)。 POST(Insert):在服务器新增一个资源。 PUT(UPDATE):在服务器更新资源(整体更新,更新全部字段)。 PATCH(UPDATE):在服务器更新资源(局部更新,只是更新部分字段)。 DELETE(DELETE):从服务器删除资源。
例如:
1 2 3 4 5 6 7 8 GET /zoos:列出所有动物园 POST /zoos:新建一个动物园 GET /zoos/ID:获取某个指定动物园的信息 PUT /zoos/ID:更新某个指定动物园的信息(提供该动物园的全部信息) PATCH /zoos/ID:更新某个指定动物园的信息(提供该动物园的部分信息) DELETE /zoos/ID:删除某个动物园 GET /zoos/ID/animals:列出某个指定动物园的所有动物 DELETE /zoos/ID/animals/ID:删除某个指定动物园的指定动物
第五:状态码
服务器向用户返回的状态码和提示信息,常见的有以下一些【方括号中是该状态码对应的HTTP动词】
1 2 3 4 5 6 7 8 9 10 11 12 200 OK - [GET]:服务器成功返回用户请求的数据 201 CreatedAtAction - [POST/PUT/PATCH]:用户新建或修改数据成功。 202 Accepted - [*]:表示一个请求已经进入后台排队(异步任务) 204 NO CONTENT - [DELETE]:用户删除数据成功。 400 INVALID REQUEST - [POST/PUT/PATCH]:用户发出的请求有错误,服务器没有进行新建或修改数据的操作 401 Unauthorized - [*]:表示请求需要用户认证信息,用户未经过身份的验证,。 403 Forbidden - [*] 表示服务端已经知道客户端的身份,只是用户未被授权,用户没有足够的权限来执行某些操作。 404 NOT FOUND - [*]:用户发出的请求针对的是不存在的记录,服务器没有进行操作,例如请求编号为1的用户,如果不存在,返回404 406 Not Acceptable - [GET]:用户请求的格式不可得(比如用户请求JSON格式,但是只有XML格式)。 410 Gone -[GET]:用户请求的资源被永久删除,且不会再得到的。 422 Unprocesable entity - [POST/PUT/PATCH] 当创建一个对象时,发生一个验证错误。 500 INTERNAL SERVER ERROR - [*]:服务器发生错误,用户将无法判断发出的请求是否成功。
以下定义的符合rest
风格
如何请求这些控制器中的方法呢? 例如:
1 2 3 4 5 6 [HttpGet("{id}" ) ] public string GetName (int id ) { return "zhangsan" + id; }
以上方法请求的方式:
1 https://localhost:7081/api/UserInfo/12
注意:在以上的地址中没有添加方法的名字。与传统MVC
访问是不同的
以上常见的restful
的风格,其他的在后面用到的时候再进行讲解。
路由:https://learn.microsoft.com/zh-cn/aspnet/core/mvc/controllers/routing?view=aspnetcore-7.0
6、REST
优缺点 优点:
通过URL对资源定位,语义更清晰;
通过HTTP谓词表示不同的操作,接口统一且具有自描述性,减少了前端开发人员对接口文档的依赖性。
例如:删除编号为12用户请求的就是:/UserInfos/12
, 而且发送的是delete
请求,这里我们不需要查看稳当就知道。
可以用GET请求做缓存
通过HTTP状态码反映服务器端的处理结果统一错误处理机制。
缺点:
真实系统资源复杂,很难清晰划分,对业务技术水平要求高;Restful 风格对设计人员的IT技能和业务知识的水平要求都非常高。
2、不是所有操作都能简单的对应到确定的HTTP谓词操作; (有些接口中包含了更新,包含了删除)
3、通过URL进行资源定位不符合中文用户习惯;
4、HTTP状态码个数有限;有些复杂的情况,仅通过状态码无法描述
选择:
第一:Rest
是学术化的概念,仅供参考。国内很多公司也并不是Restful
1
第二:根据公司情况,进行Rest
的选择和裁剪。
实现业务才是王道。
7、获取用户信息。 在这个小节中,我们需要创建两个api
接口。
第一个获取用户的所有信息,第二个获取获取指定的用户信息。
这里我们在原有的MVC
架构的基础上做修改。
使用原有项目架构的模版(使用原有的服务层,数据仓储层)
创建一个Web Api
项目,在该项中把原来MVC
项目中使用到的Filters,Attributes,AutofaceDI
文件夹,都拷贝到Web Api
项目中
然后修改命名空间的名称
1 2 3 4 namespace Cms.WebApi.Filters { public class UnitOfWorkFilter : IAsyncActionFilter {
下面修改AutofaceDI
文件夹中AutofacModuleRegister
类的命名空间
1 2 3 4 namespace Cms.WebApi.AutofaceDI { public class AutofacModuleRegister :Autofac.Module {
Attributes
目录下的类UnitOfWorkAttribute.cs
的命名空间
1 2 namespace Cms.WebApi.Attributes {
同时Cms.WebApi
这个WebApi
项目需要引入其他的类库项目
同时还需要包含如下项目的引用
1 2 3 4 5 6 <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.4"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> <PackageReference Include="Autofac" Version="7.0.0" /> <PackageReference Include="Autofac.Extensions.DependencyInjection" Version="8.0.0" />
下面在Program.cs
文件中进行注入的操作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 builder.Services.AddControllers(); builder.Services.AddDbContext<MyDbContext>(opt => { string connStr = builder.Configuration.GetConnectionString("StrConn" )!; opt.UseSqlServer(connStr); }); builder.Services.Configure<MvcOptions>(c => { c.Filters.Add<UnitOfWorkFilter>(); }); builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory()).ConfigureContainer<ContainerBuilder>(builder => { builder.RegisterModule(new AutofacModuleRegister()); });
最后,还需要注意链接字符串,在Cms.WebApi
项目的appsettings.json
配置文件中不要忘记添加数据库链接字符串
1 2 3 4 5 6 7 8 9 10 11 12 { "Logging" : { "LogLevel" : { "Default" : "Information" , "Microsoft.AspNetCore" : "Warning" } } , "AllowedHosts" : "*" , "ConnectionStrings" : { "StrConn" : "server=localhost;database=CmsDb;uid=sa;pwd=123456;TrustServerCertificate=true" } }
完成以上的配置修改以后创建控制器UsersController.cs
,代码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class UsersController : ControllerBase { private readonly IUserInfoService _userInfoService; public UsersController (IUserInfoService _userInfoService ) { this ._userInfoService = _userInfoService; } [HttpGet ] public IActionResult GetAllUsers () { var users = _userInfoService.LoadEntities(c=>true ); return Ok(users); } s }
以上实现的就是返回所有用户信息的接口实现。
1 2 3 4 5 6 [HttpGet("{id}" ) ] / public IActionResult GetUser (int id ) { var user = _userInfoService.LoadEntities(u=>u.Id==id).FirstOrDefault; return Ok(user); }
8、返回正确的状态码 在访问GetUser
方法的时候,传递过滤的用户编号是不存在的应该怎样进行处理呢?
可以测试一下,输入一个不存在的用户编号,发现返回的状态码还是200,但是这种情况下返回的状态码应该是404.
1 2 3 4 5 6 7 8 9 10 [HttpGet("{id}" ) ] public async Task<IActionResult> GetUser (int id ) { var user = await _userInfoService.LoadEntities(u=>u.Id==id).FirstOrDefaultAsync(); if (user == null ) { return NotFound($"编号为{id} 的用户不存在!!" ); } return Ok(user); }
同理修改一下GetAllUsers
这个方法,如下所示:
1 2 3 4 5 6 7 8 9 10 [HttpGet ] public IActionResult GetAllUsers () { var users = _userInfoService.LoadEntities(c => true ); if (users==null || users.Count() == 0 ) { return NotFound("用户信息不存在!!" ); } return Ok(users); }
也可以在Apifox
中进行测试
9、数据传输对象DTO
((Data Transfer Object)) 关于数据传输对象我们前面也已经讲过。
如果我们直接将数据实体模型Model
返回到前端,一般会有如下两个问题
第一个问题:直接项前端返回实体模型,会暴露系统的业务核心。
第二个问题:返回的颗粒度太粗,也就是返回到前端的数据无法精细的调整
例如,有一个电商系统。
该系统针对商品有两个价格,一个是内部的员工价格,另外一个是市场价格
公司的内网使用的是员工价格,外网使用的是市场价格
问题是:产品的数据模型只有一个,难道要因为区别不同的价格而拆分模型吗?
这样就会很麻烦,而我们这里只是要求根据不同的场景对外传递不同数据对象,而这个向外传递数据的对象就是DTO
.
简单的理解就是DTO
是面向界面的,而Model
是根据业务来进行设计的,所以它是面向业务的。
例如:前面我们所设计的UserInfo
这个实体数据模型,里面包含了关于用户很多的信息,但是如果在页面上只需要展示用户的用户名,这时候就需要用到DTO
.
所以说,使用DTO
完全可以解决上面我们提到的两个问题。
第一:使用DTO
的时候可以屏蔽我们不希望对外暴露的核心业务
第二:通过不同DTO
的组合,又可以调整输出数据的结果,从而解决颗粒度太粗的问题。
10、分离Model
与DTO
在Cms.WebApi
项目中添加一个Dtos
文件夹,在该文件夹中创建UserInfoDTO.cs
这个类,该类中的代码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 public class UserInfoDto { public long Id { get ; set ; } public string CreateTime { get ; set ; } public DateTime UpdateTime { get ; set ; } public string ? UserName { get ; set ; } public string ? UserEmail { get ; set ; } public string ? UserPhone { get ; set ; } public string ? AreaCode { get ; set ; } public int Gender { get ; set ; } public string ? PhotoUrl { get ; set ; } }
这里对比Models
中的UserInfo.cs
中发现缺少了Password
,也就是说前端展示的时候不需要展示密码。
上面的UserInfoDto.cs
中定义的属性类型也可以根据前端页面实际展示情况进行调整,例如:前端展示的用户添加日期为xxxx年xx月xx日
形式,这里就可以将CreateTime
修改成string
类型。
下面可以通过前面我们所学习的AutoMapper
实现数据模型
与DTO
之间的映射。
在Cms.WebApi
项目中安装如下包
1 Install-Package AutoMapper.Extensions.Microsoft.DependencyInjection
下面配置映射关系
在Cms.WebApi
这个项目中再创建一个Profiles
文件夹,在该文件夹中创建UserInfoProfile.cs
类,该类中的代码如下所示:
1 2 3 4 5 6 7 8 public class UserInfoProfile :Profile { public UserInfoProfile () { CreateMap<UserInfo, UserInfoDto>() .ForMember(dest=>dest.CreateTime,opt=>opt.MapFrom(src=>src.CreateTime.Year+"年" +src.CreateTime.Month+"月" +src.CreateTime.Day+"日" )); } } }
这里是将UserInfo
这个数据模型映射成UserInfoDto
,如果两个类中的属性名字,类型是一样的,会自动进行映射。
但是这里的UserInfoDto
中的CreateTime
的类型是字符串类型,与UserInfo
这个实体类中的CreateTime
的类型是不一样的,所以这里需要借助于ForMember
函数进行单独的配置映射。将UserInfo
这个实体类中的CreateTime
属性映射成UserInfoDto
中的CreateTime
属性,这时候该属性的值就变成了xxxx年xx月xx日
的形式。
下面将AutoMapper
添加到容器中
修改Program.cs
文件中的代码
1 2 3 4 builder.Services.AddAutoMapper(typeof (UserInfoProfile)); builder.Services.AddEndpointsApiExplorer();
当然,这里也可以修改成如下的形式:
1 builder.Services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies());
AppDomain
: 应用程序域,表示的是一组程序集的逻辑容器。AppDomain.CurrentDomain.GetAssemblies()
获取到当前应用程序域下的所有的程序集。综合的起来就是获取当前应用程序集下的所有继承了Profile
的类。
返回到UsersController
控制器中进行代码的修改:
1 2 3 4 5 6 7 8 9 10 public class UsersController : ControllerBase { private readonly IUserInfoService _userInfoService; private readonly IMapper mapper; public UsersController (IUserInfoService _userInfoService,IMapper mapper ) { this ._userInfoService = _userInfoService; this .mapper = mapper; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 [HttpGet("{id}" ) ] public async Task<IActionResult> GetUser (int id ) { var user = await _userInfoService.LoadEntities(u=>u.Id==id).FirstOrDefaultAsync(); var userDTO = mapper.Map<UserInfoDto>(user); if (user == null ) { return NotFound($"编号为{id} 的用户不存在!!" ); } return Ok(userDTO); }
在上面的代码中,当我们获取到了数据以后,调用Map
方法,将获取到的数据都映射到了UserInfoDto
对象的属性中。
进行测试,返回的数据如下所示:
1 2 3 4 5 6 7 8 9 10 { "id" : 1 , "createTime" : "2012年12月1日" , "updateTime" : "2012-12-03T00:00:00" , "userName" : "张三" , "userEmail" : "zhangsan@126.com" , "userPhone" : "12345678901" , "gender" : 0 , "photoUrl" : "/images/f.jpg" }
下面我们又修改了GetAllUsers
方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 [HttpGet ] public IActionResult GetAllUsers () { var users = _userInfoService.LoadEntities(c => true ); var usersDto = mapper.Map<List<UserInfoDto>>(users); if (usersDto == null || usersDto.Count() == 0 ) { return NotFound("用户信息不存在!!" ); } else { return Ok(usersDto); } }
启动项目进行测试,发现数据中的createTime
的值都发生了变化,改成了我们想要的格式。
11、获取文章信息 11.1 配置一对多关系 定义文章的数据模型,这里为了简单,定义的文章实体模型如下所示,在Cms.Entity
中创建Articel.cs
1 2 3 4 5 6 7 public class Articel :BaseEntity <long > { public string ? Title { get ; set ; } public string ? Content { get ; set ; } public UserInfo? UserInfo { get ; set ; } }
定义ArticelConfig.cs
配置类,代码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 { public class ArticelConfig : IEntityTypeConfiguration <Articel > { public void Configure (EntityTypeBuilder<Articel> builder ) { builder.ToTable("T_Articels" ); builder.Property(x => x.Title).HasMaxLength(100 ).IsRequired(); builder.Property(x => x.Content).IsRequired(); builder.HasOne(c=>c.UserInfo).WithMany(a=>a.Articels).IsRequired(); } } }
修改UserInfo.cs
这个实体类,代码如下所示:
1 2 3 4 5 public string ? PhotoUrl { get ; set ;} public List<Articel> Articels { get ; set ;}=new List<Articel>();
修改Cms.EntityFrameworkCore
项目中的MyDbContext.cs
数据上下文类,这里需要增加DbSet
1 2 3 4 public class MyDbContext :DbContext { public DbSet<UserInfo>UserInfos { get ; set ; } public DbSet<Articel> Articels { get ; set ; }
同时还需要修改MyDbContextDesign.cs
这个类,我们这里换了数据库
1 2 3 4 5 public MyDbContext CreateDbContext (string [] args ) { DbContextOptionsBuilder builder = new DbContextOptionsBuilder<MyDbContext>(); string connStr = "server=localhost;database=APIDemo;uid=sa;pwd=123456;TrustServerCertificate=true" ; builder.UseSqlServer(connStr);
这里我们更换了一个新的数据库APIDemo
,当然这里我们可以先在SQLServer
中创建APIDemo
这个数据库。
当然,Cms.WebApi
项目中的配置文件appsettings.json
也需要修改一下数据库链接字符串。
1 2 3 "ConnectionStrings" : { "StrConn" : "server=localhost;database=APIDemo;uid=sa;pwd=123456;TrustServerCertificate=true" }
链接到的数据库也是APIDemo
下面生成数据迁移
在[程序包管理器控制台]
中选择Cms.EntityFrameworkCore
,执行
1 2 Add-Migration createArticel Update-database
下面在数据库中添加测试数据。
下面再创建数据仓储
创建IArticelRepository.cs
1 2 3 4 5 6 7 namespace Cms.IRepository { public interface IArticelRepository :IBaseRepository <Articel > { } }
创建ArticelRepository.cs
1 2 3 4 5 6 7 8 namespace Cms.Repository { public class ArticelRepository :BaseRepository <Articel >,IArticelRepository { public ArticelRepository (MyDbContext context ) : base (context ) { } } }
下面创建服务
IArticelService.cs
1 2 3 4 5 6 namespace Cms.IService { public interface IArticelService :IBaseService <Articel > { } }
创建ArticelService.cs
1 2 3 4 5 6 7 8 9 10 11 12 namespace Cms.Service { public class ArticelService :BaseService <Articel >,IArticelService { private readonly IArticelRepository articelRepository; public ArticelService (IArticelRepository articelRepository ) { base .repository = articelRepository; this .articelRepository = articelRepository; } } }
11.2 实现信息的查询 在Cms.WebApi
项目中的Dtos
文件夹下面创建ArticelDto.cs
类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class ArticelDto { public long Id { get ; set ; } public DateTime CreateTime { get ; set ; } public DateTime UpdateTime { get ; set ; } public string ? Title { get ; set ; } public string ? Content { get ; set ; } }
在Profiles
文件夹下面创建ArticelProfile.cs
类,代码如下所示:
1 2 3 4 5 6 7 8 9 namespace Cms.WebApi.Profiles { public class ArticelProfile :Profile { public ArticelProfile () { CreateMap<Articel,ArticelDto>(); } } }
在UsersController.cs
控制器中添加如下代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 [HttpGet("{userId}/articels" ) ] public async Task<IActionResult> GetArticelForUser (int userId ) { var userInfo = await _userInfoService.LoadEntities(u => u.Id == userId).FirstOrDefaultAsync(); if (userInfo == null ) { return NotFound("用户不存在" ); } var articels = _articelService.LoadEntities(a => a.UserInfo == userInfo).ToList(); if (articels.Count() <= 0 ) { return NotFound("文章不存在" ); } return Ok(mapper.Map<List<ArticelDto>>(articels)); }
当然,在UsersController.cs
中需要注入文章服务。
1 2 3 4 5 6 7 8 9 10 11 12 public class UsersController : ControllerBase { private readonly IUserInfoService _userInfoService; private readonly IArticelService _articelService; private readonly IMapper mapper; public UsersController (IUserInfoService _userInfoService,IMapper mapper, IArticelService _articelService ) { this ._userInfoService = _userInfoService; this .mapper = mapper; this ._articelService = _articelService; }
12、加载所有用户以及用户发布的文章 这里,我们希望,当获取用户信息的时候,顺便把用户发布的文章也给查询出来。
我们知道,在Cms.Entity
中的UserInfo.cs
这个实体类中,定义了如下内容:
1 2 3 4 public List<Articel> Articels { get ; set ;}=new List<Articel>();
表示的是一个用户发布的多篇文章。
当然,在UserInfoDto.cs
中也可以进行表示:
1 2 3 4 5 6 7 public string ? PhotoUrl { get ; set ; } public List<ArticelDto>? Articels { get ; set ; }
下面在UsersController.cs
控制器中添加如下的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 [HttpGet("articels" ) ] public IActionResult GetUsersAndArticels () { var users = _userInfoService.LoadEntities(c => true ).Include(c=>c.Articels); var usersDto = mapper.Map<List<UserInfoDto>>(users); if (usersDto == null || usersDto.Count() == 0 ) { return NotFound("用户信息不存在!!" ); } else { return Ok(usersDto); } }
启动程序进行测试。
13、文章搜索 修改ArticelsController
这个控制器中的代码,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 namespace Cms.WebApi.Controllers { [Route("api/[controller]" ) ] [ApiController ] public class ArticelsController : ControllerBase { private readonly IArticelService _articelService; private readonly IMapper mapper; public ArticelsController (IArticelService articelService, IMapper mapper ) { this .mapper = mapper; this ._articelService = articelService; } [HttpGet ] public IActionResult GetArticel ([FromQuery]string keyword ) { var articels = _articelService.LoadEntities(a=>a.Title!.Contains(keyword)); if (articels.Count() <= 0 ) { return NotFound("没有找到文章" ); } var articelList = mapper.Map<List<ArticelDto>>(articels); return Ok(articelList); } } }
以上实现了一个基本的搜索。
如果是多条件搜索,可以继续在url
地址中添加搜索的条件。
下面我们来修改上面的代码实现多条件搜索。
在Cms.Entity
这个类库项目中创建Search
目录,在该目录中创建ArticelSeach.cs
这个类,该类中的代码如下所示:
1 2 3 4 5 6 public class ArticelSearch :BaseSearch { public string ? title { get ; set ; } public string ? FormDatepicker { get ; set ; } public string ? ToDatepicker { get ; set ; } }
同时在Seach
目录中创建BaseSearch.cs
这个类,该类中的代码如下所示:
1 2 3 4 5 6 7 public class BaseSearch { public int PageIndex { get ;set ; } public int PageSize { get ; set ; } public int TotalCount { get ; set ; } public bool Order { get ; set ; } }
修改IArticelService.cs
这个文章服务类中的代码,如下所示:
1 2 3 4 public interface IArticelService :IBaseService <Articel > { IQueryable<Articel> LoadSearchEntities (ArticelSearch articelSearch,bool delFlag ) ; }
这里增加了一个LoadSearchEntities
接口。
下面在ArticelService.cs
类中实现上面的接口,代码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 public class ArticelService :BaseService <Articel >,IArticelService { private readonly IArticelRepository articelRepository; public ArticelService (IArticelRepository articelRepository ) { base .repository = articelRepository; this .articelRepository = articelRepository; } public IQueryable<Articel> LoadSearchEntities (ArticelSearch articelSearch, bool delFlag ) { var temp = articelRepository.LoadEntities(a=>a.DelFlag==delFlag); if (!string .IsNullOrWhiteSpace(articelSearch.title)) { temp = temp.Where(a=>a.Title!.Contains(articelSearch.title)); } if (!string .IsNullOrWhiteSpace(articelSearch.FormDatepicker)&&!string .IsNullOrWhiteSpace(articelSearch.ToDatepicker)) { DateTime startDateTime = Convert.ToDateTime(articelSearch.FormDatepicker); DateTime endDateTime = Convert.ToDateTime(articelSearch.ToDatepicker); temp = temp.Where(a=>a.CreateTime>=startDateTime &&a.CreateTime<=endDateTime); } articelSearch.TotalCount = temp.Count(); if (articelSearch.Order) { return temp.OrderBy<Articel, long >(a => a.Id).Skip<Articel>((articelSearch.PageIndex - 1 ) * articelSearch.PageSize).Take<Articel>(articelSearch.PageSize); } else { return temp.OrderByDescending<Articel, long >(a => a.Id).Skip<Articel>((articelSearch.PageIndex - 1 ) * articelSearch.PageSize).Take<Articel>(articelSearch.PageSize); } } }
修改控制器ArticelsController
中的代码,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 [HttpGet ] public IActionResult GetArticel ([FromQuery]ArticelSearch articelSearch ) { articelSearch.PageSize = 5 ; articelSearch.PageIndex = 1 ; int totalCount = 0 ; articelSearch.TotalCount = totalCount; var articels = _articelService.LoadSearchEntities(articelSearch,Convert.ToBoolean(DelFlagEnum.Normal)); if ( articelSearch.TotalCount <= 0 ) { return NotFound("没有找到文章" ); } var articelList = mapper.Map<List<ArticelDto>>(articels); return Ok(articelList); }
在上面的代码中,我们用ArticelSearch
接收前端发送过滤的搜索条件的数据
当然,这里关于分页的内容,我们暂时先不考虑,所以将pageSize,pageIndex
属性的值先固定具体的值。
启动项目进行测试。
通过测试页面,我们发现这里也需要前端发送TotalCount
,这是不合理的,因为TotalCount
表示的是满足条件的总的记录数,是有服务端对数据进行过滤后统计出来的,并不是有前端发送过来的。而PageIndex,PageSize
是有前端发送过来的。
所以说这里,我们通过ArticelSearch
这个类型作为GetArticel
方法接收前端发送过来的搜索内容的类型是不合适的。
所以这里还需要进一步处理。
在Cms.WebApi
这个项目中创建文件夹ResourceParams
文件夹,该文件中创建ArticelParams.cs
这个类,该类中的代码,如下所示:
1 2 3 4 5 6 7 8 9 10 public class ArticelParams { public string ? title { get ; set ; } public string ? FormDatepicker { get ; set ; } public string ? ToDatepicker { get ; set ; } public int PageIndex { get ; set ; } public int PageSize { get ; set ; } public bool Order { get ; set ; } }
目前我们暂时不考虑分页,只考虑多条件搜索的情况。
下面在修改控制器中的代码,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 [HttpGet ] public IActionResult GetArticel ([FromQuery] ArticelParams articelParams ) { articelParams.PageIndex = 1 ; articelParams.PageSize = 10 ; int totalCount = 0 ; ArticelSearch articelSearch = new ArticelSearch() { title = articelParams.title, FormDatepicker = articelParams.FormDatepicker, ToDatepicker = articelParams.ToDatepicker, PageSize = articelParams.PageSize, PageIndex = articelParams.PageIndex, Order = articelParams.Order, TotalCount = totalCount }; var articels = _articelService.LoadSearchEntities(articelSearch,Convert.ToBoolean(DelFlagEnum.Normal)); if (articelSearch.TotalCount <= 0 ) { return NotFound("没有找到文章" ); } var articelList = mapper.Map<List<ArticelDto>>(articels); return Ok(articelList); }
启动项目进行测试。
通过测试,我们发现数据都是通过url
地址参数传递过来的。
14、创建用户 这里,我们主要学习的是post
请求。
创建用户的时候,我们需要接收前端发送过来的数据,最终保存到数据库中。
这里我们接收到的数据还是要交给DTO
对象,然后映射到数据模型中。
前面我们已经在Dtos
目录中创建了UserInfoDto.cs
,是否可以直接使用该对象呢?
不建议,前面创建的UserInfoDto.cs
是负责输出的,也就是用来展示数据的。而现在我们创建的Dto
对象是负责接收的,也就是输入的。因为,不同的操作对数据的要求是不同的,例如:在输入的时候,不需要id
,同时还需要对数据进行校验(输出的数据是最终来自数据库,不需要对数据进行校验)。Dto
对象针对的是前端的逻辑,不同的前端页面的逻辑是不一样的。这样职责更加的明确。
所以在Dtos
文件夹中创建一个UserInfoCreateDto.cs
类,作为输入用户信息的数据传输对象。
该类中的代码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 public class UserInfoCreateDto { public string ? UserName { get ; set ; } public string ? UserEmail { get ; set ; } public string ? UserPhone { get ; set ; } public string ? UserPassword { get ; set ; } public int Gender { get ; set ; } public string ? PhotoUrl { get ; set ; } }
以上代码我们是从UserInfoDto.cs
中直接拷贝过来的,但是这里缺少了Id
属性,我们知道,在添加数据的时候,前端页面不需要传递Id
,因为Id
的值是有服务端在向数据库中插入数据的时候确定的。同时也没有添加CreateTime
和UpdateTime
(因为添加信息的时候,日期时间是不需要用户输入的,只需要获取当前系统的日期时间就可以了)
但是这里需要密码,所以上面添加了UserPassword
这个属性。
同时这里我们少了 public List<ArticelDto>? Articels { get; set; }
关于Articels
这个集合的处理我们后面在进行讲解。
下面返回到控制器中,添加如下方法:
1 2 3 4 5 6 7 8 9 [HttpPost ] public IActionResult CreatUser ([FromBody]UserInfoCreateDto userInfoCreateDto ) { }
这里我们创建了CreateUser
方法来完成用户信息的创建,这里是一个Post
请求,接收前端页面发送过来的数据,需要使用到[FromBody]
,然后数据都传递给了UserInfoCreateDto
这个对象。
下面我们需要将UserInfoCreateDto
这个Dto
对象映射到UserInfo
这个实体模型类。
修改Profiles/UserInfoProfile.cs
文件的配置,如下所示:
1 2 3 4 5 6 7 public UserInfoProfile () { CreateMap<UserInfo, UserInfoDto>() .ForMember(dest=>dest.CreateTime,opt=>opt.MapFrom(src=>src.CreateTime.Year+"年" +src.CreateTime.Month+"月" +src.CreateTime.Day+"日" )); CreateMap<UserInfoCreateDto, UserInfo>().ForMember(dest=>dest.CreateTime,opt=>opt.MapFrom(src=>DateTime.Now)).ForMember(dest=>dest.UpdateTime,opt=>opt.MapFrom(src=>DateTime.Now)); }
下面修改控制器UsersController.cs
中的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 [HttpPost ] [UnitOfWork(new Type[ ] { typeof (MyDbContext) })] public async Task<IActionResult> CreatUser ([FromBody]UserInfoCreateDto userInfoCreateDto ) { var userInfoModel = mapper.Map<UserInfo>(userInfoCreateDto); await _userInfoService.InsertEntityAsync(userInfoModel); var user = mapper.Map<UserInfoDto>(userInfoModel); return CreatedAtAction("GetAllUsers" , user); }
启动项目进行测试。
15、用户发布文章 在这一小节中,我们实现的是指定的某个用户发布文章的api
接口
在Dtos
目录下面创建ArticelForCreateDto.cs
这个类,该类中的代码如下所示:
1 2 3 4 5 6 7 public class ArticelForCreateDto { public string ? Title { get ; set ; } public string ? Content { get ; set ; } }
上面代码中,表示用户在发布文章的时候,只需要输入文章的标题和文章的内容就可以了。
下面需要进行配置,也就是将ArticelForCreateDto
映射到Articel
这个实体类中(但是在映射的时候,要注意的是ArticelForCreateDto
这个数据传输对象中没有CreateTime,UpdateTime,DelFlag
属性,需要再配置中进行指定,如下所示:)。
修改Profiles
文件夹下的ArticelProfile.cs
中的代码,如下所示:
1 2 3 4 5 6 7 public ArticelProfile () { CreateMap<Articel,ArticelDto>(); CreateMap<ArticelForCreateDto, Articel>().ForMember(dest => dest.CreateTime, opt => opt.MapFrom(src => DateTime.Now)).ForMember(dest => dest.UpdateTime, opt => opt.MapFrom(src => DateTime.Now)).ForMember(dest=>dest.DelFlag,opt=>opt.MapFrom(src=>true )) ; }
下面修改UsersController.cs
控制器中的代码,该该控制器中添加如下方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 [HttpPost("{userId}/articels" ) ] [UnitOfWork(new Type[ ] { typeof (MyDbContext) })] public async Task<IActionResult> CreateArticelForUser ([FromRoute]int userId, [FromBody] ArticelForCreateDto articelForCreateDto ) { var userInfo = await _userInfoService.LoadEntities(u=>u.Id==userId).FirstOrDefaultAsync(); if (userInfo == null ) { return NotFound("用户不存在!" ); } var articelModel = mapper.Map<Articel>(articelForCreateDto); articelModel.UserInfo = userInfo; await _articelService.InsertEntityAsync(articelModel); var articel = mapper.Map<ArticelDto>(articelModel); return CreatedAtAction("GetArticelForUser" ,new { userId=userInfo.Id},articel); }
首先参数userId
是从路由中获取所以使用了[FromRoute]
,articelForCreateDto
参数接收的就是文章的数据,这是数据是通过json
格式传递过来的。所以使用了[FromBody]
.
16、数据验证 当我们接收到前端页面发送过来的数据的时候,都需要对数据进行校验。
例如:创建用户的时候,发布文章的时候,都需要对数据进行校验。
关于校验的方式在前面讲解.net core mvc
课程的时候都给大家讲解过,关键是当数据没有通过校验以后,返回给前端的状态码是多少呢?
一般返回的状态码是422,该状态码的含义是服务端接收到的前端数据是有错误的。当然,除了返回状态码,还要给前端返回错误的信息。
16.1 添加数据验证 1 2 3 4 5 6 7 8 9 public class ArticelForCreateDto { [Required(ErrorMessage ="title 不能为空!" ) ] [MaxLength(100) ] public string ? Title { get ; set ; } [Required(ErrorMessage = "content 不能为空!" ) ] public string ? Content { get ; set ; } }
启动项目进行测试,输入空字符串进行校验。
1 2 3 4 { "title": "" "content": "" }
进行测试,返回的响应报文是:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 { "type" : "https://tools.ietf.org/html/rfc7231#section-6.5.1" , "title" : "One or more validation errors occurred." , "status" : 400 , "traceId" : "00-810c18ca31baa2592ee25638dd409c64-16d3580cb5db9a6d-00" , "errors" : { "Title" : [ "title 不能为空!" ] , "Content" : [ "content 不能为空!" ] } }
当然,除了这种数据验证方式以外,还可以给当前需要校验的DTO
数据传输类添加Attribute
.
在Cms.WebApi
这个api
项中创建ValidationAttributes
文件夹,在该文件夹中创建ArticelValidateAttribute.cs
,这里必须使用Attribute
进行结尾。
ArticelValidateAttribute.cs
文件中的代码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 using Cms.WebApi.Dtos;using System.ComponentModel.DataAnnotations;namespace Cms.WebApi.ValidationAttributes { public class ArticelValidateAttribute :ValidationAttribute { protected override ValidationResult? IsValid(object ? value , ValidationContext validationContext) { var articelDto = (ArticelForCreateDto) validationContext.ObjectInstance; if (string .IsNullOrWhiteSpace(articelDto.Title)) { return new ValidationResult("title 文章标题不能为空!!" , new [] { "ArticelForCreateDto" }); } if (string .IsNullOrWhiteSpace(articelDto.Content)) { return new ValidationResult("content 文章内容不能为空!!" , new [] { "ArticelForCreateDto" }); } return ValidationResult.Success; } } }
下面使用ArticelValidateAttribute
.
修改Dtos/ArticelForCreateDto.cs
类中的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 using Cms.Entity;using Cms.WebApi.ValidationAttributes;using System.ComponentModel.DataAnnotations;namespace Cms.WebApi.Dtos { [ArticelValidate ] public class ArticelForCreateDto { public string ? Title { get ; set ; } public string ? Content { get ; set ; } } }
启动项目进行测试。
例如:如果不输入标题,会出现如下错误信息
1 2 3 4 5 6 7 8 9 10 11 { "type" : "https://tools.ietf.org/html/rfc7231#section-6.5.1" , "title" : "One or more validation errors occurred." , "status" : 400 , "traceId" : "00-9e8a2ff25d30f264e249bab2eb0eaca2-8c70b91fc63e5a93-00" , "errors" : { "ArticelForCreateDto" : [ "title 文章标题不能为空!!" ] } }
16.2 输出状态码422 现在我们可以看到,当数据没有通过校验的时候,返回的状态码是400,但是实际上在http
语义中,数据没有通过校验要求返回的状态码是422
.
修改Program.cs
文件中的代码,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 builder.Services.AddControllers().ConfigureApiBehaviorOptions(c => { c.InvalidModelStateResponseFactory = context => { var problemDetails = new ValidationProblemDetails(context.ModelState) { Title = "数据校验有问题" , Status = StatusCodes.Status422UnprocessableEntity, Instance = context.HttpContext.Request.Path }; return new UnprocessableEntityObjectResult(problemDetails) { ContentTypes = { "application/problem+json" } }; }; });
重新启动项目进行测试,这里不输入文章的标题,查看一下返回的响应信息。
17、数据更新操作 关于数据更新有两项操作
put
与patch
put
:对某个资源所有的字段进行更新操作 // update table set username=”admin”,userpwsd=123,email = where id=1
patch
:对某个资源的某几个字段进行部分更新操作。
17.1 使用put
请求更新资源 这里我们先使用put
的方式来更新用户的信息。
首先在Dtos
目录下面创建一个更新用户的数据传输对象UserInfoForUpdateDto.cs
,该类中的代码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 public class UserInfoForUpdateDto { public string ? UserName { get ; set ; } public string ? UserEmail { get ; set ; } public string ? UserPhone { get ; set ; } public string ? UserPassword { get ; set ; } public int Gender { get ; set ; } public string ? PhotoUrl { get ; set ; } }
在Profiles/UserInfoProfile.cs
中进行配置,如下所示:
1 2 3 4 5 6 7 8 public UserInfoProfile () { CreateMap<UserInfo, UserInfoDto>() .ForMember(dest=>dest.CreateTime,opt=>opt.MapFrom(src=>src.CreateTime.Year+"年" +src.CreateTime.Month+"月" +src.CreateTime.Day+"日" )); CreateMap<UserInfoCreateDto, UserInfo>().ForMember(dest=>dest.CreateTime,opt=>opt.MapFrom(src=>DateTime.Now)).ForMember(dest=>dest.UpdateTime,opt=>opt.MapFrom(src=>DateTime.Now)); CreateMap<UserInfoForUpdateDto, UserInfo>().ForMember(dest=>dest.UpdateTime,opt=>opt.MapFrom(src=>DateTime.Now)); }
下面修改UsersController.cs
控制器
在该控制器中添加了如下的方式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 [HttpPut("{userId}" ) ] [UnitOfWork(new Type[ ] { typeof (MyDbContext) })] public async Task<IActionResult> EditUser ([FromRoute]int userId, [FromBody]UserInfoForUpdateDto userInfoForUpdateDto ) { var userInfo = await _userInfoService.LoadEntities(u=>u.Id ==userId).FirstOrDefaultAsync(); if (userInfo == null ) { return NotFound("用户不存在" ); } mapper.Map(userInfoForUpdateDto,userInfo); return NoContent(); }
注意:这里是全部更新,如果某个字段的值不更新,也要给其原先的值。(但是,我们发现CreateTime
和DelFlag
两个字段中的值还是原来的值,原因是:在UserInfoForUpdateDto
中我们没有创建这两个属性,所以在将UserInfoForUpdateDto
映射给UserInfo
的时候,这两个字段,没有进行映射,当向数据库中保存的时候,还是使用的UserInfo
这个数据模型中的CreateTime和DelFlag
)
启动项目进行测试。
17.2 put
请求的数据校验 在更新用户数据的时候,也是要对数据进行校验,并且校验规则也是与创建用户的时候是一样的。
而且,我们所创建的UserInfoCreateDto.cs
与UserInfoForUpdateDto.cs
两个数据传输类中的属性都是一样的,所以这里完全可以做进一步的封装处理。
在Dtos
目录下面创建一个UserInfoForAbstractDto.cs
类,该类中的代码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public abstract class UserInfoForAbstractDto { public string ? UserName { get ; set ; } public string ? UserEmail { get ; set ; } public string ? UserPhone { get ; set ; } public string ? UserPassword { get ; set ; } public int Gender { get ; set ; } public string ? PhotoUrl { get ; set ; } }
由于这里是输入的用户DTO
对象和输出的用户DTO
对象都需要继承UserInfoForAbstractDto
这个类,并且我们也不是直接使用该类,所以UserInfoForAbstractDto
这个类定义成抽象类就可以了。
修改Dto/UserInfoCreateDto.cs
中的代码,如下所示:
1 2 3 4 5 public class UserInfoCreateDto :UserInfoForAbstractDto { }
修改Dto/UserInfoForUpdateDto.cs
中的代码,如下所示:
1 2 3 4 5 public class UserInfoForUpdateDto : UserInfoForAbstractDto { }
如果完成校验,我们还需要在ValidationAttributes
目录下面创建UserInfoValidateAttribute.cs
类,该类的代码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public class UserInfoValidateAttribute : ValidationAttribute { protected override ValidationResult? IsValid(object ? value , ValidationContext validationContext) { var userDto = (UserInfoForAbstractDto)validationContext.ObjectInstance; if (string .IsNullOrWhiteSpace(userDto.UserName)) { return new ValidationResult("UserName 用户名不能为空!!" , new [] { "UserInfoForAbstractDto" }); } if (string .IsNullOrWhiteSpace(userDto.UserPhone)) { return new ValidationResult("UserPhone 用户电话号码不能为空!!" , new [] { "UserInfoForAbstractDto" }); } return ValidationResult.Success; } }
下面在UserInfoCreateDto.cs
和UserInfoForUpdateDto.cs
这两个类中使用上面所定义的UserInfoValidateAttribute
,如下代码所示:
1 2 3 4 5 [UserInfoValidate ] public class UserInfoCreateDto :UserInfoForAbstractDto { }
1 2 3 4 5 [UserInfoValidate ] public class UserInfoForUpdateDto : UserInfoForAbstractDto { }
启动项目进行测试。
在更新的时候,如果输入的用户名是一个空字符串会给出错误提示。
问题:如果在更新的时候,UserName
如果为空的情况下,给出的错误提示与创建的时候给出的错误提示不一样应该怎样进行处理呢?
修改UserInfoForAbstractDto.cs
抽象类中的代码:
1 2 3 4 5 6 7 8 public abstract class UserInfoForAbstractDto { public virtual string ? UserName { get ; set ; } public string ? UserEmail { get ; set ; }
这里给UserName
添加了virtual
,那么对应的子类中就可以进行重写了,例如:
1 2 3 4 5 6 [UserInfoValidate ] public class UserInfoForUpdateDto : UserInfoForAbstractDto { [Required(ErrorMessage ="用户名不能为空,难道你不知道??" ) ] public override string ? UserName { get ; set ; } }
在UserInfoForUpdateDto.cs
类中,这里对UserName
进行了重写,并且通过ErrorMessage
给出了不同的错误提示。
下面可以进行测试,在创建用户和更新用户的时候,当用户名为空字符串的时候,给出的错误提示是不一样的。
17.3 patch
数据的局部更新 在上一小节中,我们通过put
完成了数据的全部更新,但是如果只是更新部分字段,还是建议使用patch
的方式。
前端发送patch
请求给服务端,服务端需要将前端发送过来的json
数据转换成以下的数据格式:如下所示:
1 2 3 4 [ { "op" : "replace" , "path" : "/UserName" , "value" : "admin" } , { "op" : "remove" , "path" : "/Gender" , } ]
以上这种表示如何修改数据的结构,我们称作JSON Patch
op
指定操作,path
指定资源,也就是对哪个属性进行操作,value
表示的就是对应的值.
当然,对没有指定的字段,会保留原有的值。
关于op
操作,包含了6个选项,分别是:add
添加某个字段,remove
:删除某个字段,replace
:替换某个字段的数据
“move”:转移,”copy”:赋值,test
:测试
服务端对patch
请求的处理,需要安装如下两个包,安装到Cms.WebApi
项目中
1 2 Install-Package microsoft.aspnetcore.jsonpatch Install-Package microsoft.aspnetcore.mvc.NewtonsoftJson
下面在UsersController.cs
控制器中添加如下方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 [HttpPatch("{userId}" ) ] [UnitOfWork(new Type[ ] { typeof (MyDbContext) })] public async Task<IActionResult> UpdateUser ([FromRoute] int userId, [FromBody] JsonPatchDocument<UserInfoForUpdateDto> patchDocument ) { var userInfo = await _userInfoService.LoadEntities(u => u.Id == userId).FirstOrDefaultAsync(); if (userInfo == null ) { return NotFound("用户不存在" ); } var userPatch = mapper.Map<UserInfoForUpdateDto>(userInfo); patchDocument.ApplyTo(userPatch); mapper.Map(userPatch,userInfo); return NoContent(); }
这里还需要修改Profiles/UserInfoProfile.cs
中的代码,在最后添加
1 CreateMap<UserInfo, UserInfoForUpdateDto>();
这里需要将UserInfo
这个数据模型,映射到UserInfoForUpdateDto
中。
最后,还需要注意的一个点,就是前端发送过来的JSON
数据,需要转换成JsonPatchDocument
,这里就需要使用到我们前面所安装的NewtonsoftJson
包,这个包需要在Program.cs
中进行配置,如下所示:
1 2 3 4 5 6 7 8 builder.Services.AddControllers().AddNewtonsoftJson(a => { a.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver(); }) .ConfigureApiBehaviorOptions(c => {
启动项目进行测试,在界面窗口中输入:
输入:
1 2 3 4 5 6 7 8 9 [ { "operationType" : 0 , "path" : "/UserName" , "op" : "replace" , "from" : "string" , "value" : "admin" } ]
主要考虑:path,op,value
在进行测试的时候,给控制器中的UpdateUser
这个方法打上断点,看一下数据的变化情况,更容易理解方法中的代码。
17.4 patch
请求的数据校验 前面在更新数据的时候,我们添加了相应的校验,那么我们来测试一下通过patch
请求更新数据的时候,校验是否还起作用。
当输入:
1 2 3 4 5 6 7 8 9 [ { "operationType" : 0 , "path" : "/UserName" , "op" : "replace" , "from" : "string" , "value" : "" } ]
发现已经定义的校验规则没有起作用。
原因是:我们现在获取到的数据是JsonPatchDocument
,而不是原来的DTO
数据传输对象。所以添加到数据传输对象上的校验规则没有起作用。
所以这里需要修改控制器中的UpdateUser
方法,代码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public async Task<IActionResult> UpdateUser ([FromRoute] int userId, [FromBody] JsonPatchDocument<UserInfoForUpdateDto> patchDocument ) { var userInfo = await _userInfoService.LoadEntities(u => u.Id == userId).FirstOrDefaultAsync(); if (userInfo == null ) { return NotFound("用户不存在" ); } var userPatch = mapper.Map<UserInfoForUpdateDto>(userInfo); patchDocument.ApplyTo(userPatch,ModelState); if (!TryValidateModel(userPatch)) { return ValidationProblem(ModelState); } mapper.Map(userPatch,userInfo); return NoContent(); }
再次进行测试。
问题,如果修改多个字段,可以采用如下方式进行测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 [ { "operationType" : 0 , "path" : "/UserName" , "op" : "replace" , "from" : "string" , "value" : "admin12" }, { "operationType" : 0 , "path" : "/UserPassword" , "op" : "replace" , "from" : "string" , "value" : "123" } ]
18、数据删除 18.1 删除指定的用户信息 1 2 3 4 5 6 7 8 9 10 11 12 [HttpDelete("{userId}" ) ] [UnitOfWork(new Type[ ] { typeof (MyDbContext) })] public async Task<IActionResult> DeleteUser ([FromRoute]int userId ) { var userInfo = await _userInfoService.LoadEntities(u => u.Id == userId).FirstOrDefaultAsync(); if (userInfo == null ) { return NotFound("用户不存在" ); } await _userInfoService.DeleteEntityAsync(userInfo); return NoContent(); }
18.2 删除用户发布的文章 这里我们需要删除的是某个用户发布的谋篇文章。
这里我们还在UsersController.cs
控制器中添加代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 [HttpDelete("{userId}/articels/{articelId}" ) ] [UnitOfWork(new Type[ ] { typeof (MyDbContext) })] public async Task<IActionResult> DeleteUserArticel ([FromRoute]int userId, [FromRoute]int articelId ) { var userInfo = await _userInfoService.LoadEntities(u => u.Id == userId).FirstOrDefaultAsync(); if (userInfo == null ) { return NotFound("用户不存在" ); } var articelInfo = await _articelService.LoadEntities(a => a.Id == articelId&&a.UserInfo==userInfo).FirstOrDefaultAsync(); if (articelInfo == null ) { return NotFound("文章不存在" ); } await _articelService.DeleteEntityAsync(articelInfo); return NoContent(); }
18.3 批量删除用户 在这一小节中,我们来看一下怎样创建批量删除用户信息的Api
接口。
批量删除的URL
构建
1 2 3 4 方式1: 参数使用?号的方式 delete api/users?Ids=1,2,3,4,5 方式2:参数构建URL片段 delete api/users/(1,2,3,4,5)
方式1,在前面讲解.net core mvc
的时候,给大家演示过。
在这以小节中,我们主要采用的是方式2.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 [HttpDelete("({userIds})" ) ] [UnitOfWork(new Type[ ] { typeof (MyDbContext) })] public async Task<IActionResult> DeleteAllUsers ([FromRoute]List<string >userIds ) { var strIds = userIds[0 ].Split(',' ); List<long > newList=new List<long >(); foreach (var id in strIds) { newList.Add(Convert.ToInt32(id)); } var userInfoList = _userInfoService.LoadEntities(u=>newList.Contains(u.Id)); foreach (var user in userInfoList) { await _userInfoService.DeleteEntityAsync(user); } return NoContent(); }
启动项目进行测试。
以下代码采用了方式1来进行处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 [HttpDelete("{userIds}/alluser" ) ] [UnitOfWork(new Type[ ] { typeof (MyDbContext) })] public async Task<IActionResult> DeleteAllUsers ([FromRoute]string userIds ) { var strIds = userIds.Split(',' ); List<long > newList=new List<long >(); foreach (var id in strIds) { newList.Add(Convert.ToInt32(id)); } var userInfoList = _userInfoService.LoadEntities(u=>newList.Contains(u.Id)); foreach (var user in userInfoList) { await _userInfoService.DeleteEntityAsync(user); } return NoContent(); }
启动项目进行测试,可以监视浏览器中的请求,看一下请求的地址。
19、JWT
19.1、JWT
介绍 参考文档:https://www.cnblogs.com/jingboweilan/p/14569434.html
https://www.cnblogs.com/yuanrw/p/10089796.html
JSON Web Token(JWT)
Internet服务无法与用户身份验证分开。一般过程如下。
1.用户向服务器发送用户名和密码。
2.验证服务器后,相关数据(如用户角色,登录时间等)将保存在当前会话中。
3.服务器向用户返回session_id,session信息都会写入到用户的Cookie。
4.用户的每个后续请求都将通过在Cookie中取出session_id传给服务器。
5.服务器收到session_id并对比之前保存的数据,确认用户的身份。
这种模式最大的问题是,没有分布式架构,无法支持横向扩展。如果使用一个服务器,该模式完全没有问题。但是,如果它是服务器群集或面向服务的跨域体系结构的话,则需要一个统一的session数据库库来保存会话数据实现共享,这样负载均衡下的每个服务器才可以正确的验证用户身份。
例如:站点A和站点B提供同一公司的相关服务。现在要求用户只需要登录其中一个网站,然后它就会自动登录到另一个网站。怎么做?
一种解决方案是听过持久化session数据,写入数据库或文件持久层等。收到请求后,验证服务从持久层请求数据。该解决方案的优点在于架构清晰,而缺点是架构修改比较费劲,整个服务的验证逻辑层都需要重写,工作量相对较大。而且由于依赖于持久层的数据库或者问题系统,会有单点风险,如果持久层失败,整个认证体系都会挂掉。
所以这里,可以通过客户端保存数据,而服务器根本不保存会话数据,每个请求都被发送回服务器。 JWT是这种解决方案的代表。
19.2. JWT的原则 (JSON Web Tocken
)
JWT的原则是在服务器身份验证之后,将生成一个JSON对象并将其发送回用户,如下所示。
1 2 3 4 5 6 7 8 9 10 { "UserName" : "Chongchong" , "Role" : "Admin" , "Expire" : "2018-08-08 20:15:56" }
之后,当用户与服务器通信时,客户在请求中发回JSON对象。服务器仅依赖于这个JSON对象来标识用户。为了防止用户篡改数据,服务器将在生成对象时添加签名
服务器不保存任何会话数据,即服务器变为无状态,使其更容易扩展。
19.3. JWT的数据结构 典型的,一个JWT看起来如下图。
改对象为一个很长的字符串,字符之间通过”.”分隔符分为三个子串。注意JWT对象为一个长字串,各字串之间也没有换行符,此处为了演示需要,我们特意分行并用不同颜色表示了。每一个子串表示了一个功能块,总共有以下三个部分:
JWT的三个部分如下。JWT头、有效载荷和签名,将它们写成一行如下。
JWT头部分是一个描述JWT元数据的JSON对象,通常如下所示。
1 2 3 4 5 6 7 8 { "alg" : "HS256" , "type" "JWT" }
在上面的代码中,alg属性表示签名使用的算法,默认为HMAC SHA256(写为HS256);typ属性表示令牌的类型,JWT令牌统一写为JWT。
最后,使用Base64 URL算法将上述JSON对象转换为字符串保存。
3.2 有效载荷
有效载荷部分,是JWT的主体内容部分,也是一个JSON对象,包含需要传递的数据。 JWT指定七个默认字段供选择。
iss:发行人
exp:到期时间
sub:主题
aud:用户
nbf:在此之前不可用
iat:发布时间
jti:JWT ID用于标识该JWT
除以上默认字段外,我们还可以自定义私有字段,如下例:
1 2 3 4 5 6 7 8 9 10 { "sub" : "1234567890" , "name" : "chongchong" , "admin" : true }
请注意,默认情况下JWT载荷是未加密的,任何人都可以解读其内容,因此不要构建隐私信息字段,存放保密信息,以防止信息泄露。
JSON对象也使用Base64 URL算法转换为字符串保存。
3.3签名哈希
签名哈希部分是对上面两部分数据签名,通过指定的算法生成哈希,以确保数据不会被篡改。
首先,需要指定一个密钥(secret)。该密码仅仅为保存在服务器中,并且不能向用户公开。然后,使用标头中指定的签名算法(默认情况下为HMAC SHA256)根据以下公式生成签名。
1 2 HMACSHA256 (base64UrlEncode (header) + "." + base64UrlEncode (payload),secret)
在计算出签名哈希后,JWT头,有效载荷和签名哈希的三个部分组合成一个字符串,每个部分用”.”分隔,就构成整个JWT对象。
4. JWT的用法
客户端接收服务器返回的JWT,将其存储在Cookie或localStorage中。
此后,客户端将在与服务器交互中都会带JWT。如果将它存储在Cookie中,就可以自动发送,但是不会跨域,因此一般是将它放入HTTP请求的Header Authorization字段中。
Authorization: Bearer
当跨域时,也可以将JWT被放置于POST请求的数据主体中
总结内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 它是一个很长的字符串,中间用点(`.`)分隔成三个部分。注意,JWT 内部是没有换行的,这里只是为了便于展示,将它写成了几行。 JWT 的三个部分依次如下。 ``` Header(头部) Payload(负载) Signature(签名) ``` 写成一行,就是下面的样子。 ``` Header.Payload.Signature ``` 下面依次介绍这三个部分。 **Header** Header 部分是一个 JSON 对象,描述 JWT 的元数据,通常是下面的样子。 ```json { "alg": "HS256", "typ": "JWT" } ``` 上面代码中,`alg`属性表示签名的算法(algorithm),默认是 HMAC SHA256(写成 HS256);`typ`属性表示这个令牌(token)的类型(type),JWT 令牌统一写为`JWT`。 最后,将上面的 JSON 对象使用 Base64URL 算法转成字符串。 **Payload** Payload 部分也是一个 JSON 对象,用来存放实际需要传递的数据。JWT 规定了7个官方字段,供选用。 ``` iss (issuer):签发人 exp (expiration time):过期时间 sub (subject):主题 aud (audience):受众 nbf (Not Before):生效时间 iat (Issued At):签发时间 jti (JWT ID):编号 ``` 除了官方字段,你还可以在这个部分定义私有字段,下面就是一个例子。 ``` { "sub": "1234567890", "name": "John Doe", "admin": true } ``` 这个 JSON 对象也要使用 Base64URL 算法转成字符串。 **Signature** Signature 部分是对前两部分的签名,防止数据篡改。 首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。 ``` HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret) ``` 算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用"点"(`.`)分隔,就可以返回给用户。
19.4 JWT
的基本使用 我们首先完成的是用户登录,登录成功以后,会创建JWT
,然后返回给客户端。
当然,在项目中使用JWT
,需要再Cms.WebApi
项目中安装如下的包。
1 Install-Package Microsoft.AspNetCore.Authentication.JwtBearer
这里首先会进行登录的校验,所以针对登录,先创建一个DTO
数据传输对象。
在Dtos
这个目录下面创建LoginDto.cs
类,该类中的代码,如下所示:
1 2 3 4 5 6 7 public class LoginDto { [Required ] public string ? UserName { get ; set ; } [Required ] public string ? UserPassword { get ; set ; } }
针对登录,很难创建一个标注的Restful
的接口,但是我们前面也已经讲解过,不用为了Restful
而Restful
。
这里创建一个LoginController.cs
控制器,该控制器中的代码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 using Cms.IService;using Cms.WebApi.Dtos;using Microsoft.AspNetCore.Http;using Microsoft.AspNetCore.Mvc;using Microsoft.EntityFrameworkCore;using Microsoft.IdentityModel.JsonWebTokens;using Microsoft.IdentityModel.Tokens;using System.IdentityModel.Tokens.Jwt;using System.Security.Claims;using System.Security.Permissions;using System.Text;namespace Cms.WebApi.Controllers { [Route("api/[controller]" ) ] [ApiController ] public class LoginController : ControllerBase { private readonly IUserInfoService _userInfoService; private readonly IConfiguration configuration; public LoginController (IUserInfoService _userInfoService,IConfiguration configuration ) { this ._userInfoService = _userInfoService; this .configuration = configuration; } [HttpPost ] public async Task<IActionResult> login ([FromBody] LoginDto loginDto ) { if (string .IsNullOrWhiteSpace(loginDto.UserName)||string .IsNullOrWhiteSpace(loginDto.UserName)) { return BadRequest("用户名或密码不能为空!" ); } var loginUsr = await _userInfoService.LoadEntities(u=>u.UserName==loginDto.UserName&&u.UserPassword==loginDto.UserPassword).FirstOrDefaultAsync(); if (loginUsr == null ) { return BadRequest("用户名或密码错误!" ); } var securityAlgorithm = SecurityAlgorithms.HmacSha256; var claims = new [] { new Claim(System.IdentityModel.Tokens.Jwt.JwtRegisteredClaimNames.Sub,loginUsr.Id.ToString()) }; var secretByte = Encoding.UTF8.GetBytes(configuration["Authentication:SecretKey" ]!); var signingKey = new SymmetricSecurityKey(secretByte); var signingCredentials = new SigningCredentials(signingKey, securityAlgorithm); var token = new JwtSecurityToken( issuer: configuration["Authentication:Issuer" ], audience: configuration["Authentication:Audience" ], claims, notBefore:DateTime.Now, expires:DateTime.Now.AddDays(1 ), signingCredentials ); var tokenStr = new JwtSecurityTokenHandler().WriteToken(token); return Ok(tokenStr); } } }
在上面的代码中,我们构建数字签名的时候,使用了私钥,私钥必须保存到服务端,同时这里我们将其保存在了appsettings.json
文件中
1 2 3 4 5 6 7 8 "ConnectionStrings" : { "StrConn" : "server=localhost;database=APIDemo;uid=sa;pwd=123456;TrustServerCertificate=true" }, "Authentication" : { "SecretKey" :"abc123456789@126.com" "Issuer" : "xxx.com" , "Audience" : "xxx.com" }
19.5 启动授权 当用户访问某些资源的时候,需要进行验证。
这就是使用前面所生成的token
.
当然,这里需要注入jwt
的认证服务。
修改Program.cs
文件中的代码,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(options => { var secretByte = Encoding.UTF8.GetBytes(builder.Configuration["Authentication:SecretKey" ]!); options.TokenValidationParameters = new TokenValidationParameters() { ValidateIssuer = true , ValidIssuer = builder.Configuration["Authentication:Issuer" ], ValidateAudience = true , ValidAudience = builder.Configuration["Authentication:Audience" ], ValidateLifetime = true , IssuerSigningKey = new SymmetricSecurityKey(secretByte) }; }); builder.Services.AddDbContext<MyDbContext>(opt =>
以上代码就是在注入DbContext
服务的上面,注入了JWT
的认证服务。
1 2 3 4 5 6 7 8 9 10 11 12 if (app.Environment.IsDevelopment()){ app.UseSwagger(); app.UseSwaggerUI(); } app.UseAuthentication(); app.UseAuthorization(); app.MapControllers();
同时,在Program.cs
文件的下面添加了app.UseAuthentication(),app.UseAuthorization();app.MapControllers();
下面,我们进行测试。
我们考虑到用户登录了才能创建其他的用户。
所以说,在UsersController.cs
控制器中的CreateUser
方法上面添加了Authorize
,进行校验。
1 2 3 4 5 6 7 8 9 [HttpPost ] [Authorize ] [UnitOfWork(new Type[ ] { typeof (MyDbContext) })] public async Task<IActionResult> CreatUser ([FromBody]UserInfoCreateDto userInfoCreateDto ) {
下面进行测试,首先在swagger
页面中进行登录,当登录成功以后,才能获取到token
信息。
下面访问[Post
]/api/Users
的地址,去创建用户,这里我们输入完用户信息以后,单击执行的Execute
按钮以后,会返回401状态码,表示要求进行身份认证,原因是:针对这次创建用户的这次POST
请求我们没有带上token
信息。
才导致了返回401
状态码,在swagger
中配置请求中添加token
稍微麻烦,这里我们可以使用ApifOX
软件。
20、分页处理 分页的参数传递方式,通常都是通过查询字符串来进行传递的。
例如:/api/users?当前页=1&每页显示记录数=10
当然,如果用户没有设置页码或者是每页显示的记录数,可以设置默认值,例如:默认页码值为1,每页显示的记录数默认值为10.
这里我们以文章分页来进行演示。
这里我们首先修改ResourceParams/ArticelParams.cs
中的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 public class ArticelParams { public string ? title { get ; set ; } public string ? FormDatepicker { get ; set ; } public string ? ToDatepicker { get ; set ; } private int _pageIndex = 1 ; public int PageIndex { get { return _pageIndex; } set { if (value >=1 ) { _pageIndex = value ; } } } private int _pageSize = 10 ; const int maxPageSize = 50 ; public int PageSize { get { return _pageSize; } set { if (value >= 1 ) { _pageSize = value > maxPageSize?maxPageSize : value ; } } } public bool Order { get ; set ; } }
在以上的代码中,通过定义私有成员变量 _pageIndex 和_pageSize
对当前的页码与每页显示的记录数都做了一定的限制。
返回到ArticelsController.cs
这控制器中,把注释测试代码注释掉。
1 2 3 4 5 6 7 8 [HttpGet ] public IActionResult GetArticel ([FromQuery] ArticelParams articelParams ) { int totalCount = 0 ;
当然,这里我们也完全可以定义一个BaseParams.cs
封装公共的分页属性,然后让ArticelParams
类继承BaseParams
怎样返回分页的数据?
在前面我们只是将数据源返回到前端了,但是相关的分页信息我们也需要返回,因为前端在进行分页的时候,还需要使用相关的信息,例如总的记录,或者是总的页数等。
这里怎样将这些信息返回到前端呢?
这里可以将分页的信息放到响应头中返回,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 public IActionResult GetArticel ([FromQuery] ArticelParams articelParams ) { int totalCount = 0 ; ArticelSearch articelSearch = new ArticelSearch() { title = articelParams.title, FormDatepicker = articelParams.FormDatepicker, ToDatepicker = articelParams.ToDatepicker, PageSize = articelParams.PageSize, PageIndex = articelParams.PageIndex, Order = articelParams.Order, TotalCount = totalCount }; var articels = _articelService.LoadSearchEntities(articelSearch,Convert.ToBoolean(DelFlagEnum.Normal)); if (articelSearch.TotalCount <= 0 ) { return NotFound("没有找到文章" ); } var articelList = mapper.Map<List<ArticelDto>>(articels); var paginationInfo = new { totalCount = articelSearch.TotalCount, pageSize = articelSearch.PageSize, pageIndex = articelSearch.PageIndex, totalPages =(int )Math.Ceiling(articelSearch.TotalCount / (double )articelSearch.PageSize) }; HttpContext.Response.Headers.Add("x-pagination" , Newtonsoft.Json.JsonConvert.SerializeObject(paginationInfo)); return Ok(articelList); }
21、数据排序 关于数据的排序,我们前面也实现了,但是我们当时将排序的字段固定死了,就是根据ID
排序,但是很多的时候,我们需要指定不同的字段来进行排序。
这里我们会使用到一个包
1 System.Linq.Dynamic.Core
这个包的作用:可以生成Linq
查询表达式。
可以将传递过来的字符串,通过这个包转换成Linq
查询表达式。
在访问排序的接口的时候,请求的地址形式如下所示:
1 /api/articels?orderBy=id,createTime desc
为了对排序进行处理,这里我们为IQueryable
接口进行了扩展,也就是为其定义了一个扩展的方法。
在Cms.Repository
这个数据仓储项目中创建一个文件夹Extensions
,在该文件夹下面创建一个RepositoryExtensions.cs
类,该类中的代码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 using System;using System.Collections.Generic;using System.Linq;using System.Reflection;using System.Text;using System.Threading.Tasks;using System.Linq.Dynamic.Core; namespace Cms.Repository.Extensions { public static class RepositoryExtensions { public static IQueryable <T > OrderByQuery <T >(this IQueryable<T> queryable, string queryString ) { if (string .IsNullOrEmpty(queryString)) { return queryable; } var orderParams = queryString.Trim().Split(',' ); var propertyInfos = typeof (T).GetProperties(BindingFlags.Public | BindingFlags.Instance); var orderQueryBuilder = new StringBuilder(); foreach (var param in orderParams) { if (string .IsNullOrEmpty(param)) { continue ; } var propertyFromQueryName = param.Split(" " )[0 ]; var objectProperty = propertyInfos.FirstOrDefault(p => p.Name.Equals(propertyFromQueryName, StringComparison.InvariantCultureIgnoreCase)); if (objectProperty == null ) { continue ; } var sortingOrder = param.EndsWith(" desc" ) ? "descending" : "ascending" ; orderQueryBuilder.Append($"{objectProperty.Name} {sortingOrder} ," ); } var orderQuery = orderQueryBuilder.ToString().TrimEnd(',' , ' ' ); return queryable.OrderBy(orderQuery); } } }
上面为IQueryable
泛型接口定义了扩展方法OrderByQuery
.下面看一下怎样调用该扩展方法。
我们可以在ArticelService.cs
这个业务类中使用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 public IQueryable<Articel> LoadSearchEntities (ArticelSearch articelSearch, bool flag ) { var temp = articelRepository.LoadEntities(a => a.DelFlag == flag); if (!string .IsNullOrEmpty(articelSearch.title)) { temp = temp.Where<Articel>(a => a.Title!.Contains(articelSearch.title)); } if (!string .IsNullOrEmpty(articelSearch.FormDatepicker)) { DateTime startDate = Convert.ToDateTime(articelSearch.FormDatepicker); temp = temp.Where<Articel>(a => a.CreateTime >= startDate); } if (!string .IsNullOrEmpty(articelSearch.ToDatepicker)) { DateTime endDate = Convert.ToDateTime((articelSearch.ToDatepicker)); temp = temp.Where<Articel>(a => a.CreateTime <= endDate); } articelSearch.TotalCount = temp.Count(); temp = temp.OrderByQuery<Articel>(articelSearch.OrderBy!).Skip<Articel>((articelSearch.PageIndex - 1 ) * articelSearch.PageSize).Take<Articel>(articelSearch.PageSize); return temp; }
在对应的分页方法中调用了排序的扩展方法。
在调用扩展方法OrderByQuery
的时候,需要传递参数,这个参数就是排序的字段与排序方式,类型是字符串。
当然,需要ArticelSearch.cs
类中定义OrderBy
属性。当然定义到了其父类BaseSearch.cs
中。
1 2 3 4 5 6 7 8 9 public class BaseSearch { public int PageIndex { get ; set ; } public int PageSize { get ; set ; } public int TotalCount { get ; set ; } public bool Order { get ; set ; } public string ? OrderBy { get ; set ; } }
下面修改ArticelsController.cs
控制器中的GetArticels
方法中的代码,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public IActionResult GetArticels ([FromQuery] ArticelParams articelParams ) { int pageIndex = articelParams.PageIndex == 0 ? 1 : articelParams.PageIndex; int pageSize = articelParams.PageSize == 0 ? 2 : articelParams.PageSize; int totalCount = 0 ; ArticelSearch articelSearch = new ArticelSearch() { FormDatepicker = articelParams.FormDatepicker, Order = articelParams.Order, PageIndex = pageIndex, PageSize = pageSize, title = articelParams.title, ToDatepicker = articelParams.ToDatepicker, TotalCount = totalCount, OrderBy = articelParams.OrderBy, };
下面,修改ArticelParams
类。
1 2 3 4 5 6 7 8 9 10 11 12 public class ArticelParams { public string ? title { get ; set ; } public string ? FormDatepicker { get ; set ; } public string ? ToDatepicker { get ; set ; } public int PageIndex { get ; set ; } public int PageSize { get ; set ; } public bool Order { get ; set ; } public string ? Fields { get ; set ; } public string ? OrderBy { get ; set ; } }
返回到浏览器中进行测试。
22、数据塑形 22.1 什么是数据塑形? 所谓的数据塑性:就是客户端可以定制化 选择后端输出数据的技术。
例如:针对用户信息,只希望返回,用户名和邮箱,这时候的api
地址就可以采用如下的形式:
1 /api/users?fields =username,useremail
数据塑性可以极大的提高数据处理的能力。例如:用户
有20个字段,每次展示50条数据,这样就是1000条数据,
如果只传递以上两个字段,只需要传输的数据就是100条
22.2 处理动态类型对象 下面我们来看一下, ArticelsController.cs
控制器中的GetArticel
方法,该方法返回的List<ArticelDto>
.
但是经过数据的塑性以后,我们不再返回ArticelDto
,而是返回一个仅仅包含所需要的字段的对象来代替。
我们会在程序运行的时候,根据ArticelDto
来动态创建这个对象。
在这一小节中,我们会处理动态类型对象dynamic object
ExpandoObject
是专为动态类型封装的类型,是对动态类型对象的处理。可以在程序运行的时候,动态的添加或者是删除一个类的成员变量。也就是可以动态的处理对象的字段。而动态处理对象字段,就是数据塑性需要具有的能力。
在Cms.WebApi
项目中创建Helper
目录,在该目录下面创建IEnumerableExtensions.cs
,该类是对IEnumerable
类型的扩展,从而实现数据的塑性。
该类中的代码,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 using System.Dynamic;using System.Reflection;namespace Cms.WebApi.Helper { public static class IEnumerableExtensions { public static IEnumerable <ExpandoObject >ShapeDatad <TSource >(this IEnumerable<TSource> source,string fields ) { if (source == null ) { throw new ArgumentNullException("source" ); } var expandoObjectList = new List<ExpandoObject>(); var propertyInfoList = new List<PropertyInfo>(); if (string .IsNullOrWhiteSpace(fields)) { var propertyInfos = typeof (TSource).GetProperties(BindingFlags.IgnoreCase|BindingFlags.Public|BindingFlags.Instance); propertyInfoList.AddRange(propertyInfos); } else { var fieldsAfterSplit = fields.Split(',' ); foreach ( var field in fieldsAfterSplit) { var propertyName = field.Trim(); var propertyInfo = typeof (TSource).GetProperty(propertyName, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance); if (propertyInfo == null ) { throw new Exception($"属性{propertyName} 找不到" ); } propertyInfoList.Add(propertyInfo); } } foreach (TSource sourceObject in source) { var dataShapedObject = new ExpandoObject(); foreach ( var propertyInfo in propertyInfoList) { var propertyValue = propertyInfo.GetValue(sourceObject); ((IDictionary<string ,object >)dataShapedObject!).Add(propertyInfo.Name, propertyValue!); } expandoObjectList.Add(dataShapedObject); } return expandoObjectList; } } }
下面修改ResourceParams/ArticelParams.cs
中的代码
1 2 3 4 5 public bool Order { get ; set ; } public string ? Fields { get ; set ; } }
这里添加了Fields
属性,通过该属性接收前端传递过来的要展示的字段的名称。
下面修改ArticelsController.cs
控制器中的代码
1 2 HttpContext.Response.Headers.Add("x-pagination" , Newtonsoft.Json.JsonConvert.SerializeObject(paginationInfo)); return Ok(articelList.ShapeDatad(articelParams.Fields!));
22.3 单一资源的塑形 在上一小节中,我们扩展了IEnumerable
,实现了对集合的塑性。
在这一小节中,我们实现对单一资源的塑性。
同样在Cms.WebApi
项目的Helper
文件夹中创建ObjectExtensions.cs
,该类中的代码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 public static class ObjectExtensions { public static ExpandoObject ShapeData <TSource >(this TSource source,string fields ) { if (source == null ) { throw new ArgumentNullException("source" ); } var dataShapedObject = new ExpandoObject(); if (string .IsNullOrWhiteSpace(fields)) { var propertyInfos = typeof (TSource).GetProperties(BindingFlags.IgnoreCase|BindingFlags.Public|BindingFlags.Instance); foreach ( var propertyInfo in propertyInfos) { var propertyValue = propertyInfo.GetValue(source); ((IDictionary<string ,object >)dataShapedObject!).Add(propertyInfo.Name, propertyValue!); } return dataShapedObject; } var fieldsAfterSplit = fields.Split(',' ); foreach (var field in fieldsAfterSplit) { var propertyName = field.Trim(); var propertyInfo = typeof (TSource) .GetProperty(propertyName, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance); if (propertyInfo == null ) { throw new Exception($"Property {propertyName} wasn't found " + $"on {typeof (TSource)} " ); } var propertyValue = propertyInfo.GetValue(source); ((IDictionary<string , object >)dataShapedObject!) .Add(propertyInfo.Name, propertyValue!); } return dataShapedObject; } }
这里是对object
对象的扩展。(因为这里是对单一的资源对象进行数据的塑性)
这里单独创建一个类来实现单一资源的数据塑性的原因是:职责明确,再有方便管理,性能高。
下面我们返回的到UsersController.cs
控制器中进行测试。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public async Task<IActionResult> GetUser (int id,string fields ) { var user = await _userInfoService.LoadEntities(u=>u.Id==id).FirstOrDefaultAsync(); var userDTO = mapper.Map<UserInfoDto>(user); if (user == null ) { return NotFound($"编号为{id} 的用户不存在!!" ); } return Ok(userDTO.ShapeData(fields)); }
GetUser
方法,是根据用户传递过来的id
,查询单一用户的方法,所以这里我们又给其添加了一个参数fields
,接收前端传递过来的需要展示的字段。
启动项目进行测试。(多个字段名之间使用逗号进行分割)
22.4 数据塑形错误的处理 当前端给fields
参数传递的是不存在的字段的时候,服务端的程序会出现500的错误。
但是,这种情况是前端输入错误,应该给出400的状态码,同时给出相应的错误提示。
具体的处理方式如下所示:
在Helper
目录下面创建PropertyCheck.cs
类,该类中的代码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 public class PropertyCheck { public static bool IsPropertiesExists <T >(string fields ) { if (string .IsNullOrWhiteSpace(fields)) { return true ; } var fieldsAfterSplit = fields.Split(',' ); foreach ( var field in fieldsAfterSplit ) { var propertyName = field.Trim(); var propertyInfo = typeof (T).GetProperty(propertyName,BindingFlags.Public|BindingFlags.IgnoreCase|BindingFlags.Instance); if (propertyInfo == null ) { return false ; } } return true ; } }
返回到UsersController.cs
控制器中的GetUser
这个方法进行测试,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 [HttpGet("{id}" ,Name ="GetUser" ) ] public async Task<IActionResult> GetUser (int id,string fields ) { if (!PropertyCheck.IsPropertiesExists<UserInfoDto>(fields)) { return BadRequest("传递的字段是错误的!" ); } var user = await _userInfoService.LoadEntities(u=>u.Id==id).FirstOrDefaultAsync(); var userDTO = mapper.Map<UserInfoDto>(user); if (user == null ) { return NotFound($"编号为{id} 的用户不存在!!" ); } return Ok(userDTO.ShapeData(fields)); }
启动项目进行测试。
23、HATEOAS
应用 23.1 HATEOAS
介绍 hateoas
是rest
架构风格中最复杂的约束,也是构建成熟rest
服务的核心,它的重要性在于打破了客户端和服务器之间严格的契约,使得客户端可以更加智能和自适应,而Rest
服务本身的演化和更新也变得更加容易。
hateoas
:Hypermedia as the engine of application state
(超媒体即应用状态引擎)
hateoas
核心思想:在响应中包含其他资源的链接,客户端可以使用这些链接与服务器进行交互。
下面通过一个例子来体会一下heateoas
在返回的资源中添加了Links
属性,表示可以对该资源还可以进行哪些操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 { "id" : 1 , "userName" : "zhangsan" , "userEmail" : "zhangsan@126.com" , "Links" : [ { "href" : "https://localhost:5050/api/users/1" , "rel" : "self" , "method" : "Get" } , { "href" : "https://localhost:5050/api/users/1" , "rel" : "更新" , "method" : "Put" } , { "href" : "https://localhost:5050/api/users/1" , "rel" : "局部更新" , "method" : "PATCH" } , { "href" : "https://localhost:5050/api/users/1" , "rel" : "删除" , "method" : "Delete" } ] }
Links
中的这些链接返回到前端以后,前端可以直接使用。
这样做的好处,就是服务端可以根据实际的情况,返回Links
,例如:后端可以根据用户具有的不同的权限,在Links
中返回不同的链接,这样前端可以根据Links
中的链接向服务端发送请求,前端不需要参考任何的接口文档。
Link
是HATEOAS
的核心
Link
中的三个成员的含义:
href
:表示的是资源的地址。
rel
: 用来描述资源和url
的关系。例如:self
:表示了url
自我描述的关系。
method
:表示的是访问资源对应的方法。
当然Links
中的内容是根据业务来定义的。
23.2 处理单一资源 在Dtos
目录中创建LinkDto.cs
类,在该类中定义Link
中的核心成员,代码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 namespace Cms.WebApi.Dtos { public class LinkDto { public string ? Href { get ; set ; } public string ? Rel { get ; set ; } public string ? Method { get ; set ; } public LinkDto (string href,string rel,string method ) { Href = href; Rel = rel; Method = method; } } }
下面修改UsersController.cs
中的代码,如下所示:
在控制器的GetUser
方法的下面定义CreateLinkForUsers
方法,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 private IEnumerable<LinkDto> CreateLinkForUsers (int id,string fields ){ var links = new List<LinkDto>(); links.Add(new LinkDto(Url.Link("GetUser" ,new {id,fields })!,"self" ,"GET" )); int userId = id; links.Add(new LinkDto(Url.Link("EditUser" , new { userId })!, "update" , "PUT" )); links.Add(new LinkDto(Url.Link("UpdateUser" , new { userId })!, "update_patch" , "PATCH" )); links.Add(new LinkDto(Url.Link("DeleteUser" , new { userId })!,"delete" ,"Delete" )); links.Add(new LinkDto(Url.Link("GetArticelForUser" , new { userId })!, "get_articels" , "GET" )); return links; }
在CreateLinkForUsers
方法中,创建一个一个的LinkDto
对象,然后添加到links
这个List
集合中。
在创建LinkDto
这个对象的时候,在确定Href
这个属性的值的时候,是通过.net core
中内置的Url.Link
方法来创建的,该方法的第一个参数是方法对应的路由的名字。
所以说,在GetUser
方法对应的HttpGet
中指定了Name
属性,取值就是GetUser
.
1 2 3 [HttpGet("{id}" ,Name ="GetUser" ) ] public async Task<IActionResult> GetUser (int id,string fields ){
下面对应的给其他的方法添加相应的name
属性值
1 2 3 4 5 6 7 8 [HttpGet("{userId}/articels" ,Name = "GetArticelForUser" ) ] public async Task<IActionResult> GetArticelForUser (int userId ) {
1 2 3 4 5 6 7 8 9 10 [HttpPut("{userId}" ,Name ="EditUser" ) ] [UnitOfWork(new Type[ ] { typeof (MyDbContext) })] public async Task<IActionResult> EditUser ([FromRoute]int userId, [FromBody]UserInfoForUpdateDto userInfoForUpdateDto ) {
1 2 3 4 5 6 7 8 9 10 [HttpPatch("{userId}" ,Name = "UpdateUser" ) ] [UnitOfWork(new Type[ ] { typeof (MyDbContext) })] public async Task<IActionResult> UpdateUser ([FromRoute] int userId, [FromBody] JsonPatchDocument<UserInfoForUpdateDto> patchDocument ) {
1 2 3 4 5 6 7 8 9 [HttpDelete("{userId}" ,Name ="DeleteUser" ) ] [UnitOfWork(new Type[ ] { typeof (MyDbContext) })] public async Task<IActionResult> DeleteUser ([FromRoute]int userId ) {
问题:在什么位置调用CreateLinkForUsers这个方法呢?
这一次是对单一资源的处理,所以这里是在GetUser
这个方法中进行调用的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 [HttpGet("{id}" ,Name ="GetUser" ) ] public async Task<IActionResult> GetUser (int id,string fields ) { if (!PropertyCheck.IsPropertiesExists<UserInfoDto>(fields)) { return BadRequest("传递的字段是错误的!" ); } var user = await _userInfoService.LoadEntities(u=>u.Id==id).FirstOrDefaultAsync(); var userDTO = mapper.Map<UserInfoDto>(user); if (user == null ) { return NotFound($"编号为{id} 的用户不存在!!" ); } var linkDtos = CreateLinkForUsers(id,fields); var result= userDTO.ShapeData(fields)as IDictionary<string , object >; result.Add("links" , linkDtos); return Ok(result); }
启动项目进行测试。在返回的响应报文中包含了links
23.2 post
请求的处理
我们知道,当创建一个用户的时候,会执行如下的CreatUser
方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 [HttpPost(Name ="CreateUser" ) ] [Authorize ] [UnitOfWork(new Type[ ] { typeof (MyDbContext) })] public async Task<IActionResult> CreatUser ([FromBody]UserInfoCreateDto userInfoCreateDto ) { var userInfoModel = mapper.Map<UserInfo>(userInfoCreateDto); await _userInfoService.InsertEntityAsync(userInfoModel); var user = mapper.Map<UserInfoDto>(userInfoModel); return CreatedAtAction("GetAllUsers" , user); }
我们这里可以看到,当创建完一个用户以后,最终返回的是一个所创建的用户的信息。
所以这里给post
请求,添加links
,也是很有必要的。
下面修改CreateUser
方法,代码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 [HttpPost(Name ="CreateUser" ) ] [Authorize ] [UnitOfWork(new Type[ ] { typeof (MyDbContext) })] public async Task<IActionResult> CreatUser ([FromBody]UserInfoCreateDto userInfoCreateDto ) { var userInfoModel = mapper.Map<UserInfo>(userInfoCreateDto); await _userInfoService.InsertEntityAsync(userInfoModel); var user = mapper.Map<UserInfoDto>(userInfoModel); var links = CreateLinkForUsers( Convert.ToInt32(userInfoModel.Id), null !); var result = user.ShapeData(null !) as IDictionary<string , object >; result.Add("links" , links); return CreatedAtAction("GetAllUsers" , result); }
由于这里,添加了 [Authorize]
,所以只能有登录的用户才能创建用户,这里登录了以后,拷贝一下token
信息,然后在apifox
软件中进行测试
添加的完用户以后,返回的信息是:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 { "id" : 0 , "createTime" : "2023年4月17日" , "updateTime" : "2023-04-17T14:58:13.4652567+08:00" , "userName" : "maliu23" , "userEmail" : "maliu2@126.com" , "userPhone" : "1312222222" , "gender" : 0 , "photoUrl" : "/images/b.jpg" , "articels" : [], "links" : [ { "href" : "http://localhost:5154/api/Users/0" , "rel" : "self" , "method" : "GET" }, { "href" : "http://localhost:5154/api/Users/0" , "rel" : "update" , "method" : "PUT" }, { "href" : "http://localhost:5154/api/Users/0" , "rel" : "update_patch" , "method" : "PATCH" }, { "href" : "http://localhost:5154/api/Users/0" , "rel" : "delete" , "method" : "Delete" }, { "href" : "http://localhost:5154/api/Users/0/articels" , "rel" : "get_articels" , "method" : "GET" } ] }
这里添加完用户以后,也将添加成功的用户的信息返回了,同时包含了links
数组。
问题:这里的用户编号为什么是0呢?
原因是与savechanges
方法的执行时机有关。解决的方案:我们可以在CreateUser
方法中,获取原有的用户数据源中最大的用户编号,然后这里进行加1的操作。
23.3 列表资源的处理 这里我们以ArticelsController
控制器中的GetArticel
方法为例来说明一下。
在ArticelsController
控制器中增加如下两个私有的方法。
当调用CreateLinksForArticelList
这个私有的方法的时候,根据传递过来的相关参数,构建Links
数组。这里只是增加了自身,其他的内容与前面讲解的是一样的。同时在该方法中还调用了私有方法createArticeParams方法
,该方法的作用就是对搜索参数的处理。
在createArticeParams
方法的内部创建了一个匿名对象,来对搜索的参数进行处理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 private IEnumerable<LinkDto> CreateLinksForArticelList (ArticelParams articelParams ) { var links = new List<LinkDto>(); links.Add(new LinkDto(Url.Link("GetArticel" , createArticeParams(articelParams))!, "self" , "GET" )); return links; } private object createArticeParams (ArticelParams articelParams ) { return new { title = articelParams.title, PageIndex = articelParams.PageIndex, PageSize = articelParams.PageSize, FormDatepicker = articelParams.FormDatepicker, ToDatepicker = articelParams.ToDatepicker, Order = articelParams.Order, Fields = articelParams.Fields, }; }
下面看一下GetArticel
方法的修改。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 HttpContext.Response.Headers.Add("x-pagination" , Newtonsoft.Json.JsonConvert.SerializeObject(paginationInfo)); var shapedDtoList = articelList.ShapeDatad(articelParams.Fields!); var linkDto = CreateLinksForArticelList(articelParams); var result = new { value = shapedDtoList, links = linkDto }; return Ok(result);
启动项目进行测试。
23.4 媒体类型 目前面临的问题:links
成为了响应数据的一部分,但是它们并不属于资源。当响应数据中包含这里links
这些链接的时候,已经违反restful
的定义了。
解决的办法就是通过内容协商来解决,
内容协商指的是:在请求中包含不同的媒体类型,服务端的api
返回不同的响应数据。
例如:假设请求中包含的媒体类型是:
1 "Accept":"application/json"
这表示请求的是一个资源类型。
什么是媒体类型呢?
Multipurpose Interrnet Mail Extensions(MIME)
是一种标准,用来表示文档,文件或字节流的性质和格式。
它的结构:
1 type/subtype // 中间不能出现空格
type
:表示的是独立类别
subtype
:表示细分后的子类型
媒体类型对大小写不敏感,但是传统写法都是小写。
常见的媒体类型结构
以上表格就是常见的媒体类型的分类。
自定义媒体类型
当然,我们开发人员也可以自定义媒体类型
格式:
1 application/vnd.mycompany.hateoas+json
vnd
:全称”vendor”的缩写,表示供应商,也就是说这个媒体类型是特定的供应商所使用的
“mycompany”:是自定义的标识,可以是公司名
“hateoas”:表示返回的响应中要包含超媒体链接
”json“:表示输出的数据结构
23.5 媒体类型应用 这里我们修改ArticelsController
控制器中的GetArticel
方法。
我们知道当请求该方法的时候,会将具体的数据资源以及hateoas对应的links
链接返回,这样会导致数据传输的性能降低。
我们希望的是,当请求的Accept
是application/json
的时候,只返回具体的资源数据,也是文章信息,不在返回hateoas
对应的links
数据。
要实现这种需求,就需要使用到自定义的媒体类型。
格式:
1 application/vnd.mycompany.hateoas+json
当然,如果请求的是以上媒体类型,才会将具体的资源数据以及hateoas
中的links
以json
格式的形式返回。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 [HttpGet(Name ="GetArticel" ) ] public IActionResult GetArticel ([FromQuery] ArticelParams articelParams, [FromHeader(Name ="Accept" )]string mediaType) { if (!MediaTypeHeaderValue.TryParse(mediaType,out MediaTypeHeaderValue? parsedMediatype)) { return BadRequest("错误的媒体类型" ); } int totalCount = 0 ; ArticelSearch articelSearch = new ArticelSearch() {
对媒体类型解析完成以后,下面就需要进行判断了,继续修改CreateArticel
这个方法中的代码,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 HttpContext.Response.Headers.Add("x-pagination" , Newtonsoft.Json.JsonConvert.SerializeObject(paginationInfo)); var shapedDtoList = articelList.ShapeDatad(articelParams.Fields!); if (parsedMediatype.MediaType == "application/vnd.xxx.hateoas+json" ) { var linkDto = CreateLinksForArticelList(articelParams); var result = new { value = shapedDtoList, links = linkDto }; return Ok(result); } return Ok(shapedDtoList);
下面进行测试
当请求的媒体类型是application/json
的时候,可以返回具体的文章数据。
这里我们测试的时候,我们需要使用Apifox
工具进行测试
这里我们首先,指定了Params
项,传递的参数是Fields
,参数值是id,title
,表示的是只展示这两个字段的数据,完成了数据的塑形操作。
这里指定的是Header
中的Accept
的值是application/vnd.xxx.hateoas+json
会出错,如果指定的是applicaiton/json
则会返回正确的数据,这里指定application/vnd.xxx.hateoas+json
出错的原因是:
对于自定义的媒体类型,需要再系统中进行注册。否则无法处理。
修改Program.cs
文件中的代码,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 builder.Services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies()); builder.Services.Configure<MvcOptions>(config => { var outputFormatter = config.OutputFormatters.OfType<NewtonsoftJsonOutputFormatter>().FirstOrDefault(); if (outputFormatter != null ) { outputFormatter.SupportedMediaTypes.Add("application/vnd.xxx.hateoas+json" ); } });
重新启动项目进行测试
当然,我们在请求头中如果不指定Accept
,默认是application/json
以上就是媒体类型的处理。