ASP .NET Core Web API_ 11_HATEOAS

HATEOAS

Hypermedia as the Engine of Application State
REST里最復(fù)雜的約束, 構(gòu)建成熟REST API的核心

  • 可進(jìn)化性, 自我描述
  • 超媒體(Hypermedia, 例如超鏈接)驅(qū)動(dòng)如何消費(fèi)和使用API

不使用HATEOAS

  • 客戶端更多的需要了解API內(nèi)在邏輯
  • 如果API發(fā)生了一點(diǎn)變化(添加了額外的規(guī)則, 改變規(guī)則)都會(huì)破壞API的消費(fèi)者.
  • API無(wú)法獨(dú)立于消費(fèi)它的應(yīng)用進(jìn)行進(jìn)化.


    No HATEOAS

使用HATEOAS

  • 這個(gè)response里面包含了若干link, 第一個(gè)link包含著獲取當(dāng)前響應(yīng)的鏈接, 第二個(gè)link則告訴客戶端如何去更新該post.
  • 不改變響應(yīng)主體結(jié)果的情況下添加另外一個(gè)刪除的功能(link), 客戶端通過(guò)響應(yīng)里的links就會(huì)發(fā)現(xiàn)這個(gè)刪除功能, 但是對(duì)其他部分都沒(méi)有影響.


    HATEOAS

展示鏈接

  • JSON和XML并沒(méi)有如何展示link的概念. 但是HTML的anchor元素卻知道: <a href="uri" rel="type" type="media type">.
    • href包含了URI
    • rel則描述了link如何和資源的關(guān)系
    • type是可選的, 它表示了媒體的類型
  • 我們的例子:
    • method: 定義了需要使用的方法
    • rel: 表明了動(dòng)作的類型
    • href: 包含了執(zhí)行這個(gè)動(dòng)作所包含的URI.


      show heteoas

實(shí)現(xiàn)

  • 靜態(tài)基類
    需要基類(包含link)和包裝類, 也就是返回的資源里面都含有l(wèi)ink, 通過(guò)繼承于同一個(gè)基類來(lái)實(shí)現(xiàn)
  • 動(dòng)態(tài)類型, 需要使用例如匿名類或ExpandoObject等
    * 對(duì)于單個(gè)資源可以使用ExpandoObject
    * 對(duì)于集合類資源則使用匿名類.
  1. LinkResource
public class LinkResource
{
   public LinkResource(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; }
}
  1. Controller中添加CreateLinksForPost
 //為每個(gè)資源創(chuàng)建鏈接link
 private IEnumerable<LinkResource> CreateLinksForPost(int id,string fields = null)
{
    var links = new List<LinkResource>();

    if (string.IsNullOrWhiteSpace(fields))
       links.Add(new LinkResource(_urlHelper.Link("GetPost", new { id }), "self", "GET"));
    else
      links.Add(new LinkResource(_urlHelper.Link("GetPost", new { id,fields}), "self", "GET"));
     
      links.Add(new LinkResource(_urlHelper.Link("DeletePost", new { id }), "delete_post", "DELETE"));
      return links;
}
  1. GETPOST中調(diào)用
//單個(gè)資源塑性
var shapedPostResource = postResource.ToDynamic(fields);

//加載link
var links = CreateLinksForPost(id, fields);

//整合返回?cái)?shù)據(jù)
var result = shapedPostResource as IDictionary<string, object>;
result.Add("links", links);

return Ok(result); 
單個(gè)資源link
//集合資源塑性
 var shapedPostResources = postResources.ToDynamicIEnumerable(postParameters.Fields);

//循環(huán)遍歷為每個(gè)資源添加link
var shapdeWithLinks = shapedPostResources.Select(x =>
{
   var dict = x as IDictionary<string, object>;
   var postLinks = CreateLinksForPost((int)dict["Id"], postParameters.Fields);
   dict.Add("links", psotLinks);
   return dict;
});
集合資源遍歷link
  1. 集合資源整體Link
 //為集合資源創(chuàng)建整體link
private IEnumerable<LinkResource> CreateLinksForPosts(PostParameters postParameters,bool hasPrevious,bool hasNext)
{
    var links = new List<LinkResource>
    { new LinkResource(CreatePostUri(postParameters,PaginationResourceUriType.CurrentPage),"self","GET") };

    if (hasPrevious)
       links.Add(new LinkResource(CreatePostUri(postParameters,PaginationResourceUriType.PreviousPage),"previous_page","GET"));
    if (hasNext)
       links.Add(new LinkResource(CreatePostUri(postParameters,PaginationResourceUriType.NextPage),"next_page","GET"));
          
   return links;
}
//集合的整體links
var links = CreateLinksForPosts(postParameters, postList.HasPrevious, postList.HasNext);

var result = new
 {
     values = shapdeWithLinks,
     links
  };
整體資源links

Vendor-specific media type

