.NET 全局异常处理:使用 IExceptionHandler 统一错误响应

前言 在前后端联调接口时,最痛苦的事情之一就是不同异常返回的格式不一致:业务异常、系统异常、模型验证错误都可能结构各异,前端处理成本高。传统做法通常是: 自定义中间件捕获异常 使用异常过滤器 (ExceptionFilter) 但这些方式存在两个问题: 某些阶段抛出的异常(例如模型绑定 / 类型转换错误)可能无法被可靠捕获; 需要手动维护额外的分层与侵入式代码。 从 .NET 8 开始,推荐的方式是使用内置的 IExceptionHandler 管道,这种方式更贴近框架设计,且更清晰集中。官方文档可参考: 处理 .NET 中的错误 - IExceptionHandler 下面演示一个统一异常处理的实际落地方案,并扩展到模型验证场景。 IExceptionHandler 的实现与示例 1. 定义统一结果模型(示例) 假设我们有一个通用的 API 返回模型: public class ApiResult { public bool Success { get; set; } public string Message { get; set; } = string.Empty; public int StatusCode { get; set; } public object? Data { get; set; } } 以及一个业务友好型异常: public class FriendlyException : Exception { public FriendlyException(string message) : base(message) { } } 2. 实现自定义异常处理器 通过实现 IExceptionHandler 接口集中处理: public class GlobalExceptionHandler : IExceptionHandler { private readonly ILogger<GlobalExceptionHandler> _logger; public GlobalExceptionHandler(ILogger<GlobalExceptionHandler> logger) => _logger = logger; public async ValueTask<bool> TryHandleAsync(HttpContext context, Exception exception, CancellationToken token) { var (statusCode, message) = exception switch { FriendlyException friendly => (StatusCodes.Status400BadRequest, friendly.Message), _ => (StatusCodes.Status500InternalServerError, exception.Message) }; var apiResult = new ApiResult { Success = false, Message = message, StatusCode = statusCode }; context.Response.StatusCode = statusCode; context.Response.ContentType = "application/json; charset=utf-8"; await context.Response.WriteAsJsonAsync(apiResult, cancellationToken: token); return true; // 表示此异常已被处理 } } FriendlyException 是自定义的一个业务友好型异常类型,通过模式匹配: ...

2025-11-03 · 3 min · 478 words · yess

利用委托与表达式树优雅批量翻译并回填实体字段

1. 背景与目标 最近项目中加入多语言翻译需求:一个实体列表里,可能有多处需要显示化 / 翻译的字段,比如枚举状态、国家名称等。一开始想用ActionFilter来实现,但是项目要用到hangfire,我们经常要用hangfire去导出excel,这里经常需要翻译,所以舍弃了这种思路,还是选择在业务代码中写入相关的翻译代码,但是写多了以后就感觉很麻烦了,于是想着再封装一个方法方便进行翻译以及赋值。 目标:以链式流式语法收集所有“待翻译文本 + 回填策略”,统一调用服务一次,最后批量回填。 2. 期望的调用方式(直觉 DSL) 最开始脑海里大概是这样设计的: var res = await users .MapTranslation(a => a.IsEnable, (a, translated) => a.IsEnableName = translated) .MapTranslation(a => a.Country, (a, translated) => a.Country = translated) .ExecuteAsync(); 语义解读: MapTranslation:收集一个“源字段 + 翻译后如何写回”的规则 ExecuteAsync():统一翻译 + 回填翻译后的内容 3. 初版实现与问题分析 下面是最初的一个版本: public static class ExpressionBuilder { public static TranslatedContext<T> BuildCollect<T>(IEnumerable<T> items) where T : class => new(items); } public class TranslatedContext<T> where T : class { private readonly IEnumerable<T> _source; private readonly List<(T item, string value, Action<T, string> action)> _translations; public TranslatedContext(IEnumerable<T> source){ _source = source; _translations=new() } public IEnumerable<T> MapTranslation(Func<T, string> selector, Action<T, string> action) { foreach (var item in _source) { var result = selector(item); _translations.Add((item, result, action)); yield return item; } } public IEnumerable<T> Execute() { foreach (var (item, enumValue, action) in _translations) action(item, enumValue); foreach (var item in _source) yield return item; } } 4. 服务注入的演进 如果把翻译Service加入流程中的话就要在 BuildCollect 里接收翻译服务: var res = await users .BuildCollect(translateService) .MapTranslation(a => a.IsEnable, (a, translated) => a.IsEnableName = translated) .ExecuteAsync(); 又觉得顺序不自然,有点反直觉了,于是思考了一下将 BuildCollect 挪进 TranslateService后,长这样: ...

2025-10-02 · 4 min · 695 words · yess

迭代器与枚举器

枚举器 枚举器的作用 如果想遍历一个集合(数组、列表等),通常会用到 foreach。其背后依赖的就是枚举器。 枚举器本质上是一种状态机:提供统一的遍历方式,同时不暴露集合的内部结构。 先看一个简单示例: public class MyIterator:IEnumerable { public IEnumerator GetEnumerator() { return new MyEnumerator(); } private class MyEnumerator : IEnumerator { private int _index = -1; public object Current { get { switch (_index) { case 0: return 1; case 1: return 2; case 2: return 3; default: throw new InvalidOperationException(); } } } public bool MoveNext() { _index++; return _index < 3; } public void Reset() { _index = -1; } } } var iterator1 = new MyIterator(); foreach (var item in iterator1) { Console.WriteLine(item); } // 输出: // 1 // 2 // 3 在上面的代码中,我们定义了一个 MyIterator 类,它实现了 IEnumerable 接口。GetEnumerator 方法返回一个实现了 IEnumerator 接口的枚举器 MyEnumerator。MyEnumerator 类中包含了三个核心成员: Current 属性:返回当前枚举器指向的元素。 MoveNext 方法:将枚举器推进到下一个元素。 Reset 方法:将枚举器重置到初始位置。 当我们使用 foreach 循环遍历 MyIterator 实例时,实际上是通过调用 GetEnumerator 方法获取枚举器,然后不断调用 MoveNext 方法来推进枚举器,并通过 Current 属性获取当前元素,直到 MoveNext 返回 false,表示枚举结束。 需要了解的是遍历并不是在集合对象上直接进行的,而是通过枚举器在不同状态下返回值的方式来推进的。 基于状态机编译可以实现延迟执行,只有在需要时才取下一条数据,而不是一次性把所有内容读进内存。在数据量很大的场景,能显著降低内存压力并且提高读取速度。 ...

2025-10-02 · 3 min · 629 words · yess

鸭子类型与 await

前言 最近在逛 Reddit 时,看到一个有趣的帖子: What is the lowest effort, highest impact helper method you’ve ever written? 大家讨论了许多常用的扩展方法,其中有一个非常亮眼的例子: public static TaskAwaiter<(T1, T2)> GetAwaiter<T1, T2>(this (Task<T1>, Task<T2>) tasks) => WhenAllResult(tasks).GetAwaiter(); public static async Task<(T1, T2)> WhenAllResult<T1, T2>(this (Task<T1>, Task<T2>) tasks) { await Task.WhenAll(tasks.Item1, tasks.Item2).ConfigureAwait(false); return (tasks.Item1.Result, tasks.Item2.Result); } var (result1, result2) = await ( GetDataAsync(), GetOtherDataAsync() ); 这样就可以通过元组优雅地获取并发结果。那么问题来了,这究竟是如何实现的? 原理在于 await 关键字实际上采用了“鸭子类型”模型,所以我们才能 await 一个元组。 什么是鸭子类型? 鸭子类型的定义是:只要一个对象“看起来像鸭子、叫起来像鸭子”,就可以当作鸭子用。也就是说,不要求类型继承某个接口或基类,只要拥有所需的方法和属性即可。 上述代码之所以能正常运行,是因为我们实现了 GetAwaiter 方法,并且返回的 Awaiter 拥有 IsCompleted、GetResult() 和 OnCompleted() 方法。在 C# 中,Awaiter 还必须实现 INotifyCompletion,否则编译器会报错。这正是鸭子类型的体现:只要有 GetAwaiter,就能被 await。 下面我们写一个简单的 Demo 体验一下: public class DemoAwaiter<TResult> : System.Runtime.CompilerServices.INotifyCompletion { private TResult _num; public DemoAwaiter(TResult num) => _num = num; public bool IsCompleted => true; public TResult GetResult() => _num; public void OnCompleted(Action continuation) { Console.WriteLine("Continuation registered."); continuation?.Invoke(); } } // 自定义 Awaitable public class DemoAwaitable<TResult> { private TResult _num; public DemoAwaitable(TResult num) => _num = num; public DemoAwaiter<TResult> GetAwaiter() => new DemoAwaiter<TResult>(_num); } public class Program { public static async Task Main(string[] args) { var result = await new DemoAwaitable<int>(123); Console.WriteLine($"Result: {result}"); } } 运行结果: ...

2024-08-13 · 2 min · 270 words · yess

C#表达式树

1、表达式树定义 先看一下微软官方文档,官方文档对表达式的解释: 表达式树是定义代码的数据结构。 表达式树基于编译器用于分析代码和生成已编译输出的相同结构。比如说下面这个例子 var sum = 1 + 2; 画成图的话就张这样 直观地看,整个语句是一棵树:应从根节点开始,浏览到树中的每个节点,以查看构成该语句的代码: 具有赋值 (var sum = 1 + 2;) 的变量声明语句 隐式变量类型声明 (var sum) 隐式 var 关键字 (var) 变量名称声明 (sum) 赋值运算符 (=) 二进制加法表达式 (1 + 2) 左操作数 (1) 加法运算符 (+) 右操作数 (2) 2、表达式树创建 这个直接用官方的示例 // Addition is an add expression for "1 + 2" ConstantExpression one = Expression.Constant(1, typeof(int)); ConstantExpression two = Expression.Constant(2, typeof(int)); BinaryExpression addition = Expression.Add(one, two); Console.WriteLine(addition); // 输出 (1 + 2) 3、表达式树的执行 只能执行表示 lambda 表达式的表达式树。表示 lambda 表达式的表达式树的类型为 LambdaExpression 或 Expression。若要执行这些表达式树,需要调用 Compile 方法以创建可执行委托,然后调用该委托。 所以如果想执行上面的示例BinaryExpression addition = Expression.Add(one, two); 那就要转换成委托然后调用,所以需要: ...

2024-05-03 · 6 min · 1254 words · yess