参照 草根专栏- ASP.NET Core + Ng6 实战:https://v.qq.com/x/page/u0765jbwc6f.html
一、POST
安全性和幂等性
- 安全性是指方法执行后并不会改变资源的表述
- 幂等性是指方法无论执行多少次都会得到同样的结果
POST添加资源:
- 不安全, 不幂等
- 参数 [FromBody]
- 返回 201 Create : CreatedAtRoute(): 它允许响应里带着Location Header,在这个Location Header里包含着一个uri,通过这个uri就可以GET到我们刚刚创建好的资源
- HATEOAS
(1)添加 PostAddResource.cs 类
namespace BlogDemo.Infrastructure.Resources { public class PostAddResource { public string Title { get; set; } public string Body { get; set; } public string Remark { get; set; } } }
(2)添加映射
namespace BlogDemo.Api.Extensions { public class MappingProfile:Profile { public MappingProfile() { CreateMap<Post, PostDTO>().ForMember(dest=>dest.Updatetime,opt=>opt.MapFrom(src=>src.LastModified)); CreateMap<PostDTO, Post>(); CreateMap<PostAddResource, Post>(); } } }
(3)添加post方法:
[HttpPost(Name = "CreatePost")] public async Task<IActionResult> Post([FromBody] PostAddResource postAddResource) { if (postAddResource == null) { return BadRequest(); } var newPost = _mapper.Map<PostAddResource, Post>(postAddResource); newPost.Author = "admin"; newPost.LastModified = DateTime.Now; _postRepository.AddPost(newPost); if (!await _unitOfWork.SaveAsync()) { throw new Exception("Save Failed!"); } var resultResource = _mapper.Map<Post, PostDTO>(newPost); var links = CreateLinksForPost(newPost.Id); var linkedPostResource = resultResource.ToDynamic() as IDictionary<string, object>; linkedPostResource.Add("links", links); return CreatedAtRoute("GetPost", new { id = linkedPostResource["Id"] }, linkedPostResource); }
(4) 测试:
二、Model验证:
定义验证规则
检查验证规则
把验证错误信息发送给API的消费者
1、验证方式:
内置验证:
DataAnnotation
ValidationAttribute
IValidatebleObject
第三方: FluentValidation
2、使用FluentValidation组件(关注点分离)
(1) 安装:
FluentValidation.AspNetCore
FluentValidation
(2) 为每一个Resource建立验证器:
继承AbstractValidator<T>
namespace BlogDemo.Infrastructure.Resources { public class PostAddResourceValidator:AbstractValidator<PostAddResource> { public PostAddResourceValidator() { RuleFor(x => x.Title).NotEmpty() .WithName("标题").WithMessage("{PropertyName}是必须填写的") .MaximumLength(50).WithMessage("{PropertyName}的最大长度是{MaxLength}"); RuleFor(x => x.Body).NotEmpty() .WithName("正文").WithMessage("{PropertyName}是必须填写的") .MinimumLength(50).WithMessage("{PropertyName}的最大长度是{MinLength}"); } } }
(3)配置:
services.AddMvc(……).AddFluentValidation();
services.AddTransient<IValidator<PostAddResource>, PostAddResourceValidator>();
(4)Action添加验证:
ModelState.IsValid
ModelState
它是一个字典,包含了Model的状态以及Model所绑定的验证
对于提交的每个属性,它都包含了一个错误信息的集合
返回: 422 Unprocessable Entity
验证错误信息在响应的body里面带回去
if (!ModelState.IsValid) { return UnprocessableEntity(ModelState); }
(5)测试
(6)Action添加Accpet和Content-Type 的自定义hateoas
[RequestHeaderMatchingMediaType("Content-Type", new[] { "application/vnd.cgzl.post.create+json" })] [RequestHeaderMatchingMediaType("Accept", new[] { "application/vnd.cgzl.hateoas+json" })]
(7) staupDevelopment 注册hateoas
services.AddMvc(option => { option.ReturnHttpNotAcceptable = true; // option.OutputFormatters.Add(new XmlDataContractSerializerOutputFormatter()); var outputFormatter = option.OutputFormatters.OfType<JsonOutputFormatter>().FirstOrDefault(); if (outputFormatter != null) { outputFormatter.SupportedMediaTypes.Add("application/vnd.cgzl.hateoas+json"); } var intputFormatter = option.InputFormatters.OfType<JsonInputFormatter>().FirstOrDefault(); if (intputFormatter != null) { intputFormatter.SupportedMediaTypes.Add("application/vnd.cgzl.post.create+json"); intputFormatter.SupportedMediaTypes.Add("application/vnd.cgzl.post.update+json"); } })
(8)测试:
3、自定义错误验证返回结果:
(1) 添加MyUnprocessableEntityObjectResult.cs ResourceValidationError.cs ResourceValidationResult.cs 类
namespace BlogDemo.Api.Helpers { public class MyUnprocessableEntityObjectResult : UnprocessableEntityObjectResult { public MyUnprocessableEntityObjectResult(ModelStateDictionary modelState) : base(new ResourceValidationResult(modelState)) { if (modelState == null) { throw new ArgumentNullException(nameof(modelState)); } StatusCode = 422; } } }
namespace BlogDemo.Api.Helpers { public class ResourceValidationError { public string ValidatorKey { get; private set; } public string Message { get; private set; } public ResourceValidationError(string message, string validatorKey = "") { ValidatorKey = validatorKey; Message = message; } } }
namespace BlogDemo.Api.Helpers { public class ResourceValidationResult : Dictionary<string, IEnumerable<ResourceValidationError>> { public ResourceValidationResult() : base(StringComparer.OrdinalIgnoreCase) { } public ResourceValidationResult(ModelStateDictionary modelState) : this() { if (modelState == null) { throw new ArgumentNullException(nameof(modelState)); } foreach (var keyModelStatePair in modelState) { var key = keyModelStatePair.Key; var errors = keyModelStatePair.Value.Errors; if (errors != null && errors.Count > 0) { var errorsToAdd = new List<ResourceValidationError>(); foreach (var error in errors) { var keyAndMessage = error.ErrorMessage.Split('|'); if (keyAndMessage.Length > 1) { errorsToAdd.Add(new ResourceValidationError(keyAndMessage[1], keyAndMessage[0])); } else { errorsToAdd.Add(new ResourceValidationError(keyAndMessage[0])); } } Add(key, errorsToAdd); } } } } }
(2)Action中添加自定义验证:
public async Task<IActionResult> Post([FromBody] PostAddResource postAddResource) { if (!ModelState.IsValid) { return new MyUnprocessableEntityObjectResult(ModelState); } }
(3)测试
三、Delete
1、 在PostRepository 添加Delete方法
public void Delete(Post post) { _myContext.Posts.Remove(post); }
2、Action添加Delete方法:
[HttpDelete("{id}", Name = "DeletePost")] public async Task<IActionResult> DeletePost(int id) { var post = await _postRepository.GetPostId(id); if (post == null) { return NotFound(); } _postRepository.Delete(post); if (!await _unitOfWork.SaveAsync()) { throw new Exception($"Deleting post {id} failed when saving."); } return NoContent(); }
四、PUT(整体更新)
1、添加PostUpdateResource.cs 类;
public class PostUpdateResource : PostAddOrUpdateResource { }
2、重构 PostAddOrUpdateResourceValidator.cs ,改成泛型
namespace BlogDemo.Infrastructure.Resources { public class PostAddOrUpdateResourceValidator<T> : AbstractValidator<T> where T: PostAddOrUpdateResource { public PostAddOrUpdateResourceValidator() { RuleFor(x => x.Title).NotEmpty() .WithName("标题").WithMessage("required|{PropertyName}是必须填写的") .MaximumLength(50).WithMessage("maxLength|{PropertyName}的最大长度是{MaxLength}"); RuleFor(x => x.Body).NotEmpty() .WithName("正文").WithMessage("required|{PropertyName}是必须填写的") .MinimumLength(10).WithMessage("minLength|{PropertyName}的最小长度是{MinLength}"); } } }
3、资源验证注册:
services.AddTransient<IValidator<PostUpdateResource>, PostAddOrUpdateResourceValidator<PostUpdateResource>>();
4、添加Map
namespace BlogDemo.Api.Extensions { public class MappingProfile:Profile { public MappingProfile() { CreateMap<Post, PostDTO>().ForMember(dest=>dest.Updatetime,opt=>opt.MapFrom(src=>src.LastModified)); CreateMap<PostDTO, Post>(); CreateMap<PostAddResource, Post>(); CreateMap<PostUpdateResource, Post>(); } } }
5、添加自定义hateoas
public void ConfigureServices(IServiceCollection services) { services.AddMvc(option => { option.ReturnHttpNotAcceptable = true; // option.OutputFormatters.Add(new XmlDataContractSerializerOutputFormatter()); var outputFormatter = option.OutputFormatters.OfType<JsonOutputFormatter>().FirstOrDefault(); if (outputFormatter != null) { outputFormatter.SupportedMediaTypes.Add("application/vnd.cgzl.hateoas+json"); } var intputFormatter = option.InputFormatters.OfType<JsonInputFormatter>().FirstOrDefault(); if (intputFormatter != null) { intputFormatter.SupportedMediaTypes.Add("application/vnd.cgzl.post.create+json"); intputFormatter.SupportedMediaTypes.Add("application/vnd.cgzl.post.update+json"); } })
}
6、Action添加PUT方法:
[HttpPut("{id}", Name = "UpdatePost")] [RequestHeaderMatchingMediaType("Content-Type", new[] { "application/vnd.cgzl.post.update+json" })] public async Task<IActionResult> UpdatePost(int id, [FromBody] PostUpdateResource postUpdate) { if (postUpdate == null) { return BadRequest(); } if (!ModelState.IsValid) { return new MyUnprocessableEntityObjectResult(ModelState); } var post = await _postRepository.GetPostId(id); if (post == null) { return NotFound(); } post.LastModified = DateTime.Now; _mapper.Map(postUpdate, post); if (!await _unitOfWork.SaveAsync()) { throw new Exception($"Updating post {id} failed when saving."); } return NoContent(); }
7、测试
五、Patch(局部更新)
1、 PostRepository 添加Update方法:
public void Update(Post post) { _myContext.Entry(post).State = EntityState.Modified; }
2、Action添加Patch方法:
[HttpPatch("{id}", Name = "PartiallyUpdatePost")] public async Task<IActionResult> PartiallyUpdateCityForCountry(int id, [FromBody] JsonPatchDocument<PostUpdateResource> patchDoc) { if (patchDoc == null) { return BadRequest(); } var post = await _postRepository.GetPostByIdAsync(id); if (post == null) { return NotFound(); } var postToPatch = _mapper.Map<PostUpdateResource>(post); patchDoc.ApplyTo(postToPatch, ModelState); TryValidateModel(postToPatch); if (!ModelState.IsValid) { return new MyUnprocessableEntityObjectResult(ModelState); } _mapper.Map(postToPatch, post); post.LastModified = DateTime.Now; _postRepository.Update(post); if (!await _unitOfWork.SaveAsync()) { throw new Exception($"Patching city {id} failed when saving."); } return NoContent(); }
3、测试
六、HTTP常用方法总结