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ì)于集合類資源則使用匿名類.
- 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
//為每個(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;
}
- 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
- 集合資源整體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));
}


