#23 Added AbpExceptionHandlingMiddleware

pull/115/head
Halil İbrahim Kalkan 8 years ago
parent fcc4c6269d
commit 4e9e27437f

@ -1,4 +1,5 @@
using Volo.Abp.AspNetCore.Mvc.Uow;
using Volo.Abp.AspNetCore.Mvc.ExceptionHandling;
using Volo.Abp.AspNetCore.Mvc.Uow;
namespace Microsoft.AspNetCore.Builder
{
@ -6,7 +7,21 @@ namespace Microsoft.AspNetCore.Builder
{
public static IApplicationBuilder UseUnitOfWork(this IApplicationBuilder app)
{
return app.UseMiddleware<AbpUnitOfWorkMiddleware>();
return app
.UseAbpExceptionHandling()
.UseMiddleware<AbpUnitOfWorkMiddleware>();
}
public static IApplicationBuilder UseAbpExceptionHandling(this IApplicationBuilder app)
{
//Prevent multiple add
if (app.Properties.ContainsKey("_AbpExceptionHandlingMiddleware_Added")) //TODO: Constant
{
return app;
}
app.Properties["_AbpExceptionHandlingMiddleware_Added"] = true;
return app.UseMiddleware<AbpExceptionHandlingMiddleware>();
}
}
}

@ -0,0 +1,7 @@
namespace Volo.Abp.AspNetCore.Mvc
{
public class AbpActionInfoInHttpContext
{
public bool IsObjectResult { get; set; }
}
}

@ -1,16 +1,12 @@
using System.Collections.Generic;
using System.Net;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Primitives;
using Volo.Abp.Authorization;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Entities;
using Volo.Abp.Http;
using Volo.Abp.Validation;
namespace Volo.Abp.AspNetCore.Mvc.ExceptionHandling
{
@ -21,10 +17,12 @@ namespace Volo.Abp.AspNetCore.Mvc.ExceptionHandling
//TODO: Use EventBus to trigger error handled event like in previous ABP
private readonly IExceptionToErrorInfoConverter _errorInfoConverter;
private readonly HttpExceptionStatusCodeFinder _statusCodeFinder;
public AbpExceptionFilter(IExceptionToErrorInfoConverter errorInfoConverter)
public AbpExceptionFilter(IExceptionToErrorInfoConverter errorInfoConverter, HttpExceptionStatusCodeFinder statusCodeFinder)
{
_errorInfoConverter = errorInfoConverter;
_statusCodeFinder = statusCodeFinder;
Logger = NullLogger<AbpExceptionFilter>.Instance;
}
@ -49,7 +47,7 @@ namespace Volo.Abp.AspNetCore.Mvc.ExceptionHandling
return;
}
context.HttpContext.Response.StatusCode = GetStatusCode(context);
context.HttpContext.Response.StatusCode = _statusCodeFinder.GetStatusCode(context.HttpContext, context.Exception);
context.HttpContext.Response.Headers.Add(new KeyValuePair<string, StringValues>("_AbpErrorFormat", "true"));
context.Result = new ObjectResult(
@ -62,27 +60,5 @@ namespace Volo.Abp.AspNetCore.Mvc.ExceptionHandling
context.Exception = null; //Handled!
}
private static int GetStatusCode(ExceptionContext context)
{
if (context.Exception is AbpAuthorizationException)
{
return context.HttpContext.User.Identity.IsAuthenticated
? (int)HttpStatusCode.Forbidden
: (int)HttpStatusCode.Unauthorized;
}
if (context.Exception is AbpValidationException)
{
return (int)HttpStatusCode.BadRequest;
}
if (context.Exception is EntityNotFoundException)
{
return (int)HttpStatusCode.NotFound;
}
return (int)HttpStatusCode.InternalServerError;
}
}
}

