C# ASP.NET核心Web API异常处理
在使用常规ASP.NETWebAPI多年后,我正在为我的新RESTAPI项目使用ASP.NETCore。在ASP.NET核心Web API中,我看不到任何处理异常的好方法。我尝试实现异常处理筛选器/属性:C# ASP.NET核心Web API异常处理,c#,exception,asp.net-core,C#,Exception,Asp.net Core,在使用常规ASP.NETWebAPI多年后,我正在为我的新RESTAPI项目使用ASP.NETCore。在ASP.NET核心Web API中,我看不到任何处理异常的好方法。我尝试实现异常处理筛选器/属性: public class ErrorHandlingFilter : ExceptionFilterAttribute { public override void OnException(ExceptionContext context) { HandleEx
public class ErrorHandlingFilter : ExceptionFilterAttribute
{
public override void OnException(ExceptionContext context)
{
HandleExceptionAsync(context);
context.ExceptionHandled = true;
}
private static void HandleExceptionAsync(ExceptionContext context)
{
var exception = context.Exception;
if (exception is MyNotFoundException)
SetExceptionResult(context, exception, HttpStatusCode.NotFound);
else if (exception is MyUnauthorizedException)
SetExceptionResult(context, exception, HttpStatusCode.Unauthorized);
else if (exception is MyException)
SetExceptionResult(context, exception, HttpStatusCode.BadRequest);
else
SetExceptionResult(context, exception, HttpStatusCode.InternalServerError);
}
private static void SetExceptionResult(
ExceptionContext context,
Exception exception,
HttpStatusCode code)
{
context.Result = new JsonResult(new ApiResponse(exception))
{
StatusCode = (int)code
};
}
}
这是我的启动过滤器注册:
services.AddMvc(options =>
{
options.Filters.Add(new AuthorizationFilter());
options.Filters.Add(new ErrorHandlingFilter());
});
我遇到的问题是,当我的AuthorizationFilter
中发生异常时,它不是由ErrorHandlingFilter
处理的。我希望它能像使用旧的ASP.NETWebAPI一样在那里被捕获
那么,如何捕获所有应用程序异常以及操作筛选器中的任何异常呢?您最好的选择是使用中间件来实现所需的日志记录。您希望将异常日志记录放在一个中间件中,然后在另一个中间件中处理显示给用户的错误页面。这允许逻辑分离,并遵循微软用两个中间件组件设计的设计。以下是指向Microsoft文档的良好链接: 对于您的特定示例,您可能希望使用中的一个扩展,或者自己使用 您可以在此处找到记录异常的示例: 如果您不喜欢特定的实现,那么您也可以使用,下面是一些示例: 如果这不符合您的需要,您可以通过查看ExceptionHandlerMiddleware和ElmMiddleware的实现来滚动自己的中间件组件,以掌握构建自己的中间件的概念
在StatusCodePages中间件下面添加异常处理中间件,但在所有其他中间件组件之上,这一点很重要。这样,您的异常中间件将捕获异常,记录它,然后允许请求进入StatusCodePage中间件,该中间件将向用户显示友好的错误页面。两种方法都使用内置的异常处理中间件。在调用
UseMvc
、UseRouting
或UseEndpoints
之前添加此代码。它将处理所有注册后的中间件的例外情况
快速简便的解决方案。 只需在ASP.NET路由之前将此中间件添加到中间件注册中即可
app.UseExceptionHandler(c => c.Run(async context =>
{
var exception = context.Features
.Get<IExceptionHandlerPathFeature>()
.Error;
var response = new { error = exception.Message };
await context.Response.WriteAsJsonAsync(response);
}));
步骤2。创建将处理所有异常并产生错误响应的控制器:
[AllowAnonymous]
[ApiExplorerSettings(IgnoreApi = true)]
public class ErrorsController : ControllerBase
{
[Route("error")]
public MyErrorResponse Error()
{
var context = HttpContext.Features.Get<IExceptionHandlerFeature>();
var exception = context.Error; // Your exception
var code = 500; // Internal Server Error by default
if (exception is MyNotFoundException) code = 404; // Not Found
else if (exception is MyUnauthException) code = 401; // Unauthorized
else if (exception is MyException) code = 400; // Bad Request
Response.StatusCode = code; // You can use HttpStatusCode enum instead
return new MyErrorResponse(exception); // Your error model
}
}
对于更简单的服务,您可能希望实现如下所示的http状态代码异常:
public class HttpStatusException : Exception
{
public HttpStatusCode Status { get; private set; }
public HttpStatusException(HttpStatusCode status, string msg) : base(msg)
{
Status = status;
}
}
using System;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Hosting;
namespace Api.Controllers
{
[ApiController]
[ApiExplorerSettings(IgnoreApi = true)]
[AllowAnonymous]
public class ErrorController : ControllerBase
{
[Route("/error")]
public IActionResult Error(
[FromServices] IWebHostEnvironment webHostEnvironment)
{
var context = HttpContext.Features.Get<IExceptionHandlerFeature>();
var exceptionType = context.Error.GetType();
if (exceptionType == typeof(ArgumentException)
|| exceptionType == typeof(ArgumentNullException)
|| exceptionType == typeof(ArgumentOutOfRangeException))
{
if (webHostEnvironment.IsDevelopment())
{
return ValidationProblem(
context.Error.StackTrace,
title: context.Error.Message);
}
return ValidationProblem(context.Error.Message);
}
if (exceptionType == typeof(NotFoundException))
{
return NotFound(context.Error.Message);
}
if (webHostEnvironment.IsDevelopment())
{
return Problem(
context.Error.StackTrace,
title: context.Error.Message
);
}
return Problem();
}
}
}
这可以通过以下方式从任何地方抛出:
throw new HttpStatusCodeException(HttpStatusCode.NotFound, "User not found");
然后,您的处理代码可以简化为:
if (exception is HttpStatusException httpException)
{
code = (int) httpException.Status;
}
HttpContext.Features.Get()
WAT
ASP.NET核心开发人员接受了中间件的概念,在中间件中,Auth、MVC、Swagger等功能的不同方面被分离,并在请求处理管道中顺序执行。每个中间件都可以访问请求上下文,并可以在需要时写入响应。如果像处理MVC异常一样处理来自非MVC中间件的错误很重要,那么将异常处理从MVC中去掉是有意义的,我发现这在现实世界的应用程序中非常常见。因此,由于内置的异常处理中间件不是MVC的一部分,MVC本身对它一无所知,反之亦然,异常处理中间件并不真正知道异常来自何处,而且它当然知道异常发生在请求执行过程中的某个地方。但两者可能都需要相互“连接”。所以,当异常在任何地方都没有被捕获时,异常处理中间件会捕获它,并重新运行在其中注册的路由的管道。这就是使用consistent或其他中间件(如果您愿意)将异常处理“传递”回MVC的方式。异常本身是从公共中间件上下文中提取的。看起来很有趣,但完成了任务:)。首先,配置ASP.NET Core 2
启动
以重新执行到错误页,查找web服务器的任何错误和任何未处理的异常
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment()) {
// Debug config here...
} else {
app.UseStatusCodePagesWithReExecute("/Error");
app.UseExceptionHandler("/Error");
}
// More config...
}
接下来,定义一个异常类型,该类型将允许您使用HTTP状态代码抛出错误
public class HttpException : Exception
{
public HttpException(HttpStatusCode statusCode) { StatusCode = statusCode; }
public HttpStatusCode StatusCode { get; private set; }
}
最后,在控制器的错误页面中,根据错误原因以及最终用户是否会直接看到响应来定制响应。此代码假定所有API URL都以/API/
开头
[AllowAnonymous]
public IActionResult Error()
{
// Gets the status code from the exception or web server.
var statusCode = HttpContext.Features.Get<IExceptionHandlerFeature>()?.Error is HttpException httpEx ?
httpEx.StatusCode : (HttpStatusCode)Response.StatusCode;
// For API errors, responds with just the status code (no page).
if (HttpContext.Features.Get<IHttpRequestFeature>().RawTarget.StartsWith("/api/", StringComparison.Ordinal))
return StatusCode((int)statusCode);
// Creates a view model for a user-friendly error page.
string text = null;
switch (statusCode) {
case HttpStatusCode.NotFound: text = "Page not found."; break;
// Add more as desired.
}
return View("Error", new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier, ErrorText = text });
}
[AllowAnonymous]
公共IActionResult错误()
{
//从异常或web服务器获取状态代码。
var statusCode=HttpContext.Features.Get()?。错误为HttpException httpEx?
httpEx.StatusCode:(HttpStatusCode)Response.StatusCode;
//对于API错误,只响应状态代码(无页面)。
if(HttpContext.Features.Get().RawTarget.StartsWith(“/api/”,StringComparison.Ordinal))
返回状态码((int)状态码);
//为用户友好的错误页面创建视图模型。
字符串文本=空;
开关(状态代码){
案例HttpStatusCode.NotFound:text=“未找到页面。”中断;
//根据需要添加更多。
}
返回视图(“错误”,新的ErrorViewModel{RequestId=Activity.Current?.Id??HttpContext.TraceIdentifier,ErrorText=text});
}
ASP.NET Core将记录错误详细信息供您调试,因此状态代码可能是您希望向(可能不受信任的)请求者提供的所有代码。如果您想显示更多信息,可以增强
HttpException
来提供它。对于API错误,您可以通过将返回状态码…
替换为返回JSON…
,将JSON编码的错误信息放入消息正文中。要配置每种异常类型的异常处理行为,您可以使用NuGet包中的中间件:
- 对于
ASP.NET Core 2.0
- 对于
李>ASP.NET Core 2.1+
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddExceptionHandlingPolicies(options =>
{
options.For<InitializationException>().Rethrow();
options.For<SomeTransientException>().Retry(ro => ro.MaxRetryCount = 2).NextPolicy();
options.For<SomeBadRequestException>()
.Response(e => 400)
.Headers((h, e) => h["X-MyCustomHeader"] = e.Message)
.WithBody((req,sw, exception) =>
{
byte[] array = Encoding.UTF8.GetBytes(exception.ToString());
return sw.WriteAsync(array, 0, array.Length);
})
.NextPolicy();
// Ensure that all exception types are handled by adding handler for generic exception at the end.
options.For<Exception>()
.Log(lo =>
{
lo.EventIdFactory = (c, e) => new EventId(123, "UnhandlerException");
lo.Category = (context, exception) => "MyCategory";
})
.Response(null, ResponseAlreadyStartedBehaviour.GoToNextHandler)
.ClearCacheHeaders()
.WithObjectResult((r, e) => new { msg = e.Message, path = r.Path })
.Handled();
});
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseExceptionHandlingPolicies();
app.UseMvc();
}
public void配置服务(IServiceCollection服务)
{
services.AddMvc();
服务.AddExceptionHandlingPolicys(选项=>
{
public class HttpException : Exception
{
public HttpException(HttpStatusCode statusCode) { StatusCode = statusCode; }
public HttpStatusCode StatusCode { get; private set; }
}
[AllowAnonymous]
public IActionResult Error()
{
// Gets the status code from the exception or web server.
var statusCode = HttpContext.Features.Get<IExceptionHandlerFeature>()?.Error is HttpException httpEx ?
httpEx.StatusCode : (HttpStatusCode)Response.StatusCode;
// For API errors, responds with just the status code (no page).
if (HttpContext.Features.Get<IHttpRequestFeature>().RawTarget.StartsWith("/api/", StringComparison.Ordinal))
return StatusCode((int)statusCode);
// Creates a view model for a user-friendly error page.
string text = null;
switch (statusCode) {
case HttpStatusCode.NotFound: text = "Page not found."; break;
// Add more as desired.
}
return View("Error", new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier, ErrorText = text });
}
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddExceptionHandlingPolicies(options =>
{
options.For<InitializationException>().Rethrow();
options.For<SomeTransientException>().Retry(ro => ro.MaxRetryCount = 2).NextPolicy();
options.For<SomeBadRequestException>()
.Response(e => 400)
.Headers((h, e) => h["X-MyCustomHeader"] = e.Message)
.WithBody((req,sw, exception) =>
{
byte[] array = Encoding.UTF8.GetBytes(exception.ToString());
return sw.WriteAsync(array, 0, array.Length);
})
.NextPolicy();
// Ensure that all exception types are handled by adding handler for generic exception at the end.
options.For<Exception>()
.Log(lo =>
{
lo.EventIdFactory = (c, e) => new EventId(123, "UnhandlerException");
lo.Category = (context, exception) => "MyCategory";
})
.Response(null, ResponseAlreadyStartedBehaviour.GoToNextHandler)
.ClearCacheHeaders()
.WithObjectResult((r, e) => new { msg = e.Message, path = r.Path })
.Handled();
});
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseExceptionHandlingPolicies();
app.UseMvc();
}
public class ExceptionHandlerMiddleware
{
private readonly RequestDelegate next;
private readonly IActionResultExecutor<ObjectResult> executor;
private readonly ILogger logger;
private static readonly ActionDescriptor EmptyActionDescriptor = new ActionDescriptor();
public ExceptionHandlerMiddleware(RequestDelegate next, IActionResultExecutor<ObjectResult> executor, ILoggerFactory loggerFactory)
{
this.next = next;
this.executor = executor;
logger = loggerFactory.CreateLogger<ExceptionHandlerMiddleware>();
}
public async Task Invoke(HttpContext context)
{
try
{
await next(context);
}
catch (Exception ex)
{
logger.LogError(ex, $"An unhandled exception has occurred while executing the request. Url: {context.Request.GetDisplayUrl()}. Request Data: " + GetRequestData(context));
if (context.Response.HasStarted)
{
throw;
}
var routeData = context.GetRouteData() ?? new RouteData();
ClearCacheHeaders(context.Response);
var actionContext = new ActionContext(context, routeData, EmptyActionDescriptor);
var result = new ObjectResult(new ErrorResponse("Error processing request. Server error."))
{
StatusCode = (int) HttpStatusCode.InternalServerError,
};
await executor.ExecuteAsync(actionContext, result);
}
}
private static string GetRequestData(HttpContext context)
{
var sb = new StringBuilder();
if (context.Request.HasFormContentType && context.Request.Form.Any())
{
sb.Append("Form variables:");
foreach (var x in context.Request.Form)
{
sb.AppendFormat("Key={0}, Value={1}<br/>", x.Key, x.Value);
}
}
sb.AppendLine("Method: " + context.Request.Method);
return sb.ToString();
}
private static void ClearCacheHeaders(HttpResponse response)
{
response.Headers[HeaderNames.CacheControl] = "no-cache";
response.Headers[HeaderNames.Pragma] = "no-cache";
response.Headers[HeaderNames.Expires] = "-1";
response.Headers.Remove(HeaderNames.ETag);
}
[DataContract(Name= "ErrorResponse")]
public class ErrorResponse
{
[DataMember(Name = "Message")]
public string Message { get; set; }
public ErrorResponse(string message)
{
Message = message;
}
}
}
public class ErrorDetails
{
public int StatusCode { get; set; }
public string Message { get; set; }
public override string ToString()
{
return JsonConvert.SerializeObject(this);
}
}
public class HttpStatusCodeException : Exception
{
public HttpStatusCode StatusCode { get; set; }
public string ContentType { get; set; } = @"text/plain";
public HttpStatusCodeException(HttpStatusCode statusCode)
{
this.StatusCode = statusCode;
}
public HttpStatusCodeException(HttpStatusCode statusCode, string message)
: base(message)
{
this.StatusCode = statusCode;
}
public HttpStatusCodeException(HttpStatusCode statusCode, Exception inner)
: this(statusCode, inner.ToString()) { }
public HttpStatusCodeException(HttpStatusCode statusCode, JObject errorObject)
: this(statusCode, errorObject.ToString())
{
this.ContentType = @"application/json";
}
}
public class CustomExceptionMiddleware
{
private readonly RequestDelegate next;
public CustomExceptionMiddleware(RequestDelegate next)
{
this.next = next;
}
public async Task Invoke(HttpContext context /* other dependencies */)
{
try
{
await next(context);
}
catch (HttpStatusCodeException ex)
{
await HandleExceptionAsync(context, ex);
}
catch (Exception exceptionObj)
{
await HandleExceptionAsync(context, exceptionObj);
}
}
private Task HandleExceptionAsync(HttpContext context, HttpStatusCodeException exception)
{
string result = null;
context.Response.ContentType = "application/json";
if (exception is HttpStatusCodeException)
{
result = new ErrorDetails()
{
Message = exception.Message,
StatusCode = (int)exception.StatusCode
}.ToString();
context.Response.StatusCode = (int)exception.StatusCode;
}
else
{
result = new ErrorDetails()
{
Message = "Runtime Error",
StatusCode = (int)HttpStatusCode.BadRequest
}.ToString();
context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
}
return context.Response.WriteAsync(result);
}
private Task HandleExceptionAsync(HttpContext context, Exception exception)
{
string result = new ErrorDetails()
{
Message = exception.Message,
StatusCode = (int)HttpStatusCode.InternalServerError
}.ToString();
context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
return context.Response.WriteAsync(result);
}
}
public static void ConfigureCustomExceptionMiddleware(this IApplicationBuilder app)
{
app.UseMiddleware<CustomExceptionMiddleware>();
}
app.ConfigureCustomExceptionMiddleware();
app.UseMvc();
try
{
IRepository<UserMaster> obj
= new Repository<UserMaster>(_objHeaderCapture, Constants.Tables.UserMaster);
var result = obj.Get()
.AsQueryable()
.Where(sb => sb.EmailId.ToLower() == objData.UserName.ToLower()
&& sb.Password == objData.Password.ToEncrypt()
&& sb.Status == (int)StatusType.Active)
.FirstOrDefault();
if (result != null)//User Found
return result;
else // Not Found
throw new HttpStatusCodeException(HttpStatusCode.NotFound,
"Please check username or password");
}
catch (Exception ex)
{
throw ex;
}
app.UseExceptionHandler(a => a.Run(async context =>
{
var exceptionHandlerPathFeature = context.Features.Get<IExceptionHandlerPathFeature>();
var exception = exceptionHandlerPathFeature.Error;
await context.Response.WriteAsJsonAsync(new { error = exception.Message });
}));
app.UseExceptionHandler(a => a.Run(async context =>
{
var exceptionHandlerPathFeature = context.Features.Get<IExceptionHandlerPathFeature>();
var exception = exceptionHandlerPathFeature.Error;
var result = JsonConvert.SerializeObject(new { error = exception.Message });
context.Response.ContentType = "application/json";
await context.Response.WriteAsync(result);
}));
public class HttpGlobalExceptionFilter : IExceptionFilter
{
public void OnException(ExceptionContext context)
{...}
}
services.AddMvc(options =>
{
options.Filters.Add(typeof(HttpGlobalExceptionFilter));
})
app.UseExceptionHandler("/error");
using System;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Hosting;
namespace Api.Controllers
{
[ApiController]
[ApiExplorerSettings(IgnoreApi = true)]
[AllowAnonymous]
public class ErrorController : ControllerBase
{
[Route("/error")]
public IActionResult Error(
[FromServices] IWebHostEnvironment webHostEnvironment)
{
var context = HttpContext.Features.Get<IExceptionHandlerFeature>();
var exceptionType = context.Error.GetType();
if (exceptionType == typeof(ArgumentException)
|| exceptionType == typeof(ArgumentNullException)
|| exceptionType == typeof(ArgumentOutOfRangeException))
{
if (webHostEnvironment.IsDevelopment())
{
return ValidationProblem(
context.Error.StackTrace,
title: context.Error.Message);
}
return ValidationProblem(context.Error.Message);
}
if (exceptionType == typeof(NotFoundException))
{
return NotFound(context.Error.Message);
}
if (webHostEnvironment.IsDevelopment())
{
return Problem(
context.Error.StackTrace,
title: context.Error.Message
);
}
return Problem();
}
}
}
using Microsoft.AspNetCore.Http;
...
public ActionResult MyAPIMethod()
{
try
{
var myObject = ... something;
return Json(myObject);
}
catch (Exception ex)
{
Log.Error($"Error: {ex.Message}");
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
private static readonly Type[] API_CATCH_EXCEPTIONS = new Type[]
{
typeof(InvalidOperationException),
typeof(ValidationException)
};
public override void OnActionExecuted(ActionExecutedContext context)
{
base.OnActionExecuted(context);
if (context.Exception != null)
{
var exType = context.Exception.GetType();
if (API_CATCH_EXCEPTIONS.Any(type => exType == type || exType.IsSubclassOf(type)))
{
context.Result = Problem(detail: context.Exception.Message);
context.ExceptionHandled = true;
}
}
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
...
app.UseExceptionHandler(a => a.Run(async context =>
{
var error = context.Features.Get<IExceptionHandlerFeature>().Error;
var problem = new ProblemDetails { Title = "Critical Error"};
if (error != null)
{
if (env.IsDevelopment())
{
problem.Title = error.Message;
problem.Detail = error.StackTrace;
}
else
problem.Detail = error.Message;
}
await context.Response.WriteAsJsonAsync(problem);
}));
...
}