ASP NET Core --- 资源塑形, HATEOAS, Media Type
参照 草根专栏- 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; } = ;
- }
- }
(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));
- }
ASP NET Core --- 资源塑形, HATEOAS, Media Type的更多相关文章
- ASP.NET Core 资源打包与压缩
ASP.NET Core 资源打包与压缩 在ASP.NET 中可以使用打包与压缩来提高Web应用程序页面加载的性能. 打包是将多个文件(CSS,JS等资源文件)合并或打包到单个文件.文件合并可减少We ...
- 【ASP.Net】 web api中的media type
1. 有什么用? 通常用来标识http请求中的内容的类型用来告诉server端如何解析client端发送的message, 或者标识client希望从server端得到的资源是什么样的类型.又被称为M ...
- asp.net core 3.0 JObject The collection type 'Newtonsoft.Json.Linq.JObject' is not supported
在asp.net core 3.0 中,如果直接在Controller中返回 Jobject 类型,会抛出如下错误: The collection type 'Newtonsoft.Json.Linq ...
- 用ASP.NET Core 2.1 建立规范的 REST API -- HATEOAS
本文所需的一些预备知识可以看这里: http://www.cnblogs.com/cgzl/p/9010978.html 和 http://www.cnblogs.com/cgzl/p/9019314 ...
- 使用静态基类方案让 ASP.NET Core 实现遵循 HATEOAS Restful Web API
Hypermedia As The Engine Of Application State (HATEOAS) HATEOAS(Hypermedia as the engine of applicat ...
- 用ASP.NET Core 2.1 建立规范的 REST API -- 翻页/排序/过滤等
本文所需的一些预备知识可以看这里: http://www.cnblogs.com/cgzl/p/9010978.html 和 http://www.cnblogs.com/cgzl/p/9019314 ...
- ASP.NET Core 2 学习笔记(十二)REST-Like API
Restful几乎已算是API设计的标准,通过HTTP Method区分新增(Create).查询(Read).修改(Update)和删除(Delete),简称CRUD四种数据存取方式,简约又直接的风 ...
- Asp.Net Core 3.0 学习3、Web Api 文件上传 Ajax请求以及跨域问题
1.创建Api项目 我用的是VS2019 Core3.1 .打开Vs2019 创建Asp.Net Core Web应用程序命名CoreWebApi 创建选择API 在Controller文件夹下面添加 ...
- ASP.NET Core多语言 (转载)
ASP.NET Core中提供了一些本地化服务和中间件,可将网站本地化为不同的语言文化.ASP.NET Core中我们可以使用Microsoft.AspNetCore.Localization库来实现 ...
随机推荐
- 【luogu P1343 地震逃生】 题解
题目链接:https://www.luogu.org/problemnew/show/P1343 菜 #include <queue> #include <cstdio> #i ...
- HTML5 小实例
1.时钟: <!doctype html> <html> <head></head> <body> <canvas id=" ...
- mssql数据库迁移到mysql
使用mysql migration toolkit工具来进行迁移.(需要安装jdk6 java的安装包) 发现数据量大的表却没能迁过来.软件使用比较容易,配置下源数据库信息,和目标数据库信息就可以进行 ...
- centos 安装配置 rabbitmq 以及nginx转发
安装erlang cd /tmp wget http://erlang.org/download/otp_src_18.3.tar.gz . cd /opt/otp_src_18. yum -y in ...
- 课时92.CSS元素显示模式转换(掌握)
我们之前学习的显示模式都可以不用记忆,因为这节课我们要学习转换,我们可以任意来进行一个转换的,上面这些东西有一个了解就行了.所有的标签都有一个属性叫做display,display的中文含义就是显示的 ...
- zepto 基础知识(4)
61.prev prev() 类型:collection prev(selector) 类型:collection 获取对相集合中每一个元素的钱一个兄弟节点,通过选择器来进行过滤 62.prev pr ...
- Lucene作为一个全文检索引擎
Lucene作为一个全文检索引擎,其具有如下突出的优点: (1)索引文件格式独立于应用平台.Lucene定义了一套以8位字节为基础的索引文件格式,使得兼容系统或者不同平台的应用能够共享建立的索引文件. ...
- git merge最简洁
一.开发分支(dev)上的代码达到上线的标准后,要合并到 master 分支 git checkout devgit pullgit checkout mastergit merge devgit p ...
- dedesmc 手机端生成静态页
dedesmc 手机端生成静态页 1.首先下载插件,下载地址:https://pan.baidu.com/s/1Nfx_KBYuxRkZ7VzoPxy28g 密码:83x7 2.进入 dedecms ...
- 源码安装CentOs7下的PHP7
首先安装APACHE环境,直接用yum安装 yum install httpd httpd-devel /etc/httpd/ systemctl start httpd.service #启动apa ...