闽公网安备 35020302035485号
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 高度相似,不再啰嗦了。