• 使用静态基类方案让 ASP.NET Core 实现遵循 HATEOAS Restful Web API


     

    Hypermedia As The Engine Of Application State (HATEOAS)

    HATEOAS(Hypermedia as the engine of application state)是 REST 架构风格中最复杂的约束,也是构建成熟 REST 服务的核心。它的重要性在于打破了客户端和服务器之间严格的契约,使得客户端可以更加智能和自适应,而 REST 服务本身的演化和更新也变得更加容易。

    HATEOAS的优点有:

    具有可进化性并且能自我描述

    超媒体(Hypermedia, 例如超链接)驱动如何消费和使用API, 它告诉客户端如何使用API, 如何与API交互, 例如: 如何删除资源, 更新资源, 创建资源, 如何访问下一页资源等等. 

    例如下面就是一个不使用HATEOAS的响应例子:

    {
        "id" : 1,
        "body" : "My first blog post",
        "postdate" : "2015-05-30T21:41:12.650Z"
    }

    如果不使用HATEOAS的话, 可能会有这些问题:

    • 客户端更多的需要了解API内在逻辑
    • 如果API发生了一点变化(添加了额外的规则, 改变规则)都会破坏API的消费者.
    • API无法独立于消费它的应用进行进化.

    如果使用HATEOAS:

    {
        "id" : 1,
        "body" : "My first blog post",
        "postdate" : "2015-05-30T21:41:12.650Z",
        "links" : [
            {
                "rel" : "self",
                "href" : http://blog.example.com/posts/{id},
                "method" : "GET"
            },
         {
           "rel": "update-blog",
           "href": http://blog.example.com/posts/{id},
           "method" "PUT"
    }
    .... ] }

    这个response里面包含了若干link, 第一个link包含着获取当前响应的链接, 第二个link则告诉客户端如何去更新该post.

    Roy Fielding的一句名言: "如果在部署的时候客户端把它们的控件都嵌入到了设计中, 那么它们就无法获得可进化性, 控件必须可以实时的被发现. 这就是超媒体能做到的." ????

    比如说针对上面的例子, 我可以在不改变响应主体结果的情况下添加另外一个删除的功能(link), 客户端通过响应里的links就会发现这个删除功能, 但是对其他部分都没有影响.

    所以说HTTP协议还是很支持HATEOAS的:

    如果你仔细想一下, 这就是我们平时浏览网页的方式. 浏览网站的时候, 我们并不关心网页里面的超链接地址是否变化了, 只要知道超链接是干什么就可以.

    我们可以点击超链接进行跳转, 也可以提交表单, 这就是超媒体驱动应用程序(浏览器)状态的例子.

    如果服务器决定改变超链接的地址, 客户端程序(浏览器)并不会因为这个改变而发生故障, 这就浏览器使用超媒体响应来告诉我们下一步该怎么做.

    那么怎么展示这些link呢? 

    JSON和XML并没有如何展示link的概念. 但是HTML却知道, anchor元素: 

    <a href="uri" rel="type"  type="media type">

    href包含了URI

    rel则描述了link如何和资源的关系

    type是可选的, 它表示了媒体的类型

    为了支持HATEOAS, 这些形式就很有用了:

    {
        ...
        "links" : [
            {
                "rel" : "self",
                "href" : http://blog.example.com/posts/{id},
                "method" : "GET"
            }
            ....
        ] 
    }

    method: 定义了需要使用的方法

    rel: 表明了动作的类型

    href: 包含了执行这个动作所包含的URI.

    为了让ASP.NET Core Web API 支持HATEOAS, 得需要自己手动编写代码实现. 有两种办法:

    静态类型方案: 需要基类(包含link)和包装类, 也就是返回的资源的ViewModel里面都含有link, 通过继承于同一个基类来实现.

    动态类型方案: 需要使用例如匿名类或ExpandoObject等, 对于单个资源可以使用ExpandoObject, 而对于集合类资源则使用匿名类.

    这一篇文章介绍如何实施第一种方案 -- 静态类型方案

    首先需要准备一个asp.net core 2.0 web api的项目. 项目搭建的过程就不介绍了, 我的很多文章里都有介绍.

    下面开始建立Domain Model -- Vehicle.cs:

    using SalesApi.Core.Abstractions.DomainModels;
    
    namespace SalesApi.Core.DomainModels
    {
        public class Vehicle: EntityBase
        {
            public string Model { get; set; }
            public string Owner { get; set; }
        }
    }

    这里的父类EntityBase是我的项目特有的, 您可能不需要.

    然后为这个类添加约束(数据库映射的字段长度, 必填等等) VehicleConfiguration.cs:

    using Microsoft.EntityFrameworkCore.Metadata.Builders;
    using SalesApi.Core.Abstractions.DomainModels;
    
    namespace SalesApi.Core.DomainModels
    {
        public class VehicleConfiguration : EntityBaseConfiguration<Vehicle>
        {
            public override void ConfigureDerived(EntityTypeBuilder<Vehicle> b)
            {
                b.Property(x => x.Model).IsRequired().HasMaxLength(50);
                b.Property(x => x.Owner).IsRequired().HasMaxLength(50);
            }
        }
    }

    然后把Vehicle添加到SalesContext.cs:

    using Microsoft.EntityFrameworkCore;
    using SalesApi.Core.Abstractions.Data;
    using SalesApi.Core.DomainModels;
    
    namespace SalesApi.Core.Contexts
    {
        public class SalesContext : DbContextBase
        {
            public SalesContext(DbContextOptions<SalesContext> options)
                : base(options)
            {
            }
    
            protected override void OnModelCreating(ModelBuilder modelBuilder)
            {
                base.OnModelCreating(modelBuilder);
                modelBuilder.ApplyConfiguration(new ProductConfiguration());
                modelBuilder.ApplyConfiguration(new VehicleConfiguration());
                modelBuilder.ApplyConfiguration(new CustomerConfiguration());
            }
    
            public DbSet<Product> Products { get; set; }
            public DbSet<Vehicle> Vehicles { get; set; }
            public DbSet<Customer> Customers { get; set; }
        }
    }

    建立IVehicleRepository.cs:

    using SalesApi.Core.Abstractions.Data;
    using SalesApi.Core.DomainModels;
    
    namespace SalesApi.Core.IRepositories
    {
        public interface IVehicleRepository: IEntityBaseRepository<Vehicle>
        {
            
        }
    }

    这里面的IEntityBaseRepository也是我项目里面的类, 您可以没有.

    然后实现这个VehicleRepository.cs:

    using SalesApi.Core.Abstractions.Data;
    using SalesApi.Core.DomainModels;
    using SalesApi.Core.IRepositories;
    
    namespace SalesApi.Repositories
    {
        public class VehicleRepository : EntityBaseRepository<Vehicle>, IVehicleRepository
        {
            public VehicleRepository(IUnitOfWork unitOfWork) : base(unitOfWork)
            {
            }
        }
    }

    具体的实现是在我的泛型父类里面了, 所以这里没有代码, 您可能需要实现一下.

    然后是重要的部分:

    建立一个LinkViewMode.cs 用其表示超链接:

    namespace SalesApi.Core.Abstractions.Hateoas
    {
        public class LinkViewModel
        {
            public LinkViewModel(string href, string rel, string method)
            {
                Href = href;
                Rel = rel;
                Method = method;
            }
            
            public string Href { get; set; }
            public string Rel { get; set; }
            public string Method { get; set; }
        }
    }

    里面的三个属性正好就是超链接的三个属性.

    然后建立LinkedResourceBaseViewModel.cs, 它将作为ViewModel的父类:

    using System.Collections.Generic;
    using SalesApi.Core.Abstractions.DomainModels;
    
    namespace SalesApi.Core.Abstractions.Hateoas
    {
        public abstract class LinkedResourceBaseViewModel: EntityBase
        {
            public List<LinkViewModel> Links { get; set; } = new List<LinkViewModel>();
        }
    }

    这样一个ViewModel就可以包含多个link了.

    然后就可以建立VehicleViewModel了:

    using SalesApi.Core.Abstractions.DomainModels;
    using SalesApi.Core.Abstractions.Hateoas;
    
    namespace SalesApi.ViewModels
    {
        public class VehicleViewModel: LinkedResourceBaseViewModel
        {
            public string Model { get; set; }
            public string Owner { get; set; }
        }
    }

    注册Repository:

    services.AddScoped<IVehicleRepository, VehicleRepository>();

    注册Model/ViewModel到AutoMapper:

    CreateMap<Vehicle, VehicleViewModel>();
    
    CreateMap<VehicleViewModel, Vehicle>();

    建立VehicleController.cs:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Threading.Tasks;
    using Microsoft.AspNetCore.Authorization;
    using Microsoft.AspNetCore.JsonPatch;
    using Microsoft.AspNetCore.Mvc;
    using Microsoft.EntityFrameworkCore;
    using SalesApi.Core.Abstractions.Hateoas;
    using SalesApi.Core.DomainModels;
    using SalesApi.Core.IRepositories;
    using SalesApi.Core.Services;
    using SalesApi.Shared.Enums;
    using SalesApi.ViewModels;
    using SalesApi.Web.Controllers.Bases;
    
    namespace SalesApi.Web.Controllers
    {
        [AllowAnonymous]
        [Route("api/sales/[controller]")]
        public class VehicleController : SalesBaseController<VehicleController>
        {
            private readonly IVehicleRepository _vehicleRepository;
            private readonly IUrlHelper _urlHelper;
    
            public VehicleController(
                ICoreService<VehicleController> coreService,
                IVehicleRepository vehicleRepository,
                IUrlHelper urlHelper) : base(coreService)
            {
                _vehicleRepository = vehicleRepository;
                this._urlHelper = urlHelper;
            }
    
            [HttpGet]
            [Route("{id}", Name = "GetVehicle")]
            public async Task<IActionResult> Get(int id)
            {
                var item = await _vehicleRepository.GetSingleAsync(id);
                if (item == null)
                {
                    return NotFound();
                }
                var vehicleVm = Mapper.Map<VehicleViewModel>(item);
                return Ok(CreateLinksForVehicle(vehicleVm));
            }
    
            [HttpPost]
            public async Task<IActionResult> Post([FromBody] VehicleViewModel vehicleVm)
            {
                if (vehicleVm == null)
                {
                    return BadRequest();
                }
    
                if (!ModelState.IsValid)
                {
                    return BadRequest(ModelState);
                }
    
                var newItem = Mapper.Map<Vehicle>(vehicleVm);
                _vehicleRepository.Add(newItem);
                if (!await UnitOfWork.SaveAsync())
                {
                    return StatusCode(500, "保存时出错");
                }
    
                var vm = Mapper.Map<VehicleViewModel>(newItem);
    
                return CreatedAtRoute("GetVehicle", new { id = vm.Id }, CreateLinksForVehicle(vm));
            }
    
            [HttpPut("{id}", Name = "UpdateVehicle")]
            public async Task<IActionResult> Put(int id, [FromBody] VehicleViewModel vehicleVm)
            {
                if (vehicleVm == null)
                {
                    return BadRequest();
                }
    
                if (!ModelState.IsValid)
                {
                    return BadRequest(ModelState);
                }
                var dbItem = await _vehicleRepository.GetSingleAsync(id);
                if (dbItem == null)
                {
                    return NotFound();
                }
                Mapper.Map(vehicleVm, dbItem);
                _vehicleRepository.Update(dbItem);
                if (!await UnitOfWork.SaveAsync())
                {
                    return StatusCode(500, "保存时出错");
                }
    
                return NoContent();
            }
    
            [HttpPatch("{id}", Name = "PartiallyUpdateVehicle")]
            public async Task<IActionResult> Patch(int id, [FromBody] JsonPatchDocument<VehicleViewModel> patchDoc)
            {
                if (patchDoc == null)
                {
                    return BadRequest();
                }
                var dbItem = await _vehicleRepository.GetSingleAsync(id);
                if (dbItem == null)
                {
                    return NotFound();
                }
                var toPatchVm = Mapper.Map<VehicleViewModel>(dbItem);
                patchDoc.ApplyTo(toPatchVm, ModelState);
    
                TryValidateModel(toPatchVm);
                if (!ModelState.IsValid)
                {
                    return BadRequest(ModelState);
                }
    
                Mapper.Map(toPatchVm, dbItem);
    
                if (!await UnitOfWork.SaveAsync())
                {
                    return StatusCode(500, "更新时出错");
                }
    
                return NoContent();
            }
    
            [HttpDelete("{id}", Name = "DeleteVehicle")]
            public async Task<IActionResult> Delete(int id)
            {
                var model = await _vehicleRepository.GetSingleAsync(id);
                if (model == null)
                {
                    return NotFound();
                }
                _vehicleRepository.Delete(model);
                if (!await UnitOfWork.SaveAsync())
                {
                    return StatusCode(500, "删除时出错");
                }
                return NoContent();
            }
    private VehicleViewModel CreateLinksForVehicle(VehicleViewModel vehicle)
            {
                vehicle.Links.Add(
                    new LinkViewModel(
                        href: _urlHelper.Link("GetVehicle", new { id = vehicle.Id }),
                        rel: "self",
                        method: "GET"));
    
                vehicle.Links.Add(
                    new LinkViewModel(
                        href: _urlHelper.Link("UpdateVehicle", new { id = vehicle.Id }),
                        rel: "update_vehicle",
                        method: "PUT"));
    
                vehicle.Links.Add(
                new LinkViewModel(
                    href: _urlHelper.Link("PartiallyUpdateVehicle", new { id = vehicle.Id }),
                    rel: "partially_update_vehicle",
                    method: "PATCH"));
    
                vehicle.Links.Add(
                new LinkViewModel(
                    href: _urlHelper.Link("DeleteVehicle", new { id = vehicle.Id }),
                    rel: "delete_vehicle",
                    method: "DELETE"));
    
                return vehicle;
            }
        }
    }

    在Controller里, 查询方法返回的都是ViewModel, 我们需要为ViewModel生成Links, 所以我建立了CreateLinksForVehicle方法来做这件事.

    假设客户通过API得到一个Vehicle的时候, 它可能会需要得到修改(整体修改和部分修改)这个Vehicle的链接以及删除这个Vehicle的链接. 所以我把这两个链接放进去了, 当然别忘了还有本身的链接也一定要放进去, 放在最前边.

    这里我使用了IURLHelper, 它会通过Action的名字来定位Action, 所以我把相应Action都赋上了Name属性.

    在ASP.NET Core 2.0里面使用IUrlHelper需要在Startup里面注册:

                services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
                services.AddSingleton<IActionContextAccessor, ActionContextAccessor>();
                services.AddScoped<IUrlHelper>(factory =>
                {
                    var actionContext = factory.GetService<IActionContextAccessor>()
                                            .ActionContext;
                    return new UrlHelper(actionContext);
                });

    最后, 在调用Get和Post方法返回的时候使用CreateLinksForVehicle方法对要返回的VehicleViewModel进行包装, 生成links.

    下面我们可以使用POSTMAN来测试一下效果:

    首先添加一笔数据:

    返回结果:

    没问题, 这就是我想要的效果.

    然后看一下GET:

    也没问题.

    针对集合类返回结果

    上面的例子都是返回单笔数据, 如果返回集合类的数据, 我当然可以遍历集合里的每一个数据, 然后做CreateLinksForVehicle. 但是这样就无法添加这个GET集合Action本身的link了. 所以针对集合类结果需要再做一个父类.

    LinkedCollectionResourceWrapperViewModel.cs:
    using System.Collections.Generic;
    
    namespace SalesApi.Core.Abstractions.Hateoas
    {
        public class LinkedCollectionResourceWrapperViewModel<T> : LinkedResourceBaseViewModel
            where T : LinkedResourceBaseViewModel
        {
            public LinkedCollectionResourceWrapperViewModel(IEnumerable<T> value)
            {
                Value = value;
            }
    
            public IEnumerable<T> Value { get; set; }
        }
    }

    这里, 我把集合数据包装到了这个类的value属性里.

    然后在Controller里面添加另外一个方法:

            private LinkedCollectionResourceWrapperViewModel<VehicleViewModel> CreateLinksForVehicle(LinkedCollectionResourceWrapperViewModel<VehicleViewModel> vehiclesWrapper)
            {
                vehiclesWrapper.Links.Add(
                    new LinkViewModel(_urlHelper.Link("GetAllVehicles", new { }),
                    "self",
                    "GET"
                ));
    
                return vehiclesWrapper;
            }

    然后针对集合查询的ACTION我这样修改:

            [HttpGet(Name = "GetAllVehicles")]
            public async Task<IActionResult> GetAll()
            {
                var items = await _vehicleRepository.All.ToListAsync();
                var results = Mapper.Map<IEnumerable<VehicleViewModel>>(items);
                results = results.Select(CreateLinksForVehicle);
                var wrapper = new LinkedCollectionResourceWrapperViewModel<VehicleViewModel>(results);
                return Ok(CreateLinksForVehicle(wrapper));
            }

    这里主要有三项工作:

    1. 通过results.Select(x => CreateLinksForVehicle(x)) 对集合的每个元素添加links.
    2. 然后把集合用上面刚刚建立的父类进行包装
    3. 使用刚刚建立的CrateLinksForVehicle重载方法对这个包装的集合添加本身的link.

    最后看看效果:

    嗯, 没问题. 

    这是第一种实现HATEOAS的方案, 另外一种等我稍微研究下再写.

  • 相关阅读:
    .net Core 使用AutoMapper
    文件批量生成IO流读写
    .net Core数据的幕等性
    .net core 拦截器的使用
    墙上你APP设计与实现
    H5 App实现热更新,不需要重新安装app
    支付宝支付接口的使用详细说明
    .net 数据源DataSet 转换成模型
    .net ajax跨域请求问题
    【系统之音】SystemUI篇(二)SysytemUI功能一览--草稿
  • 原文地址:https://www.cnblogs.com/cgzl/p/8726805.html
Copyright © 2020-2023  润新知