上一篇寫的是使用靜態基類方法的實現步驟: 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結構. 有空我再寫一個翻頁的吧.