@ -0,0 +1,92 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
using Volo.Abp.AspNetCore.Mvc.Uow;
using Volo.Abp.Http;
using Volo.Abp.Json;
namespace Volo.Abp.AspNetCore.Mvc.ExceptionHandling
{
public class AbpExceptionHandlingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<AbpUnitOfWorkMiddleware> _logger;
private readonly Func<object, Task> _clearCacheHeadersDelegate;
public AbpExceptionHandlingMiddleware(RequestDelegate next, ILogger<AbpUnitOfWorkMiddleware> logger)
{
_next = next;
_logger = logger;
_clearCacheHeadersDelegate = ClearCacheHeaders;
}
public async Task Invoke(HttpContext httpContext)
{
try
{
await _next(httpContext);
}
catch (Exception ex)
{
// We can't do anything if the response has already started, just abort.
if (httpContext.Response.HasStarted)
{
_logger.LogWarning("An exception occured, but response has already started!");
throw;
}
if (httpContext.Items["_AbpActionInfo"] is AbpActionInfoInHttpContext actionInfo)
{
if (actionInfo.IsObjectResult)
{
await HandleAndWrapException(httpContext, ex);
return;
}
}
throw;
}
}
private async Task HandleAndWrapException(HttpContext httpContext, Exception exception)
{
_logger.LogException(exception);
var errorInfoConverter = httpContext.RequestServices.GetRequiredService<IExceptionToErrorInfoConverter>();
var statusCodeFinder = httpContext.RequestServices.GetRequiredService<HttpExceptionStatusCodeFinder>();
var jsonSerializer = httpContext.RequestServices.GetRequiredService<IJsonSerializer>();
httpContext.Response.Clear();
httpContext.Response.StatusCode = statusCodeFinder.GetStatusCode(httpContext, exception);
httpContext.Response.OnStarting(_clearCacheHeadersDelegate, httpContext.Response);
httpContext.Response.Headers.Add(new KeyValuePair<string, StringValues>("_AbpErrorFormat", "true")); //TODO: Constant
await httpContext.Response.WriteAsync(
jsonSerializer.Serialize(
new RemoteServiceErrorResponse(
errorInfoConverter.Convert(exception)
)
)
);
}
private Task ClearCacheHeaders(object state)
{
var response = (HttpResponse)state;
response.Headers[HeaderNames.CacheControl] = "no-cache";
response.Headers[HeaderNames.Pragma] = "no-cache";
response.Headers[HeaderNames.Expires] = "-1";
response.Headers.Remove(HeaderNames.ETag);
return Task.CompletedTask;
}
}
}

@ -0,0 +1,35 @@
using System;
using System.Net;
using Microsoft.AspNetCore.Http;
using Volo.Abp.Authorization;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Entities;
using Volo.Abp.Validation;
namespace Volo.Abp.AspNetCore.Mvc.ExceptionHandling
{
public class HttpExceptionStatusCodeFinder : ITransientDependency
{
public virtual int GetStatusCode(HttpContext httpContext, Exception exception)
{
if (exception is AbpAuthorizationException)
{
return httpContext.User.Identity.IsAuthenticated
? (int)HttpStatusCode.Forbidden
: (int)HttpStatusCode.Unauthorized;
}
if (exception is AbpValidationException)
{
return (int)HttpStatusCode.BadRequest;
}
if (exception is EntityNotFoundException)
{
return (int)HttpStatusCode.NotFound;
}
return (int)HttpStatusCode.InternalServerError;
}
}
}

