上一篇咱们介绍了过滤与搜索、分页与排序,并在一个控制器方法中完成了对应功能的添加;本章咱们将介绍数据塑形与HATEOAS的概念,并添加对应的功能前端
注:本章内容大可能是基于solenovex的使用 ASP.NET Core 3.x 构建 RESTful Web API视频内容,若想进一步了解相关知识,请查看原视频git
数据塑形就是指API用户自由地选择本身须要的字段。举个例子,若一个Dto/ViewModel中存在不少字段,但API用户只须要其中的几个,那咱们返回API用户须要的字段就能够了,不须要所有返回。一般状况下咱们会添加一个数据塑形字段如fields,并采用QueryString的形式让API用户选择所需字段,如/api/article?fields=title,contentgithub
一、这里仍是以ArticleController控制器中的GetArticles方法作示例,其对应ArticleService中的逻辑方法返回的是ArticleListViewModel,这里咱们须要将其改变为动态类型ExpandoObject,这里咱们须要针对IEnumerable进行方法的扩展。咱们在Commen层的Helpers文件夹中添加一个名为IEnumerableExtensions的类,实现逻辑以下:json
using System; using System.Collections.Generic; using System.Dynamic; using System.Linq; using System.Reflection; namespace BlogSystem.Common.Helpers { //数据塑形——针对集合的扩展方法 public static class IEnumerableExtensions { public static IEnumerable<ExpandoObject> ShapeDataList<TSource>(this IEnumerable<TSource> source, string fields) { if (source == null) { throw new ArgumentNullException(nameof(source)); } var expandoObjectList = new List<ExpandoObject>(source.Count()); var propertyInfoList = new List<PropertyInfo>(); //field无字段则反射所有 if (string.IsNullOrWhiteSpace(fields)) { var propertyInfos = typeof(TSource).GetProperties(BindingFlags.Public | BindingFlags.Instance); propertyInfoList.AddRange(propertyInfos); } else //field有字段则去除空格并判断后添加至list { var fieldAfterSplit = fields.Split(","); foreach (var field in fieldAfterSplit) { var propertyName = field.Trim(); var propertyInfo = typeof(TSource).GetProperty(propertyName, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance); if (propertyInfo == null) { throw new Exception($"Property:{propertyName}没有找到:{typeof(TSource)}"); } propertyInfoList.Add(propertyInfo); } } foreach (TSource obj in source) { var shapedObj = new ExpandoObject(); //根据获取的属性额值添加到shapedObj中 foreach (var propertyInfo in propertyInfoList) { var propertyValue = propertyInfo.GetValue(obj); ((IDictionary<string, object>)shapedObj).Add(propertyInfo.Name, propertyValue); } expandoObjectList.Add(shapedObj); } return expandoObjectList; } } }
二、另外,咱们须要在Model层的ArticleParameters类中添加属性字段public string Fields { get; set; }
c#
三、在最终的实现层ArticleController的GetArticles方法中,将最终返回的list修改以下:后端
return Ok(list.ShapeDataList(parameters.Fields));
api
四、一样须要考虑到将生成的三个分页url中加入对应的field字段 fields=parameters.Fields
服务器
五、在field中录入但愿获得的字段信息,实现效果以下:网络
一、这里以ArticleController控制器中的GetArticleByArticleId方法作示例,咱们须要针对ExpandoObject进行方法的扩展。咱们在Commen层的Helpers文件夹中添加一个名为ObjectExtensions的类,实现逻辑与集合资源相似,可是出于性能的考虑,集合资源是将属性信息单独提取出来进行处理,而单个资源则是依次进行判断处理,具体实现以下:架构
using System; using System.Collections.Generic; using System.Dynamic; using System.Reflection; namespace BlogSystem.Common.Helpers { //数据塑形——单个资源 public static class ObjectExtensions { public static ExpandoObject ShapeData<TSource>(this TSource source, string fields) { if (source == null) { throw new ArgumentNullException(nameof(source)); } var expandoObj = new ExpandoObject(); if (string.IsNullOrWhiteSpace(fields)) { var propertyInfos = typeof(TSource).GetProperties(BindingFlags.Public | BindingFlags.IgnoreCase | BindingFlags.Instance); foreach (var propertyInfo in propertyInfos) { var propertyValue = propertyInfo.GetValue(source); ((IDictionary<string, object>)expandoObj).Add(propertyInfo.Name, propertyValue); } } else { var fieldAfterSplit = fields.Split(","); foreach (var field in fieldAfterSplit) { var propertyName = field.Trim(); var propertyInfo = typeof(TSource).GetProperty(propertyName, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance); if (propertyInfo == null) { throw new Exception($"在{typeof(TSource)}上没有找到{propertyName}这个属性"); } var propertyValue = propertyInfo.GetValue(source); ((IDictionary<string, object>)expandoObj).Add(propertyInfo.Name, propertyValue); } } return expandoObj; } } }
二、ArticleController控制器中的GetArticleByArticleId修改以下:
三、实现效果以下
一、这里咱们发现,在输入不存在的字段时,虽然会返回错误提示,可是错误代码为500,这显然是不合理的,这个是客户端引发的错误,应当返回4xx错误。咱们在Commen层的Helpers文件夹中添加一个名为PropertyCheckService的类,并定义名为IPropertyCheckService的接口,以达到复用的效果,实现逻辑以下:
using System.Reflection; namespace BlogSystem.Common.Helpers { //判断字段是否存在的服务 public class PropertyCheckService : IPropertyCheckService { public bool TypeHasProperties<T>(string fields) { if (string.IsNullOrWhiteSpace(fields)) { return true; } var fieldAfterSplit = fields.Split(","); foreach (var field in fieldAfterSplit) { var propertyName = field.Trim(); var propertyInfo = typeof(T).GetProperty(propertyName, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance); if (propertyInfo == null) { return false; } } return true; } } }
namespace BlogSystem.Common.Helpers { public interface IPropertyCheckService { bool TypeHasProperties<T>(string fields); } }
二、在BlogSystem.Core项目的StartUp类的ConfigureServices方法中进行上面接口的注入,以下:services.AddTransient<IPropertyCheckService, PropertyCheckService>();
三、在对应的ArticleController方法中进行接口的注入,在获取集合资源的方法中添加判断逻辑,以下:
在获取单个资源的方法中添加判断逻辑,以下:
数据塑形功能还能够实现父子资源的联合查询,高级过滤等,实际应用中仍是须要根据需求进行变化。上述咱们只是从功能出发自定义实现,实际上咱们可使用已经实现并封装好了的插件,如微软的OData,有兴趣的朋友能够自行研究。
HATEOAS的全程是Hypermedia As The Engine Of Application State,即超媒体做为应用程序状态引擎。它是做为REST统一界面约束中的一个子约束,是REST架构中最重要,最复杂的约束,也是构建成熟REST服务的核心。
它是REST的Richardson成熟度模型中最成熟的一个层次,达到一成熟的的API不只在响应中包含资源,也包含与之相关的连接,这些连接不只易于被发现,并且能够经过这些连接发现当前资源所支持的动做,这些动做又能驱动应用程序状态的改变。
一、上面咱们提到HATEOAS会在响应中包含连接,实际上咱们正是经过这些连接告知客户端,服务端能提供哪些服务,客户端只须要检查这些连接便可。因此咱们要作的就是展现这些link,而每一个连接包含三个属性—href、rel和method
举个例子,当获取一本图书资源时,服务器可以判断该图书是否可以被借阅,若是能够,则连接中应当包含请求借阅的API的URL和HTTP方法
二、实现HATEOAS咱们须要针对集合资源和单个资源进行不一样的考虑,而实现方案有两种,静态类型方法和动态类型方案:
静态类型方案:返回的资源中所有包含link,经过继承同一个基类进行实现;
动态类型方案:使用匿名类或以前使用过的动态类型对象ExpandoObject实现,单个资源使用ExpandoObject,而集合资源使用匿名类
这里咱们采用动态类型方案进行实现,处理的对象是ArticleController类中的GetArticleByArticleId方法
一、首先咱们在Modle层创建一个HATEOAS文件夹,里面添加一个LinkDto类,添加以下信息:
namespace BlogSystem.Model.HATEOAS { public class LinkDto { public string Href { get; } public string Rel { get; } public string Method { get; } public LinkDto(string href, string rel, string method) { Href = href; Rel = rel; Method = method; } } }
二、在ArticelController中添加建立link的方法CreateLinksForArticle,咱们在内部添加了自身link和删除文章、编辑文章的link,前提是须要为方法命名,如 [Httpxxx(Name = nameof(xxx))],实现逻辑以下:
//实现HATEOAS单个资源的简单方法 private IEnumerable<LinkDto> CreateLinksForArticle(Guid articleId, string fields) { var links = new List<LinkDto>(); if (string.IsNullOrWhiteSpace(fields)) { links.Add(new LinkDto(Url.Link(nameof(GetArticleByArticleId), new { articleId }), "self", "Get")); } else { links.Add(new LinkDto(Url.Link(nameof(GetArticleByArticleId), new { articleId, fields }), "self", "Get")); } //删除文章的link links.Add(new LinkDto(Url.Link(nameof(RemoveArticle), new { articleId, fields }), "delete_article need_auth", "DELETE")); //编辑文章的link links.Add(new LinkDto(Url.Link(nameof(EditArticle), new { articleId }), "edit_article need _auth", "PATCH")); return links; }
三、修改ArticleController类中的GetArticleByArticleId方法,以下:
四、实现效果,以下:
一、一样咱们在ArticelController中添加建立link的方法CreateLinksForArticles,该方法返回信息是包括分页信息及先后页信息的,因此咱们要借助CreateArticleUrl方法,可是在返回当前页面信息时由于页面枚举类UrlType没有添加当前页,因此没法获取,修改枚举类,实现CreateLinksForArticles方法,以下:
namespace BlogSystem.Model.Helpers { public enum UrlType { PreviousPage, NextPage, CurrentPage } }
//实现HATEOAS集合资源的简单方法,将自身的前一页信息和后一页信息也放到headoas中 private IEnumerable<LinkDto> CreateLinksForArticles(ArticleParameters parameters, bool hasPrevious, bool hasNext) { var links = new List<LinkDto>(); links.Add(new LinkDto(CreateArticleUrl(parameters, UrlType.CurrentPage), "self", "GET")); if (hasPrevious) { links.Add(new LinkDto(CreateArticleUrl(parameters, UrlType.PreviousPage), "Previous", "GET")); } if (hasNext) { links.Add(new LinkDto(CreateArticleUrl(parameters, UrlType.NextPage), "Next", "GET")); } return links; }
二、主要注意的是集合类型的结果是每条记录都有其自身的HATEOAS,而且每条记录HATEOAS都应该有先后页的信息,因此咱们要先删除以前添加的建立先后页面url的逻辑,以下:
三、修改ArticleController类中的GetArticles方法中返回结果的逻辑,实现以下:
四、实现效果以下图所示,集合自身添加先后分页信息,集合内部元素有自身支持方法的links
能够发现集合资源与其内部元素是依靠articleId来创建联系的,若是使用数据塑形功能可是没有添加articleId字段,系统会产生异常,因此这里咱们在数据塑形前加个判断逻辑,以下:
一、在实际生产中,HATEOAS常常会与单页应用一块儿被提到,而单页应用每每会存在一个"根"页面。咱们这里就不实现了,感兴趣的朋友能够本身研究下,本章一开始提到的视频内容中也是有实现过程的。
二、为方便你们更好的理解,咱们从https://www.jianshu.com/p/ecd6a4a7a2e4摘抄了部份内容,以下:
先后端分离的开发模式进一步细化了分工,但同时也引入了很多重复的工做,例如一些业务规则在后端必须实现的状况下,前端也须要再实现一遍以得到更好的用户体验。HATEOAS虽然不是惟一消除这些重复的方法,但做为一种架构原则,它更容易让团队找到消除重复的“套路”。
在非HATOEAS的项目中,因为URI是在客户端硬编码的,即便你把它们设计的很是漂亮(准确的HTTP动词,以复数命名的资源,禁止使用动词等等),也不能帮助你更容易地修改它们,由于你的重构须要前端开发者的配合,而他/她不得不停下手头的其余工做。但在采用了HATEOAS的项目中,这很容易,由于客户端是经过Link来查找API的URI,因此你能够在不破坏API Scheme的状况下修改它的URI。固然,你不可能保证全部API的URI都是经过Link来获取的,你须要安排一些Root Resource,例如 /api/currentLoggedInUser
,不然客户端没有办法发起第一次请求。
在实现HATEOAS时,咱们获得的返回结果是{values:[xx,xx,xx...],links:[xx,xx...]}格式的,它是相同资源的不一样表述方式,因此服务器应当根据客户端请求的媒体类型(Media Type)返回与之对应的表述资源,不然将破坏自我描述性约束。
这里咱们应当建立一个新的媒体类型,来应对这类状况。一般咱们会使用供应商特定媒体类型(Vendor-special media type),缩写为application/vnd.companyName.hateoas+json
一、这里咱们处理的对象是ArticleController类中的GetArticleByArticleId方法,修改以下:
二、这里使用PostMan测试返回406错误,控制台显示没有对应的输出格式,因此这里咱们在startup中添加全局的支持,以下:
三、最终实现以下:
媒体类型的能够应用在不一样的状况下,下面再介绍两种,这里就不实现了,感兴趣的朋友能够本身研究下,本章一开始提到的视频内容中也是有实现过程的。
在上面的方法中,咱们完成了根据特定的媒体类型输出不一样表述数据的功能;实际上与之对应的还有输入功能的实现,咱们经过设置Content-Type Header来接受不一样的媒体类型的输入。好比说编辑文章功能,通常来讲只是编辑文章内容,可是在一些状况下咱们还但愿能够更新建立时间CreateTime,也就是经过输入不一样的媒体类型来实现不一样的功能。
咱们还能够经过使用带有语义的媒体类型来告知API使用者数据的语义,好比说但愿看到简洁数据和完整数据两类信息,就能够设置两个媒体类型,而不一样的媒体类型则能够应对不一样的数据结果。
该项目源码已更新上传至GitHub,有须要的朋友能够下载使用:https://github.com/Jscroop/BlogSystem
本人知识点有限,若文中有错误的地方请及时指正,方便你们更好的学习和交流。
本文部份内容参考了网络上的视频内容和文章,仅为学习和交流,视频地址以下:
solenovex,ASP.NET Core 3.x 入门视频
solenovex,使用 ASP.NET Core 3.x 构建 RESTful Web API