讓代碼幫我們寫代碼(一)

来源:https://www.cnblogs.com/kklldog/archive/2022/12/05/dynamic-code-generate.html
-Advertisement-
Play Games

Hello,大家好,又是好久不見,最近太忙了(藉口)。看了下日誌,有 2 個月沒寫文章了。為了證明公眾號還活著,今天必須更新一下了。 在我們的開發過程中,總有那麼些需求是那麼的變態。常規的方案已經無法滿足。比如某些規則非常複雜,而客戶又經常要修改它。那麼我們可能需要把這部分代碼直接做為配置文件提取出 ...


Hello,大家好,又是好久不見,最近太忙了(藉口)。看了下日誌,有 2 個月沒寫文章了。為了證明公眾號還活著,今天必須更新一下了。

在我們的開發過程中,總有那麼些需求是那麼的變態。常規的方案已經無法滿足。比如某些規則非常複雜,而客戶又經常要修改它。那麼我們可能需要把這部分代碼直接做為配置文件提取出來。在每次修改後直接熱更新進我們的程式。比如我們做低代碼工具的時候可能需要根據用戶的輸入直接動態生成某些類型。再比如我們做 BI 工具的時候可能需要根據用戶選擇的表直接動態生成 Entity 的類型。碰到類似需求的時候我們該怎麼辦?今天就來整理一下 .NET 平臺關於動態代碼生成的一些技術方案。

ClassDescription

    public class ClassDescription
    {
        public string ModuleName { get; set; }

        public string AssemblyName { get; set; }

        public string ClassName { get; set; }

        public List<PropertyDescription> Properties { get; set; }
    }

    public class PropertyDescription
    {
        public string Name { get; set; }

        public Type Type { get; set; }
    }

在正式開始編寫動態代碼生成的核心代碼之前,首先我們定義一個 ClassDescription 類來幫助描述需要生成的 class 長啥樣。裡面主要是描述了一些類名,屬性名,屬性類型等信息。

Emit

在 .NET Core 之前我們要動態生成一個 class 那麼幾乎 Emit 是首先技術。當然 Emit 在 .NET Core 中依然可以使用。System.Reflection.Emit 的命名空間這樣的,所以很明顯還是反射技術的一種。普通的反射可能只是動態來獲取程式集里的元數據,然後操作或者運行它。而 Emit 可以完全動態的創建一個程式集或者類。那麼讓我們看看怎麼用 Emit 來動態生成一個 class 吧。
比如我們現在需要動態生成一個 User 類,如果正常編寫那麼大概長這樣:

public class User {
    public string Name { get;set;}
    public int Age {get;set;}
}

下麵讓我們來用 Emit 動態創建它:
首先,用 ClassDescription 來定義 User 類,它裡面有 2 個屬性 Name,Age。

        var userClassDesc = new ClassDescription()
            {
                AssemblyName = "X",
                ModuleName = "X",
                ClassName = "User",
                Properties = new List<PropertyDescription> {
                    new PropertyDescription {
                        Type = typeof(string),
                        Name = "Name"
                    },
                    new PropertyDescription
                    {
                        Type = typeof(int),
                        Name = "Age"
                    }
                }
            };

接著就是正式使用 Emit 來編寫這個類了。整個過程大概可以分這麼幾步:

  1. 定義 assembly
  2. 定義 module
  3. 定義 class
  4. 定義 properties

