在大型业务系统的开发中,我们经常依赖代码生成器。在项目中使用代码生成器的好处不言而喻:它们可以生成标准化和规范的代码,保证代码质量,并显著提高开发效率。为什么不使用它们呢?
最近,有人推荐我探索 go-zero,这是一个基于 Golang 的快速开发框架。在查看了它的文档和演示代码后,我发现它的设计理念与我使用的 C# 开发框架非常相似,功能基本相同。
我发现特别有用的一个功能是能够从 SQL 脚本生成数据表实体类。以下是 go-zero 中过程的简单描述:
CREATE TABLE `users` (
`id` int PRIMARY KEY NOT NULL AUTO_INCREMENT COMMENT 'ID',
`name` varchar(32) NOT NULL COMMENT 'Username',
`password` varchar(32) NOT NULL COMMENT 'Password',
`gender` int NOT NULL COMMENT 'Gender(0-Unknown;1-Male;2-Female)',
`age` int NOT NULL COMMENT 'Age'
)ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='用户资料';
现在,让我们回到我们熟悉的 C#。实现此功能并不困难,并且有很多方法可以实现它。如果必须选择,我会选择 Source Generators,因为我相信这是最优雅的解决方案。
说到源生成器,许多 .NET 开发人员可能会觉得它们既熟悉又陌生。他们可能已经看到或听说过它们,但很少在自己的代码中使用它们。
如果您不熟悉 Source Generators,请查看 Microsoft 官方文档。我不会在这里进行详细的解释。
https://learn.microsoft.com/en-us/dotnet/csharp/roslyn-sdk/source-generators-overview
简而言之,它是一个编译器级源代码生成器,可在编译阶段生成代码并实时参与编译。 这从根本上将其与 go-zero 提供的传统代码生成工具区分开来。两年前,我开始逐渐将 Source Generators 整合到我的项目开发中,并发现它非常棒。
让我们使用 Source Generator 来实现这一点。
首先,创建一个 .NET Standard 2.0 项目。
接下来,添加依赖项和 .请记住将 的属性设置为 。Microsoft.CodeAnalysis.CSharpMicrosoft.CodeAnalysis.AnalyzersMicrosoft.CodeAnalysis.CSharpPrivateAssets="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>
注意:不要选择过高的 Microsoft.CodeAnalysis.CSharp 版本。版本 4.8 及更高版本具有其他依赖项,这些依赖项可能对运行 Source Generator 不友好。
创建项目后,开始编写代码:
[Generator]
public class SqlCodeGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
// filter *.sql files
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>();
// Regular expressions parse the SQL script line by line to extract key information of tables and fields
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)
};
// Call the method to generate code
RenderModelCode(context, table, columns);
}
/// <summary>
/// Generate Model Class By Template
/// </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("/// <summary>")
.AppendLine($"/// Model for Table :{table.Comment}")
.AppendLine("/// <para>Generated Code</para>")
.AppendLine($"/// <para>Generated At:{DateTime.Now:yyyy-MM-dd HH:mm:ss}</para>")
.AppendLine("/// </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("/// <summary>")
.Append("\t").AppendLine($"/// {comment}")
.Append("\t").AppendLine("/// </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());
}
}
创建一个名为 SqlGenerator 项目的控制台项目,并引用之前创建的 SqlGenerator 项目。GeneratorApp
<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.sqlAdditionalFiles
接下来,生成项目。在依赖项分析器中,您将找到一个名为 .GeneratorAppUsers_GeneratorApp.Models.g.cs
双击以打开文件并查看生成的代码。您将看到一条通知,指示该文件是自动生成的,无法编辑。文件中的代码是基于 SQL 脚本生成的 Model 类。
namespace GeneratorApp.Models;
/// <summary>
/// Model for Table :用户资料
/// <para>此由SourceGenerator生成</para>
/// <para>生成时间:2024-09-16 12:49:01</para>
/// </summary>
public partial class Users {
/// <summary>
/// ID
/// </summary>
public int Id { get; set; }
/// <summary>
/// Username
/// </summary>
public string Name { get; set; }
/// <summary>
/// Password
/// </summary>
public string Password { get; set; }
/// <summary>
/// Gender(0-Unknown;1-Male;2-Female)
/// </summary>
public int Gender { get; set; }
/// <summary>
/// Age
/// </summary>
public int Age { get; set; }
public Users() { }
}
最后,打开 ,添加几行代码以使用该类,并编译并运行项目。Program.csUsers
internal class Program
{
static void Main(string[] args)
{
var user = new Models.Users()
{
Id = 1,
Age = 30,
Gender = 1,
Name = "Foo",
Password = "Bar"
};
Console.WriteLine(user.Name);
}
}
在探索 go-zero 时,我发现了一个有用的功能:从 SQL 脚本生成模型代码。然后,我使用 Source Generators 实现了一个 C# 版本,并提供了该过程的详细说明。
Source Generators 是 .NET 应用程序开发人员强烈推荐的开发工具。如果您还没有尝试过,请试一试!