使用Source Generators將SQL腳本生成C#實(shí)體類

我們做業(yè)務(wù)系統(tǒng)的開(kāi)發(fā),很多時(shí)候往往離不開(kāi)代碼生成器倒源。項(xiàng)目使用代碼生成器的好處不言而喻,生成出來(lái)的代碼標(biāo)準(zhǔn)規(guī)范句狼,代碼質(zhì)量有保障笋熬,并且還能大幅提高開(kāi)發(fā)效率,何樂(lè)不為呢腻菇?

source-gen.jpg

來(lái)自go-zero得到的靈感

最近胳螟,我在朋友推薦下研究了go-zero,一個(gè)基于golang的快速開(kāi)發(fā)框架筹吐。我翻閱了一下它的文檔和demo代碼糖耸,發(fā)現(xiàn)其的設(shè)計(jì)思想跟我用的C#開(kāi)發(fā)框架,功能上基本大同小異丘薛。

其中有一項(xiàng)功我是比較有用:根據(jù)SQL腳本來(lái)生成數(shù)據(jù)表實(shí)體類嘉竟。下面簡(jiǎn)單描述一下go-zero實(shí)現(xiàn)的過(guò)程:

  • 首先,定一個(gè)創(chuàng)建MySQL數(shù)據(jù)表的SQL腳本users.sql
CREATE TABLE `users` (  
    `id` int PRIMARY KEY NOT NULL AUTO_INCREMENT COMMENT 'ID',  
    `name` varchar(32) NOT NULL COMMENT '用戶名',  
    `password` varchar(32) NOT NULL COMMENT '密碼',  
    `gender` int NOT NULL COMMENT '性別(0-未知舍扰;1-男倦蚪;2-女)',  
    `age` int NOT NULL COMMENT '年齡'  
)ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='用戶資料';
  • 然后勤庐,使用go-zero的命令行工具goctl峦筒,執(zhí)行以下命令:
goctl model mysql ddl -src="users.sql" -dir="./model" -c
  • 最后,在所生成的usersmodel.go代碼中疯暑,得到一個(gè)名為Users結(jié)構(gòu)體勾给。
Users struct {  
    Id         int       `db:"id"`  
    Name       string    `db:"name"`     // 用戶名  
    Password   string    `db:"password"` // 密碼  
    Gender     uint64    `db:"gender"`   // 性別(0-未知滩报;1-男;2-女)  
    age        int       `db:"age"`      // 年齡  
}

用C#的Source Generators來(lái)實(shí)現(xiàn)

說(shuō)完go-zero播急,回到我們熟悉的C#脓钾。想要實(shí)現(xiàn)這個(gè)功能,其實(shí)不困難桩警,而且實(shí)現(xiàn)的方法可以有很多可训。如果讓我選我會(huì)首選用Source Generators來(lái)實(shí)現(xiàn),因?yàn)槲覀€(gè)人認(rèn)為是最最優(yōu)雅的捶枢。

1握截、什么是Source Generators?

說(shuō)起烂叔,SourceGenerator其實(shí)很多.NET開(kāi)發(fā)者會(huì)感到既熟悉又陌生谨胞,總覺(jué)得有在哪里看到過(guò)或者聽(tīng)到過(guò),但是自己開(kāi)發(fā)的代碼卻很少用到過(guò)蒜鸡。

如果大家對(duì) Source Generators 沒(méi)有概念胯努,請(qǐng)先閱讀一下微軟官方文檔,我這里不做詳細(xì)介紹的展開(kāi)逢防。

https://learn.microsoft.com/en-us/dotnet/csharp/roslyn-sdk/source-generators-overview

