使用靜態基類方案讓 ASP.NET Core 實現遵循 HATEOAS Restful Web API

来源:https://www.cnblogs.com/cgzl/archive/2018/04/06/8726805.html
-Advertisement-
Play Games

使用靜態基類方案讓 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的方案, 另外一種等我稍微研究下再寫.

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • Python的數據類型在前幾節我們都簡單的一一介紹了,接下來我們就要講到Python的控制判斷迴圈語句 在現實編程中,我們往往要利用電腦幫我們做大量重覆計算的工作,在這樣的情況下,需要機器能對某個條件進行判斷,或是對某個行為進行重覆操作 這時我們就必須要知道如何去編寫迴圈判斷語句 if... el ...
  • 1.安裝anaconda 下載地址:[清華鏡像站][url1] 針對自己的操作系統,在 下載鏈接 應用軟體 conda 中選擇合適版本。安裝過程較為簡單,這裡就不在詳細介紹。 需要註意 的是windows系統安裝過程中需要註意,勾選將軟體添加至windows路徑(也可以手動添加,即在環境變數 pat ...
  • 在項目中,遇到需求,需要進行規則入庫,想到使用正則進行表達式的拆分和分類,具體如下: 目標是:拆分介於表示邏輯運算的“And”或者“Or”的子句,比如:Operation Mode(Operation Mode_2) (Approve CR1) equals Accept;Need Physical ...
  • 題目描述 給定一個完全圖,保證$w_{u,v}=w_{v,u}$且$w_{u,u}=0$,等概率選取一個隨機生成樹,對於每一對$(u,v)$,求$dis(u,v)$的期望值對$998244353$取模。 輸入 第一行一個數$n$ 接下來$n$行,每行$n$個整數,第$i$行第$j$個整數表示$w_{ ...
  • 因為對錶達式樹有點興趣,出於練手的目的,試著寫了一個深拷貝的工具庫。支持.net standard2.0或.net framework4.5及以上。 GitHub地址https://github.com/blurhkh/DeepCopier nuget地址https://www.nuget.org/ ...
  • 一、WebApi路由機制是什麼? 路由機制通俗點來說:其實就是WebApi框架將用戶在瀏覽器中輸入的Url地址和路由表中的路由進行匹配,並根據最終匹配的路由去尋找並匹配相應的Controller和Action並執行的一個過程。 從WebApi框架接收到來自外部環境的介面調用請求到指定介面的執行大概需 ...
  • 概述 UWP Community Toolkit 中有一個為 Frmework Element 提供投影效果的控制項 - DropShadowPanel,本篇我們結合代碼詳細講解 DropShadowPanel 的實現。 DropShadowPanel 提供的陰影效果有很多應用場景,比如給文本提供陰影 ...
  • 所謂熱插拔就是插件可以 在主程式不重新啟動的情況直接更新插件, 網上有很多方案: https://www.cnblogs.com/happyframework/p/3405811.html 如下: 但是我發現有一種最簡單粗暴的辦法, 就是把插件載入到記憶體當中,然後使用Assembly從記憶體中載入DL ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...