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的方案, 另外一种等我稍微研究下再写.

原文:https://www.cnblogs.com/cgzl/p/8726805.html


.NET社区新闻,深度好文,欢迎访问公众号文章汇总 http://www.csharpkit.com 

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

  1. 【视频教程】使用 ASP.NET Core 3.x 构建 RESTful Web API 已完结

    使用 ASP.NET Core 3.x 构建 RESTful Web API 的视频教程已经完结,共50讲,约10.5小时. B站可看,点击原文链接. 度娘盘可下载完整视频: https://pan. ...

  2. 在ASP.NET Core 2.0中创建Web API

    目录 介绍 先决条件 软件 技能 使用代码 第01步 - 创建项目 第02步 - 安装Nuget包 步骤03 - 添加模型 步骤04 - 添加控制器 步骤05 - 设置依赖注入 步骤06 - 运行We ...

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

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

  4. ASP.NET Core 3.1 系列之 Web API 添加身份验证Jwt

    ASP.NET Core 3.1 系列之 Web API 中间件篇 (一) 身份验证(Jwt)中间件使用步骤 添加 NuGet程序包 添加包:Microsoft.AspNetCore.Authenti ...

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

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

  6. 用 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 ...

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

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

  8. ASP.NET Core微服务(一)——【完整API搭建及访问过程】

    ASP.NET Core微服务(一)--[完整API搭建及访问过程]: 环境:win10专业版+vs2019+sqlserver2014/2019 对应练习demo下载路径(1积分):[https:/ ...

  9. 使用ASP.NET Core 3.x 构建 RESTful API - 3.4 内容协商

    现在,当谈论起 RESTful Web API 的时候,人们总会想到 JSON.但是实际上,JSON 和 RESTful API 没有半毛钱关系,只不过 JSON 恰好是RESTful API 结果的 ...

最新文章

  1. 对象检测和图像分割有什么区别?
  2. gnome mysql client_configure: error: Not found mysqlclient library
  3. 谋定5G+工业互联网-陈肇雄:经信研究体系化应用部署规划
  4. 更精炼更专注的RTMPClient客户端EasyRTMPClient,满足直播、转发、分析等各种需求...
  5. [6]Windows内核情景分析 --APC
  6. 一文详解深度相机之TOF成像
  7. 多目标优化问题和遗传算法学习
  8. ASD: Average Surface Distance
  9. java中udp的使用
  10. The `certs(%1$s)` contains the merchant‘s certificate serial number(%2$s) which is not allowed here.
  11. 【HTML+CSS】移动端布局之流式布局
  12. Labelling tools 的环境配置
  13. 一个大二前端的2017自我总结
  14. 怎么安装sqlserver2000数据库出现挂起提示
  15. 戴尔-卡耐基:《人性的优点 How to stop worrying and start living》
  16. 一款好用的手机云便签APP和电脑云便签软件
  17. Python——通过while、for、if—else完成一个地铁乘车消费计算器
  18. 一句话题解(20170801~20170125)
  19. iOS 渐变色 以及 镂空效果的实现(Mask的妙用)以及镂空文字的实现
  20. 台式计算机硬件组装步骤,电脑硬件组装详细步骤有哪些

热门文章

  1. Dubbo源码解析之Zookeeper连接
  2. no ip domain-lookup 什么意思
  3. 最近对kafka的移植工作
  4. DISCUZ7.2在通达OA2009桌面显示技巧
  5. 使用 C# 实现 URL 安全的 Base62 转码
  6. .NET+Sqlite如何支持加密
  7. 值得永久收藏的 C# 设计模式套路(二)
  8. 集成Dapr的 Azure 容器应用
  9. 使用Blazor开发内部后台(一):认识Blazor
  10. 我是如何保持长期写作的