安全性&幂等性
- 安全性☞方法执行后并不会改变资源的表述
-
幂等性☞方法无论执行多少次都会得到同样的结果
POST 添加资源
不安全,不幂等
- 参数[FromBody]
- 返回201 Created
* CreatedAtRoute(): 它允许响应里带着LocationHeader,其中包含着一个URI,通过这个URI就可以GET到我们刚刚创建好的资源。 - HATEOAS
- PostAddResource
public class PostAddResource
{
public string Title { get; set; }
public string Body { get; set; }
}
- MappingProfile
public MappingProfile()
{
CreateMap<Post, PostResource>()
.ForMember(dest => dest.UpdateTime, opt => opt.MapFrom(src => src.LastModified));
CreateMap<PostResource, Post>();
CreateMap<PostAddResource,Post>();
}
- Action中Post方法
[HttpPost(Name ="CreatePost")]
public async Task<IActionResult> Post([FromBody] PostAddResource postAddResource)
{
if (postAddResource == null)
{
return BadRequest("not data!");
}
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 post data Failed!");
}
var resultResource = _mapper.Map<Post, PostResource>(newPost);
//HATEOAS
var links = CreateLinksForPost(newPost.Id);
var linkedPostResource = resultResource.ToDynamic() as IDictionary<string, object>;
linkedPostResource.Add("links", links);
//return Ok(resultResource);//200
return CreatedAtRoute("GetPost",new { id = linkedPostResource["Id"] },linkedPostResource); //201
}
Model 验证
- 定义验证规则
- 检查验证规则
- 把验证错误信息发送给API消费者
- 内置验证:
-
DataAnnotation
- ValidationAttribute
- IValidatebleObject
-
- 第三方
FluentValidation
- 关注点分离(SOC,Seperation of Concerns)
- 安装包
* FluentValidation
* FluentValidation.AspNetCore - 为每一个Resource建立验证器
-
继承AbstractValidator<T>
-
public class PostAddResourceValidator:AbstractValidator<PostAddResource>
{
public PostAddResourceValidator()
{
RuleFor(x => x.Title)
.NotNull()
.WithName("标题")
.WithMessage("{PropertyName}是必填的")
.MaximumLength(50)
.WithMessage("{PropertyName}的最大长度是{MaxLength}");
RuleFor(x => x.Body)
.NotNull()
.WithName("正文")
.WithMessage("{PropertyName}是必填的")
.MinimumLength(50)
.WithMessage("{PropertyName}的最小长度是{MaxLength}");
}
}
- 配置
//注册FluentValidator
services.AddTransient<IValidator<PostAddResource>, PostAddResourceValidator>();
services.AddMvc(
options =>
{
options.ReturnHttpNotAcceptable = true; //开启406
//options.OutputFormatters.Add(new XmlDataContractSerializerOutputFormatter());
//自定义mediaType
var outputFormatter = options.OutputFormatters.OfType<JsonOutputFormatter>().FirstOrDefault();
if (outputFormatter != null)
{
outputFormatter.SupportedMediaTypes.Add("application/vnd.enfi.hateoas+json");
}
})
.AddJsonOptions(options =>
{
options.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
})
.AddFluentValidation();
- 验证
- ModelStatus.IsValid
- ModelState
- 字典,包含Model的状态以及Model所绑定的验证
- 对于提交的每个属性,它都包含了一个错误信息的集合
- 返回:422 UnprocessableEntity
- 验证错误信息在响应的body里面带回去
if (!ModelState.IsValid)
{
return UnprocessableEntity(ModelState);
}
- MediaType
var inputFormatter = options.InputFormatters.OfType<JsonInputFormatter>().FirstOrDefault();
if (inputFormatter!=null)
{
inputFormatter.SupportedMediaTypes.Add("application/vnd.enfi.post.create+json");
}
[HttpPost(Name ="CreatePost")]
[RequestHeaderMatchingMediaType("Content-Type", new[] { "application/vnd.enfi.post.create+json" })]
[RequestHeaderMatchingMediaType("Accept", new[] { "application/vnd.enfi.hateoas+json" })]
public async Task<IActionResult> Post([FromBody] PostAddResource postAddResource)
{
...
}
Headers
Body
Repson Body
POST 一次性添加集合资源
- 把整个集合看作一种资源
- 参数[FromBody]IEnumerable<T>
- 返回201,CreatedAtRoute(),带着ID的集合
- GET方法参数为ID的集合,用于查询创建的集合资源
- ArrayModelBinder:IModelBinder
自定义验证错误返回结果
- 满足Angular客户端表单验证要求:
- 错误的类型:required,maxLength ...
- MyUnprocessableEntityObjectResult
- 继承:ObjectResult
- ResourceValidationResult:Dictionary<string,IEnumerable<ResourceValidationError>>
- PostAddResourceValidator
public class PostAddResourceValidator:AbstractValidator<PostAddResource>
{
public PostAddOrUpdateResourceValidator()
{
RuleFor(x => x.Title)
.NotNull()
.WithName("标题")
.WithMessage("required|{propertyName}是必填的")
.MaximumLength(50)
.WithMessage("maxlength|{PropertyName}的最大长度是{MaxLength}");
RuleFor(x => x.Body)
.NotNull()
.WithName("正文")
.WithMessage("required|{PropertyName}是必填的")
.MinimumLength(50)
.WithMessage("minlength|{PropertyName}的最小长度是{MinLength}");
}
}
- ResourceValidationError
public class ResourceValidationError
{
public ResourceValidationError(string message,string validatorKey ="")
{
Message = message;
ValidatorKey = validatorKey;
}
public string Message { get; private set; }
public string ValidatorKey { get; private set; }
}
- ResourceValidationResult
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);
}
}
}
}
- MyUnprocessableEntityObjectResult
public class MyUnprocessableEntityObjectResult : UnprocessableEntityObjectResult
{
public MyUnprocessableEntityObjectResult(ModelStateDictionary modelState) : base(new ResourceValidationResult(modelState))
{
if (modelState == null)
{
throw new ArgumentNullException(nameof(modelState));
}
StatusCode = 422;
}
}
- 使用
if (!ModelState.IsValid)
{
return new MyUnprocessableEntityObjectResult(ModelState);
//return UnprocessableEntity(ModelState);
}
满足Angular响应要求
DELETE
- 参数:
ID
- 返回:
204 No Content
- 不安全
- 幂等:多次请求的副作用和单次请求的副作用是一样的,每次发送DELETE请求后,服务器的状态是一样的
[HttpDelete("{id}",Name ="DeletePost")]
public async Task<IActionResult> DeletePost(int id)
{
var post = await _postRepository.GetPostByIdAsync(id);
if (post ==null)
{
return NotFound();
}
_postRepository.DeletePost(post);
if (!await _unitOfWork.SaveAsync())
{
throw new Exception($"Deleting post {id} failed when saving.");
}
return NoContent();
}
PUT 整体更新
- 参数:
ID
[FromBody]不需要ID属性- 单独的Resource Model.
- 返回:
204 No Content
202 OK
- 不安全
- 幂等
- 整体更新 容易引起问题
- 集合资源整体更新
- 抽象父类
public class PostAddOrUpdateResource
{
public string Title { get; set; }
public string Body { get; set; }
}
- 继承
public class PostUpdateResource:PostAddOrUpdateResource
{
}
- 修改FluentValidator
public class PostAddOrUpdateResourceValidator<T>:AbstractValidator<T> where T:PostAddOrUpdateResource
{
......
}
- 修改注册
//注册FluentValidator
services.AddTransient<IValidator<PostAddResource>, PostAddOrUpdateResourceValidator<PostAddResource>>();
services.AddTransient<IValidator<PostUpdateResource>, PostAddOrUpdateResourceValidator<PostUpdateResource>>();
- 添加mappingProfile
CreateMap<PostUpdateResource,Post>();
6.Action>>>Post
[HttpPut("{id}",Name ="UpdatePost")]
//注意要在mvc中注册 Content-Type
[RequestHeaderMatchingMediaType("Content-Type", new[] { "application/vnd.enfi.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.GetPostByIdAsync(id);
if (post == null)
{
return NotFound("Cannot found the data for update.");
}
post.LastModified = DateTime.Now;
_mapper.Map(postUpdate, post);
if (!await _unitOfWork.SaveAsync())
{
throw new Exception($"Deleting post {id} failed when updating.");
}
return NoContent();
}
PATCH 局部更新
-
application/json-patch+json
PATCH - 参数:
ID
[FromBody] JsonPatchDocument<T> - patchDoc.ApplyTo()
- 返回:
204 No Content
202 OK
- 不安全
- 不幂等
- Repository中添加Update方法
public void UpdatePost(Post post)
{
_applicationContext.Entry(post).State = EntityState.Modified;
}
- Action 中添加Update方法
[HttpPatch("{id}",Name ="PartiallyUpdatePost")]
public async Task<IActionResult> PartiallyUpdatePost(int id,[FromBody] JsonPatchDocument<PostUpdateResource> pathDoc)
{
if (pathDoc ==null)
{
return BadRequest();
}
var post = await _postRepository.GetPostByIdAsync(id);
if (post ==null)
{
return NotFound("Cannot found the data for update.");
}
var postToPatch = _mapper.Map<PostUpdateResource>(post);
pathDoc.ApplyTo(postToPatch, ModelState);
TryValidateModel(postToPatch);
if (!ModelState.IsValid)
{
return new MyUnprocessableEntityObjectResult(ModelState);
}
_mapper.Map(postToPatch, post);
post.LastModified = DateTime.Now;
_postRepository.UpdatePost(post);
if (!await _unitOfWork.SaveAsync())
{
throw new Exception($"post {id} failed when partially updating patch.");
}
return NoContent();
}
Headers
Body
网友评论