ASP.NET Core WebApi下的數據塑形


  在這個前后端分離開發的今天,前端通過調用后端提供的api接口,實現頁面數據的展示。而往往在實際場景中,會出現兩個版塊調用的數據極度相似的情況,A頁面與B頁面所展示的列表,僅僅相差了幾個字段。

  如果這個時候,我們選擇將數據的所有字段一起返回,則會增大了Http請求的體積,好處是后續版塊需求變化時,前端直接替換對應的字段即可,后端不需要修改返回的數據,但這樣明顯是不符合規范的,而且在特定情況下會導致信息泄露。

  還有一種方法,我們針對A頁面與B頁面各自返回一個DTO或VO,這樣信息就不會泄露,但這樣卻違背了RESTful的原則,且加大了視圖(UI)與應用層(Application)之間的耦合度,應用層返回什么數據原則上不應該由視圖決定。

  所以,我們需要一個可以由前端來指定api返回字段的方式,那就是數據塑形。

  首先,為DTO設計一個塑形接口,並將接口方法默認實現:

 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下:

public static bool TryAdd<TKey, TValue>(this IDictionary<TKey, TValue> dictionary, TKey key, TValue value) where TKey : notnull;

   接下來,根據功能需求設計一個DTO類,並實現IShapeDto

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中編寫業務:

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進行擴展?類似於這些問題,我只想說都可以,上面的代碼完全是我個人的一個編碼習慣,不能成為一個指導性的東西。在代碼設計層面,每個人都會有不一樣的看法,歡迎大家評論交流。

  author:https://www.cnblogs.com/abnerwong/


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM