【转载】从头编写 asp.net core 2.0 web api 基础框架 (2)

Github源码地址是: https://github.com/solenovex/Building-asp.net-core-2-web-api-starter-template-from-scratchjavascript

本文讲的是里面的Step 2.html

上一次, 咱们使用asp.net core 2.0 创建了一个Empty project, 而后作了一些基本的配置, 并创建了两个Controller, 写了一些查询方法.java

下面咱们继续:git

POST

POST通常用来表示建立资源, 也就是新增.github

先看看Model, 其中的Id属性, 通常是建立的时候服务器自动生成的, 因此若是客户端在进行Post(建立)的时候, 它是不会提供Id属性的.web

复制代码
    public class Product
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public float Price { get; set; }
        public ICollection<Material> Materials { get; set; }
    }
复制代码

因此, 能够这样作, 再创建一个Dto, 专门用于建立: ProductCreation.cs: json

复制代码
namespace CoreBackend.Api.Dtos
{
    public class ProductCreation
    {
        public string Name { get; set; }
        public float Price { get; set; }
    }
}
复制代码

这里去掉了Id和Materials这个导航属性.api

其实也可使用同一个Model来作全部的操做, 由于它们的大部分属性都是相同的, 可是,服务器

仍是建议针对查询, 建立, 修改, 使用单独的Model, 这样之后修改和重构会简单一些, 再说他们的验证也是不同的.app

建立Post Action

复制代码
     [Route("{id}", Name = "GetProduct")]
        public IActionResult GetProduct(int id)
        {
            var product = ProductService.Current.Products.SingleOrDefault(x => x.Id == id);
            if (product == null)
            {
                return NotFound();
            }
            return Ok(product);
        }

        [HttpPost]
        public IActionResult Post([FromBody] ProductCreation product)
        {
            if (product == null)
            {
                return BadRequest();
            }
            var maxId = ProductService.Current.Products.Max(x => x.Id);
            var newProduct = new Product
            {
                Id = ++maxId,
                Name = product.Name,
                Price = product.Price
            };
            ProductService.Current.Products.Add(newProduct);

            return CreatedAtRoute("GetProduct", new { id = newProduct.Id }, newProduct);
        }
复制代码

 

[HttpPost] 表示请求的谓词是Post. 加上Controller的Route前缀, 那么访问这个Action的地址就应该是: 'api/product'

后边也能够跟着自定义的路由地址, 例如 [HttpPost("create")], 那么这个Action的路由地址就应该是: 'api/product/create'.

[FromBody] , 请求的body里面包含着方法须要的实体数据, 方法须要把这个数据Deserialize成ProductCreation, [FromBody]就是干这些活的.

客户端程序可能会发起一个Bad的Request, 致使数据不能被Deserialize, 这时候参数product就会变成null. 因此这是一个客户端发生的错误, 程序为让客户端知道是它引发了错误, 就应该返回一个Bad Request 400 (Bad Request表示客户端引发的错误)的 Status Code.

传递进来的model类型是 ProductCreation, 而咱们最终操做的类型是Product, 因此须要进行一个Map操做, 目前仍是挨个属性写代码进行Map吧, 之后会改为Automapper.

返回 CreatedAtRoute: 对于POST, 建议的返回Status Code 是 201 (Created), 可使用CreatedAtRoute这个内置的Helper Method. 它能够返回一个带有地址Header的Response, 这个Location Header将会包含一个URI, 经过这个URI能够找到咱们新建立的实体数据. 这里就是指以前写的GetProduct(int id)这个方法. 可是这个Action必须有一个路由的名字才能够引用它, 因此在GetProduct方法上的Route这个attribute里面加上Name="GetProduct", 而后在CreatedAtRoute方法第一个参数写上这个名字就能够了, 尽管进行了引用, 可是Post方法走完的时候并不会调用GetProduct方法. CreatedAtRoute第二个参数就是对应着GetProduct的参数列表, 使用匿名类便可, 最后一个参数是咱们刚刚建立的数据实体

运行程序试验一下, 注意须要在Headers里面设置Content-Type: application/json. 结果如图:

返回的状态是201.

看一下那一堆Headers:

里面的location 这个Header, 因此客户端就知道之后想找这个数据, 就须要访问这个地址, 咱们能够如今就试试:

嗯. 没什么问题.

 Validation 验证

针对上面的Post方法,  若是请求没有Body, 参数product就会是null, 这个咱们已经判断了; 若是body里面的数据所包含的属性在product中不存在, 那么这个属性就会被忽略.

可是若是body数据的属性有问题, 好比说name没有填写, 或者name太长, 那么在执行action方法的时候就会报错, 这时候框架会自动抛出500异常, 表示是服务器的错误, 这是不对的. 这种错误是由客户端引发的, 因此须要返回400 Bad Request错误.