上面的代碼,如果看過 IL 的同學就比較熟悉了,這個代碼基本就是在手寫 IL 了。其中要註意的是:屬性的定義要分 2 步,除了定義屬性外,還需要定義 Get Set 方法,然後跟屬性關聯起來。因為大家都知道,屬性其實只是封裝了方法而已。

   public Type Generate(ClassDescription clazz)
        {
            MethodAttributes getSetAttr =
               MethodAttributes.Public | MethodAttributes.SpecialName |
                   MethodAttributes.HideBySig;

            // define class
            var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName(clazz.AssemblyName), AssemblyBuilderAccess.Run);
            var moduleBuilder = assemblyBuilder.DefineDynamicModule(clazz.ModuleName);
            var typeBuilder = moduleBuilder.DefineType(clazz.ClassName, TypeAttributes.Public | TypeAttributes.Class | TypeAttributes.AutoClass | TypeAttributes.AnsiClass);

            foreach (var item in clazz.Properties)
            {
                var propName = item.Name;
                var fieldName = $"_{propName}";
                var typee = item.Type;

                //define field
                var fieldBuilder = typeBuilder.DefineField(fieldName,
                                                             typee,
                                                            FieldAttributes.Private);
                //define property
                var propBuilder = typeBuilder.DefineProperty(propName, PropertyAttributes.SpecialName, typee, Type.EmptyTypes);

                //define getter
                var getPropMthdBldr = typeBuilder.DefineMethod($"get{fieldName}", getSetAttr, typee, Type.EmptyTypes);
                var getIL = getPropMthdBldr.GetILGenerator();
                getIL.Emit(OpCodes.Ldarg_0);
                getIL.Emit(OpCodes.Ldfld, fieldBuilder);
                getIL.Emit(OpCodes.Ret);
                //define setter
                var setPropMthdBldr = typeBuilder.DefineMethod($"set{fieldName}", getSetAttr, null, new Type[] { typee });
                var idSetIL = setPropMthdBldr.GetILGenerator();
                idSetIL.Emit(OpCodes.Ldarg_0);
                idSetIL.Emit(OpCodes.Ldarg_1);
                idSetIL.Emit(OpCodes.Stfld, fieldBuilder);
                idSetIL.Emit(OpCodes.Ret);

                // connect prop to getter setter
                propBuilder.SetGetMethod(getPropMthdBldr);
                propBuilder.SetSetMethod(setPropMthdBldr);
            }

            //create type
            var type = typeBuilder.CreateType();

            return type;
        }

下麵讓我們編寫一個單元測試來測試一下:

            var userClassDesc = new ClassDescription()
            {
                AssemblyName = "X",
                ModuleName = "X",
                ClassName = "User",
                Properties = new List<PropertyDescription> {
                    new PropertyDescription {
                        Type = typeof(string),
                        Name = "Name"
                    },
                    new PropertyDescription
                    {
                        Type = typeof(int),
                        Name = "Age"
                    }
                }
            };

            var generator = new ClassGeneratorByEmit();
            var type = generator.Generate(userClassDesc);

            dynamic user = Activator.CreateInstance(type, null);
            Assert.IsNotNull(user);

            user.Name = "mj";
            Assert.AreEqual("mj", user.Name);

            user.Age = 18;
            Assert.AreEqual(18, user.Age);

獲得 type 之後,我們使用反射來創建 User 的實例對象。然後通過 dynamic 來給屬性賦值跟取值,避免了繁瑣的反射代碼。
運行上面的測試代碼,單元測試綠色,通過了。

Roslyn

Roslyn 是微軟最新開源的代碼分析,編譯工具。它提供了非常多的高級 API 來讓用戶在運行時分析代碼,生成程式集、類。所以它現在是運行時代碼生成的首選項。下麵讓我們看看怎麼使用 Roslyn 來實現動態生成一個 User class 。
在使用 Roslyn 之前我們需要安裝一個 nuget 包:

Microsoft.CodeAnalysis.CSharp

我們平時正常編寫的代碼,其實就是一堆字元串,通過編譯器編譯後變成了 IL 代碼。那麼使用的 Roslyn 的時候過程也是一樣的。我們首先就是要使用代碼來生成這個 User class 的字元串模板。然後把這段字元串交給 Roslyn 去分析與編譯。編譯完後就可以獲得這個 class 的 Type 了。

 public Type Generate(ClassDescription clazz)
        {
            const string clzTemp =
                @"
                using System;
                using System.Runtime;
                using System.IO;

                namespace WdigetEngine 
                {
                
                    public class @className 
                    {
                        @properties
                    }
                
                }
                ";

            const string propTemp =
                @"
                public @type @propName { get;set; }
                ";

            var properties = new StringBuilder("");

            foreach (var item in clazz.Properties)
            {
                string strProp = propTemp.Replace("@type", item.Type.Name).Replace("@propName", item.Name);
                properties.AppendLine(strProp);
            }

            string sourceCode = clzTemp.Replace("@className", clazz.ClassName).Replace("@properties", properties.ToString());

            Console.Write(sourceCode);

            var syntaxTree = SyntaxFactory.ParseSyntaxTree(sourceCode);

            var compilation = CSharpCompilation.Create(
            syntaxTrees: new[] { syntaxTree },
            assemblyName: $"{clazz.AssemblyName}.dll",
            options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary),
            references: AppDomain.CurrentDomain.GetAssemblies().Where(x=> !x.IsDynamic).Select(x => MetadataReference.CreateFromFile(x.Location))
            );

            Assembly compiledAssembly;
            using (var stream = new MemoryStream())
            {
                var compileResult = compilation.Emit(stream);
                if (compileResult.Success)
                {
                    compiledAssembly = Assembly.Load(stream.GetBuffer());
                }
                else
                {
                    throw new Exception("Roslyn compile err .");
                }
            }
            var types = compiledAssembly.GetTypes();

            return types.FirstOrDefault(c => c.Name == clazz.ClassName);

        }

