【连载5】C# MVC 异常处理避坑指南:异步操作与静态资源错误解决方案
在 C# MVC 开发中,异常处理看似简单,实则暗藏诸多陷阱。尤其是异步操作和静态资源的错误处理,常常让开发者头疼不已。本文将揭示这些 “坑” 的真面目,并提供实用的解决方案。
坑 1:异步 Action 异常捕获问题
问题分析
异步 Action 中直接通过 Task.Run
抛出异常时,异常会被封装到 Task
对象中。若未通过 await
正确等待该任务,异常可能无法传递到调用栈上层,导致全局异常过滤器失效。
解决方案
使用 await
显式等待异步操作完成,确保异常能向上冒泡:
public async Task<ActionResult> GetData()
{await Task.Run(() => {throw new Exception("数据获取失败");});return View();
}
优化建议
- 避免在
Task.Run
中直接抛出异常,改为返回错误状态:
var result = await Task.Run(() =>
{if (errorCondition) return (false, null, "错误信息");return (true, data, null);
});
if (!result.success) return BadRequest(result.error);
- 配置全局异常过滤器(以 ASP.NET Core 为例):
services.AddControllers(options =>
{options.Filters.Add<GlobalExceptionFilter>();
});
关键点
async/await
会解包Task
的异常,使其能被try-catch
或过滤器捕获- 避免在异步方法中嵌套同步代码块抛出异常
坑 2:静态资源错误捕获解决方案
Web.config 配置
在 Web.config
文件中添加以下配置以确保静态资源错误被正确捕获和处理:
<configuration><system.webServer><modules runAllManagedModulesForAllRequests="true" /><httpErrors errorMode="Custom" existingResponse="Replace"><remove statusCode="404" subStatusCode="-1" /><error statusCode="404" path="/Error/NotFound" responseMode="ExecuteURL" /><remove statusCode="500" subStatusCode="-1" /><error statusCode="500" path="/Error/ServerError" responseMode="ExecuteURL" /></httpErrors></system.webServer><system.web><customErrors mode="On" defaultRedirect="~/Error/ServerError"><error statusCode="404" redirect="~/Error/NotFound" /><error statusCode="500" redirect="~/Error/ServerError" /></customErrors></system.web>
</configuration>
Application_Error 处理
在 Global.asax
文件中添加以下代码,以捕获和处理静态资源错误:
protected void Application_Error(object sender, EventArgs e)
{var exception = Server.GetLastError();var httpException = exception as HttpException;Logger.Error(exception, "应用程序发生未处理异常");if (httpException != null){int statusCode = httpException.GetHttpCode();if (IsStaticResource(Request.Url.AbsolutePath)){Response.Clear();Server.ClearError();Response.TrySkipIisCustomErrors = true;Response.Redirect($"~/Error/StaticResourceError?path={Request.Url.AbsolutePath}&code={statusCode}");}}
}private bool IsStaticResource(string path)
{var staticExtensions = new[] { ".css", ".js", ".png", ".jpg", ".jpeg", ".gif", ".ico", ".svg" };return staticExtensions.Any(ext => path.EndsWith(ext, StringComparison.OrdinalIgnoreCase));
}
关键点说明
runAllManagedModulesForAllRequests="true"
确保所有请求都经过 ASP.NET 管道处理,包括静态资源请求。
httpErrors
配置用于 IIS 级别的错误处理,customErrors
用于 ASP.NET 级别的错误处理。
IsStaticResource
方法用于判断请求是否为静态资源,根据文件扩展名进行匹配。
坑 3:异步过滤器中的异常处理不当
在异步过滤器(如 IAsyncActionFilter
)中正确处理异常至关重要,可以避免应用程序不稳定。以下是一个改进后的实现方案,确保异常被妥善处理并记录。
实现异步过滤器
public class AsyncLoggingFilter : IAsyncActionFilter
{private readonly ILogger _logger;public AsyncLoggingFilter(ILogger logger){_logger = logger;}public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next){var stopwatch = Stopwatch.StartNew();try{var resultContext = await next();stopwatch.Stop();_logger.LogInformation($"Action执行完成,耗时: {stopwatch.ElapsedMilliseconds}ms");}catch (Exception ex){stopwatch.Stop();_logger.LogError(ex, $"Action执行失败,耗时: {stopwatch.ElapsedMilliseconds}ms");context.Result = new ViewResult{ViewName = "Error",ViewData = new ViewDataDictionary<HandleErrorInfo>(new HandleErrorInfo(ex, context.RouteData.Values["controller"].ToString(), context.RouteData.Values["action"].ToString()))};context.ExceptionHandled = true;}}
}
关键点说明
- 异常捕获:在
try-catch
块中调用await next()
,确保所有后续过滤器和 Action 的异常都能被捕获。 - 日志记录:使用
ILogger
记录执行时间和异常信息,便于问题排查。 - 异常处理:
- 设置
context.Result
返回错误视图或自定义错误响应。 - 将
context.ExceptionHandled
设为true
,阻止异常继续传播。
- 设置
- 性能监控:通过
Stopwatch
记录 Action 执行时间,帮助优化性能。
注册过滤器
在 Startup.cs
中注册过滤器:
services.AddScoped<AsyncLoggingFilter>();
services.AddControllers(options =>
{options.Filters.Add<AsyncLoggingFilter>();
});
可选扩展
- 全局异常处理:如果不希望在过滤器中处理异常,可以重新抛出异常,由全局异常处理中间件捕获。
- 自定义错误响应:根据 API 或 MVC 需求,返回 JSON 错误响应或重定向到错误页面。
在 MVC 异常处理中,开发者常遇到一些典型问题,以下是常见场景及解决方案的整理:
异步操作中的异常处理
异步方法(如 async/await
)的异常容易被忽略或未正确捕获,导致错误信息丢失。
- 问题表现:全局异常过滤器(如
IExceptionFilter
)可能无法捕获Task
中未观察的异常。 - 解决方案:
在异步控制器方法中显式捕获异常并通过HttpContext
传递:
配置public async Task<ActionResult> GetDataAsync() {try {var data = await _service.FetchDataAsync();return View(data);} catch (Exception ex) {HttpContext.Items["Exception"] = ex; // 传递到全局过滤器throw; // 重新抛出以触发过滤器} }
TaskScheduler.UnobservedTaskException
处理未观察的异步异常。
静态资源的错误处理
静态文件(如 CSS、JS)返回 404 或 500 错误时,默认不会触发 MVC 异常处理逻辑。
- 问题表现:
web.config
中的customErrors
或中间件可能无法覆盖静态资源请求。 - 解决方案:
在Startup.cs
中使用中间件拦截静态资源错误:
结合app.UseStaticFiles(new StaticFileOptions {OnPrepareResponse = ctx => {if (ctx.Context.Response.StatusCode == 404) {ctx.Context.Response.Redirect("/Error/NotFound");}} });
IApplicationBuilder.UseExceptionHandler
统一处理异常路由。
全局异常过滤器的局限性
全局过滤器无法处理非 MVC 管道中的异常(如中间件、身份验证阶段)。
- 解决方案:
组合使用中间件和过滤器:
在过滤器中补充处理控制器特定逻辑。app.UseExceptionHandler(errorApp => {errorApp.Run(async context => {var exception = context.Features.Get<IExceptionHandlerFeature>();await context.Response.WriteAsync("全局错误捕获");}); });
自定义错误页的动态传递
需在错误页中显示原始异常信息,但默认模型可能丢失细节。
- 解决方案:
通过ControllerContext.HttpContext.Items
传递异常数据:public class CustomExceptionFilter : IExceptionFilter {public void OnException(ExceptionContext context) {context.HttpContext.Items["ErrorDetail"] = context.Exception.Message;context.Result = new RedirectToRouteResult("Error", null);} }
其他常见问题
- 跨域请求异常:需在中间件中显式处理 CORS 错误。
- 模型绑定错误:通过
ModelState.AddModelError
捕获并返回验证信息。
开发者可根据具体场景组合上述方案。若存在其他未覆盖的问题,可进一步讨论具体案例。