从头编写 asp.net core 2.0 web api 基础框架

从头编写 asp.net core 2.0 web api 基础框架

  • 商品编号:
    #07385608_073
    • 原价:
      免费
    • 会员价:
      免费
  • 分类:
    • V1.0.0
  • 数量:
    限购1件

购物车中已存在此商品(限购一件),单击跳转购物车

  • 开发者:草根专栏
  • 开发语言:C#
  • 开发环境:Visual Studio
  • 数据库:sqlserver
  • 商品架构:B/S
  • 代码管理工具:github
  • 大小(M):0 M
  • 编码格式:utf-8
  • 是否开源:是
  • 开源协议:BSD

上一篇是: http://www.cnblogs.com/cgzl/p/7637250.html

本文讲的是里面的Step 2.

上一次, 我们使用asp.net core 2.0 建立了一个Empty project, 然后做了一些基本的配置, 并建立了两个Controller, 写了一些查询方法.

下面我们继续:

POST

POST一般用来表示创建资源, 也就是新增.

先看看Model, 其中的Id属性, 一般是创建的时候服务器自动生成的, 所以如果客户端在进行Post(创建)的时候, 它是不会提供Id属性的.

    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: 

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

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

其实也可以使用同一个Model来做所有的操作, 因为它们的大部分属性都是相同的, 但是,

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

创建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:

986268-20171009152823418-814183684

里面的location 这个Header, 所以客户端就知道以后想找这个数据, 就需要访问这个地址, 我们可以现在就试试:

986268-20171009153050965-2128754352

嗯. 没什么问题.

 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的参数一起返回到前台.

我们试试:

986268-20171009160247371-946158815

986268-20171009160254871-2018393900

如果通过Data Annotation的方式不能实现比较复杂验证的需求, 那就需要写代码了. 这时, 如果验证失败, 我们可以错误信息添加到ModelState里面,

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

看看运行结果: 

986268-20171009160818621-416640542

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作为参数一起传到前台.两种效果如图:

986268-20171009162551059-1734666844

986268-20171009162650059-1686059717

注意: 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; }
    }
}

然后在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();
}

然后我们用PUT进行实验单个属性修改:这对这条数据:

986268-20171009163540012-1763903752

我们修改name和price属性:

986268-20171009163602449-137910782

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

986268-20171009163627105-918611630

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().

试一下:

986268-20171009180145215-1564638402

然后查询一下:

986268-20171009180246449-1317939048

与期待的结果一样.

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

986268-20171009180452777-1377010198

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

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

986268-20171009180735793-1351009709

返回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里面.

再试一次上面的操作:

986268-20171009181420934-2019349865

这回对了.

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();

试一下:

986268-20171009181905559-2121157549

成功.

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


权利声明:本站所有商品信息、客户评价等信息是初心商城重要的数据资源,未经许可,禁止非法转载使用。 注:本站商品信息均来自初心商城,其真实性、准确性和合法性由初心商城负责。

                  初心源说明:初心商城主要为程序员提供开发基础的代码源以及成熟项目,网站中所有的商品有提供收费版本的, 也有提供免费版本的,按照大家各自不同的需求进行购买。实实在在的让程序员只用专注于自己的业务实现你的小梦想, 如果您对我们的成果表示认同并且觉得对你有所帮助我们愿意接受来自各方面的支持^_^。

                  支持:用手机扫描二维码支付

                  支付宝支持我们 微信支持我们

                  您的支持将被用于:
                  1、持续深入的上传更多更好的源代码
                  2、建立更加完善的技术社区
                  3、完善现在系统出现各种问题
                  4、购买域名和租赁服务器

                  1、交易规则

                  2、发货方式

                  1、自动:在上方保障服务中标有自动发货的商品,拍下后,将会自动收到来自卖家的商品获取(下载)链接

                  2、手动:在上方保障服务中标有手动发货的商品,拍下后,卖家会收到邮件,也可通过QQ或订单中的电话联系对方。

                  3、退款说明

                  1、描述:源码描述(含标题)与实际源码不一致的(例:描述PHP实际为ASP、描述的功能实际缺少、版本不符等)

                  2、演示:有演示站时,与实际源码小于95%一致的(但描述中有"不保证完全一样、有变化的可能性"类似显著声明的除外)

                  3、发货:手动发货源码,在卖家未发货前,已申请退款的

                  4、服务:卖家不提供安装服务或需额外收费的(但描述中有显著声明的除外)

                  5、其它:如质量方面的硬性常规问题等

                  备注:经核实符合上述任一,均支持退款,但卖家予以积极解决问题则除外。交易中的商品,卖家无法对描述进行修改!

                  4、注意事项

                  1、客户买完之后未确认收货,将不会收到下载地址和下载码,确认收货之后才能收到下载地址和下载码。

                  2、在未拍下前,双方在QQ上所商定的内容,亦可成为纠纷评判依据(商定与描述冲突时,商定为准);

                  3、在商品同时有网站演示与图片演示,且站演与图演不一致时,默认按图演作为纠纷评判依据(特别声明或有商定除外);

                  4、在没有"无任何正当退款依据"的前提下,写有"一旦售出,概不支持退款"等类似的声明,视为无效声明;

                  5、虽然交易产生纠纷的几率很小,但请尽量保留如聊天记录这样的重要信息,以防产生纠纷时出现问题不明确的情况。

                  5、交易声明

                  1、本站作为直卖平台,依据交易合同(商品描述、交易前商定的内容)来保障交易的安全及买卖双方的权益;

                  2、非平台线上交易的商品,出现任何后果均与本站无关;无论卖家以何理由要求线下交易的,请联系管理举报。

                  初心Logo

                  初心商城| 初心系列| 初心博客| 初心历史| 系统反馈

                  chuxinm.com 京ICP备16055626号 © 2016-2018 山西米立信息技术有限公司 保留所有权利
                  违法和不良信息举报电话:186-2950-9347,本网站所列数据,除特殊说明,所有数据均出自我工作室
                  本网站兼容所有主流浏览器,不支持手机自适应