使用同樣的測試用例來測試一下 :


            var generator = new ClassGeneratorByRoslyn();
            var type = generator.Generate(userClassDesc);

            dynamic user = Activator.CreateInstance(type, null);
            Assert.IsNotNull(user);

            user.Name = "mj";
            Assert.AreEqual("mj", user.Name);

            user.Age = 18;
            Assert.AreEqual(18, user.Age);

測試同樣通過了。
通過以上代碼我們可以發現使用 Roslyn 來動態生成代碼的難度其實要比 Emit 簡單不少。因為使用 Roslyn 的過程更接近於我們手寫代碼,而 Emit 的話是手寫 IL ,顯然手寫 IL 對於一般同學來說是更困難的。

Natasha

如果還是覺得 Roslyn 操作起來麻煩,那麼還可以使用 NCC 旗下開源項目 Natasha。Natasha 做為 Roslyn 的封裝,所以放到 Roslyn 下麵一起講。
什麼是 Natasha ?
Natasha 是基於 Roslyn 的 C# 動態程式集構建庫,該庫允許開發者在運行時使用 C# 代碼構建域 / 程式集 / 類 / 結構體 / 枚舉 / 介面 / 方法等,使得程式在運行的時候可以增加新的模塊及功能。Natasha 集成了域管理/插件管理,可以實現域隔離,域卸載,熱拔插等功能。 該庫遵循完整的編譯流程,提供完整的錯誤提示, 可自動添加引用,完善的數據結構構建模板讓開發者只專註於程式集腳本的編寫,相容 netcoreapp3.0+, 跨平臺,統一、簡便的鏈式 API。

https://github.com/dotnetcore/Natasha

下麵我們演示下使用 Natasha 來構建這個 User Class :
首先使用 nuget 安裝 natasha 類庫:

DotNetCore.Natasha.CSharp

編寫 class 生成的代碼:

        public Type Generate()
        {
            NClass nClass = NClass.DefaultDomain();
            nClass
              .Namespace("MyNamespace")
              .Public()
              .Name("User")
              .Property(prop => prop
                .Type(typeof(string))
                .Name("Name")
                .Public()
              )
              .Property(prop => prop
                .Type(typeof(int))
                .Name("Age")
                .Public()
              );

            return nClass.GetType();
        }

以上就是使用 natasha 動態編譯一個類型的代碼,代碼量直線下降,而且支持鏈式調用,非常的優雅。

CodeDom

在沒有 Roslyn 之前,微軟還有一項技術 CodeDom ,同樣可以根據字元串模板來運行時生成代碼。他的使用跟 Roslyn 非常相似,同樣是在模擬手寫代碼的過程。但是現在這項技術僅限於 .Net Framework 上使用了,微軟並沒有合併到 .NET Core 上來,github 上也有相關討論,因為已經有了 Roslyn ,微軟覺得這個技術已經沒有意義了。
不管怎麼樣這裡還是演示一下如何使用 CodeDom 來動態生成代碼:

  public Type Generate(ClassDescription clazz)
        {
            const string clzTemp =
                @"
                namespace WdigetEngine {
                
                    public class @className 
                    {
                        @properties
                    }
                
                }
                ";

            const string propTemp =
                @"
                public @type @propName { get;set; }
                ";

            var properties = new StringBuilder("");

            foreach (var item in clazz.Properties)
            {
                string strProp = propTemp.Replace("@type", item.Type.Name).Replace("@propName", item.Name);
                properties.AppendLine(strProp);
            }

            string sourceCode = clzTemp.Replace("@className", clazz.ClassName).Replace("@properties", properties.ToString());

            Console.Write(sourceCode);

            var codeProvider = new CSharpCodeProvider();
            CompilerParameters param = new CompilerParameters(new string[] { "System.dll" });
            CompilerResults result = codeProvider.CompileAssemblyFromSource(param, sourceCode);
            Type t = result.CompiledAssembly.GetType(clazz.ClassName);

            return t;
        }