總結(jié)成一句話:它是編譯器級(jí)別的源碼生成器叶沛,在編譯階段生成代碼,并即時(shí)參與編譯忘朝。 這就決定了它與go-zero提供的傳統(tǒng)代碼生成工具灰署,有本質(zhì)上的區(qū)別。早在兩年前局嘁,已經(jīng)將開(kāi)始逐步將Source Generators應(yīng)用到我的項(xiàng)目開(kāi)發(fā)中溉箕,感覺(jué)非常棒。

  • 支持動(dòng)態(tài)代碼生成:修改應(yīng)用程序的上下文代碼或者資源文件配置悦昵,Source Generators立刻就能幫你生成出相應(yīng)的代碼约巷,沒(méi)有半點(diǎn)滯后和延遲。不像供傳統(tǒng)代碼生成器旱捧,還需要另外執(zhí)行腳本独郎。
  • 代碼模板維護(hù)非常簡(jiǎn)單:源代碼生成器可以自動(dòng)創(chuàng)建樣板踩麦,確保一致性并減少手動(dòng)工作。
  • 性能優(yōu)化:通過(guò)在編譯時(shí)生成代碼的能力氓癌,開(kāi)發(fā)人員可以引入針對(duì)應(yīng)用程序需求量身定制的特定優(yōu)化谓谦,通常會(huì)產(chǎn)生更高性能和更高效的代碼。
  • 增強(qiáng)代碼可維護(hù)性:代碼編譯時(shí)自動(dòng)生成贪婉,從而大大減少源碼的代碼量反粥,從而變得更容易維護(hù)管理。
  • 需要調(diào)試的手動(dòng)代碼更少疲迂,生成的代碼遵循一致的模式才顿,使其更易于理解和管理。
  • 一致性和標(biāo)準(zhǔn)化:當(dāng)代碼自動(dòng)生成時(shí)尤蒿,它遵循設(shè)定的模式或標(biāo)準(zhǔn)郑气。這確保了團(tuán)隊(duì)成員始終在同一頁(yè)面上,從而減少差異和沖突腰池。

2尾组、開(kāi)始使用

下面請(qǐng)跟隨我一起,用Source Generators來(lái)實(shí)現(xiàn)這個(gè)目標(biāo)示弓。

首先讳侨,創(chuàng)建一個(gè).NET Standard 2.0的項(xiàng)目。

image.png

接著奏属,添加依賴Microsoft.CodeAnalysis.CSharpMicrosoft.CodeAnalysis.Analyzers跨跨。其中,需要將Microsoft.CodeAnalysis.CSharp的屬性設(shè)置為PrivateAssets="all"囱皿。

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>  
    <TargetFramework>netstandard2.0</TargetFramework>  
      <Version>1.0.0</Version>  
      <LangVersion>latest</LangVersion>
  </PropertyGroup>
    <ItemGroup>  
       <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0" PrivateAssets="all" />  
       <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" />  
    </ItemGroup>  
</Project>

請(qǐng)注意:Microsoft.CodeAnalysis.CSharp的版本不要選太高的勇婴,4.8以上的會(huì)有額外的依賴對(duì) 運(yùn)行Source Generator不太友好。

創(chuàng)建好項(xiàng)目铆帽,接著開(kāi)始編寫代碼:

[Generator]  
public class SqlCodeGenerator : IIncrementalGenerator  
{  
    public void Initialize(IncrementalGeneratorInitializationContext context)  
    {
        // 僅讀取.sql后綴的文件
        var provider = context.AdditionalTextsProvider.Where(static file => file.Path.EndsWith(".sql"));  
        context.RegisterSourceOutput(provider, Execute);  
    }  
  
    const string TABLE_PTN = @"CREATE TABLE `(\w+)`";  
    const string COLUMN_PTN = @"`(\w+)` (.*?) COMMENT '(.*?)'";  
    const string LAST_PTN = @"(.*?) COMMENT='(.*?)';";  
  
