學習ASP.NET Core(09)-數據塑形與HATEOAS及內容協商

上一篇咱們介紹了過濾與搜索、分頁與排序,並在一個控制器方法中完成了對應功能的添加;本章咱們將介紹數據塑形與HATEOAS的概念,並添加對應的功能前端


注:本章內容大可能是基於solenovex的使用 ASP.NET Core 3.x 構建 RESTful Web API視頻內容,若想進一步瞭解相關知識,請查看原視頻git

1、數據塑形

一、定義介紹

數據塑形就是指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,有興趣的朋友能夠自行研究。

2、HATEOAS

一、定義介紹

HATEOAS的全程是Hypermedia As The Engine Of Application State,即超媒體做爲應用程序狀態引擎。它是做爲REST統一界面約束中的一個子約束,是REST架構中最重要,最複雜的約束,也是構建成熟REST服務的核心。

它是REST的Richardson成熟度模型中最成熟的一個層次,達到一成熟的的API不只在響應中包含資源,也包含與之相關的連接,這些連接不只易於被發現,並且能夠經過這些連接發現當前資源所支持的動做,這些動做又能驅動應用程序狀態的改變。

二、實際應用

一、上面咱們提到HATEOAS會在響應中包含連接,實際上咱們正是經過這些連接告知客戶端,服務端能提供哪些服務,客戶端只須要檢查這些連接便可。因此咱們要作的就是展現這些link,而每一個連接包含三個屬性—href、rel和method

  • href:用戶能夠檢查資源或者改變應用狀態的URL
  • rel:描述href指向資源和現有資源的關係
  • method:請求該URL要使用的HTTP方法

舉個例子,當獲取一本圖書資源時,服務器可以判斷該圖書是否可以被借閱,若是能夠,則連接中應當包含請求借閱的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,不然客戶端沒有辦法發起第一次請求。

3、內容協商

一、定義介紹

在實現HATEOAS時,咱們獲得的返回結果是{values:[xx,xx,xx...],links:[xx,xx...]}格式的,它是相同資源的不一樣表述方式,因此服務器應當根據客戶端請求的媒體類型(Media Type)返回與之對應的表述資源,不然將破壞自我描述性約束。

二、實際應用

這裏咱們應當建立一個新的媒體類型,來應對這類狀況。一般咱們會使用供應商特定媒體類型(Vendor-special media type),縮寫爲application/vnd.companyName.hateoas+json

  • vnd爲Vendor的縮寫,表示媒體類型是供應商特定的
  • companyName爲自定義的Vendor標識,一般爲公司的名稱,固然也能夠包括額外的信息
  • hateoas是媒體類型的名稱,它表示返回的響應裏面包含連接信息
  • +json表示數據爲Json格式,它會告知客戶端應當如何處理響應信息

三、功能實現

一、這裏咱們處理的對象是ArticleController類中的GetArticleByArticleId方法,修改以下:

二、這裏使用PostMan測試返回406錯誤,控制檯顯示沒有對應的輸出格式,因此這裏咱們在startup中添加全局的支持,以下:

三、最終實現以下:

四、其餘說明

媒體類型的能夠應用在不一樣的狀況下,下面再介紹兩種,這裏就不實現了,感興趣的朋友能夠本身研究下,本章一開始提到的視頻內容中也是有實現過程的。

4.一、Vendor-Specific Media Type輸入

在上面的方法中,咱們完成了根據特定的媒體類型輸出不一樣表述數據的功能;實際上與之對應的還有輸入功能的實現,咱們經過設置Content-Type Header來接受不一樣的媒體類型的輸入。好比說編輯文章功能,通常來講只是編輯文章內容,可是在一些狀況下咱們還但願能夠更新建立時間CreateTime,也就是經過輸入不一樣的媒體類型來實現不一樣的功能。

4.二、帶有語義的媒體類型Semantic Media Types

咱們還能夠經過使用帶有語義的媒體類型來告知API使用者數據的語義,好比說但願看到簡潔數據和完整數據兩類信息,就能夠設置兩個媒體類型,而不一樣的媒體類型則能夠應對不一樣的數據結果。

本章完~

該項目源碼已更新上傳至GitHub,有須要的朋友能夠下載使用:https://github.com/Jscroop/BlogSystem

本人知識點有限,若文中有錯誤的地方請及時指正,方便你們更好的學習和交流。

本文部份內容參考了網絡上的視頻內容和文章,僅爲學習和交流,視頻地址以下:

solenovex,ASP.NET Core 3.x 入門視頻

solenovex,使用 ASP.NET Core 3.x 構建 RESTful Web API

聲明

相關文章
相關標籤/搜索