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 长啥样。里面主要是描述了一些类名,属性名,属性类型等信息。
在 .NET Core 之前我们要动态生成一个 class 那么几乎 Emit 是首先技术。当然 Emit 在 .NET Core 中依然可以使用。System.Reflection.Emit 的命名空间这样的,所以很明显还是反射技术的一种。普通的反射可能只是动态来获取程序集里的元数据,然后操作或者运行它。而 Emit 可以完全动态的创建一个程序集或者类。那么让我们看看怎么用 Emit 来动态生成一个 class 吧。
public class User { public string Name { get;set;} public int Age {get;set;} }下面让我们来用 Emit 动态创建它:
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 来编写这个类了。整个过程大概可以分这么几步:
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); } // 堆代码 duidaima.com //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 来给属性赋值跟取值,避免了繁琐的反射代码。
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);测试同样通过了。
什么是 Natasha ?
DotNetCore.Natasha.CSharp
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 高度相似,不再啰嗦了。