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();
b.Property(x => x.Owner).IsRequired().HasMaxLength();
}
}
}

然后把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(, "保存时出错");
} 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(, "保存时出错");
} 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(, "更新时出错");
} 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(, "删除时出错");
}
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的方案, 另外一种等我稍微研究下再写.

使用静态基类方案让 ASP.NET Core 实现遵循 HATEOAS Restful Web API的更多相关文章

  1. 使用Http-Repl工具测试ASP.NET Core 2.2中的Web Api项目

    今天,Visual Studio中没有内置工具来测试WEB API.使用浏览器,只能测试http GET请求.您需要使用Postman,SoapUI,Fiddler或Swagger等第三方工具来执行W ...

  2. 【ASP.NET Core】体验一下 Mini Web API

    在上一篇水文中,老周给大伙伴们简单演示了通过 Socket 编程的方式控制 MPD (在树莓派上).按照计划,老周还想给大伙伴们演示一下使用 Web API 来封装对 MPD 控制.思路很 Easy, ...

  3. ASP.NET Core 中文文档 第二章 指南(2)用 Visual Studio 和 ASP.NET Core MVC 创建首个 Web API

    原文:Building Your First Web API with ASP.NET Core MVC and Visual Studio 作者:Mike Wasson 和 Rick Anderso ...

  4. 在ASP.NET Core MVC中构建简单 Web Api

    Getting Started 在 ASP.NET Core MVC 框架中,ASP.NET 团队为我们提供了一整套的用于构建一个 Web 中的各种部分所需的套件,那么有些时候我们只需要做一个简单的 ...

  5. 在ASP.NET Core 2.2 中创建 Web API并结合Swagger

    一.创建 ASP.NET Core WebApi项目 二.添加 三. ----------------------------------------------------------- 一.创建项 ...

  6. 【翻译】在Visual Studio中使用Asp.Net Core MVC创建第一个Web Api应用(二)

    运行应用 In Visual Studio, press CTRL+F5 to launch the app. Visual Studio launches a browser and navigat ...

  7. [ASP.NET MVC 小牛之路]18 - Web API

    Web API 是ASP.NET平台新加的一个特性,它可以简单快速地创建Web服务为HTTP客户端提供API.Web API 使用的基础库是和一般的MVC框架一样的,但Web API并不是MVC框架的 ...

  8. ASP.NET MVC 提供与訪问 Web Api

    ASP.NET MVC 提供与訪问 Web Api 一.提供一个 Web Api 新建一个项目.类型就选 "Web Api". 我用的是MVC5,结果生成的项目一大堆东西.还编译只 ...

  9. 使用ASP.NET Core 3.x 构建 RESTful API - 2. 什么是RESTful API

    1. 使用ASP.NET Core 3.x 构建 RESTful API - 1.准备工作 什么是REST REST一词最早是在2000年,由Roy Fielding在他的博士论文<Archit ...

随机推荐

  1. 标注-CRF条件随机场

    1 概率无向图模型1.1 模型定义1.2 因子分解2 条件随机场的定义2.2 条件随机场的参数化形式2.3 条件随机场的简化形式2.4 条件随机场的矩阵形式 3 条件随机场的概率计算问题 3.1 前向 ...

  2. 踩坑系列の Oracle dbms_job简单使用

    二话不说先上代码 --创建存储过程 create or replace procedure job_truncateState is begin --此处就是要定时执行的sql execute imm ...

  3. java支付宝开发-02-手机网站支付

    源码已上传github,欢迎专注:https://github.com/shirayner/alipay-wap 一.基础部分 1.手机网站支付产品介绍 1.1 阅读官方介绍: 手机网站支付产品介绍 ...

  4. TP5 模型类和Db类的使用区别

    原文:http://www.upwqy.com/details/3.html 总结 在控制器中  模型操作  get() 和 all()  只能单独使用来查询数据   想要链式操作查询数据 需要使用f ...

  5. Google Chrome Plus——绿色便携多功能谷歌浏览器

    我更新浏览器的时候一般没有时间更新这个帖子,所以具体请看我网盘下载链接里面的更新日志,请自行查看最新版本下载,谢谢. 近期更新日期:2016.8.15(此时间可能不是最新,请看我网盘里面的更新日志) ...

  6. 让wordpress标签云显示文章数的正确方法

    先看一下效果 在百度经验找到一个教程,可惜,根据实践发现方法是错误的, 百度经验上的代码: 1 2 3 4 5 6 7 8 9 10 11 12 //标签tag所包含的文章数量 function Ta ...

  7. Win 及 Linux 查找mac地址的方法

    1. Windows系统中 - 调出cmd命令行 - 运行Getmac命令.命令行中输入: getmac /v /fo list 并按下回车键 - 查找物理地址.这是MAC地址的另一种描述方式.因为在 ...

  8. Linux压缩命令总结

    2018-02-28  10:43:18 linux压缩和解压缩命令大全 tar命令:tar本身仅是一个打包的命令,不具有压缩的功能.打包后源文件仍然存在,具有将多个文件归档成一个文件的功能[root ...

  9. Python 基础语法复习

    由于选修了<人工智能模式识别>的课程,要求用phthon来实现算法,乘着周三晚上没课,就来回顾一下python的主要语法. 环境:   Anaconda    Python3.6 1.变量 ...

  10. fail2ban防止SSH暴力破解

    [root@kazihuo /srv]# wget https://github.com/fail2ban/fail2ban/archive/0.8.14.tar.gz [root@kazihuo / ...