0%

[DotnetCore]ORM系列-SqlSugar:實體產生器-進階

前情提要

筆者這篇主要是想寫實體產生器的進階版本,怎麼會說是進階版本呢,若有用過EFCore的dotnet ef dbcontext scaffold指令會知道,預設會是符合C#命名規則,即Pascal Case,每個英文單字連在一起,每個單字的第一個字為大寫。scaffold指令還特別開放options:-use-database-names,即可保留其資料庫中的命名。

筆者當初也沒有特別注意這些細節,因為是有踩過一些雷,例:製作前端使用ViewModel時,從上述scaffold指令產出的Entities複製其對應的屬性宣告,再補一些額外的屬性,然後透過Dapper套件Binding屬性時發現,怎麼有些屬性明明資料庫就有回傳,怎麼沒有binding成功,仔細一看,資料庫端是使用下底線,到C#端時scaffold會把下底線去掉,因此直接從Entities中複製貼上則會是不一樣的屬性名稱,自從那一次後,就注意到該細節。

講到此篇主題,剛好手邊新專案,資料庫使用MySQL,筆者想保持資料庫命名規則,主要是有兩點

  • 小寫為主
  • 每個單字透過下底線連結

另一方面,筆者透過SqlSugarCore提供的實體產生相關API產生相關實體時,又想保持C#命名規則,最好是跟EFCore scaffold一樣可以保有複數機制,這為此篇主要解決目的。

內容

關於此篇想達成的目的,主要工作有三個

  • SqlSugarCore自定義命名方法
  • 字串轉PascalCase命名規則
  • 字串轉複數命名機制

SqlSugarCore自定義命名方法

SqlSugarCore本身有提供產生實體時可以做客製化,轉換邏輯自定義,或者產生實體時的Class及Property客製化,對於筆者這邊的情境來說只要能Class及Property客製化宣告即可,筆者這邊主要參考

https://www.donet5.com/Home/Doc?typeId=1207

直接來看Code比較有感覺

1
2
3
4
5
6
7
8
9
10
foreach (var item in db.DbMaintenance.GetTableInfoList())
{
string entityName = item.Name.ToUpper();/*实体名大写*/
db.MappingTables.Add(entityName , item.Name);
foreach (var col in db.DbMaintenance.GetColumnInfosByTableName(item.Name))
{
db.MappingColumns.Add(col.DbColumnName.ToUpper() /*类的属性大写*/, col.DbColumnName, entityName);
}
}
db.DbFirst.IsCreateAttribute().CreateClassFile("c:\\Demo\\8", "Models");

主要是有兩個地方要改寫

  • entityName的轉換邏輯:即Table名稱轉換成Class Name邏輯
  • DbColumnName的轉換:即Column名稱轉換成Property Name邏輯

SqlSugarCore官網說明這邊主要是簡單改寫成全大寫的形式,筆者介紹完下面兩個章節後再回來修改這邊的程式邏輯。

PascalCase轉換

筆者這邊是完全遵照MySQL命名規則,所以簡單來說一下邏輯是這樣

  • 用下底線_符號切割字串
  • 將上述切割出來的字串,將第一個字母轉大寫
  • 最後將所有字串Join回去
1
2
3
4
5
6
7
8
9
10
11
public static class StringExtension
{
public static string ToPascalCase(this string value)
{
var words = value.Split(new[] { "_", "-", " " }, StringSplitOptions.RemoveEmptyEntries);
words = words
.Select(word => char.ToUpper(word[0]) + word.Substring(1))
.ToArray();
return string.Join(string.Empty, words);
}
}

透過Linqpad測試一下其轉換結果

1
2
3
4
5
6
7
8
void Main()
{
var testStr = "vendor_id";
testStr.ToPascalCase().Dump();
}

// Linqpad結果
// VendorId

複數命名轉換

筆者這邊有查到 .net framework時有內建的服務PluralizationService,但到轉換到dotnet core時,只查得到相關套件,就以google到的第一個套件為主去試看看吧。

1
dotnet add package PluralizeService.Core --version 1.2.21147.2

接著撰寫程式吧,筆者沿用上章節所用到的StringExtension,完善它。

1
2
3
4
5
6
7
8
public static class StringExtension
{
// 以上省略
public static string Pluralize(this string value)
{
return PluralizationProvider.Pluralize(value);
}
}

透過Linqpad測試一下其效果

1
2
3
4
5
6
7
8
void Main()
{
var testStr = "customer";
testStr.ToPascalCase().Pluralize().Dump();
}

// Linqpad結果
// Customers

