使用静态基类方案让 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));
? ? ? ? }
這里主要有三項工作:
通過results.Select(x => CreateLinksForVehicle(x)) 對集合的每個元素添加links.
然后把集合用上面剛剛建立的父類進行包裝
使用剛剛建立的CrateLinksForVehicle重載方法對這個包裝的集合添加本身的link.
最后看看效果:
嗯, 沒問題.??
這是第一種實現HATEOAS的方案, 另外一種等我稍微研究下再寫.
原文:https://www.cnblogs.com/cgzl/p/8726805.html
.NET社區新聞,深度好文,歡迎訪問公眾號文章匯總 http://www.csharpkit.com
總結
以上是生活随笔為你收集整理的使用静态基类方案让 ASP.NET Core 实现遵循 HATEOAS Restful Web API的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: ASP.NET Core依赖注入深入讨论
- 下一篇: 【招聘(北京)】.NETCORE开发工程