開源 - Ideal庫 - Excel幫助類,TableHelper實現(二)

IT规划师發表於2024-11-30

書接上回,我們今天開始實現物件集合與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

相關文章