前面學習了一些Source Generators的基礎只是,接下來就來實踐一下,用這個來生成我們所需要的程式碼。
本文將透過讀取swagger.json的內容,解析並生成對應的請求響應類的程式碼。
建立專案
首先還是先建立兩個專案,一個控制檯程式,一個類庫。
新增swagger檔案
在控制檯程式中新增Files目錄,並把swagger檔案放進去。別忘了還需要新增AdditionalFiles。
<ItemGroup>
<AdditionalFiles Include="Files\swagger.json" />
</ItemGroup>
實現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。
點開一個檔案,可以看到內容,並且在上方提示自動生成,無法編輯。
到這我們就完成了透過swagger來生成我們的請求和響應類的功能。
結語
本文章應用SourceGenerator,在編譯時讀取swagger.json的內容並解析,成功生成了我們API的請求和響應類的程式碼。
我們可以發現,程式碼生成沒有問題,無法移動或者編輯生成的程式碼。
下一篇文章我們就來學習下如何輸出SourceGenerator生成的程式碼檔案到我們的檔案目錄。
本文程式碼倉庫地址https://github.com/fanslead/Learn-SourceGenerator