如何在调用ASP.NET Core 3.1 Web API的ASP.NET MVC应用中结合HttpContent处理并发冲突(409状态码)错误?
你遇到的问题很典型——当错误处理逻辑分散在Repo、服务和控制器层时,如何让上层代码能清晰识别特定的HTTP错误状态,而不是靠解析异常消息这种不够优雅的方式。下面给你几个可行的方案,按推荐程度排序:
1. 创建自定义异常(最推荐)
这是类型安全、可读性最高的方式。你可以定义一个专门的异常类来标识409冲突场景,这样控制器里能直接捕获这个特定异常,逻辑非常清晰。
首先,创建自定义异常:
public class ConflictException : Exception { // 基本构造函数,带错误消息 public ConflictException(string message) : base(message) { } // 带内部异常的构造函数,保留原始错误信息用于调试 public ConflictException(string message, Exception innerException) : base(message, innerException) { } }
然后修改你的UserRepo.Update方法,在捕获到409时抛出这个自定义异常,而不是原始的HttpRequestException:
public async Task<User> Update(User user) { HttpClient client = _clientFactory.CreateClient("namedClient"); HttpResponseMessage response = await client.PutAsync($"api/Users/{user.Id}", ContentEncoder.Encode(user)); try { response.EnsureSuccessStatusCode(); } catch (Exception ex) { if (response.StatusCode == StatusCodes.Status409Conflict) { // 抛出自定义异常,带上友好提示和原始异常 throw new ConflictException("用户数据已过时,请刷新页面后重试", ex); } // 其他错误继续抛出,让上层处理或者统一捕获 throw; } return await response.Content.ReadFromJsonAsync<User>(); }
最后在控制器里,直接捕获ConflictException并处理:
public async Task<IActionResult> Update(UserViewModel model) { try { var user = MapViewModelToUser(model); await _userRepo.Update(user); return RedirectToAction("Index"); } catch (ConflictException ex) { // 给用户显示友好提示 ModelState.AddModelError(string.Empty, ex.Message); // 回显用户输入的数据,或者刷新最新数据后返回视图 return View(model); } catch (Exception ex) { // 处理其他未知错误 ModelState.AddModelError(string.Empty, "更新失败,请稍后重试"); return View(model); } }
这种方式的优势在于:上层代码不需要关心底层HTTP状态码的细节,只需要处理业务层面的异常类型,代码更易维护,也符合异常处理的最佳实践(用异常表示异常情况,而非正常流程)。
2. 给现有异常添加额外数据(备选)
如果不想创建新的异常类,你可以利用Exception.Data字典来附加状态码信息,这样控制器里可以通过这个字典判断错误类型。
修改UserRepo的catch块:
catch (Exception ex) { if (response.StatusCode == StatusCodes.Status409Conflict) { ex.Data["StatusCode"] = (int)StatusCodes.Status409Conflict; throw; } throw; }
然后在控制器里捕获并检查:
catch (HttpRequestException ex) { if (ex.Data.Contains("StatusCode") && (int)ex.Data["StatusCode"] == StatusCodes.Status409Conflict) { ModelState.AddModelError(string.Empty, "用户数据已过时,请刷新后重试"); return View(model); } // 其他HttpRequestException处理 }
这种方式的缺点是不够类型安全——你需要确保键名拼写正确,而且如果底层抛出的不是HttpRequestException,还需要额外判断,可读性和维护性不如自定义异常。
3. 返回结果对象(无异常方案)
如果你的团队偏好不用异常来控制业务流程,可以定义一个通用的结果类,让Repo方法返回操作状态、数据和错误信息,而不是抛出异常。
首先定义结果类:
public class RepositoryResult<T> { public bool IsSuccess { get; set; } public T Data { get; set; } public string ErrorMessage { get; set; } public int? StatusCode { get; set; } }
修改UserRepo.Update方法返回这个结果:
public async Task<RepositoryResult<User>> Update(User user) { HttpClient client = _clientFactory.CreateClient("namedClient"); HttpResponseMessage response = await client.PutAsync($"api/Users/{user.Id}", ContentEncoder.Encode(user)); if (response.IsSuccessStatusCode) { var updatedUser = await response.Content.ReadFromJsonAsync<User>(); return new RepositoryResult<User> { IsSuccess = true, Data = updatedUser }; } else if (response.StatusCode == StatusCodes.Status409Conflict) { return new RepositoryResult<User> { IsSuccess = false, ErrorMessage = "用户数据已过时,请刷新后重试", StatusCode = (int)StatusCodes.Status409Conflict }; } else { // 其他错误情况,返回通用错误信息 return new RepositoryResult<User> { IsSuccess = false, ErrorMessage = "更新失败,请稍后重试", StatusCode = (int)response.StatusCode }; } }
然后控制器里直接判断结果:
public async Task<IActionResult> Update(UserViewModel model) { var user = MapViewModelToUser(model); var result = await _userRepo.Update(user); if (result.IsSuccess) { return RedirectToAction("Index"); } else if (result.StatusCode == StatusCodes.Status409Conflict) { ModelState.AddModelError(string.Empty, result.ErrorMessage); return View(model); } else { ModelState.AddModelError(string.Empty, result.ErrorMessage); return View(model); } }
这种方案适合那些认为“异常应该只用于真正的意外错误”的场景,但会让Repo方法的返回值更复杂,需要上层代码手动处理各种状态。
内容的提问来源于stack exchange,提问作者Lukas

