参照 草根专栏- 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">.
  1. href包含了URI
  2. rel则描述了link如何和资源的关系
  3. type是可选的, 它表示了媒体的类型
  • 我们的例子:
  1. method: 定义了需要使用的方法
  2. rel: 表明了动作的类型
  3. href: 包含了执行这个动作所包含的URI.

      5、如何实现HATEOAS

  • 静态基类
  1. 需要基类(包含link)和包装类, 也就是返回的资源里面都含有link, 通过继承于同一个基类来实现
  • 动态类型, 需要使用例如匿名类或ExpandoObject等
  1. 对于单个资源可以使用ExpandoObject
  2. 对于集合类资源则使用匿名类.

      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
  1. vnd是vendor的缩写,这一条是mime type的原则,表示这个媒体类型是供应商特定的
  2. 自定义的标识,也可能还包括额外的值,这里我是用的是公司名,随后是hateoas表示返回的响应里面要包含链接
  3. “+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的更多相关文章

  1. ASP.NET Core 资源打包与压缩

    ASP.NET Core 资源打包与压缩 在ASP.NET 中可以使用打包与压缩来提高Web应用程序页面加载的性能. 打包是将多个文件(CSS,JS等资源文件)合并或打包到单个文件.文件合并可减少We ...

  2. 【ASP.Net】 web api中的media type

    1. 有什么用? 通常用来标识http请求中的内容的类型用来告诉server端如何解析client端发送的message, 或者标识client希望从server端得到的资源是什么样的类型.又被称为M ...

  3. 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 ...

  4. 用ASP.NET Core 2.1 建立规范的 REST API -- HATEOAS

    本文所需的一些预备知识可以看这里: http://www.cnblogs.com/cgzl/p/9010978.html 和 http://www.cnblogs.com/cgzl/p/9019314 ...

  5. 使用静态基类方案让 ASP.NET Core 实现遵循 HATEOAS Restful Web API

    Hypermedia As The Engine Of Application State (HATEOAS) HATEOAS(Hypermedia as the engine of applicat ...

  6. 用ASP.NET Core 2.1 建立规范的 REST API -- 翻页/排序/过滤等

    本文所需的一些预备知识可以看这里: http://www.cnblogs.com/cgzl/p/9010978.html 和 http://www.cnblogs.com/cgzl/p/9019314 ...

  7. ASP.NET Core 2 学习笔记(十二)REST-Like API

    Restful几乎已算是API设计的标准,通过HTTP Method区分新增(Create).查询(Read).修改(Update)和删除(Delete),简称CRUD四种数据存取方式,简约又直接的风 ...

  8. Asp.Net Core 3.0 学习3、Web Api 文件上传 Ajax请求以及跨域问题

    1.创建Api项目 我用的是VS2019 Core3.1 .打开Vs2019 创建Asp.Net Core Web应用程序命名CoreWebApi 创建选择API 在Controller文件夹下面添加 ...

  9. ASP.NET Core多语言 (转载)

    ASP.NET Core中提供了一些本地化服务和中间件,可将网站本地化为不同的语言文化.ASP.NET Core中我们可以使用Microsoft.AspNetCore.Localization库来实现 ...

随机推荐

  1. 学大伟业 Day 2 培训总结

    一.dp 动态规划的本质 是一种思想.通过对原问题划分成子问题,寻找子问题之间的联系,通过求解子问题得出原问题的解.与贪心不同的是,动归是深谋远虑,考虑全局最优解:而贪心则目光短浅,只考虑局部最优解. ...

  2. C#基础 一(方法详解)

    需要知道:类和方法的关系 方法和参数修饰符 自定义方法可以有或没有参数,也可以有或没有返回值.可以被各种关键字(static.virtual.public.new等)修饰以限制其行为. C#参数修饰符 ...

  3. js面向对象编程——创建对象

    JavaScript对每个创建的对象都会设置一个原型,指向它的原型对象. 当我们用obj.xxx访问一个对象的属性时,JavaScript引擎先在当前对象上查找该属性,如果没有找到,就到其原型对象上找 ...

  4. vue2.0移除或更改的一些东西

    一.vue2.0移除了$index和$key 虽然说现在很多文章说他们的代码是vue2.0版本的,但是有一些仔细一看,发现并不全是2.0版本,有些语法还是1.0的版本,比如这个$index,$key, ...

  5. PTA 最多删除3个字符(DP) - 30分

    给定一个全部由小写英文字母组成的字符串,允许你至多删掉其中 3 个字符,结果可能有多少种不同的字符串? 输入格式: 输入在一行中给出全部由小写英文字母组成的.长度在区间 [4, 1] 内的字符串. 输 ...

  6. 选择客栈(noip2011 day1 t2)

    题目描述 丽江河边有 n 家很有特色的客栈,客栈按照其位置顺序从 1 到 nn n 编号.每家客栈都按照某一种色调进行装饰(总共 k 种,用整数 0 ~ k−1 表示),且每家客栈都设有一家咖啡店,每 ...

  7. CentOS 7.4使用yum源安装php7.2

    1.如果之前已经安装我们先卸载一下 yum -y remove php* 2.由于linux的yum源不存在php7.x,所以我们要更改yum源 rpm -Uvh https://dl.fedorap ...

  8. JS如何给ul下的所有li绑定点击事件,点击使其弹出下标和内容

    这是一个非常常见的面试题,出题方式多样,但考察点相同,下面我们来看看这几种方法:方法一: var itemli = document.getElementsByTagName("li&quo ...

  9. 修改二维码生成插件jquery.qrcode.js支持加入自定义LOGO

    1,将jquery.qrcode.min.js和jquery添加到您的网页中 <script src="jquery.min.js"></script> & ...

  10. phpredis命令

    <?php //redis //检查一个扩展是否已经加载.大小写不敏感. if (!function_exists('redis')) { echo '不支持 redis'; return ; ...