ASP .NET Core Web API_ 11_HATEOAS

HATEOAS

Hypermedia as the Engine of Application State
REST里最复杂的约束, 构建成熟REST API的核心

  • 可进化性, 自我描述
  • 超媒体(Hypermedia, 例如超链接)驱动如何消费和使用API

不使用HATEOAS

  • 客户端更多的需要了解API内在逻辑
  • 如果API发生了一点变化(添加了额外的规则, 改变规则)都会破坏API的消费者.
  • API无法独立于消费它的应用进行进化.


    No HATEOAS

使用HATEOAS

  • 这个response里面包含了若干link, 第一个link包含着获取当前响应的链接, 第二个link则告诉客户端如何去更新该post.
  • 不改变响应主体结果的情况下添加另外一个删除的功能(link), 客户端通过响应里的links就会发现这个删除功能, 但是对其他部分都没有影响.


    HATEOAS

展示链接

  • JSON和XML并没有如何展示link的概念. 但是HTML的anchor元素却知道: <a href="uri" rel="type" type="media type">.
    • href包含了URI
    • rel则描述了link如何和资源的关系
    • type是可选的, 它表示了媒体的类型
  • 我们的例子:
    • method: 定义了需要使用的方法
    • rel: 表明了动作的类型
    • href: 包含了执行这个动作所包含的URI.


      show heteoas

实现

  • 静态基类
    需要基类(包含link)和包装类, 也就是返回的资源里面都含有link, 通过继承于同一个基类来实现
  • 动态类型, 需要使用例如匿名类或ExpandoObject等
    * 对于单个资源可以使用ExpandoObject
    * 对于集合类资源则使用匿名类.
  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
 //为每个资源创建链接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中调用
//单个资源塑性
var shapedPostResource = postResource.ToDynamic(fields);

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

//整合返回数据
var result = shapedPostResource as IDictionary<string, object>;
result.Add("links", links);

return Ok(result); 
单个资源link
//集合资源塑性
 var shapedPostResources = postResources.ToDynamicIEnumerable(postParameters.Fields);

//循环遍历为每个资源添加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
 //为集合资源创建整体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

创建供应商特定媒体类型
上例中使用application/json会破坏了资源的自我描述性这条约束, API消费者无法从content-type的类型来正确的解析响应.

  • application/vnd.mycompany.hateoas+json
    * vnd是vendor的缩写,这一条是mime type的原则,表示这个媒体类型是供应商特定的
    • 自定义的标识,也可能还包括额外的值,这里我是用的是公司名,随后是hateoas表示返回的响应里面要包含链接
    • +json
  • 在Startup里注册.
services.AddMvc(
  options=>
      {
          options.ReturnHttpNotAcceptable = true; //开启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);

                //循环遍历为每个资源添加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返回,元数据包含在返回的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约束分解为两个方法

 [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);

            //循环遍历为每个资源添加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返回,元数据包含在返回的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));
        }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,776评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,527评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,361评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,430评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,511评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,544评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,561评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,315评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,763评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,070评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,235评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,911评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,554评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,173评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,424评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,106评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,103评论 2 352

推荐阅读更多精彩内容

  • # Python 资源大全中文版 我想很多程序员应该记得 GitHub 上有一个 Awesome - XXX 系列...
    aimaile阅读 26,463评论 6 428
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,647评论 18 139
  • 一说到REST,我想大家的第一反应就是“啊,就是那种前后台通信方式。”但是在要求详细讲述它所提出的各个约束,以及如...
    时待吾阅读 3,421评论 0 19
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,094评论 1 32
  • 为什么要把自己弄得像个可悲的木偶 为什么总是临风长嗟对月洒泪 为什么要在黑夜里乞求黎明 为什么总感叹青春不再韶华不...
    单身的姿势阅读 211评论 0 0