創(chuàng)建供應(yīng)商特定媒體類型
上例中使用application/json會(huì)破壞了資源的自我描述性這條約束, API消費(fèi)者無(wú)法從content-type的類型來(lái)正確的解析響應(yīng).

  • application/vnd.mycompany.hateoas+json
    * vnd是vendor的縮寫(xiě),這一條是mime type的原則,表示這個(gè)媒體類型是供應(yīng)商特定的
    • 自定義的標(biāo)識(shí),也可能還包括額外的值,這里我是用的是公司名,隨后是hateoas表示返回的響應(yīng)里面要包含鏈接
    • +json
  • 在Startup里注冊(cè).
services.AddMvc(
  options=>
      {
          options.ReturnHttpNotAcceptable = true; //開(kāi)啟406
          
          //支持xml
          //options.OutputFormatters.Add(new XmlDataContractSerializerOutputFormatter());

          //自定義mediaType
          var outputFormatter =  options.OutputFormatters.OfType<JsonOutputFormatter>().FirstOrDefault();
          if (outputFormatter!=null)
          {
              outputFormatter.SupportedMediaTypes.Add("application/vnd.enfi.hateoas+json");
          }
      })
         .AddJsonOptions(options=>
          {
            options.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
          });
  • 判斷Media Type類型
    * [FromHeader(Name = "Accept")] string mediaType
    * 自定義Action約束.
        [HttpGet(Name = "GetPosts")]
        public async Task<IActionResult> Get(PostParameters postParameters,
            [FromHeader(Name = "Accept")] string mediaType)
        {
            if (!_propertyMappingContainer.ValidateMappingExistsFor<PostResource, Post>(postParameters.OrderBy))
            {
                return BadRequest("cannot finds fields for sorting.");

            }
            if (!_typeHelperService.TypeHasProperties<PostResource>(postParameters.Fields))
            {
                return BadRequest("Fields not exist.");
            }
            var postList = await _postRepository.GetAllPostsAsync(postParameters);
            var postResources = _mapper.Map<IEnumerable<Post>, IEnumerable<PostResource>>(postList);

            //判斷mediaType
            if (mediaType == "application/vnd.enfi.hateoas+json")
            {
                //集合資源塑性
                var shapedPostResources = postResources.ToDynamicIEnumerable(postParameters.Fields);

                //循環(huán)遍歷為每個(gè)資源添加link
                var shapdeWithLinks = shapedPostResources.Select(x =>
                {
                    var dict = x as IDictionary<string, object>;
                    var postLinks = CreateLinksForPost((int)dict["Id"], postParameters.Fields);
                    dict.Add("links", postLinks);
                    return dict;
                });

                //集合的整體links
                var links = CreateLinksForPosts(postParameters, postList.HasPrevious, postList.HasNext);

                var result = new
                {
                    values = shapdeWithLinks,
                    links
                };

                //var previousPageLink = postList.HasPrevious ? CreatePostUri(postParameters, PaginationResourceUriType.PreviousPage) : null;
                //var nextPageLink = postList.HasNext ? CreatePostUri(postParameters, PaginationResourceUriType.NextPage) : null;
                var meta = new
                {
                    postList.PageSize,
                    postList.PageIndex,
                    postList.TotalItemsCount,
                    postList.PageCount,
                    //previousPageLink,
                    //nextPageLink
                };
                Response.Headers.Add("X-Pagination", JsonConvert.SerializeObject(meta, new JsonSerializerSettings
                {
                    //使得命名符合駝峰命名法
                    ContractResolver = new CamelCasePropertyNamesContractResolver()
                }));
                return Ok(result);
            }

            else  //不是自定義的mediaType按json返回,元數(shù)據(jù)包含在返回的head中
            {
                var previousPageLink = postList.HasPrevious ? CreatePostUri(postParameters, PaginationResourceUriType.PreviousPage) : null;
                var nextPageLink = postList.HasNext ? CreatePostUri(postParameters, PaginationResourceUriType.NextPage) : null;
                var meta = new
                {
                    postList.PageSize,
                    postList.PageIndex,
                    postList.TotalItemsCount,
                    postList.PageCount,
                    previousPageLink,
                    nextPageLink
                };
                Response.Headers.Add("X-Pagination", JsonConvert.SerializeObject(meta, new JsonSerializerSettings
                {
                    //使得命名符合駝峰命名法
                    ContractResolver = new CamelCasePropertyNamesContractResolver()
                }));
                return Ok(postResources.ToDynamicIEnumerable(postParameters.Fields));
            }
        }
application/json

application/vnd.enfi.hateoas+json

application/vnd.enfi.hateoas+json

使用Action約束分解為兩個(gè)方法

 [AttributeUsage(AttributeTargets.All, Inherited = true, AllowMultiple = true)]
    public class RequestHeaderMatchingMediaTypeAttribute : Attribute, IActionConstraint
    {
        private readonly string _requestHeaderToMatch;
        private readonly string[] _mediaTypes;

        public RequestHeaderMatchingMediaTypeAttribute(string requestHeaderToMatch, string[] mediaTypes)
        {
            _requestHeaderToMatch = requestHeaderToMatch;
            _mediaTypes = mediaTypes;
        }

        public bool Accept(ActionConstraintContext context)
        {
            var requestHeaders = context.RouteContext.HttpContext.Request.Headers;
            if (!requestHeaders.ContainsKey(_requestHeaderToMatch))
            {
                return false;
            }

            foreach (var mediaType in _mediaTypes)
            {
                var mediaTypeMatches = string.Equals(requestHeaders[_requestHeaderToMatch].ToString(),
                    mediaType, StringComparison.OrdinalIgnoreCase);
                if (mediaTypeMatches)
                {
                    return true;
                }
            }

            return false;
        }

        public int Order { get; } = 0;
    }
