使用静态基类方案让 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 服务的核心。

 

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

 

下面是我的关于ASP.NET Core Web API相关技术的公众号--草根专栏:

目录
相关文章
|
3月前
|
开发框架 .NET C#
ASP.NET Core Blazor 路由配置和导航
大家好,我是码农刚子。本文系统介绍Blazor单页应用的路由机制,涵盖基础配置、路由参数、编程式导航及高级功能。通过@page指令定义路由,支持参数约束、可选参数与通配符捕获,结合NavigationManager实现页面跳转与参数传递,并演示用户管理、产品展示等典型场景,全面掌握Blazor路由从入门到实战的完整方案。
335 6
|
9月前
|
数据采集 自然语言处理 JavaScript
Playwright多语言生态:跨Python/Java/.NET的统一采集方案
随着数据采集需求的增加,传统爬虫工具如Selenium、Jsoup等因语言割裂、JS渲染困难及代理兼容性差等问题,难以满足现代网站抓取需求。微软推出的Playwright框架,凭借多语言支持(Python/Java/.NET/Node.js)、统一API接口和优异的JS兼容性,解决了跨语言协作、动态页面解析和身份伪装等痛点。其性能优于Selenium与Puppeteer,在学术数据库(如Scopus)抓取中表现出色。行业应用广泛,涵盖高校科研、大型数据公司及AI初创团队,助力构建高效稳定的爬虫系统。
537 2
Playwright多语言生态:跨Python/Java/.NET的统一采集方案
|
9月前
|
人工智能 搜索推荐 IDE
突破网页数据集获取难题:Web Unlocker API 助力 AI 训练与微调数据集全方位解决方案
本文介绍了Web Unlocker API、Web-Scraper和SERP API三大工具,助力解决AI训练与微调数据集获取难题。Web Unlocker API通过智能代理和CAPTCHA绕过技术,高效解锁高防护网站数据;Web-Scraper支持动态内容加载,精准抓取复杂网页信息;SERP API专注搜索引擎结果页数据抓取,适用于SEO分析与市场研究。这些工具大幅降低数据获取成本,提供合规保障,特别适合中小企业使用。粉丝专属体验入口提供2刀额度,助您轻松上手!
459 2
|
人工智能 前端开发 API
Gemini Coder:基于 Google Gemini API 的开源 Web 应用生成工具,支持实时编辑和预览
Gemini Coder 是一款基于 Google Gemini API 的 AI 应用生成工具,支持通过文本描述快速生成代码,并提供实时代码编辑和预览功能,简化开发流程。
905 38
Gemini Coder:基于 Google Gemini API 的开源 Web 应用生成工具,支持实时编辑和预览
|
人工智能 搜索推荐 API
自学记录鸿蒙API 13:实现人脸比对Core Vision Face Comparator
在完成文本识别和人脸检测项目后,我深入学习了HarmonyOS Next API 13中的Core Vision Face Comparator API,开发了一个简单的人脸比对工具。该API能进行高精度人脸比对并给出相似度评分,应用场景广泛,如照片分类、身份认证、个性化服务等。通过初始化服务、加载图片、实现比对功能和构建用户界面,最终实现了可靠的人脸比对功能。未来计划将此技术应用于更复杂的场景,如照片管理和个性化服务,并探索与其他AI能力的结合。如果你也对人脸比对感兴趣,不妨从简单的比对功能开始,逐步实现自己的创意!
380 61
|
10月前
|
XML JSON API
Understanding RESTful API and Web Services: Key Differences and Use Cases
在现代软件开发中,RESTful API和Web服务均用于实现系统间通信,但各有特点。RESTful API遵循REST原则,主要使用HTTP/HTTPS协议,数据格式多为JSON或XML,适用于无状态通信;而Web服务包括SOAP和REST,常用于基于网络的API,采用标准化方法如WSDL或OpenAPI。理解两者区别有助于选择适合应用需求的解决方案,构建高效、可扩展的应用程序。
|
10月前
|
机器学习/深度学习 开发框架 API
Python 高级编程与实战:深入理解 Web 开发与 API 设计
在前几篇文章中,我们探讨了 Python 的基础语法、面向对象编程、函数式编程、元编程、性能优化、调试技巧以及数据科学和机器学习。本文将深入探讨 Python 在 Web 开发和 API 设计中的应用,并通过实战项目帮助你掌握这些技术。
|
12月前
|
开发框架 数据可视化 .NET
.NET 中管理 Web API 文档的两种方式
.NET 中管理 Web API 文档的两种方式
237 14
|
人工智能 自然语言处理 文字识别
自学记录鸿蒙API 13:实现智能文本识别Core Vision Text Recognition
在完成语音助手项目后,我尝试了HarmonyOS Next API 13中的Core Vision Text Recognition API,体验其强大的文本识别功能。该API支持多语言高精度识别,能快速将图像中的文本提取为结构化信息,适用于文档扫描、票据管理和实时翻译等场景。通过权限配置、初始化服务、实现识别功能和构建用户界面,我完成了文本识别应用的开发,并探索了性能优化与功能扩展。鸿蒙生态的强大支持让开发者能更便捷地实现复杂功能。未来计划将此技术应用于实际项目,如票据管理或实时翻译工具。如果你也对文本识别感兴趣,不妨一起探索!
276 11
|
人工智能 监控 安全
自学记录鸿蒙 API 13:骨骼点检测应用Core Vision Skeleton Detection
骨骼点检测技术能够从图片中识别出人体的关键骨骼点位置,如头部、肩部、手肘等,广泛应用于运动健身指导、游戏交互、医疗辅助、安全监控等领域。我决定深入学习HarmonyOS Next API 13中的Skeleton Detection API,并开发一个简单的骨骼点检测应用。通过理解API核心功能、项目初始化与配置、实现检测功能、构建用户界面,以及性能优化和功能扩展,逐步实现这一技术的应用。未来计划将其应用于健身指导和智能监控领域,探索与其他AI能力的结合,开发更智能的解决方案。如果你也对骨骼点检测感兴趣,不妨一起进步!
447 9