zoukankan      html  css  js  c++  java
  • 使用 dynamic 类型让 ASP.NET Core 实现 HATEOAS 结构的 RESTful API

    上一篇写的是使用静态基类方法的实现步骤:  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结构. 有空我再写一个翻页的吧.

  • 相关阅读:
    el自定义函数库
    DOM4J
    【转载】SqlServer日期时间函数
    【原创】C#认识/理解/运用 StreamReader,StreamWriter,StringReader,StreamWriter
    【原创】C#认识/理解/运用 FileStream
    【原创】C#操作XML(带命名空间)
    【原创】ASP.NET MVC3使用html编辑器(kindeditor)
    【原创】ASP.NET MVC3 从零开始一步步构建Web
    【转载】MVC使用jqGrid
    【原创】C#使用HttpWebRequest,HttpWebResponse
  • 原文地址:https://www.cnblogs.com/cgzl/p/8745631.html
Copyright © 2011-2022 走看看