使用 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:
結果:
由于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結構. 有空我再寫一個翻頁的吧.
原文地址?https://www.cnblogs.com/cgzl/p/8745631.html
.NET社區新聞,深度好文,歡迎訪問公眾號文章匯總 http://www.csharpkit.com
總結
以上是生活随笔為你收集整理的使用 dynamic 类型让 ASP.NET Core 实现 HATEOAS 结构的 RESTful API的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 谈谈ASP.NET Core中的Resp
- 下一篇: 中国到底有多少个.NET 程序员?都在哪