當然該套件不只變成複數,將複數轉換成單數也是可以的,且一行指令就完成,滿無腦套用的。

實體產生器改造

最後要來套用在轉換真實資料庫上,筆者就改造之前的文章[DotnetCore]ORM系列-SqlSugar:實體產生器-進階裡的程式內容

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
using CommandLine;
using PluralizeService.Core;
using SqlSugar;

Parser.Default.ParseArguments<Options>(args).WithParsed(Run);

static void Run(Options option)
{
using var _db = new SqlSugarScope(new ConnectionConfig()
{
DbType = SqlSugar.DbType.MySql,
ConnectionString = option.ConnectionString,
InitKeyType = InitKeyType.Attribute,
IsAutoCloseConnection = true
});
// 這篇主要是多這段,產生實體前宣告命名規則
foreach (var item in _db.DbMaintenance.GetTableInfoList())
{
// entityName套上PascalCase及Pluralize效果
string entityName = item.Name.ToPascalCase().Pluralize();
_db.MappingTables.Add(entityName, item.Name);
foreach (var col in _db.DbMaintenance.GetColumnInfosByTableName(item.Name))
{
// 資料欄位則僅套上PascalCase效果
_db.MappingColumns.Add(col.DbColumnName.ToPascalCase(), col.DbColumnName, entityName);
}
}
_db.DbFirst.IsCreateAttribute().CreateClassFile(option.ModelPath, option.ModelNameSpace);
}
public class Options
{
[Option('c', "connectionstring", Required = true, HelpText = "資料庫連線字串")]
public string ConnectionString { get; set; }

[Option('p', "path", Required = true, HelpText = "Models檔案放置路徑")]
public string ModelPath { get; set; }

[Option('n', "namespace", Required = true, HelpText = "Model命名空間")]
public string ModelNameSpace { get; set; }
}

public static class StringExtension
{
public static string ToPascalCase(this string value)
{
var words = value.Split(new[] { "_", "-", " " }, StringSplitOptions.RemoveEmptyEntries);
words = words
.Select(word => char.ToUpper(word[0]) + word.Substring(1))
.ToArray();
return string.Join(string.Empty, words);
}

public static string Pluralize(this string value)
{
return PluralizationProvider.Pluralize(value);
}
}

最終呈現效果

筆者先貼上其資料庫欄位樣子

1
2
3
4
5
6
7
8
9
CREATE TABLE `product` (
`id` bigint NOT NULL AUTO_INCREMENT,
`brand` varchar(50) DEFAULT NULL,
`timing_kit_no` text,
`customer_product_no` text,
`memo` text,
`weight` decimal(8,3) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

透過dotnet run指令產生其對應的實體

1
dotnet run -- -c "連線字串" -p "實體檔案放置位置" -n "命名空間"

最終得到的實體

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
///<summary>
///
///</summary>
[SugarTable("product")]
public partial class Products
{
public Products(){

}
/// <summary>
/// Desc:
/// Default:
/// Nullable:False
/// </summary>
[SugarColumn(IsPrimaryKey=true,IsIdentity=true,ColumnName="id")]
public long Id {get;set;}

/// <summary>
/// Desc:
/// Default:
/// Nullable:True
/// </summary>
[SugarColumn(ColumnName="brand")]
public string Brand {get;set;}

/// <summary>
/// Desc:
/// Default:
/// Nullable:True
/// </summary>
[SugarColumn(ColumnName="timing_kit_no")]
public string TimingKitNo {get;set;}

/// <summary>
/// Desc:
/// Default:
/// Nullable:True
/// </summary>
[SugarColumn(ColumnName="customer_product_no")]
public string CustomerProductNo {get;set;}

/// <summary>
/// Desc:
/// Default:
/// Nullable:True
/// </summary>
[SugarColumn(ColumnName="memo")]
public string Memo {get;set;}

/// <summary>
/// Desc:
/// Default:
/// Nullable:True
/// </summary>
[SugarColumn(ColumnName="weight")]
public decimal? Weight {get;set;}

}

結論

沒想到小小的實體產生器,也有如此大的學問,且透過SqlSugarCore產生出來的實體,預設為Partial Class,因此可以宣告一個Products.Partial.cs來撰寫其特定方法,與自動產生的檔案做切割,自定義的Method不會被影響,筆者覺得Partial Class真的是一個好物,只可惜不可跨assembly使用,不過它套用的前提是一樣的Namespace下,不可跨assembly似乎也是合理,這篇就到這邊了,希望對有這個需求的各位有所幫助。

參考