使用源生成器从 SQL 脚本生成 C# 模型类

作者:微信公众号:【架构师老卢】
9-23 18:29
145

在大型业务系统的开发中,我们经常依赖代码生成器。在项目中使用代码生成器的好处不言而喻:它们可以生成标准化和规范的代码,保证代码质量,并显著提高开发效率。为什么不使用它们呢?

go-zero 的灵感

最近,有人推荐我探索 go-zero,这是一个基于 Golang 的快速开发框架。在查看了它的文档和演示代码后,我发现它的设计理念与我使用的 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 '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# 源生成器实现

现在,让我们回到我们熟悉的 C#。实现此功能并不困难,并且有很多方法可以实现它。如果必须选择,我会选择 Source Generators,因为我相信这是最优雅的解决方案。

1. 什么是 Source Generators?

说到源生成器,许多 .NET 开发人员可能会觉得它们既熟悉又陌生。他们可能已经看到或听说过它们,但很少在自己的代码中使用它们。

如果您不熟悉 Source Generators,请查看 Microsoft 官方文档。我不会在这里进行详细的解释。

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

简而言之,它是一个编译器级源代码生成器,可在编译阶段生成代码并实时参与编译。 这从根本上将其与 go-zero 提供的传统代码生成工具区分开来。两年前,我开始逐渐将 Source Generators 整合到我的项目开发中,并发现它非常棒。

  • 支持动态代码生成: 修改你的应用程序或资源文件配置的上下文代码,Source Generators 会立即生成相应的代码,没有任何延迟或延迟。与传统代码生成器不同,您不需要执行单独的脚本。
  • 轻松的代码模板维护: 源生成器可以自动创建模板,确保一致性并减少手动工作。
  • 性能优化: 通过在编译时生成代码,开发人员可以引入针对应用程序需求量身定制的特定优化,这通常会产生更高的性能和更高效的代码。
  • 增强的代码可维护性: 代码在编译过程中自动生成,大大减少了源代码的数量,使其更易于维护和管理。
  • 需要调试的手动代码更少: 生成的代码遵循一致的模式,使其更易于理解和管理。
  • 一致性和标准化: 当代码自动生成时,它遵循既定的模式或标准。这可确保团队成员始终在同一页面上,从而减少差异和冲突。

2. 开始使用

让我们使用 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());  
    }
}

3. 项目使用

创建一个名为 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 应用程序开发人员强烈推荐的开发工具。如果您还没有尝试过,请试一试!

相关留言评论
昵称:
邮箱:
阅读排行