验证Model/实体, asp.net core 内置可使用 Data Annotations进行: 

复制代码
using System;
using System.ComponentModel.DataAnnotations;

namespace CoreBackend.Api.Dtos
{
    public class ProductCreation
    {
        [Display(Name = "产品名称")]
        [Required(ErrorMessage = "{0}是必填项")]
        // [MinLength(2, ErrorMessage = "{0}的最小长度是{1}")]
        // [MaxLength(10, ErrorMessage = "{0}的长度不能够超过{1}")]
     [StringLength(10, MinimumLength = 2, ErrorMessage = "{0}的长度应该不小于{2}, 不大于{1}")]
public string Name { get; set; } [Display(Name = "价格")] [Range(0, Double.MaxValue, ErrorMessage = "{0}的值必须大于{1}")] public float Price { get; set; } } }
复制代码

这些Data Annotation (理解为用于验证的注解), 能够在System.ComponentModel.DataAnnotation找到, 例如[Required]表示必填, [MinLength]表示最小长度, [StringLength]能够同时验证最小和最大长度, [Range]表示数值的范围等等不少.

[Display(Name="xxx")]的用处是, 给属性起一个比较友好的名字.

其余的验证注解都有一个属性叫作ErrorMessage (string), 表示若是验证失败, 就会把ErrorMessage的内容添加到错误结果里面去. 这个ErrorMessage可使用参数, {0}表示Display的Name属性, {1}表示当前注解的第一个变量, {2}表示当前注解的第二个变量.

在Controller里面添加验证逻辑:

复制代码
     [HttpPost]
        public IActionResult Post([FromBody] ProductCreation product)
        {
            if (product == null)
            {
                return BadRequest();
            }

            if (!ModelState.IsValid)
            {
                return BadRequest(ModelState);
            }

            var maxId = ProductService.Current.Products.Max(x => x.Id);
            var newProduct = new Product
            {
                Id = ++maxId,
                Name = product.Name,
                Price = product.Price
            };
            ProductService.Current.Products.Add(newProduct);

            return CreatedAtRoute("GetProduct", new { id = newProduct.Id }, newProduct);
        }
复制代码

ModelState: 是一个Dictionary, 它里面是请求提交到Action的Name和Value的对们, 一个name对应着model的一个属性, 它也包含了一个针对每一个提交的属性的错误信息的集合.

每次请求进到Action的时候, 咱们在ProductCreationModel添加的那些注解的验证, 就会被检查. 只要其中有一个验证没经过, 那么ModelState.IsValid属性就是False. 能够设置断点查看ModelState里面都有哪些东西.

若是有错误的话, 咱们能够把ModelState看成Bad Request的参数一块儿返回到前台.

咱们试试:

若是经过Data Annotation的方式不能实现比较复杂验证的需求, 那就须要写代码了. 这时, 若是验证失败, 咱们能够错误信息添加到ModelState里面,

            if (product.Name == "产品")
            {
                ModelState.AddModelError("Name", "产品的名称不能够是'产品'二字");
            }        

看看运行结果: 

Good. 

可是这种经过注解的验证方式把验证的代码和Model的代码混到了一块儿, 并非很好的Separationg of Concern, 并且同时在Model和Controller里面为Model写验证相关的代码也不太好. 

这是方式是asp.net core 内置的, 因此简单的状况下仍是能够用的. 若是需求比较复杂, 可使用FluentValidation, 之后会加入这个库.

PUT

put应该用于对model进行完整的更新. 

首先最好仍是单独为Put写一个Dto Model, 尽管属性可能都是同样的, 可是也建议这样写, 实在不想写也能够.

ProducModification.cs

复制代码
    public class ProductModification
    {
        [Display(Name = "产品名称")]
        [Required(ErrorMessage = "{0}是必填项")]
        [StringLength(10, MinimumLength = 2, ErrorMessage = "{0}的长度应该不小于{2}, 不大于{1}")]
        public string Name { get; set; }

        [Display(Name = "价格")]
        [Range(0, Double.MaxValue, ErrorMessage = "{0}的值必须大于{1}")]
        public float Price { get; set; }
    }
复制代码

而后编写Controller的方法:

复制代码
     [HttpPut("{id}")]
        public IActionResult Put(int id, [FromBody] ProductModification product)
        {
            if (product == null)
            {
                return BadRequest();
            }

            if (product.Name == "产品")
            {
                ModelState.AddModelError("Name", "产品的名称不能够是'产品'二字");
            }

            if (!ModelState.IsValid)
            {
                return BadRequest(ModelState);
            }

            var model = ProductService.Current.Products.SingleOrDefault(x => x.Id == id);
            if (model == null)
            {
                return NotFound();
            }
            model.Name = product.Name;
            model.Price = product.Price;

            // return Ok(model);
            return NoContent();
        }
