使用 dynamic 类型让 ASP.NET Core 实现 HATEOAS 结构的 RESTful API

简介: 上一篇写的是使用静态基类方法的实现步骤:  http://www.cnblogs.com/cgzl/p/8726805.html 使用dynamic (ExpandoObject)的好处就是可以动态组建返回类型, 之前使用的是ViewModel, 如果想返回结果的话, 肯定需要把ViewModel所有的属性都返回, 如果属性比较多, 就有可能造成性能和灵活性等问题.

上一篇写的是使用静态基类方法的实现步骤:  http://www.cnblogs.com/cgzl/p/8726805.html

使用dynamic (ExpandoObject)的好处就是可以动态组建返回类型, 之前使用的是ViewModel, 如果想返回结果的话, 肯定需要把ViewModel所有的属性都返回, 如果属性比较多, 就有可能造成性能和灵活性等问题. 而使用ExpandoObject(dynamic)就可以解决这个问题.

返回一个对象

返回一个dynamic类型的对象, 需要把所需要的属性从ViewModel抽取出来并转化成dynamic对象, 这里所需要的属性通常是从参数传进来的, 例如针对下面的CustomerViewModel类, 参数可能是这样的: "Name, Company":

using System;
using SalesApi.Core.Abstractions.DomainModels;

namespace SalesApi.ViewModels
{
    public class CustomerViewModel: EntityBase
    {
        public string Company { get; set; }
        public string Name { get; set; }
        public DateTimeOffset EstablishmentTime { get; set; }
    }
}

还需要一个Extension Method可以把对象按照需要的属性转化成dynamic类型:

using System;
using System.Collections.Generic;
using System.Dynamic;
using System.Reflection;

namespace SalesApi.Shared.Helpers
{
    public static class ObjectExtensions
    {
        public static ExpandoObject ToDynamic<TSource>(this TSource source, string fields = null)
        {
            if (source == null)
            {
                throw new ArgumentNullException("source");
            }

            var dataShapedObject = new ExpandoObject();
            if (string.IsNullOrWhiteSpace(fields))
            {
                // 所有的 public properties 应该包含在ExpandoObject里 
                var propertyInfos = typeof(TSource).GetProperties(BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance);
                foreach (var propertyInfo in propertyInfos)
                {
                    // 取得源对象上该property的值
                    var propertyValue = propertyInfo.GetValue(source);
                    // 为ExpandoObject添加field
                    ((IDictionary<string, object>)dataShapedObject).Add(propertyInfo.Name, propertyValue);
                }
                return dataShapedObject;
            }

            // field是使用 "," 分割的, 这里是进行分割动作.
            var fieldsAfterSplit = fields.Split(',');
            foreach (var field in fieldsAfterSplit)
            {
                var propertyName = field.Trim();

                // 使用反射来获取源对象上的property
                // 需要包括public和实例属性, 并忽略大小写.
                var propertyInfo = typeof(TSource).GetProperty(propertyName, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance);
                if (propertyInfo == null)
                {
                    throw new Exception($"没有在‘{typeof(TSource)}’上找到‘{propertyName}’这个Property");
                }

                // 取得源对象property的值
                var propertyValue = propertyInfo.GetValue(source);
                // 为ExpandoObject添加field
                ((IDictionary<string, object>)dataShapedObject).Add(propertyInfo.Name, propertyValue);
            }

            return dataShapedObject;
        }
    }
}

注意: 这里的逻辑是如果没有选择需要的属性的话, 那么就返回所有合适的属性.

然后在CustomerController里面:

首先创建为对象添加link的方法:

        private IEnumerable<LinkViewModel> CreateLinksForCustomer(int id, string fields = null)
        {
            var links = new List<LinkViewModel>();
            if (string.IsNullOrWhiteSpace(fields))
            {
                links.Add(
                    new LinkViewModel(_urlHelper.Link("GetCustomer", new { id = id }),
                    "self",
                    "GET"));
            }
            else
            {
                links.Add(
                    new LinkViewModel(_urlHelper.Link("GetCustomer", new { id = id, fields = fields }),
                    "self",
                    "GET"));
            }

            links.Add(
                new LinkViewModel(_urlHelper.Link("DeleteCustomer", new { id = id }),
                "delete_customer",
                "DELETE"));

            links.Add(
                new LinkViewModel(_urlHelper.Link("CreateCustomer", new { id = id }),
                "create_customer",
                "POST"));

            return links;
        }

针对返回一个对象, 添加了本身的连接, 添加的连接 以及 删除的连接.