以上代碼需要在 .NET Framework 上測試。整個過程跟 Roslyn 高度相似,不再啰嗦了。

總結

通過以上我們大概總結了 3 種方案(Emit , Roslyn (含 natasha) , CodeDom)來實現運行時代碼生成。現在最推薦的是 Roslyn 方案。因為它的過程比較符合手寫代碼的感覺,而且他還提供了代碼分析功能,能返回編寫代碼的語法錯誤等信息,非常有助於 debug 。如果你現在有動態代碼生成的需求,那麼 Roslyn 是你的最佳選擇。

未完待續

除了以上 3 種代碼生成技術,其實還有一種代碼生成技術: Source Generator 。Source Generator 在最近幾個版本的 .NET 中是一個非常重要的技術。通過它可以讓程式的性能很大的提升。下一篇我們就來說說 Source Generator 。

敬請期待。

QQ群:1022985150 VX:kklldog 一起探討學習.NET技術
作者:Agile.Zhou(kklldog)
出處:http://www.cnblogs.com/kklldog/
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接,否則保留追究法律責任的權利。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 說明: 1. 本文基於Spring-Framework 5.1.x版本講解 2. 建議讀者對Mybatis有基本的使用經驗 概述 這一篇我們講講org.springframework.beans.factory.FactoryBean介面,這個介面功能非常強大,可以集成不同的中間件或組件到Sprin ...
  • 本文基於 newbeemall 項目升級Spring Boot3.0踩坑總結而來,附帶更新說明: Spring-Boot-3.0-發佈說明 Spring-Boot-3.0.0-M5-發佈說明 一. 編譯報錯,import javax.servlet.*; 不存在 這個報錯主要是Spring Boot ...
  • 前言 近期在對開發框架安全策略方面進行升級優化,提供一些通用場景的解決方案,本文針對前後端數據傳輸加密進行簡單的分享,處理流程設計如下圖所示,本加密方法對原有項目相容性較好,只需要更換封裝好的加密Ajax請求方法,後端統一攔截判斷是否需要解密即可 生成DESKey 生成的DES加密密鑰一定是8的整數 ...
  • ​ 這裡有個坑 1:轉賬低於5毛會失敗 2:轉賬金額需要自己取整一下,微信官方金額是 分 為單位,換算成 元 時可能會除不盡 { "code":"PARAM_ERROR", "detail":{ "location":"body", "value":7.000000000000001 // 微信金額 ...
  • 普通的切片對迭代器無法實行切片操作 1 from itertools import islice 2 3 4 def func(): 5 for i in [4, 9, 6, 2]: 6 if i % 2 == 0: 7 yield i 8 9 10 f = func() 11 res = isli ...
  • noi 1.5 39 與 7 無 關 的 數 。 1.描述 一個正整數,如果它能被7整除,或者它的十進位表示法中某一位上的數字為7,則稱其為與7相關的數.現求所有小於等於n(n < 100)的與7無關的正整數的平方和. 2.輸入 輸入為一行,正整數n(n < 100) 3.輸出 輸出一行,包含一個整 ...
  • 說明: 1. 本文基於Spring-Framework 5.1.x版本講解 2. 建議讀者對創建對象部分源碼有一定瞭解 概述 這篇講講Spring迴圈依賴的問題,網上講迴圈依賴的帖子太多太多了,相信很多人也多多少少瞭解一點,那我還是把這個問題自己梳理一遍,主要是基於以下出發點: 1. Spring到 ...
  • JZ31 棧的壓入、彈出序列 描述 輸入兩個整數序列,第一個序列表示棧的壓入順序,請判斷第二個序列是否可能為該棧的彈出順序。假設壓入棧的所有數字均不相等。例如序列1,2,3,4,5是某棧的壓入順序,序列4,5,3,2,1是該壓棧序列對應的一個彈出序列,但4,3,5,1,2就不可能是該壓棧序列的彈出序 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...