学习Source Generators之从swagger中生成类

学习Source Generators之从swagger中生成类 Generators swagger Source 中生成类 学习 之从

前面学习了一些Source Generators的基础只是,接下来就来实践一下,用这个来生成我们所需要的代码。
本文将通过读取swagger.json的内容,解析并生成对应的请求响应类的代码。

创建项目

首先还是先创建两个项目,一个控制台程序,一个类库。
image.png

添加swagger文件

在控制台程序中添加Files目录,并把swagger文件放进去。别忘了还需要添加AdditionalFiles。

<ItemGroup>
  <AdditionalFiles Include="Files\swagger.json" />
</ItemGroup>

image.png

实现ClassFromSwaggerGenerator

安装依赖

由于我们需要解析swagger,所以需要安装一下JSON相关的包。这里我们安装了Newtonsoft.Json。
需要注意的是,依赖第三方包的时候需要在项目文件添加下面内容:

<PropertyGroup>
  <GetTargetPathDependsOn>$(GetTargetPathDependsOn);GetDependencyTargetPaths</GetTargetPathDependsOn>
</PropertyGroup>
<Target Name="GetDependencyTargetPaths" AfterTargets="ResolvePackageDependenciesForBuild">
  <ItemGroup>
    <TargetPathWithTargetPlatformMoniker Include="@(ResolvedCompileFileDefinitions)" IncludeRuntimeDependency="false" />
  </ItemGroup>
</Target>

否则编译时会出现FileNotFound的异常。

构建管道

这里我们通过AdditionalTextsProvider筛选以及过滤我们的swagger文件。

var pipeline = context.AdditionalTextsProvider.Select(static (text, cancellationToken) =>
  {
      if (!text.Path.EndsWith("swagger.json", StringComparison.OrdinalIgnoreCase))
      {
          return default;
      }

      return JObject.Parse(text.GetText(cancellationToken)!.ToString());
  })
    .Where((pair) => pair is not null);

实现生成代码逻辑

接下来我们就解析Swagger中的内容,并且动态拼接代码内容。主要代码部分如下:

context.RegisterSourceOutput(pipeline, static (context, swagger) =>
 {

     List<(string name, string sourceString)> sources = new List<(string name, string sourceString)>();


     #region 生成实体
     var schemas = (JObject)swagger["components"]!["schemas"]!;
     foreach (JProperty item in schemas.Properties())
     {
         if (item != null)
         {
             sources.Add((HandleClassName(item.Name), $@"#nullable enable
using System;
using System.Collections.Generic;

namespace SwaggerEntities;
public {ClassOrEnum((JObject)item.Value)} {HandleClassName(item.Name)} 
{{
    {BuildProperty((JObject)item.Value)}
}}
"));
         }
     }
     foreach (var (name, sourceString) in sources)
     {
         var sourceText = SourceText.From(sourceString, Encoding.UTF8);

         context.AddSource($"{name}.g.cs", sourceText);
     }
     #endregion
     });

完整的代码如下:

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;

namespace GenerateClassFromSwagger.Analysis
{
    [Generator]
    public class ClassFromSwaggerGenerator : IIncrementalGenerator
    {
        public void Initialize(IncrementalGeneratorInitializationContext context)
        {
            var pipeline = context.AdditionalTextsProvider.Select(static (text, cancellationToken) =>
            {
                if (!text.Path.EndsWith("swagger.json", StringComparison.OrdinalIgnoreCase))
                {
                    return default;
                }

                return JObject.Parse(text.GetText(cancellationToken)!.ToString());
            })
            .Where((pair) => pair is not null);

            context.RegisterSourceOutput(pipeline, static (context, swagger) =>
            {

                List<(string name, string sourceString)> sources = new List<(string name, string sourceString)>();


                #region 生成实体
                var schemas = (JObject)swagger["components"]!["schemas"]!;
                foreach (JProperty item in schemas.Properties())
                {
                    if (item != null)
                    {
                        sources.Add((HandleClassName(item.Name), $@"#nullable enable
using System;
using System.Collections.Generic;

namespace SwaggerEntities;
public {ClassOrEnum((JObject)item.Value)} {HandleClassName(item.Name)} 
{{
    {BuildProperty((JObject)item.Value)}
}}
                "));
                    }
                }
                foreach (var (name, sourceString) in sources)
                {
                    var sourceText = SourceText.From(sourceString, Encoding.UTF8);

                    context.AddSource($"{name}.g.cs", sourceText);
                }
                #endregion
            });
        }

        static string HandleClassName(string name)
        {
            return name.Split('.').Last().Replace("<", "").Replace(">", "").Replace(",", "");
        }
        static string ClassOrEnum(JObject value)
        {
            return value.ContainsKey("enum") ? "enum" : "partial class";
        }


        static string BuildProperty(JObject value)
        {
            var sb = new StringBuilder();
            if (value.ContainsKey("properties"))
            {
                var propertys = (JObject)value["properties"]!;
                foreach (JProperty item in propertys!.Properties())
                {
                    sb.AppendLine($@"
    public {BuildProertyType((JObject)item.Value)} {ToUpperFirst(item.Name)}  {{ get; set; }}
");
                }
            }
            if (value.ContainsKey("enum"))
            {
                foreach (var item in JsonConvert.DeserializeObject<List<int>>(value["enum"]!.ToString())!)
                {
                    sb.Append($@"
    _{item},
");
                }
                sb.Remove(sb.Length - 1, 1);
            }
            return sb.ToString();
        }

        static string BuildProertyType(JObject value)
        {
            ;
            var type = GetType(value);
            var nullable = value.ContainsKey("nullable") ? value["nullable"]!.Value<bool?>() switch
            {
                true => "?",
                false => "",
                _ => ""
            } : "";
            return type + nullable;
        }

        static string GetType(JObject value)
        {
            return value.ContainsKey("type") ? value["type"]!.Value<string>() switch
            {
                "string" => "string",
                "boolean" => "bool",
                "number" => value["format"]!.Value<string>() == "float" ? "float" : "double",
                "integer" => value["format"]!.Value<string>() == "int32" ? "int" : "long",
                "array" => ((JObject)value["items"]!).ContainsKey("items") ?
                $"List<{HandleClassName(value["items"]!["$ref"]!.Value<string>()!)}>"
                : $"List<{GetType((JObject)value["items"]!)}>",
                "object" => value.ContainsKey("additionalProperties") ? $"Dictionary<string, {GetType((JObject)value["additionalProperties"]!)}>" : "object",
                _ => "object"
            } : value.ContainsKey("$ref") ? HandleClassName(value["$ref"]!.Value<string>()!) : "object";
        }

        static unsafe string ToUpperFirst(string str)
        {
            if (str == null) return null;
            string ret = string.Copy(str);
            fixed (char* ptr = ret)
                *ptr = char.ToUpper(*ptr);
            return ret;
        }
    }
}

详细的处理过程大家可以仔细看看代码,这里就不一一解释了。

启动编译

接下来编译控制台程序。编译成功后可以看到生成了很多cs的代码。若是看不见,可以重启VS。
image.png
点开一个文件,可以看到内容,并且在上方提示自动生成,无法编辑。
image.png
到这我们就完成了通过swagger来生成我们的请求和响应类的功能。

结语

本文章应用SourceGenerator,在编译时读取swagger.json的内容并解析,成功生成了我们API的请求和响应类的代码。
我们可以发现,代码生成没有问题,无法移动或者编辑生成的代码。
下一篇文章我们就来学习下如何输出SourceGenerator生成的代码文件到我们的文件目录。

本文代码仓库地址https://github.com/fanslead/Learn-SourceGenerator

评论