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

Posted dotNET跨平台

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了使用 dynamic 类型让 ASP.NET Core 实现 HATEOAS 结构的 RESTful API相关的知识,希望对你有一定的参考价值。

使用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:

结果:

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

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

下面试一下GET:

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

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

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

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();

然后再试试:

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

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);

        }

红色部分是相关代码.

测试一下:

不选择属性:

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

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

选择部分属性:

OK. 

HATEOAS这部分就写到这.

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


以上是关于使用 dynamic 类型让 ASP.NET Core 实现 HATEOAS 结构的 RESTful API的主要内容,如果未能解决你的问题,请参考以下文章

ASP .NET WebAPI 路由数据架构

Asp.net mvc基础Controller给View传递数据的方式

ASP.NET MVC中viewDataviewBag和templateData的使用与区别

Asp.net MVC中的ViewData与ViewBag

经典 ASP 和 ASP.NET 集成

ViewBag & ViewData