    private void Execute(SourceProductionContext context, AdditionalText file)  
    {  
        var sqlText = file.GetText().ToString();  
        var lines = sqlText.Split("\r\n".ToCharArray(), StringSplitOptions.RemoveEmptyEntries);  
        var tableName = "";  
        var tableComment = "";  
        var columns = new List<DataFieldInfo>();  
        // 正則表達(dá)式逐行解析SQL腳本咆耿,提取表和字段的關(guān)鍵信息
        foreach (var line in lines)  
        {  
            var text = line.Trim();  
            if (string.IsNullOrEmpty(tableName))  
            {  
                var m = Regex.Match(line, TABLE_PTN);  
                if (m.Success)  
                {  
                    tableName = m.Groups[1].Value;  
                    continue;  
                }  
            }  
            var match = Regex.Match(line, COLUMN_PTN);  
            if (match.Success)  
            {  
                var prop = new DataFieldInfo()  
                {  
                    FieldName = match.Groups[1].Value,  
                    Comment = match.Groups[3].Value  
                };  
                var others = match.Groups[2].Value.Split(" ".ToCharArray(), StringSplitOptions.RemoveEmptyEntries);  
                prop.DbType= others[0].ToUpper();  
                prop.CodeType = MapToCodeType(prop.DbType);  
                prop.NotNull = (others.Contains("NOT") && others.Contains("NULL"));  
                prop.IsPrimary = (others.Contains("PRIMARY") && others.Contains("KEY"));  
                prop.AutoIncrement = others.Contains("AUTO_INCREMENT");  
                prop.PropertyName = ConvertDBNameToPascalCase(prop.FieldName);  
                columns.Add(prop);  
                  
                continue;  
            }  
            match = Regex.Match(line, LAST_PTN);  
            if (match.Success)  
            {  
                tableComment = match.Groups[2].Value;  
            }  
        }  
  
        var table = new DataTableInfo  
        {  
            TableName = tableName,  
            Comment = string.IsNullOrEmpty(tableComment) ? tableName : tableComment,  
            ClassName = ConvertDBNameToPascalCase(tableName)  
        };  

        // 調(diào)用模板輸出代碼
        RenderModelCode(context, table, columns);
    }  
  
  
    /// <summary>  
    /// 生成Model類模板代碼  
    /// </summary>  
    /// <param name="context"></param>
    /// <param name="table"></param>
    /// <param name="columns"></param>
    private void RenderModelCode(SourceProductionContext context,DataTableInfo table,List<DataFieldInfo> columns)  
    {  
        const string nameSpace = "GeneratorApp.Models";  
        var sb = new StringBuilder(); 
        sb.AppendLine($"namespace {nameSpace};").AppendLine();  
        sb.AppendLine("http:/// <summary>")  
            .AppendLine($"http:/// Model for Table :{table.Comment}")  
            .AppendLine("http:/// <para>此代碼由SourceGenerator生成</para>")  
            .AppendLine($"http:/// <para>生成時(shí)間:{DateTime.Now:yyyy-MM-dd HH:mm:ss}</para>")  
            .AppendLine("http:/// </summary>");  
        sb.AppendLine($"public partial class {table.ClassName} {{").AppendLine();  
  
        foreach(var props in columns)  
        {  
            if (string.IsNullOrWhiteSpace(props.FieldName)|| string.IsNullOrWhiteSpace(props.CodeType)) continue;  
            var comment = props.Comment;  
            if (string.IsNullOrWhiteSpace(comment)) comment = "Field:" + props.FieldName;  
            sb.Append("\t").AppendLine("http:/// <summary>")  
                .Append("\t").AppendLine($"http:/// {comment}")  
                .Append("\t").AppendLine("http:/// </summary>");  
            sb.Append("\t").Append($"public {props.CodeType} {props.PropertyName} {{ get; set; }}");  
            sb.AppendLine();  
        }  
  
        sb.AppendLine();  
        sb.Append("\t").Append($"public {table.ClassName}() {{ }}").AppendLine().AppendLine();  
        sb.AppendLine("}");  
  
        context.AddSource($"{table.ClassName}_{nameSpace}.g.cs", sb.ToString());  
    }
}

3德谅、項(xiàng)目調(diào)用

新建一個(gè)名為GeneratorApp的控制臺(tái)項(xiàng)目爹橱,然后引用剛才的SqlGenerator項(xiàng)目。

<Project Sdk="Microsoft.NET.Sdk">  
  <PropertyGroup>  
    <OutputType>Exe</OutputType>  
    <TargetFramework>net8.0</TargetFramework>  
    <ImplicitUsings>enable</ImplicitUsings>  
    <Nullable>enable</Nullable>  
  </PropertyGroup> 
    <ItemGroup>  
       <ProjectReference Include="..\SqlGenerator\SqlGenerator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />  
    </ItemGroup>  
</Project>

把剛才的users.sqlCopy到項(xiàng)目里窄做,并且將其編譯動(dòng)作設(shè)為AdditionalFiles愧驱。

image.png

接下來(lái)啟動(dòng)編譯項(xiàng)目,在GeneratorApp的依賴項(xiàng)的分析器中會(huì)出現(xiàn)一個(gè)名為Users_GeneratorApp.Models.g.cs的文件椭盏。

image.png

雙擊打開(kāi)可以看到生成的代碼组砚。并且會(huì)提示該文件是自動(dòng)生成的,無(wú)法編輯掏颊。
可以看到糟红,文件中的代碼便是我們通過(guò)SQL腳本艾帐,生成的Model類。

namespace GeneratorApp.Models;  
  
