書接上回,我們今天開始實現物件集合與DataTable的相互轉換。
01、介面設計
上文中已經詳細講解了整體設計思路以及大致設計了需要哪些方法。下面我們先針對上文設計思想確定對外提供的介面。具體介面如下:
//根據列名陣列建立表格
public static DataTable Create(string[] columnNames, string? tableName = null);
//根據列名-型別鍵值對建立表格
public static DataTable Create(Dictionary<string, Type> columns, string? tableName = null);
//根據類建立表格
//如果設定DescriptionAttribute,則將特性值作為表格的列名稱
//否則將屬性名作為表格的列名稱
public static DataTable Create<T>(string? tableName = null);
//把表格轉換為實體物件集合
//如果設定DescriptionAttribute,則將特性值作為表格的列名稱
//否則將屬性名作為表格的列名稱
public static IEnumerable<T> ToModels<T>(DataTable dataTable);
//把實體物件集合轉為表格
//如果設定DescriptionAttribute,則將特性值作為表格的列名稱
//否則將屬性名作為表格的列名稱
public static DataTable ToDataTable<T>(IEnumerable<T> models, string? tableName = null);
//把一維陣列作為一列轉換為表格
public static DataTable ToDataTableWithColumnArray<TColumn>(TColumn[] array, string? tableName = null, string? columnName = null);
//把一維陣列作為一行轉換為表格
public static DataTable ToDataTableWithRowArray<TRow>(TRow[] array, string? tableName = null);
//行列轉置
public static DataTable Transpose(DataTable dataTable, bool isColumnNameAsData = true);
02、根據列名陣列建立表格
該方法實現比較簡單,我們直接看程式碼:
//根據列名陣列建立表格
public static DataTable Create(string[] columnNames, string? tableName = null)
{
var table = new DataTable(tableName);
foreach (var columnName in columnNames)
{
table.Columns.Add(columnName);
}
return table;
}
我們進行一個簡單的單元測試:
[Fact]
public void Create()
{
//正常建立成功
var columnNames = new string[] { "A", "B" };
var table = TableHelper.Create(columnNames);
Assert.Equal("", table.TableName);
Assert.Equal(2, table.Columns.Count);
Assert.Equal(columnNames[0], table.Columns[0].ColumnName);
Assert.Equal(columnNames[1], table.Columns[1].ColumnName);
Assert.Equal(typeof(string), table.Columns[0].DataType);
Assert.Equal(typeof(string), table.Columns[1].DataType);
//驗證表名
table = TableHelper.Create(columnNames, "test");
Assert.Equal("test", table.TableName);
//驗證列名不能重複
columnNames = new string[] { "A", "A" };
Assert.Throws<DuplicateNameException>(() => TableHelper.Create(columnNames));
}
03、根據列名-型別鍵值對建立表格
此方法是上一個方法的補充,預設直接根據列名建立表格,則所有列的資料型別都是string型別,而此方法可以指定每列的資料型別,實現也很簡單,程式碼如下:
//根據列名-型別鍵值對建立表格
public static DataTable Create(Dictionary<string, Type> columns, string? tableName = null)
{
var table = new DataTable(tableName);
foreach (var column in columns)
{
table.Columns.Add(column.Key, column.Value);
}
return table;
}
04、根據類建立表格
該方法是將類的屬性名作為表格的列名稱,屬性對應的型別作為表格列的資料型別,把類轉為表格。
同時我們需要約束類只能為結構體或類,而不能是列舉、基礎型別、以及集合型別、委託、介面等。顯然我們很難透過泛型約束達到我們的需求,因此我們首先需要對該方法的泛型進行型別校驗。
校驗成功後,我們只需要透過反射即可拿到類的所有屬性資訊,即可建立表格。同時我們約定如果屬性設定了DescriptionAttribute特性,則特性值作為列名,如果沒有設定特性則取屬性名稱作為列名。
//根據類建立表格
//如果設定DescriptionAttribute,則將特性值作為表格的列名稱
//否則將屬性名作為表格的列名稱
public static DataTable Create<T>(string? tableName = null)
{
//T必須是結構體或類,並且不能是集合型別
AssertTypeValid<T>();
//獲取類的所有公共屬性
var properties = typeof(T).GetProperties();
var columns = new Dictionary<string, Type>();
foreach (var property in properties)
{
//根據屬性獲取列名
var columnName = GetColumnName(property);
//組織列名-型別鍵值對
columns.Add(columnName, property.PropertyType);
}
return Create(columns, tableName);
}
//斷言型別有效性
private static void AssertTypeValid<T>()
{
var type = typeof(T);
if (type.IsValueType && !type.IsEnum && !type.IsPrimitive)
{
//是值型別,但是不是列舉或基礎型別
return;
}
else if (typeof(T).IsClass && !typeof(IEnumerable).IsAssignableFrom(typeof(T)))
{
//是類型別,但是不是集合型別,同時也不是委託、介面型別
return;
}
throw new InvalidOperationException("T must be a struct or class and cannot be a collection type.");
}
//根據屬性獲取列名稱
private static string GetColumnName(PropertyInfo property)
{
//獲取描述特性
var attribute = property.GetCustomAttribute<DescriptionAttribute>();
//如果存在描述特性則返回描述,否則返回屬性名稱
return attribute?.Description ?? property.Name;
}
下面我們針對列舉、字串,基礎型別、集合等情況進行詳細的單元測試,程式碼如下:
[Fact]
public void Create_T()
{
//驗證列舉
var expectedMessage = "T must be a struct or class and cannot be a collection type.";
var exception1 = Assert.Throws<InvalidOperationException>(() => TableHelper.Create<StatusEnum>());
Assert.Equal(expectedMessage, exception1.Message);
//驗證基礎型別
var exception2 = Assert.Throws<InvalidOperationException>(() => TableHelper.Create<int>());
Assert.Equal(expectedMessage, exception2.Message);
//驗證字串型別
var exception3 = Assert.Throws<InvalidOperationException>(() => TableHelper.Create<string>());
Assert.Equal(expectedMessage, exception3.Message);
//驗證集合
var exception4 = Assert.Throws<InvalidOperationException>(() => TableHelper.Create<Dictionary<string, Type>>());
Assert.Equal(expectedMessage, exception4.Message);
//驗證正常情況
var table = TableHelper.Create<Student<double>>();
Assert.Equal("", table.TableName);
Assert.Equal(3, table.Columns.Count);
Assert.Equal("標識", table.Columns[0].ColumnName);
Assert.Equal("姓名", table.Columns[1].ColumnName);
Assert.Equal("Age", table.Columns[2].ColumnName);
Assert.Equal(typeof(string), table.Columns[0].DataType);
Assert.Equal(typeof(string), table.Columns[1].DataType);
Assert.Equal(typeof(double), table.Columns[2].DataType);
}
注:測試方法程式碼以及示例原始碼都已經上傳至程式碼庫,有興趣的可以看看。https://gitee.com/hugogoos/Ideal