然后修改Get和Post的Action:

        [HttpGet]
        [Route("{id}", Name = "GetCustomer")]
        public async Task<IActionResult> Get(int id, string fields)
        {
            var item = await _customerRepository.GetSingleAsync(id);
            if (item == null)
            {
                return NotFound();
            }
            var customerVm = Mapper.Map<CustomerViewModel>(item);
            var links = CreateLinksForCustomer(id, fields);
            var dynamicObject = customerVm.ToDynamic(fields) as IDictionary<string, object>;
            dynamicObject.Add("links", links);
            return Ok(dynamicObject);
        }

        [HttpPost(Name = "CreateCustomer")]
        public async Task<IActionResult> Post([FromBody] CustomerViewModel customerVm)
        {
            if (customerVm == null)
            {
                return BadRequest();
            }

            if (!ModelState.IsValid)
            {
                return BadRequest(ModelState);
            }

            var newItem = Mapper.Map<Customer>(customerVm);
            _customerRepository.Add(newItem);
            if (!await UnitOfWork.SaveAsync())
            {
                return StatusCode(500, "保存时出错");
            }

            var vm = Mapper.Map<CustomerViewModel>(newItem);

            var links = CreateLinksForCustomer(vm.Id);
            var dynamicObject = vm.ToDynamic() as IDictionary<string, object>;
            dynamicObject.Add("links", links);

            return CreatedAtRoute("GetCustomer", new { id = dynamicObject["Id"] }, dynamicObject);
        }

红色部分是相关的代码. 创建links之后把vm对象按照需要的属性转化成dynamic对象. 然后往这个dynamic对象里面添加links属性. 最后返回该对象.

下面测试一下.

POST:

结果:

由于POST方法里面没有选择任何fields, 所以返回所有的属性.

下面试一下GET:

 

再试一下GET, 选择几个fields:

OK, 效果都如预期.

但是有一个问题, 因为返回的json的Pascal case的(只有dynamic对象返回的是Pascal case, 其他ViewModel现在返回的都是camel case的), 而camel case才是更好的选择 .

所以在Startup里面可以这样设置:

            services.AddMvc(options =>
            {
                options.ReturnHttpNotAcceptable = true;
                // the default formatter is the first one in the list.
                options.OutputFormatters.Remove(new XmlDataContractSerializerOutputFormatter());

                // set authorization on all controllers or routes
                var policy = new AuthorizationPolicyBuilder()
                    .RequireAuthenticatedUser()
                    .Build();
                options.Filters.Add(new AuthorizeFilter(policy));
            })
            .AddJsonOptions(options =>
            {
                options.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
            })
            .AddFluetValidations();

然后再试试:

OK.

 

返回集合

 首先编写创建links的方法:

        private IEnumerable<LinkViewModel> CreateLinksForCustomers(string fields = null)
        {
            var links = new List<LinkViewModel>();
            if (string.IsNullOrWhiteSpace(fields))
            {
                links.Add(
                   new LinkViewModel(_urlHelper.Link("GetAllCustomers", new { fields = fields }),
                   "self",
                   "GET"));
            }
            else
            {
                links.Add(
                   new LinkViewModel(_urlHelper.Link("GetAllCustomers", new { }),
                   "self",
                   "GET"));
            }
            return links;
        }

这个很简单.

然后需要针对IEnumerable<T>类型创建把ViewModel转化成dynamic对象的Extension方法:

using System;
using System.Collections.Generic;
using System.Dynamic;
using System.Reflection;

