在這個先後端分離開發的今天,前端經過調用後端提供的api接口,實現頁面數據的展現。而每每在實際場景中,會出現兩個版塊調用的數據極度類似的狀況,A頁面與B頁面所展現的列表,僅僅相差了幾個字段。前端
若是這個時候,咱們選擇將數據的全部字段一塊兒返回,則會增大了Http請求的體積,好處是後續版塊需求變化時,前端直接替換對應的字段便可,後端不須要修改返回的數據,但這樣明顯是不符合規範的,並且在特定狀況下會致使信息泄露。後端
還有一種方法,咱們針對A頁面與B頁面各自返回一個DTO或VO,這樣信息就不會泄露,但這樣卻違背了RESTful的原則,且加大了視圖(UI)與應用層(Application)之間的耦合度,應用層返回什麼數據原則上不該該由視圖決定。api
因此,咱們須要一個能夠由前端來指定api返回字段的方式,那就是數據塑形。數據結構
首先,爲DTO設計一個塑形接口,並將接口方法默認實現:app
1 public interface IShapeDto 2 { 3 /// <summary> 4 /// 數據塑形 5 /// </summary> 6 /// <param name="fields">指定的返回字段;字段之間用,分隔</param> 7 /// <returns></returns> 8 dynamic ShapeData(string fields) 9 { 10 //驗證字段是否爲空 11 if (string.IsNullOrEmpty(fields)) 12 return this; 13 //拆分字段 14 var fieldsAfterSplit = fields.Split(',', StringSplitOptions.RemoveEmptyEntries); 15 //驗證可用數量 16 if (fieldsAfterSplit.Length == 0) 17 return this; 18 //獲得當前DTO的類型 19 var dtoType = GetType(); 20 //開闢一個用於存儲有效屬性的內存 21 var newFields = new Queue<PropertyInfo>(); 22 //public指定公共成員要包括在搜索中 Instance指定實例成員要包括在搜索中 IgnoreCase指定在綁定時不該考慮成員名稱的大小寫 23 var bindingFlasgs = BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase; 24 foreach (var field in fieldsAfterSplit) 25 { 26 //搜索指定名稱的屬性 27 var propertyInfo = dtoType.GetProperty(field, bindingFlasgs); 28 //壓入符合的屬性 29 if (propertyInfo != null) 30 newFields.Enqueue(propertyInfo); 31 } 32 //建立一個即將返回的DTO對象 33 var newDto = new ExpandoObject(); 34 while (newFields.Count > 0) 35 { 36 //彈出符合的屬性 37 var newField = newFields.Dequeue(); 38 //添加自定義字段及字段值 39 newDto.TryAdd(newField.Name, newField.GetValue(this)); 40 } 41 return newDto; 42 }
這裏須要提到的是,ExpandoObject的數據結構本質上是一個Dictionary對象,其自身實現了IDictionary<string,object>,因此咱們能夠經過TryAdd()爲其添加自定義的屬性和值,從而構建了一個動態對象:前後端分離
// // 摘要: // Represents an object whose members can be dynamically added and removed at run // time. public sealed class ExpandoObject : ICollection<KeyValuePair<string, object>>, IEnumerable<KeyValuePair<string, object>>, IEnumerable, IDictionary<string, object>, INotifyPropertyChanged, IDynamicMetaObjectProvider { // // 摘要: // Initializes a new ExpandoObject that does not have members. public ExpandoObject(); }
TryAdd()在System.Collections.Generic.CollectionExtensions下:async
public static bool TryAdd<TKey, TValue>(this IDictionary<TKey, TValue> dictionary, TKey key, TValue value) where TKey : notnull;
接下來,根據功能需求設計一個DTO類,並實現IShapeDto:ide
1 public class GetPersonDto : IShapeDto 2 { 3 public string Name { set; get; } 4 public int Age { set; get; } 5 public string Address { set; get; } 6 }
而後,咱們在Service中編寫業務:this
1 public async Task<GetPersonDto> GetPerson(int id) 2 { 3 var personEntity = await personRepository.GetPersonAsync(id); 4 return mapper.Map<GetPersonDto>(personEntity);//任意形式的映射 將 Entity 轉 DTO 5 }
最後,在Controller中塑形:編碼
1 [HttpGet("{id}")] 2 public async Task<ActionResult<string>> Get(int id, string fields) 3 { 4 var resultDto= await personService.GetPerson(id) as IShapeDto; 5 return new JsonResult(resultDto.ShapeData(fields)); 6 }
須要注意的是,上面的代碼只能運用於單個DTO對象的塑形,若是是基於IEnumerable<IShapeDto>,不建議使用Select(dto => dto.ShapeData(fields))的形式,緣由是ShapeData()中反射屬性名的代碼不須要執行屢次,建議將其提取出來。
而碰到這個狀況,咱們應該針對IEnumerable<IShapeDto>編寫擴展方法,不瞭解的夥伴能夠參考個人代碼自行編寫,這裏我就不過多贅述。
public static IEnumerable<dynamic> ShapeData(this IEnumerable<IShapeDto> dtos, string fields)
最後,有的小夥伴可能會問,塑形動做應該是放在接口層仍是放在應用層?IShapeDto爲何不寫成抽象類進行繼承?爲何不寫擴展方法對全部object進行擴展?相似於這些問題,我只想說均可以,上面的代碼徹底是我我的的一個編碼習慣,不能成爲一個指導性的東西。在代碼設計層面,每一個人都會有不同的見解,歡迎你們評論交流。