1. 为什么需要动态代码
在C#开发中,动态代码生成和执行是一个强大但容易被滥用的特性。我第一次真正体会到它的价值是在开发一个规则引擎时,当时需要处理数百种业务规则配置,如果为每种规则都硬编码实现,代码库会变得臃肿不堪。
动态代码最常见的应用场景包括:
- 需要运行时根据配置生成不同算法逻辑的场合
- 实现插件系统或脚本扩展功能
- 构建DSL(领域特定语言)解释器
- 性能敏感场景中避免反射开销
注意:动态代码不是银弹。在.NET 5+和CoreCLR环境中,反射性能已有显著提升,很多场景下反射可能是更安全的选择。
2. 动态代码实现方案对比
2.1 Expression Trees表达式树
表达式树是.NET 3.5引入的编译时安全方案。我曾用它重构过一个电商促销计算模块,性能比之前的反射方案提升了近40倍。
典型构建模式:
csharp复制var param = Expression.Parameter(typeof(Order), "order");
var property = Expression.Property(param, "TotalAmount");
var condition = Expression.GreaterThan(property, Expression.Constant(1000m));
var lambda = Expression.Lambda<Func<Order, bool>>(condition, param);
优势:
- 强类型检查
- 可缓存编译结果
- 调试友好
局限:
- 语法复杂度高
- 不支持完整语句
2.2 Roslyn脚本API
在开发配置化报表引擎时,我评估过各种方案,最终选择了Roslyn。它的Script类允许执行任意C#代码片段:
csharp复制var script = CSharpScript.Create<int>("X * Y", globalsType: typeof(Globals));
script.Compile();
var result = await script.RunAsync(new Globals { X = 2, Y = 3 });
实际使用中发现几个关键点:
- 需要引用Microsoft.CodeAnalysis.CSharp.Scripting包
- 首次编译有约200ms开销(可预编译缓解)
- 通过ScriptOptions控制引用和导入
2.3 Emit动态程序集
在构建高性能协议解析器时,我不得不深入ILGenerator。这是一个真实案例:
csharp复制var method = new DynamicMethod("Parse", typeof(int), new[] { typeof(byte[]) });
var il = method.GetILGenerator();
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldc_I4_0);
il.Emit(OpCodes.Ldelem_U1);
il.Emit(OpCodes.Ret);
经验教训:
- 需要扎实的IL指令知识
- 调试极其困难
- 但性能接近原生代码
2.4 动态语言运行时(DLR)
在需要与Python脚本互操作的金融分析系统中,我使用了ScriptRuntime:
csharp复制var runtime = ScriptRuntime.CreateFromConfiguration();
dynamic python = runtime.UseFile("analysis.py");
var result = python.CalculateRisk(model);
注意事项:
- 需要IronPython等实现
- 有跨语言调用开销
- 类型转换需要特别处理
3. 安全实践与性能优化
3.1 代码注入防护
在一次安全审计中,我们发现动态SQL构建存在注入风险。解决方案:
- 白名单校验:
csharp复制var validMethods = new HashSet<string> { "ToString", "Trim" };
if (!validMethods.Contains(methodName)) throw new SecurityException();
- 沙箱执行:
csharp复制var perm = new PermissionSet(PermissionState.None);
perm.AddPermission(new SecurityPermission(SecurityPermissionFlag.Execution));
var sandbox = new AppDomainSetup { ApplicationBase = AppDomain.CurrentDomain.BaseDirectory };
var secureDomain = AppDomain.CreateDomain("Sandbox", null, sandbox, perm);
3.2 缓存策略
在Web应用中缓存编译结果可提升性能。我的实现方案:
csharp复制private static readonly ConcurrentDictionary<string, Func<object, object>> _cache = new();
public static Func<object, object> GetOrAddCompiler(string expression)
{
return _cache.GetOrAdd(expression, expr => {
// 编译逻辑
return compiledDelegate;
});
}
实测数据显示,缓存使平均执行时间从15ms降至0.3ms。
3.3 调试支持
为动态代码添加调试信息是个挑战。在Roslyn方案中可这样配置:
csharp复制var options = ScriptOptions.Default
.WithFilePath("DynamicScript.csx")
.WithEmitDebugInformation(true);
这样在Visual Studio中就能像调试普通代码一样设置断点。
4. 实战案例:规则引擎实现
4.1 需求分析
最近完成的保险理赔系统需要处理200+条业务规则,包括:
- 投保人年龄限制
- 特殊疾病除外条款
- 地域限制规则
传统硬编码方式会导致:
- 每次规则变更需要重新部署
- 开发周期长
- 测试覆盖困难
4.2 架构设计
最终方案采用三层结构:
- 规则配置层:JSON定义
- 代码生成层:Roslyn脚本
- 执行引擎层:动态编译+缓存
核心接口:
csharp复制public interface IRuleEngine
{
Task<bool> ValidateAsync(Policy policy, string ruleDefinition);
}
4.3 性能对比
测试数据(10000次执行):
| 方案 | 平均耗时 | 内存占用 |
|---|---|---|
| 反射 | 450ms | 120MB |
| 表达式树 | 15ms | 20MB |
| Roslyn | 60ms* | 45MB |
*包含首次编译时间
4.4 错误处理经验
在实现过程中,这些经验特别有价值:
- 为动态代码添加上下文信息:
csharp复制try {
// 执行动态代码
} catch (Exception ex) {
throw new RuleExecutionException(ruleId, ex);
}
- 超时控制:
csharp复制var cts = new CancellationTokenSource(TimeSpan.FromSeconds(1));
await Task.Run(() => engine.Execute(code), cts.Token);
- 内存监控:
csharp复制var before = GC.GetTotalMemory(true);
ExecuteDynamicCode();
var usage = GC.GetTotalMemory(false) - before;
if (usage > 10_000_000) // 10MB
throw new MemoryLimitExceededException();
5. 高级技巧与边界情况
5.1 动态代码中的闭包处理
在实现计时器回调时遇到闭包陷阱:
错误示例:
csharp复制for (int i = 0; i < 5; i++) {
dynamicCode += $"Task.Run(() => Console.WriteLine({i}));";
}
// 输出全是5
正确做法:
csharp复制for (int i = 0; i < 5; i++) {
var temp = i;
dynamicCode += $"Task.Run(() => Console.WriteLine({temp}));";
}
5.2 类型加载隔离
当需要卸载动态生成的类型时,必须使用单独的AssemblyLoadContext:
csharp复制var alc = new AssemblyLoadContext("DynamicCode", true);
try {
var assembly = alc.LoadFromStream(ms);
// 使用程序集
} finally {
alc.Unload(); // 允许垃圾回收
}
5.3 跨平台注意事项
在Linux环境下发现的一些差异:
- 大小写敏感的类型名称
- 不同的路径分隔符
- 原生依赖部署
解决方案:
csharp复制var osDependentPath = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
? @"C:\temp\dynamic.dll"
: "/tmp/dynamic.so";
6. 监控与诊断
6.1 日志记录策略
建议采用结构化日志:
csharp复制logger.LogInformation("Executing dynamic code {Hash} on {Input}",
SHA256.HashData(Encoding.UTF8.GetBytes(code)),
JsonSerializer.Serialize(input));
6.2 性能计数器
在Windows上可以这样监控:
csharp复制var counter = new PerformanceCounter(
".NET CLR Loading",
"Total Assemblies",
Process.GetCurrentProcess().ProcessName);
var loadCount = counter.NextValue();
6.3 动态代码版本化
我采用的版本管理方案:
csharp复制public class DynamicCodeVersion
{
public string Hash { get; set; }
public DateTimeOffset Created { get; set; }
public string Author { get; set; }
public string OriginalSource { get; set; }
}
7. 替代方案评估
当动态代码不是最佳选择时,可以考虑:
7.1 预编译代码生成
在CI流水线中生成代码:
bash复制dotnet new console -o GeneratedCode
roslynator generate GeneratedCode/Rules.cs
7.2 解释器模式
对于简单规则:
csharp复制public interface IExpression
{
bool Interpret(Context context);
}
public class AgeExpression : IExpression
{
public int Min { get; set; }
public int Max { get; set; }
public bool Interpret(Context context)
=> context.Age >= Min && context.Age <= Max;
}
7.3 配置化规则引擎
如NRules、WorkflowCore等现成方案。
