参照 草根专栏- ASP.NET Core + Ng6 实战:https://v.qq.com/x/page/d07652pu1zi.html
一、Get返回资源塑形
1、添加集合塑形EnumerableExtensions.cs,单个塑形类ObjectExtensions.cs:
namespace BlogDemo.Infrastructure.Extensions { public static class EnumerableExtensions { public static IEnumerable<ExpandoObject> ToDynamicIEnumerable<TSource>(this IEnumerable<TSource> source, string fields = null) { if (source == null) { throw new ArgumentNullException(nameof(source)); } var expandoObjectList = new List<ExpandoObject>(); var propertyInfoList = new List<PropertyInfo>(); if (string.IsNullOrWhiteSpace(fields)) { var propertyInfos = typeof(TSource).GetProperties(BindingFlags.Public | BindingFlags.Instance); propertyInfoList.AddRange(propertyInfos); } else { var fieldsAfterSplit = fields.Split(',').ToList(); foreach (var field in fieldsAfterSplit) { var propertyName = field.Trim(); if (string.IsNullOrEmpty(propertyName)) { continue; } var propertyInfo = typeof(TSource).GetProperty(propertyName, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance); if (propertyInfo == null) { throw new Exception($"Property {propertyName} wasn't found on {typeof(TSource)}"); } propertyInfoList.Add(propertyInfo); } } foreach (TSource sourceObject in source) { var dataShapedObject = new ExpandoObject(); foreach (var propertyInfo in propertyInfoList) { var propertyValue = propertyInfo.GetValue(sourceObject); ((IDictionary<string, object>)dataShapedObject).Add(propertyInfo.Name, propertyValue); } expandoObjectList.Add(dataShapedObject); } return expandoObjectList; } } }
namespace BlogDemo.Infrastructure.Extensions { public static class ObjectExtensions { public static ExpandoObject ToDynamic<TSource>(this TSource source, string fields = null) { if (source == null) { throw new ArgumentNullException(nameof(source)); } var dataShapedObject = new ExpandoObject(); if (string.IsNullOrWhiteSpace(fields)) { var propertyInfos = typeof(TSource).GetProperties(BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance); foreach (var propertyInfo in propertyInfos) { var propertyValue = propertyInfo.GetValue(source); ((IDictionary<string, object>)dataShapedObject).Add(propertyInfo.Name, propertyValue); } return dataShapedObject; } var fieldsAfterSplit = fields.Split(',').ToList(); foreach (var field in fieldsAfterSplit) { var propertyName = field.Trim(); var propertyInfo = typeof(TSource).GetProperty(propertyName, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance); if (propertyInfo == null) { throw new Exception($"Can't found property ¡®{typeof(TSource)}¡¯ on ¡®{propertyName}¡¯"); } var propertyValue = propertyInfo.GetValue(source); ((IDictionary<string, object>)dataShapedObject).Add(propertyInfo.Name, propertyValue); } return dataShapedObject; } } }
2、Controller修改Action方法:
(1) 集合塑形:
[HttpGet(Name = "GetPosts")] public async Task<IActionResult> Get(PostParameters parameters) { var posts = await _postRepository.GetPostsAsync(parameters); var postDto=_mapper.Map<IEnumerable<Post>,IEnumerable<PostDTO>>(posts); var shapePostDTO= postDto.ToDynamicIEnumerable(parameters.Fields); var previousPageLink = posts.HasPrevious ? CreatePostUri(parameters, PaginationResourceUriType.PreviousPage) : null; var nextPageLink = posts.HasNext ? CreatePostUri(parameters, PaginationResourceUriType.NextPage) : null; var meta = new { PageSize = posts.PageSize, PageIndex = posts.PageIndex, TotalItemCount = posts.TotalItemsCount, PageCount = posts.PageCount, previousPageLink, nextPageLink }; Response.Headers.Add("X-Pagination", JsonConvert.SerializeObject(meta, new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() })); return Ok(shapePostDTO); }
(2)单个塑形:
[HttpGet("{Id}")] public async Task<IActionResult> Get(int Id,string fields=null) { var post = await _postRepository.GetPostId(Id); if(post==null) { return NotFound(); } var postDTO = _mapper.Map<Post, PostDTO>(post); var shapePostDTO = postDTO.ToDynamic(fields); return Ok(shapePostDTO); }
3. 将json返回的首字母转化为小写:
services.AddMvc(option => { option.ReturnHttpNotAcceptable = true; option.OutputFormatters.Add(new XmlDataContractSerializerOutputFormatter()); }).AddJsonOptions(options=> { options.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver(); });
4、Postman测试:
(1)集合塑形
(2)单个塑形:
5、Action中验证filed是否存在:
//验证排序属性映射是否存在 if (!_propertyMappingContainer.ValidateMappingExistsFor<PostDTO, Post>(parameters.OrderBy)) { return BadRequest("Can't finds fields for sorting."); } //验证Filed是否存在 if (!_typeHelperService.TypeHasProperties<PostDTO>(parameters.Fields)) { return BadRequest("Filed not exits"); }
services.AddTransient<ITypeHelperService, TypeHelperService>();
二、HATEOAS (Hypermedia as the Engine of Application State)
1、 REST里最复杂的约束, 构建成熟REST API的核心
- 可进化性, 自我描述
- 超媒体(Hypermedia, 例如超链接)驱动如何消费和使用API
2、不使用HATEOAS
- 客户端更多的需要了解API内在逻辑
- 如果API发生了一点变化(添加了额外的规则, 改变规则)都会破坏API的消费者.
- API无法独立于消费它的应用进行进化.
3、使用HATEOAS
- 这个response里面包含了若干link, 第一个link包含着获取当前响应的链接, 第二个link则告诉客户端如何去更新该post.
- 不改变响应主体结果的情况下添加另外一个删除的功能(link), 客户端通过响应里的links就会发现这个删除功能, 但是对其他部分都没有影响.
4、HATEOAS – 展示链接
- JSON和XML并没有如何展示link的概念. 但是HTML的anchor元素却知道: <a href="uri" rel="type" type="media type">.
- href包含了URI
- rel则描述了link如何和资源的关系
- type是可选的, 它表示了媒体的类型
- 我们的例子:
- method: 定义了需要使用的方法
- rel: 表明了动作的类型
- href: 包含了执行这个动作所包含的URI.
5、如何实现HATEOAS
- 静态基类
- 需要基类(包含link)和包装类, 也就是返回的资源里面都含有link, 通过继承于同一个基类来实现
- 动态类型, 需要使用例如匿名类或ExpandoObject等
- 对于单个资源可以使用ExpandoObject
- 对于集合类资源则使用匿名类.
6、HATEOAS – 动态类型方案
(1) 建立 LinkResource.cs 类
namespace BlogDemo.Infrastructure.Resources { 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; } } }
(2)单个对象
Controller中添加 CreateLinksForPost() 方法
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; }
[HttpGet("{Id}", Name = "GetPost")] public async Task<IActionResult> Get(int Id,string fields=null) { //验证Filed是否存在 if (!_typeHelperService.TypeHasProperties<PostDTO>(fields)) { return BadRequest("Filed not exits"); } var post = await _postRepository.GetPostId(Id); if(post==null) { return NotFound(); } var postDTO = _mapper.Map<Post, PostDTO>(post); var shapePostDTO = postDTO.ToDynamic(fields); var links = CreateLinksForPost(Id, fields); var result = (IDictionary<string, object>)shapePostDTO; result.Add("links", links); return Ok(result); }
(3)集合对象
在Controller中添加 CreateLinksForPosts() 方法:
private IEnumerable<LinkResource> CreateLinksForPosts(PostParameters postResourceParameters, bool hasPrevious, bool hasNext) { var links = new List<LinkResource> { new LinkResource( CreatePostUri(postResourceParameters, PaginationResourceUriType.CurrentPage), "self", "GET") }; if (hasPrevious) { links.Add( new LinkResource( CreatePostUri(postResourceParameters, PaginationResourceUriType.PreviousPage), "previous_page", "GET")); } if (hasNext) { links.Add( new LinkResource( CreatePostUri(postResourceParameters, PaginationResourceUriType.NextPage), "next_page", "GET")); } return links; }
7、自定义Media Type
创建供应商特定媒体类型 Vendor-specific media type 上例中使用application/json会破坏了资源的自我描述性这条约束, API消费者无法从content-type的类型来正确的解析响应.
- application/vnd.mycompany.hateoas+json
- vnd是vendor的缩写,这一条是mime type的原则,表示这个媒体类型是供应商特定的
- 自定义的标识,也可能还包括额外的值,这里我是用的是公司名,随后是hateoas表示返回的响应里面要包含链接
- “+json”
- 在Startup里注册.
(1) 创建RequestHeaderMatchingMediaTypeAttribute.cs类
namespace BlogDemo.Api.Helpers { [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; } }
(2)注册自定义mediatype
services.AddMvc(option => { option.ReturnHttpNotAcceptable = true; // option.OutputFormatters.Add(new XmlDataContractSerializerOutputFormatter()); var outputFormatter = option.OutputFormatters.OfType<JsonOutputFormatter>().FirstOrDefault(); if(outputFormatter!=null) { outputFormatter.SupportedMediaTypes.Add("application/vnd.cfy.hateoas+json"); } })
(3)修改Action
--> MediaType="application/vnd.cgzl.hateoas+json"
[HttpGet(Name = "GetPosts")] [RequestHeaderMatchingMediaType("Accept", new[] { "application/vnd.cgzl.hateoas+json" })] public async Task<IActionResult> GetHateoas(PostParameters parameters,[FromHeader(Name ="Accept")] string mediaType) { //验证排序属性映射是否存在 if (!_propertyMappingContainer.ValidateMappingExistsFor<PostDTO, Post>(parameters.OrderBy)) { return BadRequest("Can't finds fields for sorting."); } //验证Filed是否存在 if (!_typeHelperService.TypeHasProperties<PostDTO>(parameters.Fields)) { return BadRequest("Filed not exits"); } var posts = await _postRepository.GetPostsAsync(parameters); var postDto=_mapper.Map<IEnumerable<Post>,IEnumerable<PostDTO>>(posts); var shapePostDTO = postDto.ToDynamicIEnumerable(parameters.Fields); var previousPageLink = posts.HasPrevious ? CreatePostUri(parameters, PaginationResourceUriType.PreviousPage) : null; var nextPageLink = posts.HasNext ? CreatePostUri(parameters, PaginationResourceUriType.NextPage) : null; var shapedWithLinks = shapePostDTO.Select(x => { var dict = x as IDictionary<string, object>; var postLinks = CreateLinksForPost((int)dict["Id"], parameters.Fields); dict.Add("links", postLinks); return dict; }); var links = CreateLinksForPosts(parameters, posts.HasPrevious, posts.HasNext); var result = new { value = shapedWithLinks, links }; var meta = new { PageSize = posts.PageSize, PageIndex = posts.PageIndex, TotalItemCount = posts.TotalItemsCount, PageCount = posts.PageCount, previousPageLink, nextPageLink }; Response.Headers.Add("X-Pagination", JsonConvert.SerializeObject(meta, new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() })); return Ok(result); }
--> MediaType="application/json"
[HttpGet(Name = "GetPosts")] [RequestHeaderMatchingMediaType("Accept", new[] { "application/json" })] public async Task<IActionResult> Get(PostParameters postParameters) { if (!_propertyMappingContainer.ValidateMappingExistsFor<PostDTO, Post>(postParameters.OrderBy)) { return BadRequest("Can't finds fields for sorting."); } if (!_typeHelperService.TypeHasProperties<PostDTO>(postParameters.Fields)) { return BadRequest("Fields not exist."); } var postList = await _postRepository.GetPostsAsync(postParameters); var postResources = _mapper.Map<IEnumerable<Post>, IEnumerable<PostDTO>>(postList); var previousPageLink = postList.HasPrevious ? CreatePostUri(postParameters, PaginationResourceUriType.PreviousPage) : null; var nextPageLink = postList.HasNext ? CreatePostUri(postParameters, PaginationResourceUriType.NextPage) : null; var meta = new { postList.TotalItemsCount, postList.PageSize, postList.PageIndex, postList.PageCount, previousPageLink, nextPageLink }; Response.Headers.Add("X-Pagination", JsonConvert.SerializeObject(meta, new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() })); return Ok(postResources.ToDynamicIEnumerable(postParameters.Fields)); }