ASP .NET Core Web API_12_ POST PUT PATCH DELETE

安全性&幂等性

  • 安全性☞方法执行后并不会改变资源的表述
  • 幂等性☞方法无论执行多少次都会得到同样的结果


POST 添加资源

不安全,不幂等

  • 参数[FromBody]
  • 返回201 Created
    * CreatedAtRoute(): 它允许响应里带着LocationHeader,其中包含着一个URI,通过这个URI就可以GET到我们刚刚创建好的资源。
  • HATEOAS
  1. PostAddResource
  public class PostAddResource
    {
        public string Title { get; set; }
        public string Body { get; set; }
     }
  1. MappingProfile
 public MappingProfile()
        {
            CreateMap<Post, PostResource>()
                .ForMember(dest => dest.UpdateTime, opt => opt.MapFrom(src => src.LastModified));
            CreateMap<PostResource, Post>();

            CreateMap<PostAddResource,Post>();

        }
  1. 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消费者
  1. 内置验证:
    • DataAnnotation


    • ValidationAttribute
    • IValidatebleObject
  2. 第三方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>>
  1. 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}");
    }
}
  1. 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; }
    }
  1. 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);
                }
            }
        }
    }
  1. MyUnprocessableEntityObjectResult
 public class MyUnprocessableEntityObjectResult : UnprocessableEntityObjectResult
    {
        public MyUnprocessableEntityObjectResult(ModelStateDictionary modelState) : base(new ResourceValidationResult(modelState))
        {
            if (modelState == null)
            {
                throw new ArgumentNullException(nameof(modelState));
            }
            StatusCode = 422;
        }
    }
  1. 使用
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
  • 不安全
  • 幂等
  • 整体更新 容易引起问题
  • 集合资源整体更新
  1. 抽象父类
public class PostAddOrUpdateResource
{
        public string Title { get; set; }
        public string Body { get; set; }
}
  1. 继承
public class PostUpdateResource:PostAddOrUpdateResource
{
}
  1. 修改FluentValidator
 public class PostAddOrUpdateResourceValidator<T>:AbstractValidator<T> where  T:PostAddOrUpdateResource
{
  ......
}
  1. 修改注册
 //注册FluentValidator
 services.AddTransient<IValidator<PostAddResource>, PostAddOrUpdateResourceValidator<PostAddResource>>();
 services.AddTransient<IValidator<PostUpdateResource>, PostAddOrUpdateResourceValidator<PostUpdateResource>>();
  1. 添加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
  • 不安全
  • 不幂等
  1. Repository中添加Update方法
 public void UpdatePost(Post post)
        {
            _applicationContext.Entry(post).State = EntityState.Modified;
        }
  1. 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

总结

Http常用方法
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335

推荐阅读更多精彩内容

  • 前言 HTTP Method的历史: HTTP 0.9 这个版本只有GET方法 HTTP 1.0 这个版本有G...
    老马的春天阅读 25,886评论 3 9
  • 什么是REST REST 是 Representational State Transfer 的缩写. 它是一种架...
    xtddw阅读 655评论 0 0
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,497评论 18 139
  • 一说到REST,我想大家的第一反应就是“啊,就是那种前后台通信方式。”但是在要求详细讲述它所提出的各个约束,以及如...
    时待吾阅读 3,394评论 0 19
  • API定义规范 本规范设计基于如下使用场景: 请求频率不是非常高:如果产品的使用周期内请求频率非常高,建议使用双通...
    有涯逐无涯阅读 2,509评论 0 6