如何用.NET Emit为运行时的异步无返回Action添加Try-Catch异常捕获?
解决PubSub中Async Void处理程序的异常捕获问题
问题本质
你遇到的核心问题是async void方法的异常无法被常规try-catch捕获——这类方法抛出的异步异常会直接冒泡到当前同步上下文(比如ASP.NET的请求上下文),甚至导致进程崩溃,而且很难追踪。要解决这个问题,核心思路是把每个async void的Action<T>包装在带异常捕获的逻辑里,不管是编译时静态包装,还是运行时动态生成包装方法。
方案一:静态委托包装(无需Emit,推荐)
这是最简单的实现方式,不需要涉及IL生成,直接用C#的委托特性包装原Action:
实现代码
public static class PubSubErrorHandlingExtensions { // 替换成你实际的日志实例(比如通过DI注入) private static readonly ILogger _logger = LoggerFactory.Create(builder => builder.AddConsole()).CreateLogger("PubSub"); public static Action<T> WithErrorHandling<T>(this Action<T> originalAction) { return async arg => { try { // 把原Action<T>转成Func<T, Task>,从而可以await捕获异步异常 await ((Func<T, Task>)(async x => originalAction(x)))(arg); } catch (Exception ex) { _logger.LogError(ex, "PubSub事件处理失败,主题:{Topic}", typeof(T).Name); } }; } }
使用方式
订阅时直接调用扩展方法包装你的处理程序:
SubscriberService.Subscribe(new SubscribeRequest<string> { Topic = TopicName, Action = (async pubSubEvent => await DoSomthingAsync()).WithErrorHandling() });
这个方案的优势是:代码简洁易维护,不需要了解IL指令,同时能完整捕获异步异常。
方案二:用Emit动态生成包装方法
如果必须在运行时动态生成带try-catch的方法(比如需要批量处理大量不同类型的Action),可以用ILGenerator实现。不过要注意:async void的异步异常需要通过await Task的方式捕获,所以动态生成的方法内部要把原Action转成可await的Task。
实现代码
using System.Reflection; using System.Reflection.Emit; public static class DynamicPubSubWrapper { private static readonly ILogger _logger = LoggerFactory.Create(builder => builder.AddConsole()).CreateLogger("DynamicPubSub"); public static Action<T> CreateWrappedAction<T>(Action<T> originalAction) { // 1. 创建动态程序集、模块和类型 var assemblyName = new AssemblyName($"DynamicPubSub_{Guid.NewGuid()}"); var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run); var moduleBuilder = assemblyBuilder.DefineDynamicModule("WrapperModule"); var typeBuilder = moduleBuilder.DefineType($"ActionWrapper_{typeof(T).FullName}", TypeAttributes.Public | TypeAttributes.Sealed); // 2. 定义包装方法:静态,参数为T,返回void var methodBuilder = typeBuilder.DefineMethod( "HandleEvent", MethodAttributes.Public | MethodAttributes.Static, typeof(void), new[] { typeof(T) }); var il = methodBuilder.GetILGenerator(); var exceptionType = typeof(Exception); var logMethod = typeof(ILogger).GetMethod("LogError", new[] { exceptionType, typeof(string) }); // 3. 构建try-catch块 var tryBlock = il.BeginExceptionBlock(); // --- Try块逻辑:调用原Action并等待异步完成 --- // 加载方法参数(T类型的事件对象) il.Emit(OpCodes.Ldarg_0); // 创建Func<T, Task>委托,包装原Action的调用(支持异步) il.Emit(OpCodes.Ldftn, originalAction.Method); il.Emit(OpCodes.Newobj, typeof(Func<T, Task>).GetConstructor(new[] { typeof(object), typeof(IntPtr) })); // 调用Func的Invoke方法,得到Task il.Emit(OpCodes.Callvirt, typeof(Func<T, Task>).GetMethod("Invoke")); // 等待Task完成(同步等待,若需非阻塞异步需生成async状态机,逻辑更复杂) il.Emit(OpCodes.Call, typeof(Task).GetMethod("Wait")); // 离开try块 il.Emit(OpCodes.Leave_S, tryBlock); // --- Catch块逻辑:捕获异常并日志 --- il.BeginCatchBlock(exceptionType); // 加载日志实例 il.Emit(OpCodes.Ldsfld, typeof(DynamicPubSubWrapper).GetField("_logger", BindingFlags.NonPublic | BindingFlags.Static)); // 加载异常对象(catch块的异常在栈顶) il.Emit(OpCodes.Ldarg_0); // 加载日志消息 il.Emit(OpCodes.Ldstr, "动态包装的PubSub事件处理失败"); // 调用LogError方法 il.Emit(OpCodes.Callvirt, logMethod); // 结束异常块 il.EndExceptionBlock(); // 返回 il.Emit(OpCodes.Ret); // 4. 创建类型并生成委托 var wrapperType = typeBuilder.CreateType(); var wrappedMethod = wrapperType.GetMethod("HandleEvent"); return (Action<T>)Delegate.CreateDelegate(typeof(Action<T>), wrappedMethod); } }
使用方式
var wrappedAction = DynamicPubSubWrapper.CreateWrappedAction<string>(async pubSubEvent => await DoSomthingAsync()); SubscriberService.Subscribe(new SubscribeRequest<string> { Topic = TopicName, Action = wrappedAction });
注意事项
- Emit的代码对IL指令细节要求很高,参数加载、方法调用的指令顺序不能出错;
- 异步方法的状态机生成逻辑复杂,上面的代码用
Task.Wait()做了同步等待简化,若需真正的异步非阻塞,需要生成完整的async方法状态机,会大幅增加代码复杂度; - 动态生成的程序集会留在内存中,大量使用可能导致内存占用过高,建议缓存生成的委托。
内容的提问来源于stack exchange,提问作者nive-2510
相关产品推荐
相关产品推荐