namespace SalesApi.Shared.Helpers
{
    public static class IEnumerableExtensions
    {
        public static IEnumerable<ExpandoObject> ToDynamicIEnumerable<TSource>(this IEnumerable<TSource> source, string fields)
        {
            if (source == null)
            {
                throw new ArgumentNullException("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(',');
                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($"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;
        }
    }
}

注意: 反射的开销很大, 注意性能.

然后修改GetAll方法:

        [HttpGet(Name = "GetAllCustomers")]
        public async Task<IActionResult> GetAll(string fields)
        {
            var items = await _customerRepository.GetAllAsync();
            var results = Mapper.Map<IEnumerable<CustomerViewModel>>(items);
            var dynamicList = results.ToDynamicIEnumerable(fields);
            var links = CreateLinksForCustomers(fields);
            var dynamicListWithLinks = dynamicList.Select(customer =>
            {
                var customerDictionary = customer as IDictionary<string, object>;
                var customerLinks = CreateLinksForCustomer(
                    (int)customerDictionary["Id"], fields);
                customerDictionary.Add("links", customerLinks);
                return customerDictionary;
            });
            var resultWithLink = new {
                Value = dynamicListWithLinks,
                Links = links
            };
            return Ok(resultWithLink);
        }

 

红色部分是相关代码.

测试一下:

不选择属性:

选择部分属性:

OK.

 

HATEOAS这部分就写到这.

其实 翻页的逻辑很适合使用HATEOAS结构. 有空我再写一个翻页的吧.

 

下面是我的关于ASP.NET Core Web API相关技术的公众号--草根专栏:

目录
相关文章
|
5天前
|
SQL 缓存 测试技术
构建高性能RESTful API:最佳实践与避坑指南###
—— 本文深入探讨了构建高性能RESTful API的关键技术要点,从设计原则、状态码使用、版本控制到安全性考虑,旨在为开发者提供一套全面的最佳实践框架。通过避免常见的设计陷阱,本文将指导你如何优化API性能,提升用户体验,确保系统的稳定性和可扩展性。 ###
37 12
|
2天前
|
JSON JavaScript API
深入浅出Node.js:从零开始构建RESTful API
【10月更文挑战第39天】 在数字化时代的浪潮中,API(应用程序编程接口)已成为连接不同软件应用的桥梁。本文将带领读者从零基础出发,逐步深入Node.js的世界,最终实现一个功能完备的RESTful API。通过实践,我们将探索如何利用Node.js的异步特性和强大的生态系统来构建高效、可扩展的服务。准备好迎接代码和概念的碰撞,一起解锁后端开发的新篇章。
|
4天前
|
存储 API 开发者
深入理解RESTful API设计原则
本文探讨了RESTful API的设计原则,强调了其在现代Web服务中的重要性。通过分析状态表示转移(REST)的概念、核心约束以及最佳实践,本文旨在为开发者提供构建高效、可扩展和易于维护的API的指导。文章还讨论了常见的设计陷阱和如何避免它们,以确保API设计的健壮性和灵活性。
|
6天前
|
JSON 缓存 API
构建高效RESTful API的最佳实践
【10月更文挑战第34天】在数字时代的浪潮中,后端开发扮演着至关重要的角色。本文将带你深入探索如何构建高效的RESTful API,从设计原则到实际编码技巧,再到性能优化和错误处理,我们将一一解锁这些技能。你将学会如何打造一个既优雅又强大的后端服务,让你的应用程序在激烈的市场竞争中脱颖而出。那么,让我们一起踏上这段精彩的旅程吧!
17 2
|
7天前
|
XML JSON API
【PHP开发专栏】PHP RESTful API设计与开发
随着互联网技术的发展,前后端分离成为Web开发的主流模式。本文介绍RESTful API的基本概念、设计原则及在PHP中的实现方法。RESTful API是一种轻量级、无状态的接口设计风格,通过HTTP方法(GET、POST、PUT、DELETE)操作资源,使用JSON或XML格式传输数据。在PHP中,通过定义路由、创建控制器、处理HTTP请求和响应等步骤实现RESTful API,并强调了安全性的重要性。
15 2
|
10天前
|
存储 安全 API
深入理解RESTful API设计原则
本文旨在探讨RESTful API设计的基本原则和最佳实践,帮助开发者构建高效、可维护的Web服务。通过分析REST架构的核心概念,如资源、统一接口、无状态通信等,本文将指导读者如何设计符合REST原则的API,以及如何处理常见的设计挑战,如版本控制、错误处理和安全性问题。
|
12天前
|
存储 缓存 API
深入理解RESTful API设计原则
【10月更文挑战第28天】 在现代软件开发中,RESTful API已经成为了前后端分离架构下不可或缺的一部分。本文将探讨RESTful API的核心设计原则,包括资源导向、无状态性、统一的接口以及可缓存性等关键概念,并通过实例解析如何在实际应用中遵循这些原则来设计高效、可扩展的API。我们将深入了解REST架构风格的理论基础,并讨论其对提升系统互操作性和简化客户端实现的重要性。
45 3
|
12天前
|
JavaScript 中间件 API
Node.js进阶:Koa框架下的RESTful API设计与实现
【10月更文挑战第28天】本文介绍了如何在Koa框架下设计与实现RESTful API。首先概述了Koa框架的特点,接着讲解了RESTful API的设计原则,包括无状态和统一接口。最后,通过一个简单的博客系统示例,详细展示了如何使用Koa和koa-router实现常见的CRUD操作,包括获取、创建、更新和删除文章。
34 4
|
14天前
|
XML API 网络架构
深入理解RESTful API设计原则与实践
【10月更文挑战第26天】在数字化浪潮中,API(应用程序编程接口)成为连接不同软件组件的桥梁。本文将深入浅出地探讨如何根据REST(Representational State Transfer)原则设计高效、易于维护和扩展的API,同时分享一些实用的代码示例,帮助开发者构建更加健壮和用户友好的服务。
|
5天前
|
JavaScript 前端开发 NoSQL
深入浅出:使用Node.js构建RESTful API
【10月更文挑战第35天】在数字时代的浪潮中,后端技术如同海洋中稳固的灯塔,为前端应用提供数据和逻辑支撑。本文旨在通过浅显易懂的方式,带领读者了解如何利用Node.js这一强大的后端平台,搭建一个高效、可靠的RESTful API。我们将从基础概念入手,逐步深入到代码实践,最终实现一个简单的API示例。这不仅是对技术的探索,也是对知识传递方式的一次创新尝试。让我们一起启航,探索Node.js的奥秘,解锁后端开发的无限可能。