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库来实现 ...
随机推荐
- 学大伟业 Day 2 培训总结
一.dp 动态规划的本质 是一种思想.通过对原问题划分成子问题,寻找子问题之间的联系,通过求解子问题得出原问题的解.与贪心不同的是,动归是深谋远虑,考虑全局最优解:而贪心则目光短浅,只考虑局部最优解. ...
- C#基础 一(方法详解)
需要知道:类和方法的关系 方法和参数修饰符 自定义方法可以有或没有参数,也可以有或没有返回值.可以被各种关键字(static.virtual.public.new等)修饰以限制其行为. C#参数修饰符 ...
- js面向对象编程——创建对象
JavaScript对每个创建的对象都会设置一个原型,指向它的原型对象. 当我们用obj.xxx访问一个对象的属性时,JavaScript引擎先在当前对象上查找该属性,如果没有找到,就到其原型对象上找 ...
- vue2.0移除或更改的一些东西
一.vue2.0移除了$index和$key 虽然说现在很多文章说他们的代码是vue2.0版本的,但是有一些仔细一看,发现并不全是2.0版本,有些语法还是1.0的版本,比如这个$index,$key, ...
- PTA 最多删除3个字符(DP) - 30分
给定一个全部由小写英文字母组成的字符串,允许你至多删掉其中 3 个字符,结果可能有多少种不同的字符串? 输入格式: 输入在一行中给出全部由小写英文字母组成的.长度在区间 [4, 1] 内的字符串. 输 ...
- 选择客栈(noip2011 day1 t2)
题目描述 丽江河边有 n 家很有特色的客栈,客栈按照其位置顺序从 1 到 nn n 编号.每家客栈都按照某一种色调进行装饰(总共 k 种,用整数 0 ~ k−1 表示),且每家客栈都设有一家咖啡店,每 ...
- CentOS 7.4使用yum源安装php7.2
1.如果之前已经安装我们先卸载一下 yum -y remove php* 2.由于linux的yum源不存在php7.x,所以我们要更改yum源 rpm -Uvh https://dl.fedorap ...
- JS如何给ul下的所有li绑定点击事件,点击使其弹出下标和内容
这是一个非常常见的面试题,出题方式多样,但考察点相同,下面我们来看看这几种方法:方法一: var itemli = document.getElementsByTagName("li&quo ...
- 修改二维码生成插件jquery.qrcode.js支持加入自定义LOGO
1,将jquery.qrcode.min.js和jquery添加到您的网页中 <script src="jquery.min.js"></script> & ...
- phpredis命令
<?php //redis //检查一个扩展是否已经加载.大小写不敏感. if (!function_exists('redis')) { echo '不支持 redis'; return ; ...