• 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;
            }
        }
    }
    View Code
    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;
            }
        }
    }
    View Code

    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;
            }
    View Code
            [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;
            }
    View Code

          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; } = 0;
        }
    }
    View Code

              (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);
            }
    View Code

     

                     --> 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));
            }
    View Code

     

  • 相关阅读:
    深入分析java的clone
    java实现文件夹遍历
    插入排序的C语言实现
    java动态代理工厂类
    HTML 理解标签 帧
    贝塞尔曲线初探
    警告:未初始化的变量引起运行问题
    解释string类型的输入操作符和getline函数分别如何处理空白符
    程序书写规范笔记
    vector
  • 原文地址:https://www.cnblogs.com/fuyouchen/p/9593373.html
Copyright © 2020-2023  润新知