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无法独立于消费它的应用进行进化.


    ASP .NET Core Web API_ 11_HATEOAS_第1张图片
    No HATEOAS

使用HATEOAS

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


    ASP .NET Core Web API_ 11_HATEOAS_第2张图片
    HATEOAS

展示链接

实现

  • 静态基类
    需要基类(包含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 CreateLinksForPost(int id,string fields = null)
{
    var links = new List();

    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;
result.Add("links", links);

return Ok(result); 
ASP .NET Core Web API_ 11_HATEOAS_第4张图片
单个资源link
//集合资源塑性
 var shapedPostResources = postResources.ToDynamicIEnumerable(postParameters.Fields);

//循环遍历为每个资源添加link
var shapdeWithLinks = shapedPostResources.Select(x =>
{
   var dict = x as IDictionary;
   var postLinks = CreateLinksForPost((int)dict["Id"], postParameters.Fields);
   dict.Add("links", psotLinks);
   return dict;
});
ASP .NET Core Web API_ 11_HATEOAS_第5张图片
集合资源遍历link
  1. 集合资源整体Link
 //为集合资源创建整体link
private IEnumerable CreateLinksForPosts(PostParameters postParameters,bool hasPrevious,bool hasNext)
{
    var links = new List
    { 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
  };
ASP .NET Core Web API_ 11_HATEOAS_第6张图片
整体资源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().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 Get(PostParameters postParameters,
            [FromHeader(Name = "Accept")] string mediaType)
        {
            if (!_propertyMappingContainer.ValidateMappingExistsFor(postParameters.OrderBy))
            {
                return BadRequest("cannot finds fields for sorting.");

            }
            if (!_typeHelperService.TypeHasProperties(postParameters.Fields))
            {
                return BadRequest("Fields not exist.");
            }
            var postList = await _postRepository.GetAllPostsAsync(postParameters);
            var postResources = _mapper.Map, IEnumerable>(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;
                    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));
            }
        }
ASP .NET Core Web API_ 11_HATEOAS_第7张图片
application/json

ASP .NET Core Web API_ 11_HATEOAS_第8张图片
application/vnd.enfi.hateoas+json

ASP .NET Core Web API_ 11_HATEOAS_第9张图片
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 GetHateoas(PostParameters postParameters)
        {
            if (!_propertyMappingContainer.ValidateMappingExistsFor(postParameters.OrderBy))
            {
                return BadRequest("cannot finds fields for sorting.");
            }
            if (!_typeHelperService.TypeHasProperties(postParameters.Fields))
            {
                return BadRequest("Fields not exist.");
            }
            var postList = await _postRepository.GetAllPostsAsync(postParameters);
            var postResources = _mapper.Map, IEnumerable>(postList);

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

            //循环遍历为每个资源添加link
            var shapdeWithLinks = shapedPostResources.Select(x =>
            {
                var dict = x as IDictionary;
                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 Get(PostParameters postParameters)
        {
            if (!_propertyMappingContainer.ValidateMappingExistsFor(postParameters.OrderBy))
            {
                return BadRequest("cannot finds fields for sorting.");
            }
            if (!_typeHelperService.TypeHasProperties(postParameters.Fields))
            {
                return BadRequest("Fields not exist.");
            }
            var postList = await _postRepository.GetAllPostsAsync(postParameters);
            var postResources = _mapper.Map, IEnumerable>(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));
        }

你可能感兴趣的:(ASP .NET Core Web API_ 11_HATEOAS)