0%

[DotnetCore]泛型運用系列-Attribute設計篇

前情提要

前提情要請參考[DotnetCore]泛型運用系列-Model設計篇,不過筆者還是在這邊再列一下會用到的技術觀念與套件:

  • AutoMapper:運用於將檔案內容對應的物件轉成EFCore的物件時
  • Generic Class/Method: 不想寫多個Service去處理多個檔案,設計成泛型形式以符合各種檔案類型
  • Attribute: 透過Description Attribute,註記其對應的Comlumn欄位順序
  • Coravel:透過該套件,將寫好的Service掛成排程任務
  • Extension Method:有一些通用的Method,不要落落等塞在一個Service中,因此轉換成可以共用的Extension Method
  • EFCore:資料存取用開發套件

內容

這篇主要是存取Attribute的方式設定出泛型在指派值的邏輯,以筆者這套實作邏輯中會用到Column順序,這就因人而異,剛好筆者要解析的資料內容,會將每個Column的值使用一個特殊符號隔開,因此筆者設計一個Column順序的值設定於Attribute中,使泛型Method中取得TypeGetProperties方法並取得對應的CustomAttribute的設定內容,因而可以寫出共用的實體屬性值指派的商業邏輯。

以上述描述的情境來說,筆者不想要再宣告其他自定義的Attribute,因此偷懶使用既有的Description Attribute當作Column順序的設定值,示意如下

1
2
3
4
5
6
7
8
9
10
[Description("A.txt")]
public class ADomainModel : BaseModel
{
[Description("1")]
public string AProperty1 { get; set; }
[Description("2")]
public string AProperty2 { get; set; }
[Description("3")]
public string AProperty3 { get; set; }
}

筆者將Description Attribute用得淋漓盡致,Class層級的Description中設定其對應的txt檔案名稱Property層級的Description則設定其Column順序值。接著不想要寫落落等取得Description Attribute中的設定值,寫一個Extension Method,名為CustomTypeExtension

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public static class CustomTypeExtension
{
// 取得Class層級的Attribute設定值
public static string GetDescription(Type type)
{
var descriptions = (DescriptionAttribute[])
type.GetCustomAttributes(typeof(DescriptionAttribute), false);

if (descriptions.Length == 0)
{
return null;
}
return descriptions[0].Description;
}

// 取得Property層級的Attribute設定值
// 包裝成Dictionary的形式將Property Name及Column順序整理起來
public static Dictionary<int, string> GetPropertiesWithOrderDescription(Type type)
{
Dictionary<int, string> propertyDic = new Dictionary<int, string>();
foreach (var property in type.GetProperties())
{
var order = property.GetCustomAttribute<DescriptionAttribute>()?.Description;
if (string.IsNullOrEmpty(order) == false)
propertyDic.Add(int.Parse(order), property.Name);
}
return propertyDic;
}
}

接著寫一個共用的Extension Method,將透過Activator.CreateInstance<T>()初始化的實體,針對實體中的屬性指派值的商業邏輯抽離至Extension Method

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public static class ObjectExtension
{
public static List<TResult> GetModelList<TResult>(string localFilePath
, Encoding encoding
, Dictionary<int, string> orderDict
, char splictChar)
{
var modelList = new List<TResult>();
// 依照傳入的Encoding讀取檔案內容
using var sr = new StreamReader(localFilePath, encoding);
while (sr.Peek() >= 0)
{
var line = sr.ReadLine();
// 防呆:若為空資料列則跳過
if (string.IsNullOrEmpty(line))
continue;
// 使用傳入的分隔符號分割資料
var infoList = line.Split(splictChar);
// 資料Binding
var model = Activator.CreateInstance<TResult>();
for (int i = 0; i < infoList.Length; i++)
{
var propertyInfo = typeof(TResult).GetProperties()
.First(x => x.Name == orderDict.First(x => x.Key == (i + 1)).Value);
propertyInfo.SetValue(model, infoList[i].Trim());
}
modelList.Add(model);
}
return modelList;
}
}

跟著筆者來看看怎麼運用吧

1
2
3
4
5
6
7
8
9
10
11
12
// Startup.cs
// 記得使用Encoding為Big5則須於Startup中宣告定義以下語法
// Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);

// 使用CustomTypeExtension Method
var fileName = CustomTypeExtension.GetDescription(_model.GetType());
var orderDict = CustomTypeExtension.GetPropertiesWithOrderDescription(_model.GetType());

// 使用ObjectExtension Method
var filePath = Path.Combine(_env.ContentRootPath, "upload", "Downloads", fileName);
var modelList = ObjectExtension.GetModelList<T>(filePath, Encoding.GetEncoding("Big5")
, orderDict, '=');

結論

透過此篇的解構,筆者相信真正的Domain Service會變得很乾淨,且此篇撰寫出來的Extension Method可以給其他服務使用,當然筆者最終的目標是將下載檔案,將檔案中的資料內容存進資料庫這樣的行為變成是一個透過設定就能使用的服務,離目標不遠了,下篇見啦。