[HttpGet(Name = "GetPosts")]
[RequestHeaderMatchingMediaType("Accept", new[] { "application/vnd.enfi.hateoas+json" })]
        public async Task<IActionResult> GetHateoas(PostParameters postParameters)
        {
            if (!_propertyMappingContainer.ValidateMappingExistsFor<PostResource, Post>(postParameters.OrderBy))
            {
                return BadRequest("cannot finds fields for sorting.");
            }
            if (!_typeHelperService.TypeHasProperties<PostResource>(postParameters.Fields))
            {
                return BadRequest("Fields not exist.");
            }
            var postList = await _postRepository.GetAllPostsAsync(postParameters);
            var postResources = _mapper.Map<IEnumerable<Post>, IEnumerable<PostResource>>(postList);

            //集合資源塑性
            var shapedPostResources = postResources.ToDynamicIEnumerable(postParameters.Fields);

            //循環(huán)遍歷為每個(gè)資源添加link
            var shapdeWithLinks = shapedPostResources.Select(x =>
            {
                var dict = x as IDictionary<string, object>;
                var postLinks = CreateLinksForPost((int)dict["Id"], postParameters.Fields);
                dict.Add("links", postLinks);
                return dict;
            });

            //集合的整體links
            var links = CreateLinksForPosts(postParameters, postList.HasPrevious, postList.HasNext);

            var result = new
            {
                values = shapdeWithLinks,
                links
            };

            var meta = new
            {
                postList.PageSize,
                postList.PageIndex,
                postList.TotalItemsCount,
                postList.PageCount,

            };
            Response.Headers.Add("X-Pagination", JsonConvert.SerializeObject(meta, new JsonSerializerSettings
            {
                //使得命名符合駝峰命名法
                ContractResolver = new CamelCasePropertyNamesContractResolver()
            }));
            return Ok(result);
        }
  [HttpGet(Name = "GetPosts")]
        [RequestHeaderMatchingMediaType("Accept", new[] { "application/json" })] //不是自定義的mediaType按json返回,元數(shù)據(jù)包含在返回的head中
        public async Task<IActionResult> Get(PostParameters postParameters)
        {
            if (!_propertyMappingContainer.ValidateMappingExistsFor<PostResource, Post>(postParameters.OrderBy))
            {
                return BadRequest("cannot finds fields for sorting.");
            }
            if (!_typeHelperService.TypeHasProperties<PostResource>(postParameters.Fields))
            {
                return BadRequest("Fields not exist.");
            }
            var postList = await _postRepository.GetAllPostsAsync(postParameters);
            var postResources = _mapper.Map<IEnumerable<Post>, IEnumerable<PostResource>>(postList);

            var previousPageLink = postList.HasPrevious ? CreatePostUri(postParameters, PaginationResourceUriType.PreviousPage) : null;
            var nextPageLink = postList.HasNext ? CreatePostUri(postParameters, PaginationResourceUriType.NextPage) : null;
            var meta = new
            {
                postList.PageSize,
                postList.PageIndex,
                postList.TotalItemsCount,
                postList.PageCount,
                previousPageLink,
                nextPageLink
            };
            Response.Headers.Add("X-Pagination", JsonConvert.SerializeObject(meta, new JsonSerializerSettings
            {
                //使得命名符合駝峰命名法
                ContractResolver = new CamelCasePropertyNamesContractResolver()
            }));
            return Ok(postResources.ToDynamicIEnumerable(postParameters.Fields));
        }
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • # Python 資源大全中文版 我想很多程序員應(yīng)該記得 GitHub 上有一個(gè) Awesome - XXX 系列...
    aimaile閱讀 26,832評(píng)論 6 427
  • Spring Cloud為開(kāi)發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見(jiàn)模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,534評(píng)論 19 139
  • 一說(shuō)到REST,我想大家的第一反應(yīng)就是“啊,就是那種前后臺(tái)通信方式。”但是在要求詳細(xì)講述它所提出的各個(gè)約束,以及如...
    時(shí)待吾閱讀 3,595評(píng)論 0 19
  • Swift1> Swift和OC的區(qū)別1.1> Swift沒(méi)有地址/指針的概念1.2> 泛型1.3> 類型嚴(yán)謹(jǐn) 對(duì)...
    cosWriter閱讀 11,629評(píng)論 1 32
  • 為什么要把自己弄得像個(gè)可悲的木偶 為什么總是臨風(fēng)長(zhǎng)嗟對(duì)月灑淚 為什么要在黑夜里乞求黎明 為什么總感嘆青春不再韶華不...
    單身的姿勢(shì)閱讀 223評(píng)論 0 0

友情鏈接更多精彩內(nèi)容