我們做業(yè)務(wù)系統(tǒng)的開(kāi)發(fā),很多時(shí)候往往離不開(kāi)代碼生成器倒源。項(xiàng)目使用代碼生成器的好處不言而喻,生成出來(lái)的代碼標(biāo)準(zhǔn)規(guī)范句狼,代碼質(zhì)量有保障笋熬,并且還能大幅提高開(kāi)發(fā)效率,何樂(lè)不為呢腻菇?
來(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)目。
接著奏属,添加依賴Microsoft.CodeAnalysis.CSharp
和Microsoft.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.sql
Copy到項(xiàng)目里窄做,并且將其編譯動(dòng)作設(shè)為AdditionalFiles
愧驱。
接下來(lái)啟動(dòng)編譯項(xiàng)目,在GeneratorApp
的依賴項(xiàng)的分析器中會(huì)出現(xiàn)一個(gè)名為Users_GeneratorApp.Models.g.cs
的文件椭盏。
雙擊打開(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)烈推薦給大家啃匿!