动态代码
C# 中实现动态代码的方式有很多,比如 反射、表达式树、EMIT、Roslyn、Source Generators 等,C# 各类框架中几乎都有动态代码技术的使用,比如依赖注入、对象关系映射、AOP 技术等。由于动态代码技术在 C# 中的使用场景非常广泛,因此在本章中,笔者将会介绍多种动态代码技术,以及完成部分实践,完成常见几种框架技术的编写方法。
阅读本章内容之前,需要读者熟练掌握反射技术,需要学习反射技术,可以参考笔者的在线电子书课程: https://reflect.whuanle.cn/
EMIT
EMIT 是一种使用 C# 编排生成 IL 代码的技术,IL 是 .NET 平台的中间语言,由于 IL 的高性能的特点,很多框架都使用 EMIT 技术动态生成代码,最广泛的使用是编写 AOP 框架。在本节中,笔者将会介绍 AOP 的实现原理,以及使用 EMIT 编写一个简单的 AOP 程序。
创建控制台项目,引入 CZGL.AOP
包,示例代码请参考 Demo.CZGLAOP 项目。
有以下接口和类型:
public interface ITest
{
void MyMethod();
}
public class Test : ITest
{
public virtual string A { get; set; }
public Test()
{
Console.WriteLine("构造函数没问题");
}
public virtual void MyMethod()
{
Console.WriteLine("运行中");
}
}
我们希望,在执行 MyMethod 方法时,能够在执行前后打印出日志,这时可以先编写一个特性类,继承 ActionAttribute ,实现 Before 和 After 接口。
public class LogAttribute : ActionAttribute
{
public override void Before(AspectContext context)
{
Console.WriteLine("--执行前--");
}
public override object After(AspectContext context)
{
Console.WriteLine("--执行后--");
if (context.IsMethod)
return context.MethodResult;
else if (context.IsProperty)
return context.PropertyValue;
return null;
}
}
然后改造 Test 类型。
[Interceptor]
public class Test : ITest
{
[Log]
public virtual string A { get; set; }
public Test()
{
Console.WriteLine("构造函数");
}
[Log]
public virtual void MyMethod()
{
Console.WriteLine("运行中");
}
}
然后创建 AOP 类型:
ITest test1 = AopInterceptor.CreateProxyOfInterface<ITest, Test>();
test1.MyMethod();
Test test2 = AopInterceptor.CreateProxyOfClass<Test>();
test2.MyMethod();
运行项目,会输出:
构造函数
--执行前--
运行中
--执行后--
构造函数
--执行前--
运行中
AOP 实现原理
AOP(Aspect-oriented Programming) 即面向切片编程,在 C# 中有动态 AOP 和静态 AOP 两种,如果是在程序启动后生成的,为动态 AOP,这类框架有 Castle 、AspectCore 等,它们都使用了 EMIT 技术,在代码编译时即生成的,为静态 AOP,这类框架有 Fody 等。
实现 AOP 的前提
请看如下所示的代码,当调用 Voice()
方法时,请思考控制台会打印什么内容。
public class Program
{
static void Main()
{
Animal c = new Cat();
Console.WriteLine(c.Voice());
}
public abstract class Animal
{
public string Voice() => "null";
}
public class Cat: Animal
{
public new string Voice() => "喵";
}
}
如果你有运行代码,会发现打印结果是 null
,虽然 c 是 Cat 类型,但是这里我们使用的是 Animal 类型,CLR 首先判断 Voice 方法是否为抽象方法或虚方法,如果不是则直接调用,不会往子类中查找。
我们使用工具查看Animal 中 Voice 方法的 IL 代码:
// Methods
.method public hidebysig
instance string Voice () cil managed
那么,同样的代码,在 java 中,又会发生什么呢?
public class Main {
public static void main(String[] args) {
Animal animal = new Cat();
System.out.println(animal.Voice());
}
}
class Animal{
public String Voice(){
return "null";
}
}
class Cat extends Animal{
public String Voice(){
return "喵";
}
}
运行这段代码后会发现,打印出来的是 喵
。因为 java 中的方法默认是虚方法,而 C# 中的 方法需要加上关键字 virtual 才是虚方法。
为了能够在使用父类方法时,执行的是子类的代码,我们需要将代码改成:
public abstract class Animal
{
public virtual string Voice() => "null";
}
public class Cat : Animal
{
public override string Voice() => "喵";
}
// Methods
.method public hidebysig newslot virtual
instance string Voice () cil managed
那么,虚方法跟实现 AOP 有啥关系呢?其实,使用 EMIT 技术编写 AOP 框架的思路很简单,那就是继承,比如我们要给 A 类型的 A 方法实现 AOP,那么 A 方法就必须得是抽象方法或虚方法,然后我们通过 EMIT 技术生成一个类型 B 继承 A,然后创建 A 类型时实际上创建的是 B 类型。此时,调用 A 中的 A 方法,CLR 会执行 B 中的 A 方法。
A a = new B();
在使用 EMIT 技术实现 AOP 之前,我们可以通过容器依赖注入来领会 AOP 是如何通过继承实现的。
有个 UserService 服务,实现了登录过程。
public class UserService
{
public virtual bool Login(string name, string passeword)
{
return true;
}
}
然后我们使用一个新的类型重写 Login 方法,在执行方法之前和之后打印日志。
public class UserServiceAop : UserService
{
public override bool Login(string name, string passeword)
{
Console.WriteLine($"用户开始登录:{name}");
var result = base.Login(name, passeword);
Console.WriteLine($"用户 {name} 登录结果 {result}");
return result;
}
}
然后通过注入服务实现 AOP:
IServiceCollection ioc = new ServiceCollection();
ioc.AddScoped<UserService, UserServiceAop>();
var services = ioc.BuildServiceProvider();
var userService = services.GetRequiredService<UserService>();
userService.Login("工良", "123456");
如果需要 AOP 的是接口,那就更加简单,我们只需要生成一个继承接口的类型即可,完全不需要继承父类,而父类也不需要标记为虚方法或抽象方法。
public interface IUserService
{
bool Login(string name, string passeword);
}
public class UserService : IUserService
{
public bool Login(string name, string passeword)
{
return true;
}
}
public class UserServiceAop : IUserService
{
private readonly UserService _service;
public UserServiceAop()
{
_service = new UserService();
}
public bool Login(string name, string passeword)
{
Console.WriteLine($"用户开始登录:{name}");
var result = _service.Login(name, passeword);
Console.WriteLine($"用户 {name} 登录结果 {result}");
return result;
}
}
IServiceCollection ioc = new ServiceCollection();
ioc.AddScoped<IUserService, UserServiceAop>();
var services = ioc.BuildServiceProvider();
var userService = services.GetRequiredService<IUserService>();
userService.Login("工良", "123456");
实际上,接口方法本身就是抽象方法,所以因此无需处理接口即可直接使用 AOP 生成代理类型。
// Methods
.method public hidebysig newslot abstract virtual
instance bool Login (
string name,
string passeword
) cil managed
{
} // end of method IUserService::Login
通过这个例子你应该可以领会到使用 EMIT 技术实现 AOP ,最基础最本质的是实现一个新的类型基础父类或接口。由于 AOP 的类型是动态生成的,我们在开发是无法创建,所以 AOP 一般需要结合 IOC 使用,我们可以拦截容器中的 <IUserService, UserService>
,替换成 <IUserService, UserServiceAop>
。或者提供一个工厂服务,用于获取 AOP 后的对象。
EMIT 实现 AOP
本节代码可以在 Demo8.Console、Demo8.FxConsole 中查看。
ILSpy 是一个反编译工具,能够帮助我们查看代码生成的 IL,这样一来,即使我们对 IL 不熟悉,也可以通过编译好的 IL 代码中抄过来。
在 Visual Studio 中 安装扩展 ILSpy ,安装或手动下载(https://github.com/icsharpcode/ILSpy)。
创建一个项目,然后创建接口和类型。
public interface IUserService
{
bool Login(string name, string passeword);
}
public class UserService : IUserService
{
public bool Login(string name, string passeword)
{
return true;
}
}
public class UserServiceAop : IUserService
{
private readonly UserService _service;
public UserServiceAop()
{
_service = new UserService();
}
public bool Login(string name, string passeword)
{
Console.WriteLine($"用户开始登录:{name}");
var result = _service.Login(name, passeword);
Console.WriteLine($"用户 {name} 登录结果 {result}");
return result;
}
}
然后编译项目生成 dll 文件,将 Demo8.Console.dll 拖动放到 ILSpy 中,你可以查看到 UserServiceAop 的全部 IL 代码,我们只需要抄即可!
接下来,我们开始使用 EMIT 技术,动态生成一个 UserServiceAop 类型。
首先是动态构建程序集并创建一个新的类型。
public static Assembly Build()
{
// 构建运行时程序集
AssemblyName assemblyName = new AssemblyName("AopTmp");
assemblyName.SetPublicKeyToken(new Guid().ToByteArray());
AssemblyBuilder assBuilder = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.RunAndCollect);
// 构建模块
ModuleBuilder moduleBuilder = assBuilder.DefineDynamicModule(assemblyName.Name);
/// 构建类型,命名空间+类名
TypeBuilder typeBuilder = moduleBuilder.DefineType("Aop.UserServiceAop",
TypeAttributes.Public, parent: null, interfaces: typeof(UserService).GetInterfaces());
// 构建字段
// field private initonly class Program/UserService _service
var fieldBuilder = typeBuilder.DefineField("_service", typeof(UserService), FieldAttributes.Private | FieldAttributes.InitOnly);
BuildCtor(typeBuilder, fieldBuilder);
BuildMethod(typeBuilder, fieldBuilder);
var type = typeBuilder.CreateType();
return assBuilder;
}
创建类型后,首先给类型添加构造函数。
private static void BuildCtor(TypeBuilder typeBuilder, FieldBuilder fieldBuilder)
{
// 构造函数
// .method public hidebysig specialname rtspecialname
var ctorBuilder = typeBuilder.DefineConstructor(MethodAttributes.Public,
CallingConventions.Standard,
Type.EmptyTypes);
var il = ctorBuilder.GetILGenerator();
// _service = new UserService();
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Newobj, typeof(UserService).GetConstructors()[0]);
il.Emit(OpCodes.Stfld, fieldBuilder);
il.Emit(OpCodes.Ret);
}
然后生成 Login
方法。
private static void BuildMethod(TypeBuilder typeBuilder, FieldBuilder fieldBuilder)
{
var baseMethod = typeof(UserService).GetMethod("Login");
// .method public final hidebysig newslot virtual
var methodBuilder = typeBuilder.DefineMethod(baseMethod.Name,
baseMethod.Attributes,
baseMethod.CallingConvention,
// 返回值和参数
baseMethod.ReturnType, baseMethod.GetParameters().Select(x => x.ParameterType).ToArray());
var sType = typeof(DefaultInterpolatedStringHandler);
ILGenerator il = methodBuilder.GetILGenerator();
// 定义本地变量
il.DeclareLocal(typeof(bool));
il.DeclareLocal(sType);
// Console.WriteLine("用户开始登录:" + name);
il.Emit(OpCodes.Ldstr, "用户开始登录: ");
il.Emit(OpCodes.Ldarg_1);
il.Emit(OpCodes.Call, typeof(String).GetMethod("Concat", new Type[] { typeof(string), typeof(string) }));
il.Emit(OpCodes.Call, typeof(Console).GetMethod("WriteLine", new Type[] { typeof(string) }));
// bool result = _service.Login(name, passeword);
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldfld, fieldBuilder);
il.Emit(OpCodes.Ldarg_1);
il.Emit(OpCodes.Ldarg_2);
il.Emit(OpCodes.Callvirt, baseMethod);
il.Emit(OpCodes.Stloc_0);
// Console.WriteLine($"用户 {name} 登录结果 {result}");
// DefaultInterpolatedStringHandler defaultInterpolatedStringHandler = new DefaultInterpolatedStringHandler(9, 2);
il.Emit(OpCodes.Ldloca_S, 1);
il.Emit(OpCodes.Ldc_I4_S, 9);
il.Emit(OpCodes.Ldc_I4_2);
il.Emit(OpCodes.Call, sType.GetConstructor(new Type[] { typeof(int), typeof(int) }));
// defaultInterpolatedStringHandler.AppendLiteral("用户 ");
il.Emit(OpCodes.Ldloca_S, 1);
il.Emit(OpCodes.Ldstr, "用户: ");
il.Emit(OpCodes.Call, sType.GetMethod("AppendLiteral", new Type[] { typeof(string) }));
// defaultInterpolatedStringHandler.AppendFormatted(name);
il.Emit(OpCodes.Ldloca_S, 1);
il.Emit(OpCodes.Ldarg_1);
il.Emit(OpCodes.Call, sType.GetMethod("AppendFormatted", new Type[] { typeof(string) }));
// defaultInterpolatedStringHandler.AppendLiteral(" 登录结果 ");
il.Emit(OpCodes.Ldloca_S, 1);
il.Emit(OpCodes.Ldstr, " ,登录结果: ");
il.Emit(OpCodes.Call, sType.GetMethod("AppendLiteral", new Type[] { typeof(string) }));
// defaultInterpolatedStringHandler.AppendFormatted(result); AppendFormatted<bool>(result)
il.Emit(OpCodes.Ldloca_S, 1);
il.Emit(OpCodes.Ldloc_0);
il.Emit(OpCodes.Call, sType.GetMethods()
.FirstOrDefault(x => x.Name == "AppendFormatted" && x.IsGenericMethod && x.GetParameters().Length == 1).MakeGenericMethod(typeof(bool)));
// Console.WriteLine(defaultInterpolatedStringHandler.ToStringAndClear());
il.Emit(OpCodes.Ldloca_S, 1);
il.Emit(OpCodes.Call, sType.GetMethod("ToStringAndClear", Type.EmptyTypes));
il.Emit(OpCodes.Call, typeof(Console).GetMethod("WriteLine", new Type[] { typeof(string) }));
// return result;
il.Emit(OpCodes.Ldloc_0);
il.Emit(OpCodes.Ret);
}
最后通过依赖注入添加服务:
static void Main()
{
var assembly = AopHelper.Build();
var newType = assembly.GetType("Aop.UserServiceAop");
IServiceCollection ioc = new ServiceCollection();
ioc.Add(new ServiceDescriptor(typeof(IUserService), newType, ServiceLifetime.Scoped));
var services = ioc.BuildServiceProvider();
var userService = services.GetRequiredService<IUserService>();
userService.Login("工良", "123456");
}
到目前为止,我们已经介绍了如何使用 EMIT 动态生成一个新的类型,我们的例子仅仅是针对 IUserService 生成一个 UserServiceAop 类型,。
表达式树
表达式树的应用很广泛,表达式树的使用方法主要分为两种情况,生成动态代码执行,比如延迟查询、yeild return,第二种是解析表达式,比如编写 ORM 框架。
表达式树生成
笔者写过表达式树系列的教程,欢迎阅读 https://ex.whuanle.cn/
表达式树是程序运行时动态生成代码的一种方法,通过定义各种表达式来组合代码逻辑,最后编译执行。
在第八章中,编写事件总线方法时,我们就使用了表达式树组装方法,以便将不同形式的方法统一起来。
比如,有以下代码。
int Sum(int i, int j, int x, int y)
{
return (i * j) + (x * y);
}
如果我们需要使用表达式树的形式编写该逻辑,示例代码如下:
ParameterExpression a = Expression.Parameter(typeof(int), "i");
ParameterExpression b = Expression.Parameter(typeof(int), "j");
Expression r1 = Expression.Multiply(a, b); //乘法运行
ParameterExpression c = Expression.Parameter(typeof(int), "x");
ParameterExpression d = Expression.Parameter(typeof(int), "y");
Expression r2 = Expression.Multiply(c, d); //乘法运行
Expression result = Expression.Add(r1, r2); //相加
为了使用表达式树表示代码,需要将表达式树做得很复杂,需要表达常量变量,逻辑运算、条件控制、循环控制等等,还要访问变量的字段、属性、方法等。
笔者将表达式树的学习分为以下部分:
- 变量、常量、赋值
- 运算符
- 条件控制
- 循环控制
- 对象、泛型、集合和实例化
- 访问对象成员,字段、属性、函数
表达式树主要分为三个逻辑,其中最重要的是组合表达式树,然后是编译、执行。
由于表达式树的本身很复杂,因此,本书只是简单介绍表达式树如何使用,以及使用场景,只需要掌握简单的表达式树编写即可,以便理解后面的表达式树如何进行解析。
如果我们碰到需要编写表达式树的场景,我们可以像编写 AOP 一样,首先简化 C# 代码,然后借助工具先查出这段 C# 代码生成的表达式树的大概写法,然后再借鉴这些表达式树到项目当中。
比如,我们可以使用 LinqPad ,首先将需要生成表达式树的代码放到编辑器中,然后查看该代码对应的表达式树。
变量常量和赋值
在 C# 中,变量分为以下几种类型:
- 值类型(Value types)
- 引用类型(Reference types)
- 指针类型(Pointer types)
一般上,只用到值类型和引用类型,这里不会说到指针类型。
创建一个变量:
ParameterExpression varA = Expression.Variable(typeof(int), "x");
创建一个函数参数:
ParameterExpression varB = Expression.Parameter(typeof(int), "y");
创建一个常量:
ConstantExpression constant = Expression.Constant(100);
ConstantExpression constant1 = Expression.Constant(100, typeof(int));
逻辑运算
在 C# 中,算术运算符,有以下类型
- 算术运算符
- 关系运算符
- 逻辑运算符
- 位运算符
- 赋值运算符
- 其他运算符
由于 C# 中的运算符非常多,笔者就不一一介绍了,推荐读者阅读 https://ex.whuanle.cn/4.operator.html
比如,要将两个 int 类型的变量相加或相减,可以使用 Expression.Add
和 Expression.Subtract
。
ParameterExpression a = Expression.Parameter(typeof(int), "a");
ParameterExpression b = Expression.Parameter(typeof(int), "b");
// = a + b
BinaryExpression ab = Expression.Add(a, b);
// = a - b
BinaryExpression ab = Expression.Subtract(a, b);
调用方法
Expression.Call
是表达式树种调用类型方法的接口,其定义如下:
static MethodCallExpression Call(Expression? instance, MethodInfo method, params Expression[]? arguments)
有如下 C# 代码:
int a = 100;
int b = 200;
var ab = a + b;
Console.WriteLine(ab);
使用表达式树调用 Console.WriteLine()
静态方法打印信息:
ParameterExpression a = Expression.Parameter(typeof(int), "a");
ParameterExpression b = Expression.Parameter(typeof(int), "b");
// ab = a + b
BinaryExpression ab = Expression.Add(a, b);
// 打印 a + b 的值
MethodCallExpression method = Expression.Call(null, typeof(Console).GetMethod("WriteLine", new Type[] { typeof(int) }), ab);
Expression.Call
也可以调用实例的方法:
public class Test
{
public void Print(string info)
{
Console.WriteLine(info);
}
}
Test test = new Test();
test.Print("打印出来");
// Test test
ParameterExpression test = Expression.Variable(typeof(Test), "test");
// test.Print("打印出来");
MethodCallExpression method = Expression.Call(
test,
typeof(Test).GetMethod("Print", new Type[] { typeof(string) }),
Expression.Constant("打印出来")
);
// 编译生成并执行
Expression<Action<Test>> lambda = Expression.Lambda<Action<Test>>(method, test);
lambda.Compile()(new Test());
编写对象映射框架
示例代码请参考 Demo8.Mapper 项目。
// 利用泛型缓存,提升访问速度,以及简化缓存结构
public static class TypeMembers<TTarget>
where TTarget : class
{
private static readonly MemberInfo[] MemberInfos;
public static MemberInfo[] Members => MemberInfos;
static TypeMembers()
{
MemberInfos = typeof(TTarget)
.GetMembers(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
.Where(x => (x is FieldInfo) || (x is PropertyInfo)).ToArray();
}
}
public class MapperBuilder<TSource, TTarget>
where TSource : class
where TTarget : class, new()
{
// 生成的值映射委托
private Delegate? MapDelegate;
// 缓存用户自定义映射委托
private readonly Dictionary<MemberInfo, Delegate> MapExpressions = new();
}
var builder = new MapperBuilder<A, B>();
public class A
{
public string C { get; set; }
public int D { get; set; }
}
public class B
{
public string C { get; set; }
public string D { get; set; }
}
// b.D = a.D.ToString()
builder.Set(a => a.D.ToString(), b => b.D);
// 构建关系映射
builder.Build();
// 单独设置表达式赋值
public MapperBuilder<TSource, TTarget> Set<TValue, TField>(Func<TSource, TValue> buildValue,
Expression<Func<TTarget, TField>> targetField)
{
MemberInfo p = GetMember(targetField);
MapExpressions[p] = buildValue;
return this;
}
// 从表达式中识别出对象的成员名称。
// 如 (a => a.Value) ,解析出 Value
private MemberInfo GetMember<TField>(Expression<Func<TTarget, TField>> field)
{
var body = field.Body;
string name = "";
// 提取 (a=> a.Value)
if (body is MemberExpression memberExpression)
{
MemberInfo member = memberExpression.Member;
name = member.Name;
}
// 提取 (a=> a.Value)
else if (body is ParameterExpression parameterExpression)
{
name = parameterExpression.Name ?? "-";
}
// 提取 (a=> "Value") 字符串表达式
else if (body is ConstantExpression constantExpression)
{
name = constantExpression.Value?.ToString() ?? "-";
}
else
{
throw new KeyNotFoundException($"{typeof(TTarget).Name} 中不存在名为 {body.ToString()} 的字段或属性,请检查表达式!");
}
var p = TypeMembers<TTarget>.Members.FirstOrDefault(x => x.Name == name);
if (p == null)
{
throw new KeyNotFoundException($"{typeof(TTarget).Name} 中不存在名为 {body.ToString()} 的字段或属性,请检查表达式!");
}
return p;
}
// 构建对象映射
public void Build()
{
List<Expression> exList = new List<Expression>();
// TSource a;
// TTarget b;
ParameterExpression sourceParameter = Expression.Parameter(typeof(TSource), "_a");
ParameterExpression targetParameter = Expression.Parameter(typeof(TTarget), "_b");
foreach (var item in TypeMembers<TTarget>.Members)
{
// 如果用户设置了自定义赋值表达式
if (MapExpressions.TryGetValue(item, out var @delegate))
{
exList.Add(BuildAssign(sourceParameter, targetParameter, item, @delegate));
continue;
}
if (item is FieldInfo field)
{
// 忽略属性的私有字段
if (item.Name.EndsWith(">k__BackingField")) continue;
Expression assignDel = MapFieldOrProperty(sourceParameter, targetParameter, field);
exList.Add(assignDel);
}
else if (item is PropertyInfo property)
{
if (!property.CanWrite) continue;
Expression assignDel = MapFieldOrProperty(sourceParameter, targetParameter, property);
exList.Add(assignDel);
}
}
var block = Expression.Block(exList);
var del = Expression.Lambda(block, sourceParameter, targetParameter).Compile();
MapDelegate = del;
}
// 构建映射表达式
private Expression BuildAssign(ParameterExpression sourceParameter, ParameterExpression targetParameter, MemberInfo memberInfo, Delegate @delegate)
{
// b.Value
MemberExpression targetMember;
if (memberInfo is FieldInfo field)
{
targetMember = Expression.Field(targetParameter, field);
}
else if (memberInfo is PropertyInfo property)
{
targetMember = Expression.Property(targetParameter, property);
}
else
{
throw new InvalidCastException($"{memberInfo.DeclaringType?.Name}.{memberInfo.Name} 不是字段或属性");
}
// 调用用户自定义委托
var instance = Expression.Constant(@delegate.Target);
MethodCallExpression delegateCall = Expression.Call(instance, @delegate.Method, sourceParameter);
// b.Value = @delegate.DynamicInvoke(a);
BinaryExpression assign = Expression.Assign(targetMember, delegateCall);
return assign;
}
// 默认字段映射规则,根据同名字段或属性赋值
private Expression MapFieldOrProperty(ParameterExpression sourceParameter, ParameterExpression targetParameter, MemberInfo targetField)
{
// b.Value
MemberExpression targetMember;
Type targetFieldType;
{
if (targetField is FieldInfo fieldInfo)
{
targetFieldType = fieldInfo.FieldType;
targetMember = Expression.Field(targetParameter, fieldInfo);
}
else if (targetField is PropertyInfo propertyInfo)
{
targetFieldType = propertyInfo.PropertyType;
targetMember = Expression.Property(targetParameter, propertyInfo);
}
else
{
throw new InvalidCastException(
$"框架处理出错,请提交 Issue! {typeof(TTarget).Name}.{targetField.Name} 既不是字段也不是属性");
}
}
var sourceField = typeof(TSource).GetMember(targetField.Name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic).FirstOrDefault();
// 在 TSource 中搜索不到对应字段时,b.Value 使用默认值
if (sourceField == null)
{
// 生成表达式 b.Value = default;
return Expression.Assign(targetMember, Expression.Default(targetFieldType));
}
MemberExpression sourceMember;
Type sourceFieldType;
{
if (sourceField is FieldInfo fieldInfo)
{
sourceFieldType = fieldInfo.FieldType;
sourceMember = Expression.Field(sourceParameter, fieldInfo);
}
else if (sourceField is PropertyInfo propertyInfo)
{
sourceFieldType = propertyInfo.PropertyType;
sourceMember = Expression.Property(sourceParameter, propertyInfo);
}
else
{
throw new InvalidCastException(
$"框架处理出错,请提交 Issue! {typeof(TSource).Name}.{sourceField.Name} 既不是字段也不是属性");
}
}
if (targetFieldType != sourceFieldType)
throw new InvalidCastException(
$"类型不一致! {typeof(TSource).Name}.{sourceField.Name} 与 {typeof(TTarget).Name}.{targetField.Name}");
// 生成表达式 b.Value = a.Value
return Expression.Assign(targetMember, sourceMember);
}
完整使用示例:
var builder = new MapperBuilder<A, B>();
// c.D = a.D.ToString()
builder.Set(a => a.D.ToString(), b => b.D);
builder.Build();
A a = new A()
{
C = "C",
D = 123
};
var b = builder.Map(a);
表达式树解析
日常业务开发少不了 ORM 框架,ORM 框架为我们的操作数据库代理了极大的便利,ORM 中也大量使用了表达式树技术,不过跟动态生成代码截然相反,ORM 使用的是解析表达式树。
本节讲解如何使用表达式树完成一个简单的 ORM 框架,示例代码请参考 Demo8.ORM 项目。
请在 mysql 数据库中执行以下 SQL,以便创建和插入表数据:
CREATE TABLE `Test` (
`Id` int NOT NULL,
`Name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
PRIMARY KEY (`Id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
INSERT INTO `Test` VALUES (1, '工良');
INSERT INTO `Test` VALUES (2, '工良');
INSERT INTO `Test` VALUES (3, '工良');
INSERT INTO `Test` VALUES (4, '工良');
为了让示例项目更简单,该 ORM 只包含简单的查询功能。
使用示例:
var context = new MyDBContext(connction);
var list = await context.Select<Test>()
.Where(x => x.Id >= 2 && x.Name.Contains("工良") && x.Id < 4)
.ToListAsync();
创建上下文类型 MyDBContext :
public class MyDBContext
{
protected readonly IDbConnection _connction;
public MyDBContext(IDbConnection connction)
{
_connction = connction;
}
public MyDBContext<T> Select<T>() where T : class, new()
{
return new MyDBContext<T>(_connction);
}
}
泛型 MyDBContext<T>
用于构造和生成 SQL 语句:
public class MyDBContext<T> : MyDBContext
where T : class, new()
{
public MyDBContext(IDbConnection connction) : base(connction)
{
}
private readonly StringBuilder _strBuilder = new StringBuilder();
public MyDBContext<T> Where(Expression<Func<T, bool>> predicate)
{
var bin = predicate.Body as BinaryExpression;
ArgumentNullException.ThrowIfNull(bin);
var content = $"{Parse(bin.Left)} {GetChar(bin.NodeType)} {Parse(bin.Right)}";
_strBuilder.Append(content);
return this;
}
// 解析条件
private static string Parse(Expression ex)
{
if (ex is BinaryExpression bin)
{
ArgumentNullException.ThrowIfNull(bin);
var left = bin.Left;
var right = bin.Right;
var content = $"{Parse(bin.Left)} {GetChar(bin.NodeType)} {Parse(bin.Right)}";
return content;
}
else if (ex is MemberExpression p)
{
var name = $"`{p.Member.Name}`";
return name;
}
else if (ex is ConstantExpression c)
{
var obj = c.Value;
if (obj == null) return "null";
var typeCode = TypeInfo.GetTypeCode(obj.GetType());
if (typeCode == TypeCode.String) return $"'{obj.ToString()}'";
return obj.ToString();
}
else if (ex is MethodCallExpression m)
{
if (m.Method.Name == "Contains")
{
return $"{Parse(m.Object)} like '%{Parse(m.Arguments.FirstOrDefault()).Trim('\'')}%'";
}
}
throw new InvalidOperationException("不支持的表达式");
}
// 解析连接符
private static string GetChar(ExpressionType type)
{
switch (type)
{
case ExpressionType.And: return "&";
case ExpressionType.AndAlso: return "&&";
case ExpressionType.Or: return "|";
case ExpressionType.OrElse: return "||";
case ExpressionType.Equal: return "=";
case ExpressionType.NotEqual: return "!=";
case ExpressionType.GreaterThan: return ">";
case ExpressionType.GreaterThanOrEqual: return ">=";
case ExpressionType.LessThan: return "<";
case ExpressionType.LessThanOrEqual: return "<=";
}
throw new InvalidOperationException("不支持的表达式");
}
public async Task<List<T>> ToListAsync()
{
var sql = $"SELECT * FROM {typeof(T).Name} Where {_strBuilder.ToString()}";
_connction.Open();
var command = new MySqlCommand();
command.Connection = _connction as MySqlConnection;
command.CommandText = sql;
var reader = await command.ExecuteReaderAsync();
List<T> list = new List<T>();
var ps = typeof(T).GetProperties();
while (await reader.ReadAsync())
{
T t = new T();
list.Add(t);
for (int i = 0; i < ps.Length; i++)
{
var p = ps[i];
object v = null;
// 只处理一些简单的数据库类型
switch (TypeInfo.GetTypeCode(p.PropertyType))
{
case TypeCode.Int32: v = reader.GetInt32(i); break;
case TypeCode.Int64: v = reader.GetInt64(i); break;
case TypeCode.Double: v = reader.GetDouble(i); break;
case TypeCode.String: v = reader.GetString(i); break;
default: v = null; break;
}
p.SetValue(t, v);
}
}
return list;
}
public async Task<T> FirstAsync()
{
return (await ToListAsync()).FirstOrDefault();
}
}
通过 nuget 包引入 MySqlConnector,通过 MyDBContext
查询从数据库查询数据。
public class Test
{
public int Id { get; set; }
public string Name { get; set; }
}
public class Program
{
static async Task Main()
{
var builder = new MySqlConnectionStringBuilder
{
Server = "localhost:3306",
Database = "test",
UserID = "root",
Password = "123456",
SslMode = MySqlSslMode.Required,
};
IDbConnection connction = new MySqlConnection(builder.ConnectionString);
var context = new MyDBContext(connction);
var list = await context.Select<Test>()
.Where(x => x.Id >= 2 && x.Name.Contains("工良") && x.Id < 4)
.ToListAsync();
}
}
Roslyn
Roslyn 是一种用来编译代码和分析代码的技术,主要有两种使用方式,一种是动态编译代码,与 EMIT 技术不同的是, Roslyn 可以直接编译字符串代码,也可以编译完整的项目代码,而不需要 .NET SDK 或 MSBuild,国内有开发者利用 Natasha 技术编写了 Natasha 框架。另一种使用方式是作为代码分析器,使用非常广泛,.NET 5 以后的版本都自带了代码分析器,我们在 IDE 中可以看到。
在后面的章节中笔者会使用介绍如何编写一个代码分析器,所以在本章中,笔者只介绍如何使用 Roslyn 编译代码。
使用 Roslyn
示例代码请参考 Demo8.Roslyn 项目。
首先通过 nuget 引入 Microsoft.CodeAnalysis.CSharp 包。
创建一个 DomainOptions 类型,用来存储编译程序集时的配置。
public class DomainOptions
{
// 当前要编译的程序集是何种类型的项目
public OutputKind OutputKind { get; set; } = OutputKind.DynamicallyLinkedLibrary;
// Debug 还是 Release
public OptimizationLevel OptimizationLevel { get; set; } = OptimizationLevel.Release;
// 是否允许使用不安全代码
public bool AllowUnsafe { get; set; } = false;
// 生成目标平台,如 X64、x86
public Platform Platform { get; set; } = Platform.AnyCpu;
// 是否检查边界
public bool CheckOverflow { get; set; } = false;
// 语言版本
public LanguageVersion LanguageVersion { get; set; } = LanguageVersion.CSharp7_3;
// 环境变量
public HashSet<string> Environments { get; } = new HashSet<string>();
}
然后创建一个构造器,用来构造 DomainOptions 类型。
// 程序集编译配置构建器
public class DomainOptionBuilder
{
private readonly DomainOptions _option = new DomainOptions();
internal LanguageVersion LanguageVersion => _option.LanguageVersion;
internal string[] Environments => _option.Environments.ToArray();
internal CSharpCompilationOptions Build()
{
if (_option.Environments.Count == 0)
{
_option.Environments.Add(_option.OptimizationLevel == OptimizationLevel.Debug ? "DEBUG" : "RESEALE");
}
return new CSharpCompilationOptions(
concurrentBuild: true,
metadataImportOptions: MetadataImportOptions.All,
outputKind: _option.OutputKind,
optimizationLevel: _option.OptimizationLevel,
allowUnsafe: _option.AllowUnsafe,
platform: _option.Platform,
checkOverflow: _option.CheckOverflow,
assemblyIdentityComparer: DesktopAssemblyIdentityComparer.Default);
}
// 程序集要编译成何种项目,比如控制台、桌面程序等,默认编译成动态库。
public DomainOptionBuilder WithKind(OutputKind kind = OutputKind.DynamicallyLinkedLibrary)
{
_option.OutputKind = kind;
return this;
}
// 配置程序集是否使用 DEBUG 条件编译,默认使用 RELEASE 编译程序
public DomainOptionBuilder WithDebug(bool isDebug = false)
{
_option.OptimizationLevel = isDebug ? OptimizationLevel.Debug : OptimizationLevel.Release;
return this;
}
// 是否允许项目使用不安全代码
public DomainOptionBuilder WIthAllowUnsafe(bool isAllow = false)
{
_option.AllowUnsafe = isAllow;
return this;
}
// 指定公共语言运行库(CLR)的哪个版本可以运行程序集,默认为可移植的
public DomainOptionBuilder WithPlatform(Platform platform = Platform.AnyCpu)
{
_option.Platform = platform;
return this;
}
// 是否在默认情况下强制执行整数算术的边界检查
public DomainOptionBuilder WithCheckOverflow(bool checkOverflow = false)
{
_option.CheckOverflow = checkOverflow;
return this;
}
// 要使用的语言版本,<para>如果直接通过代码生成,代码版本任意;如果通过 API 生成,目前项目的语法只考虑到 7.3
public DomainOptionBuilder WithLanguageVersion(LanguageVersion version = LanguageVersion.CSharp7_3)
{
_option.LanguageVersion = version;
return this;
}
// 编译条件字符串,#if 中使用到的条件编译,如 Debug 这些符号
public DomainOptionBuilder WithEnvironment(params string[] environment)
{
foreach (var item in environment)
{
_option.Environments.Add(item);
}
return this;
}
}
最后,编写一个编译器,用来编译代码成程序集。
// 程序集编译构建器
public class CompilationBuilder
{
/// 通过代码生成程序集
// code: 字符串代码
// assemblyPath: 程序集路径
// assemblyName: 程序集名称
// option: 程序集配置
// messages: 编译时的消息
public static bool CreateDomain(string code,
string assemblyPath,
string assemblyName,
DomainOptionBuilder option,
out ImmutableArray<Diagnostic> messages)
{
HashSet<PortableExecutableReference> references = new HashSet<PortableExecutableReference>();
// 设置依赖的程序集列表,这里使用跟 Demo8.Roslyn 一样的依赖
// 读者可以根据自己的需求添加
var refAssemblys = AppDomain.CurrentDomain.GetAssemblies()
.Where(i => !i.IsDynamic && !string.IsNullOrWhiteSpace(i.Location))
.Distinct()
.Select(i => MetadataReference.CreateFromFile(i.Location)).ToList();
foreach (var item in refAssemblys)
{
references.Add(item);
}
CSharpCompilationOptions options = (option ?? new DomainOptionBuilder()).Build();
var syntaxTree = ParseToSyntaxTree(code, option);
var result = BuildCompilation(assemblyPath, assemblyName, new SyntaxTree[] { syntaxTree }, references.ToArray(), options);
messages = result.Diagnostics;
return result.Success;
}
// 将代码转为语法树
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static SyntaxTree ParseToSyntaxTree(string code, DomainOptionBuilder option)
{
var parseOptions = new CSharpParseOptions(option.LanguageVersion, preprocessorSymbols: option.Environments);
return CSharpSyntaxTree.ParseText(code, parseOptions);
}
// 编译代码
// path: 程序集位置
// assemblyName: 程序集名称
// syntaxTrees: 代码语法树
// references: 依赖
// options: 编译配置
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static EmitResult BuildCompilation(
string path,
string assemblyName,
SyntaxTree[] syntaxTrees,
PortableExecutableReference[] references,
CSharpCompilationOptions options)
{
var compilation = CSharpCompilation.Create(assemblyName, syntaxTrees, references, options);
var result = compilation.Emit(Path.Combine(path, assemblyName));
return result;
}
}
然后在 Main 方法中,我们开始进行编译。
定义字符串代码:
const string code =
"""
using System;
namespace MySpace
{
public class Test
{
public int Sum(int a, int b)
{
return a + b;
}
}
}
""";
配置编译选项:
// 编译选项
// 编译选项可以不配置
DomainOptionBuilder option = new DomainOptionBuilder()
.WithPlatform(Platform.AnyCpu) // 生成可移植程序集
.WithDebug(false) // 使用 Release 编译
.WithKind(OutputKind.DynamicallyLinkedLibrary) // 生成动态库
.WithLanguageVersion(LanguageVersion.CSharp7_3); // 使用 C# 7.3
最后编译代码并获取编译结果:
// 编译代码
var isSuccess = CompilationBuilder.CreateDomain(code,
assemblyPath: "./",
assemblyName: "test.dll",
option: option,
out var messages);
检查编译结果以及动态调用程序集:
// 编译失败,输出错误信息
if (!isSuccess)
{
foreach (var item in messages)
{
Console.WriteLine(
$"""
ID:{item.Id}
严重程度:{item.Severity}
位置:{item.Location.SourceSpan.Start}~{item.Location.SourceSpan.End}
消息:{item.Descriptor.Title} {item}
""");
}
return;
}
// 编译成功,反射调用程序集代码
var curPath = Directory.GetParent(typeof(Program).Assembly.Location).FullName;
var assembly = Assembly.LoadFile($"{curPath}/test.dll");
var type = assembly.GetType("MySpace.Test");
var method = type.GetMethod("Sum");
object obj = Activator.CreateInstance(type);
int result = (int)method.Invoke(obj, new object[] { 1, 2 });
Console.WriteLine(result);
使用 Natasha
Natasha 开源项目地址:https://github.com/dotnetcore/Natasha
基于 Roslyn 的 C# 动态程序集构建库,该库允许开发者在运行时使用 C# 代码构建域 / 程序集 / 类 / 结构体 / 枚举 / 接口 / 方法等,使得程序在运行的时候可以增加新的模块及功能。Natasha 集成了域管理/插件管理,可以实现域隔离,域卸载,热拔插等功能。
示例项目在 Demo8.Natasha 中。
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="DotNetCore.Compile.Environment" Version="3.2.0" />
<PackageReference Include="DotNetCore.Natasha.CSharp" Version="5.2.2.1" />
</ItemGroup>
</Project>
static void Main()
{
const string code =
"""
using System;
namespace MySpace
{
public class Test
{
public int Sum(int a, int b)
{
return a + b;
}
}
}
""";
//初始化 Natasha 编译组件及环境
NatashaInitializer.Preheating();
//创建编译单元,并指定程序集名
AssemblyCSharpBuilder oop = new AssemblyCSharpBuilder("myAssembly");
//编译单元使用从域管理分配出来的随机域
oop.Domain = DomainManagement.Random();
//增加代码到编译单元中
oop.Add(code);
// 生成程序集
Assembly assembly = oop.GetAssembly();
var type = assembly.GetTypes().FirstOrDefault(x => x.Name == "Test");
var result = type.GetMethod("Sum", BindingFlags.Instance | BindingFlags.Public).Invoke(Activator.CreateInstance(type), new object[] { 1, 2 });
Console.Write(result);
}
Source Generators
Source Generators 是一种很复杂的技术,用于生成源代码,Source Generators 技术的基础是 Roslyn。
https://wengier.com/SourceGeneratorPlayground/
首先创建一个用于生成代码的项目,名为 Demo8.SGBuild,Demo8.SGBuild.csproj
文件内容如下所示:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>11.0</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.5.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all" />
</ItemGroup>
</Project>
然后我们实现一个源代码生成器,在项目编译时,代码生成器会查找项目中 Main 方法所在的命名空间,然后生成一个 Test 类型存储在 MyAOP.cs 中,新生成的代码会随着项目一起编译。
[Generator]
public class MySourceGenerator : ISourceGenerator
{
public void Execute(GeneratorExecutionContext context)
{
// 查找 Main 方法
var mainMethod = context.Compilation.GetEntryPoint(context.CancellationToken);
// 生成新的代码
string source = $@"
using System;
namespace {mainMethod.ContainingNamespace.ToDisplayString()}
{{
public class Test : ITest
{{
public int Sum(int a, int b)
{{
return a + b;
}}
}}
}}
";
// 生成新的代码到文件
context.AddSource($"MyAOP.cs", source);
}
public void Initialize(GeneratorInitializationContext context)
{
}
}
然后我们再创建一个 Demo8.UseSG 项目,引用 Demo8.SGBuild 。
<ItemGroup>
<ProjectReference Include="..\Demo8.SGBuild\Demo8.SGBuild.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
</ItemGroup>
Demo8.UseSG 中的代码示例如下:
class Program
{
static void Main(string[] args)
{
var assembly = typeof(Program).Assembly;
var testType = assembly.GetTypes().FirstOrDefault(x => x.GetInterfaces().Any(i => i == typeof(ITest)));
var test = Activator.CreateInstance(testType) as ITest;
var sum = test.Sum(1, 2);
Console.WriteLine(sum);
}
}
public interface ITest
{
int Sum(int a, int b);
}