/// <summary>  
/// Model for Table :用戶資料  
/// <para>此由SourceGenerator生成</para>  
/// <para>生成時(shí)間:2024-09-16 12:49:01</para>  
/// </summary>  
public partial class Users {  
  
    /// <summary>  
    /// ID
    /// </summary>
    public int Id { get; set; }  
    /// <summary>  
    /// 用戶名  
    /// </summary>  
    public string Name { get; set; }  
    /// <summary>  
    /// 密碼  
    /// </summary>  
    public string Password { get; set; }  
    /// <summary>  
    /// 性別(0-未知盆偿;1-男柒爸;2-女)  
    /// </summary>  
    public int Gender { get; set; }  
    /// <summary>  
    /// 年齡  
    /// </summary>  
    public int Age { get; set; }  
  
    public Users() { }  
}

最后,打開(kāi)Program.cs寫幾行代碼來(lái)實(shí)現(xiàn)Users類的調(diào)用事扭。接著捎稚,直接編譯運(yùn)行。

internal class Program  
{  
    static void Main(string[] args)  
    {   
        var user = new Models.Users()  
        {  
            Id = 1,   
            Age = 30,  
            Gender = 1,  
            Name = "Sam",  
            Password = "111"  
        };  
  
        Console.WriteLine(user.Name);
    }
}

總結(jié)

我從go-zero中發(fā)現(xiàn)了用SQL腳本來(lái)生成model代碼的功能求橄,感覺(jué)非常棒今野。于是,我使用C# Source Generators來(lái)實(shí)現(xiàn)了一個(gè)類似的功能罐农。

對(duì).NET開(kāi)發(fā)者而言条霜,Source Generators絕對(duì)是寶藏級(jí)的開(kāi)發(fā)工具。強(qiáng)烈推薦給大家啃匿!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末蛔外,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子溯乒,更是在濱河造成了極大的恐慌夹厌,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,816評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件裆悄,死亡現(xiàn)場(chǎng)離奇詭異矛纹,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)光稼,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,729評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門或南,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人艾君,你說(shuō)我怎么就攤上這事采够。” “怎么了冰垄?”我有些...
    開(kāi)封第一講書人閱讀 158,300評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵蹬癌,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我虹茶,道長(zhǎng)逝薪,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書人閱讀 56,780評(píng)論 1 285
  • 正文 為了忘掉前任蝴罪,我火速辦了婚禮董济,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘要门。我一直安慰自己虏肾,他們只是感情好廓啊,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,890評(píng)論 6 385
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著封豪,像睡著了一般崖瞭。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上撑毛,一...
    開(kāi)封第一講書人閱讀 50,084評(píng)論 1 291
  • 那天书聚,我揣著相機(jī)與錄音,去河邊找鬼藻雌。 笑死雌续,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的胯杭。 我是一名探鬼主播驯杜,決...
    沈念sama閱讀 39,151評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼做个!你這毒婦竟也來(lái)了鸽心?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書人閱讀 37,912評(píng)論 0 268
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤居暖,失蹤者是張志新(化名)和其女友劉穎顽频,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體太闺,經(jīng)...
    沈念sama閱讀 44,355評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡糯景,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,666評(píng)論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了省骂。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蟀淮。...
    茶點(diǎn)故事閱讀 38,809評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖钞澳,靈堂內(nèi)的尸體忽然破棺而出怠惶,到底是詐尸還是另有隱情,我是刑警寧澤轧粟,帶...
    沈念sama閱讀 34,504評(píng)論 4 334
  • 正文 年R本政府宣布策治,位于F島的核電站,受9級(jí)特大地震影響逃延,放射性物質(zhì)發(fā)生泄漏览妖。R本人自食惡果不足惜轧拄,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,150評(píng)論 3 317
  • 文/蒙蒙 一揽祥、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧檩电,春花似錦拄丰、人聲如沸府树。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 30,882評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)奄侠。三九已至,卻和暖如春载矿,著一層夾襖步出監(jiān)牢的瞬間垄潮,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 32,121評(píng)論 1 267
  • 我被黑心中介騙來(lái)泰國(guó)打工闷盔, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留弯洗,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,628評(píng)論 2 362
  • 正文 我出身青樓逢勾,卻偏偏與公主長(zhǎng)得像牡整,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子溺拱,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,724評(píng)論 2 351

推薦閱讀更多精彩內(nèi)容