@ -1,5 +1,4 @@
using System;
using System.Threading.Tasks;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Volo.Abp.Uow;
@ -8,15 +7,17 @@ namespace Volo.Abp.AspNetCore.Mvc.Uow
public class AbpUnitOfWorkMiddleware
{
private readonly RequestDelegate _next;
private readonly IUnitOfWorkManager _unitOfWorkManager;
public AbpUnitOfWorkMiddleware(RequestDelegate next)
public AbpUnitOfWorkMiddleware(RequestDelegate next, IUnitOfWorkManager unitOfWorkManager)
{
_next = next;
_unitOfWorkManager = unitOfWorkManager;
}
public async Task Invoke(HttpContext httpContext, IUnitOfWorkManager unitOfWorkManager)
public async Task Invoke(HttpContext httpContext)
{
using (var uow = unitOfWorkManager.Reserve(AbpUowActionFilter.UnitOfWorkReservationName))
using (var uow = _unitOfWorkManager.Reserve(AbpUowActionFilter.UnitOfWorkReservationName))
{
await _next(httpContext);
await uow.CompleteAsync(httpContext.RequestAborted);

@ -1,6 +1,8 @@
using System;
using System.Net.Http;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Options;
@ -33,6 +35,8 @@ namespace Volo.Abp.AspNetCore.Mvc.Uow
var methodInfo = context.ActionDescriptor.GetMethodInfo();
var unitOfWorkAttr = UnitOfWorkHelper.GetUnitOfWorkAttributeOrNull(methodInfo);
SetAbpActionInfoToHttpContext(context.HttpContext, methodInfo);
if (unitOfWorkAttr?.IsDisabled == true)
{
await next();
@ -62,6 +66,14 @@ namespace Volo.Abp.AspNetCore.Mvc.Uow
}
}
private static void SetAbpActionInfoToHttpContext(HttpContext context, MethodInfo methodInfo)
{
context.Items["_AbpActionInfo"] = new AbpActionInfoInHttpContext
{
IsObjectResult = ActionResultHelper.IsObjectResult(methodInfo.ReturnType)
};
}
private UnitOfWorkOptions CreateOptions(ActionExecutingContext context, UnitOfWorkAttribute unitOfWorkAttribute)
{
var options = new UnitOfWorkOptions();

@ -1,7 +1,5 @@
using System;
using System.Data;
using System.Threading;
using JetBrains.Annotations;
namespace Volo.Abp.Uow
{

@ -14,5 +14,10 @@ namespace Volo.Abp.AspNetCore.App
{
return View();
}
public ActionResult ExceptionOnRazor()
{
return View();
}
}
}

@ -0,0 +1,5 @@
@using Volo.Abp.Ui
<h2>Exception test</h2>
@{
throw new UserFriendlyException("test exception!");
}

@ -1,6 +1,7 @@
using System.Threading.Tasks;
using Shouldly;
using Volo.Abp.AspNetCore.App;
using Volo.Abp.Ui;
using Xunit;
namespace Volo.Abp.AspNetCore.Mvc
@ -26,5 +27,16 @@ namespace Volo.Abp.AspNetCore.Mvc
result.Trim().ShouldBe("<h2>About</h2>");
}
[Fact]
public async Task ActionResult_ViewResult_Exception()
{
await Assert.ThrowsAsync<UserFriendlyException>(async () =>
{
await GetResponseAsStringAsync(
GetUrl<SimpleController>(nameof(SimpleController.ExceptionOnRazor))
);
});
}
}
}

@ -1,7 +1,10 @@
using System.Net;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Shouldly;
using Volo.Abp.Http;
using Volo.Abp.Json;
using Xunit;
namespace Volo.Abp.AspNetCore.Mvc.Uow
@ -19,7 +22,14 @@ namespace Volo.Abp.AspNetCore.Mvc.Uow
[Fact]
public async Task Should_Gracefully_Handle_Exceptions_On_Complete()
{
var result = await GetResponseAsObjectAsync<RemoteServiceErrorResponse>("/api/unitofwork-test/ExceptionOnComplete", HttpStatusCode.InternalServerError);
var response = await GetResponseAsync("/api/unitofwork-test/ExceptionOnComplete", HttpStatusCode.InternalServerError);
response.Headers.GetValues("_AbpErrorFormat").FirstOrDefault().ShouldBe("true");
var resultAsString = await response.Content.ReadAsStringAsync();
var result = ServiceProvider.GetRequiredService<IJsonSerializer>().Deserialize<RemoteServiceErrorResponse>(resultAsString);
result.Error.ShouldNotBeNull();
result.Error.Message.ShouldBe(TestUnitOfWorkConfig.ExceptionOnCompleteMessage);
}

Loading…
Cancel
Save