复制代码

按照Http Put的约定, 须要一个id这样的参数, 用于查找现有的model.

因为Put作的是完整的更新, 因此把ProducModification整个Model做为参数.

进来以后, 进行了一套和POST一摸同样的验证, 这地方确定能够改进, 若是验证逻辑比较复杂的话, 处处写一样验证逻辑确定是很差的, 因此建议使用FluentValidation.

而后, 把ProductModification的属性都映射查询找到给Product, 这个之后用AutoMapper来映射.

返回: PUT建议返回NoContent(), 由于更新是客户端发起的, 客户端已经有了最新的值, 无需服务器再给它传递一次, 固然了, 若是有些值是在后台更新的, 那么也可使用Ok(xxx)而后把更新后的model做为参数一块儿传到前台.两种效果如图:

注意: PUT是总体更新/修改, 可是若是只想修改部分属性的时候, 咱们看看会发生什么.

首先在Product相关Dto里面再加上一个属性Description吧.

namespace CoreBackend.Api.Dtos
{
    public class Product
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public float Price { get; set; }
        public string Description { get; set; }
        public ICollection<Material> Materials { get; set; }
    }
}

namespace CoreBackend.Api.Dtos
{
    public class ProductCreation
    {
        [Display(Name = "产品名称")]
        [Required(ErrorMessage = "{0}是必填项")]
        [StringLength(10, MinimumLength = 2, ErrorMessage = "{0}的长度应该不小于{2}, 不大于{1}")]
        public string Name { get; set; }

        [Display(Name = "价格")]
        [Range(0, Double.MaxValue, ErrorMessage = "{0}的值必须大于{1}")]
        public float Price { get; set; }

        [Display(Name = "描述")]
        [MaxLength(100, ErrorMessage = "{0}的长度不能够超过{1}")]
        public string Description { get; set; }
    }
}

namespace CoreBackend.Api.Dtos
{
    public class ProductModification
    {
        [Display(Name = "产品名称")]
        [Required(ErrorMessage = "{0}是必填项")]
        [StringLength(10, MinimumLength = 2, ErrorMessage = "{0}的长度应该不小于{2}, 不大于{1}")]
        public string Name { get; set; }

        [Display(Name = "价格")]
        [Range(0, Double.MaxValue, ErrorMessage = "{0}的值必须大于{1}")]
        public float Price { get; set; }

        [Display(Name = "描述")]
        [MaxLength(100, ErrorMessage = "{0}的长度不能够超过{1}")]
        public string Description { get; set; }
    }
}
View Code

而后在POST和PUT的方法里面映射那部分, 添加上相应的代码, (若是有AutoMapper, 这不操做就不须要作了):

        [HttpPost]
        public IActionResult Post([FromBody] ProductCreation product)
        {
            if (product == null)
            {
                return BadRequest();
            }

            if (product.Name == "产品")
            {
                ModelState.AddModelError("Name", "产品的名称不能够是'产品'二字");
            }

            if (!ModelState.IsValid)
            {
                return BadRequest(ModelState);
            }

            var maxId = ProductService.Current.Products.Max(x => x.Id);
            var newProduct = new Product
            {
                Id = ++maxId,
                Name = product.Name,
                Price = product.Price,
                Description = product.Description
            };
            ProductService.Current.Products.Add(newProduct);

            return CreatedAtRoute("GetProduct", new { id = newProduct.Id }, newProduct);
        }

        [HttpPut("{id}")]
        public IActionResult Put(int id, [FromBody] ProductModification product)
        {
            if (product == null)
            {
                return BadRequest();
            }

            if (product.Name == "产品")
            {
                ModelState.AddModelError("Name", "产品的名称不能够是'产品'二字");
            }

            if (!ModelState.IsValid)
            {
                return BadRequest(ModelState);
            }

            var model = ProductService.Current.Products.SingleOrDefault(x => x.Id == id);
            if (model == null)
            {
                return NotFound();
            }
            model.Name = product.Name;
            model.Price = product.Price;
            model.Description = product.Description;
            
            return NoContent();
        }
View Code

而后咱们用PUT进行实验单个属性修改:

这对这条数据:

咱们修改name和price属性:

而后再看一下修改后的数据:

Description被设置成null. 这就是HTTP PUT标准的本意: 总体修改, 更新全部属性, 尽管你的代码可能不这么作.

Patch 部分更新

 Http Patch 就是作部分更新的, 它的Request Body应该包含须要更新的属性名 和 值, 甚至也能够包含针对这个属性要进行的相应操做.

