HATEOAS
Hypermedia as the Engine of Application State
REST里最复杂的约束, 构建成熟REST API的核心
- 可进化性, 自我描述
- 超媒体(Hypermedia, 例如超链接)驱动如何消费和使用API
不使用HATEOAS
- 客户端更多的需要了解API内在逻辑
- 如果API发生了一点变化(添加了额外的规则, 改变规则)都会破坏API的消费者.
-
API无法独立于消费它的应用进行进化.
使用HATEOAS
- 这个response里面包含了若干link, 第一个link包含着获取当前响应的链接, 第二个link则告诉客户端如何去更新该post.
-
不改变响应主体结果的情况下添加另外一个删除的功能(link), 客户端通过响应里的links就会发现这个删除功能, 但是对其他部分都没有影响.
展示链接
- JSON和XML并没有如何展示link的概念. 但是HTML的anchor元素却知道: .
- href包含了URI
- rel则描述了link如何和资源的关系
- type是可选的, 它表示了媒体的类型
- 我们的例子:
- method: 定义了需要使用的方法
- rel: 表明了动作的类型
-
href: 包含了执行这个动作所包含的URI.
实现
- 静态基类
需要基类(包含link)和包装类, 也就是返回的资源里面都含有link, 通过继承于同一个基类来实现 -
动态类型
, 需要使用例如匿名类或ExpandoObject等
* 对于单个资源可以使用ExpandoObject
* 对于集合类资源则使用匿名类.
- 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; }
}
- 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;
}
- GETPOST中调用
//单个资源塑性
var shapedPostResource = postResource.ToDynamic(fields);
//加载link
var links = CreateLinksForPost(id, fields);
//整合返回数据
var result = shapedPostResource as IDictionary;
result.Add("links", links);
return Ok(result);
//集合资源塑性
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;
});
- 集合资源整体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
};
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));
}
}
使用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));
}