我们做业务系统的开发,很多时候往往离不开代码生成器。项目使用代码生成器的好处不言而喻,生成出来的代码标准规范,代码质量有保障,并且还能大幅提高开发效率,何乐不为呢?
来自go-zero得到的灵感
最近,我在朋友推荐下研究了go-zero,一个基于golang的快速开发框架。我翻阅了一下它的文档和demo代码,发现其的设计思想跟我用的C#开发框架,功能上基本大同小异。
其中有一项功我是比较有用:根据SQL脚本来生成数据表实体类。下面简单描述一下go-zero实现的过程:
- 首先,定一个创建MySQL数据表的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
,执行以下命令:
goctl model mysql ddl -src="users.sql" -dir="./model" -c
- 最后,在所生成的
usersmodel.go
代码中,得到一个名为Users
结构体。
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来实现
说完go-zero,回到我们熟悉的C#。想要实现这个功能,其实不困难,而且实现的方法可以有很多。如果让我选我会首选用Source Generators来实现,因为我个人认为是最最优雅的。
1、什么是Source Generators?
说起,SourceGenerator其实很多.NET开发者会感到既熟悉又陌生,总觉得有在哪里看到过或者听到过,但是自己开发的代码却很少用到过。
如果大家对 Source Generators 没有概念,请先阅读一下微软官方文档,我这里不做详细介绍的展开。
https://learn.microsoft.com/en-us/dotnet/csharp/roslyn-sdk/source-generators-overview
总结成一句话:它是编译器级别的源码生成器,在编译阶段生成代码,并即时参与编译。 这就决定了它与go-zero提供的传统代码生成工具,有本质上的区别。早在两年前,已经将开始逐步将Source Generators应用到我的项目开发中,感觉非常棒。
- 支持动态代码生成:修改应用程序的上下文代码或者资源文件配置,Source Generators立刻就能帮你生成出相应的代码,没有半点滞后和延迟。不像供传统代码生成器,还需要另外执行脚本。
- 代码模板维护非常简单:源代码生成器可以自动创建样板,确保一致性并减少手动工作。
- 性能优化:通过在编译时生成代码的能力,开发人员可以引入针对应用程序需求量身定制的特定优化,通常会产生更高性能和更高效的代码。
- 增强代码可维护性:代码编译时自动生成,从而大大减少源码的代码量,从而变得更容易维护管理。
- 需要调试的手动代码更少,生成的代码遵循一致的模式,使其更易于理解和管理。
- 一致性和标准化:当代码自动生成时,它遵循设定的模式或标准。这确保了团队成员始终在同一页面上,从而减少差异和冲突。
2、开始使用
下面请跟随我一起,用Source Generators来实现这个目标。
首先,创建一个.NET Standard 2.0的项目。
接着,添加依赖Microsoft.CodeAnalysis.CSharp
和Microsoft.CodeAnalysis.Analyzers
。其中,需要将Microsoft.CodeAnalysis.CSharp
的属性设置为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>
请注意:Microsoft.CodeAnalysis.CSharp的版本不要选太高的,4.8以上的会有额外的依赖对 运行Source Generator不太友好。
创建好项目,接着开始编写代码:
[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>();
// 正则表达式逐行解析SQL脚本,提取表和字段的关键信息
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)
};
// 调用模板输出代码
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("/// <summary>")
.AppendLine($"/// Model for Table :{table.Comment}")
.AppendLine("/// <para>此代码由SourceGenerator生成</para>")
.AppendLine($"/// <para>生成时间:{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());
}
}
3、项目调用
新建一个名为GeneratorApp
的控制台项目,然后引用刚才的SqlGenerator项目。
<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到项目里,并且将其编译动作设为AdditionalFiles
。
接下来启动编译项目,在GeneratorApp
的依赖项的分析器中会出现一个名为Users_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>
/// 用户名
/// </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() { }
}
最后,打开Program.cs
写几行代码来实现Users
类的调用。接着,直接编译运行。
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);
}
}
总结
我从go-zero中发现了用SQL脚本来生成model代码的功能,感觉非常棒。于是,我使用C# Source Generators来实现了一个类似的功能。
对.NET开发者而言,Source Generators绝对是宝藏级的开发工具。强烈推荐给大家!