针对Request Body这种状况, 有一个标准叫作 Json Patch RFC 6092, 它定义了一种json数据的结构 能够表示上面说的那些东西. 

Json Patch定义的操做包含替换, 复制, 移除等操做.

这对咱们的Product, 它的结构应该是这样的:

op 表示操做, replace 是指替换; path就是属性名, value就是值.

相应的Patch方法:

复制代码
        [HttpPatch("{id}")]
        public IActionResult Patch(int id, [FromBody] JsonPatchDocument<ProductModification> patchDoc)
        {
            if (patchDoc == null)
            {
                return BadRequest();
            }
            var model = ProductService.Current.Products.SingleOrDefault(x => x.Id == id);
            if (model == null)
            {
                return NotFound();
            }
            var toPatch = new ProductModification
            {
                Name = model.Name,
                Description = model.Description,
                Price = model.Price };
            patchDoc.ApplyTo(toPatch, ModelState); if (!ModelState.IsValid)
            {
                return BadRequest(ModelState); }

            model.Name = toPatch.Name;
            model.Description = toPatch.Description;
       model.Price = toPatch.Price;
return NoContent(); }
复制代码

HttpPatch, 按约定方法有一个参数id, 还有一个JsonPatchDocument类型的参数, 它的泛型应该是用于Update的Dto, 因此选择的是ProductionModification. 若是使用Product这个Dto的话, 那么它包含id属性, 而id属性是不更改的. 但若是你没有针对不一样的操做使用不一样的Dto, 那么别忘了检查传入Dto的id 要和参数id一致才行.

而后把查询出来的product转化成用于更新的ProductModification这个Dto, 而后应用于Patch Document 就是指为toPatch这个model更新那些须要更新的属性, 是使用ApplyTo方法实现的.

可是这时候可能会出错, 好比说修改一个根本不存在的属性, 也就是说客户端可能引发了错误, 这时候就须要它进行验证, 并返回Bad Request. 因此就加上ModelState这个参数. 而后进行判断便可.

而后就是和PUT同样的更新操做, 把toPatch这个Update的Dto再总体更新给model. 其实里面无论怎么实现, 只要按约定执行就好.

而后按建议, 返回NoContent().

试一下:

而后查询一下:

与期待的结果同样.

而后试一下传入一个不存在的属性:

结果显示找不到这个属性.

再试一下, ProductModification 这个model上的验证: 例如删除name这个属性的值:

返回204, 表示成功, 可是name是必填的, 因此代码还有问题.

咱们作了ModelState检查, 可是为何没有验证出来呢? 这是由于, Patch方法的Model参数是JsonPatchDocument而不是ProductModification, 上面传进去的参数对于JsonPatchDocument来讲是没有问题的.

因此咱们须要对toPatch这个model进行验证:

复制代码
[HttpPatch("{id}")]
        public IActionResult Patch(int id, [FromBody] JsonPatchDocument<ProductModification> patchDoc)
        {
            if (patchDoc == null)
            {
                return BadRequest();
            }
            var model = ProductService.Current.Products.SingleOrDefault(x => x.Id == id);
            if (model == null)
            {
                return NotFound();
            }
            var toPatch = new ProductModification
            {
                Name = model.Name,
                Description = model.Description,
                Price = model.Price
            };
            patchDoc.ApplyTo(toPatch, ModelState);

            if (!ModelState.IsValid)
            {
                return BadRequest(ModelState);
            }

            if (toPatch.Name == "产品")
            {
                ModelState.AddModelError("Name", "产品的名称不能够是'产品'二字");
            }
            TryValidateModel(toPatch);
            if (!ModelState.IsValid)
            {
                return BadRequest(ModelState); } 
            model.Name = toPatch.Name;
            model.Description = toPatch.Description;
            model.Price = toPatch.Price;

            return NoContent();
        }
复制代码

使用TryValidateModel(xxx)对model进行手动验证, 结果也会反应在ModelState里面.

再试一次上面的操做:

这回对了.

DELETE 删除

这个比较简单:

复制代码
        [HttpDelete("{id}")]
        public IActionResult Delete(int id)
        {
            var model = ProductService.Current.Products.SingleOrDefault(x => x.Id == id);
            if (model == null)
            {
                return NotFound();
            }
            ProductService.Current.Products.Remove(model);
            return NoContent();
        }
复制代码

按Http Delete约定, 参数为id, 若是操做成功就回NoContent();

试一下:

成功.

目前, CRUD最基本的操做先告一段落.

上班了比较忙了, 今天先写这些.....................................................

 

转自:http://www.cnblogs.com/cgzl/p/7640077.html

相关文章
相关标签/搜索