#90 Wrap result only for services return object. And only on error case.

pull/112/head
Halil İbrahim Kalkan 8 years ago
parent 2b90ba164b
commit 75e0ed2ee6

@ -1,5 +1,6 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.AspNetCore.Mvc.ExceptionHandling;
namespace Volo.Abp.AspNetCore.Mvc
{
@ -23,7 +24,7 @@ namespace Volo.Abp.AspNetCore.Mvc
//options.Filters.AddService(typeof(AbpAuditActionFilter));
//options.Filters.AddService(typeof(AbpValidationActionFilter));
//options.Filters.AddService(typeof(AbpUowActionFilter));
//options.Filters.AddService(typeof(AbpExceptionFilter));
options.Filters.AddService(typeof(AbpExceptionFilter));
//options.Filters.AddService(typeof(AbpResultFilter));
}

@ -0,0 +1,35 @@
using System;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
namespace Volo.Abp.AspNetCore.Mvc
{
public static class ActionResultHelper
{
public static bool IsObjectResult(Type returnType)
{
//Get the actual return type (unwrap Task)
if (returnType == typeof(Task))
{
returnType = typeof(void);
}
else if (returnType.GetTypeInfo().IsGenericType && returnType.GetGenericTypeDefinition() == typeof(Task<>))
{
returnType = returnType.GenericTypeArguments[0];
}
if (typeof(IActionResult).GetTypeInfo().IsAssignableFrom(returnType))
{
if (typeof(JsonResult).GetTypeInfo().IsAssignableFrom(returnType) || typeof(ObjectResult).GetTypeInfo().IsAssignableFrom(returnType))
{
return true;
}
return false;
}
return true;
}
}
}

@ -0,0 +1,84 @@
using System.Net;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
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
{
public class AbpExceptionFilter : IExceptionFilter, ITransientDependency
{
public ILogger<AbpExceptionFilter> Logger { get; set; }
//TODO: Use EventBus to trigger error handled event like in previous ABP
private readonly IExceptionToErrorInfoConverter _errorInfoConverter;
public AbpExceptionFilter(IExceptionToErrorInfoConverter errorInfoConverter)
{
_errorInfoConverter = errorInfoConverter;
Logger = NullLogger<AbpExceptionFilter>.Instance;
}
public void OnException(ExceptionContext context)
{
if (!context.ActionDescriptor.IsControllerAction())
{
return;
}
//TODO: Create DontWrap attribute to control wrapping..?
Logger.LogException(context.Exception);
HandleAndWrapException(context);
}
private void HandleAndWrapException(ExceptionContext context)
{
if (!ActionResultHelper.IsObjectResult(context.ActionDescriptor.GetMethodInfo().ReturnType))
{
return;
}
context.HttpContext.Response.StatusCode = GetStatusCode(context);
context.Result = new ObjectResult(
new RemoteServiceErrorResponse(
_errorInfoConverter.Convert(context.Exception)
)
);
//EventBus.Trigger(this, new AbpHandledExceptionData(context.Exception));
context.Exception = null; //Handled!
}
private 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,209 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Volo.Abp.Authorization;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Entities;
using Volo.Abp.Http;
using Volo.Abp.Ui;
using Volo.Abp.Validation;
namespace Volo.Abp.AspNetCore.Mvc.ExceptionHandling
{
public class DefaultExceptionToErrorInfoConverter : IExceptionToErrorInfoConverter, ITransientDependency
{
public bool SendAllExceptionsToClients { get; set; } = false;
public RemoteServiceErrorInfo Convert(Exception exception)
{
var errorInfo = CreateErrorInfoWithoutCode(exception);
if (exception is IHasErrorCode)
{
errorInfo.Code = (exception as IHasErrorCode).Code;
}
return errorInfo;
}
private RemoteServiceErrorInfo CreateErrorInfoWithoutCode(Exception exception)
{
if (SendAllExceptionsToClients)
{
return CreateDetailedErrorInfoFromException(exception);
}
if (exception is AggregateException && exception.InnerException != null)
{
var aggException = exception as AggregateException;
if (aggException.InnerException is UserFriendlyException ||
aggException.InnerException is AbpValidationException)
{
exception = aggException.InnerException;
}
}
if (exception is UserFriendlyException)
{
var userFriendlyException = exception as UserFriendlyException;
return new RemoteServiceErrorInfo(userFriendlyException.Message, userFriendlyException.Details);
}
if (exception is AbpValidationException)
{
return new RemoteServiceErrorInfo(L("ValidationError"))
{
ValidationErrors = GetValidationErrorInfos(exception as AbpValidationException),
Details = GetValidationErrorNarrative(exception as AbpValidationException)
};
}
if (exception is EntityNotFoundException)
{
var entityNotFoundException = exception as EntityNotFoundException;
if (entityNotFoundException.EntityType != null)
{
return new RemoteServiceErrorInfo(
string.Format(
L("EntityNotFound"),
entityNotFoundException.EntityType.Name,
entityNotFoundException.Id
)
);
}
return new RemoteServiceErrorInfo(
entityNotFoundException.Message
);
}
if (exception is AbpAuthorizationException)
{
var authorizationException = exception as AbpAuthorizationException;
return new RemoteServiceErrorInfo(authorizationException.Message);
}
return new RemoteServiceErrorInfo(L("InternalServerError"));
}
private RemoteServiceErrorInfo CreateDetailedErrorInfoFromException(Exception exception)
{
var detailBuilder = new StringBuilder();
AddExceptionToDetails(exception, detailBuilder);
var errorInfo = new RemoteServiceErrorInfo(exception.Message, detailBuilder.ToString());
if (exception is AbpValidationException)
{
errorInfo.ValidationErrors = GetValidationErrorInfos(exception as AbpValidationException);
}
return errorInfo;
}
private void AddExceptionToDetails(Exception exception, StringBuilder detailBuilder)
{
//Exception Message
detailBuilder.AppendLine(exception.GetType().Name + ": " + exception.Message);
//Additional info for UserFriendlyException
if (exception is UserFriendlyException)
{
var userFriendlyException = exception as UserFriendlyException;
if (!string.IsNullOrEmpty(userFriendlyException.Details))
{
detailBuilder.AppendLine(userFriendlyException.Details);
}
}
//Additional info for AbpValidationException
if (exception is AbpValidationException)
{
var validationException = exception as AbpValidationException;
if (validationException.ValidationErrors.Count > 0)
{
detailBuilder.AppendLine(GetValidationErrorNarrative(validationException));
}
}
//Exception StackTrace
if (!string.IsNullOrEmpty(exception.StackTrace))
{
detailBuilder.AppendLine("STACK TRACE: " + exception.StackTrace);
}
//Inner exception
if (exception.InnerException != null)
{
AddExceptionToDetails(exception.InnerException, detailBuilder);
}
//Inner exceptions for AggregateException
if (exception is AggregateException)
{
var aggException = exception as AggregateException;
if (aggException.InnerExceptions.IsNullOrEmpty())
{
return;
}
foreach (var innerException in aggException.InnerExceptions)
{
AddExceptionToDetails(innerException, detailBuilder);
}
}
}
private RemoteServiceValidationErrorInfo[] GetValidationErrorInfos(AbpValidationException validationException)
{
var validationErrorInfos = new List<RemoteServiceValidationErrorInfo>();
foreach (var validationResult in validationException.ValidationErrors)
{
var validationError = new RemoteServiceValidationErrorInfo(validationResult.ErrorMessage);
if (validationResult.MemberNames != null && validationResult.MemberNames.Any())
{
validationError.Members = validationResult.MemberNames.Select(m => m.ToCamelCase()).ToArray();
}
validationErrorInfos.Add(validationError);
}
return validationErrorInfos.ToArray();
}
private string GetValidationErrorNarrative(AbpValidationException validationException)
{
var detailBuilder = new StringBuilder();
detailBuilder.AppendLine(L("ValidationNarrativeTitle"));
foreach (var validationResult in validationException.ValidationErrors)
{
detailBuilder.AppendFormat(" - {0}", validationResult.ErrorMessage);
detailBuilder.AppendLine();
}
return detailBuilder.ToString();
}
private string L(string name)
{
//TODO: Localization?
//try
//{
// return _localizationManager.GetString(AbpWebConsts.LocalizaionSourceName, name);
//}
//catch (Exception)
//{
// return name;
//}
return name;
}
}
}

@ -0,0 +1,19 @@
using System;
using Volo.Abp.Http;
namespace Volo.Abp.AspNetCore.Mvc.ExceptionHandling
{
/// <summary>
/// This interface can be implemented to convert an <see cref="Exception"/> object to an <see cref="RemoteServiceErrorInfo"/> object.
/// Implements Chain Of Responsibility pattern.
/// </summary>
public interface IExceptionToErrorInfoConverter
{
/// <summary>
/// Converter method.
/// </summary>
/// <param name="exception">The exception</param>
/// <returns>Error info or null</returns>
RemoteServiceErrorInfo Convert(Exception exception);
}
}

@ -1,6 +1,6 @@
namespace Volo.Abp.Http
{
public abstract class RemoteServiceErrorResponse
public class RemoteServiceErrorResponse
{
public RemoteServiceErrorInfo Error { get; set; }
@ -8,5 +8,10 @@
/// A special signature of ABP.
/// </summary>
public bool __abp { get; } = true;
public RemoteServiceErrorResponse(RemoteServiceErrorInfo error)
{
Error = error;
}
}
}

@ -0,0 +1,105 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Volo.Abp.Logging;
using Volo.Abp.Validation;
namespace Microsoft.Extensions.Logging
{
public static class LoggerExtensions
{
public static void LogWithLevel(this ILogger logger, LogLevel logLevel, string message)
{
switch (logLevel)
{
case LogLevel.Critical:
logger.LogCritical(message);
break;
case LogLevel.Error:
logger.LogError(message);
break;
case LogLevel.Warning:
logger.LogWarning(message);
break;
case LogLevel.Information:
logger.LogInformation(message);
break;
case LogLevel.Trace:
logger.LogTrace(message);
break;
default: // LogLevel.Debug || LogLevel.None
logger.LogDebug(message);
break;
}
}
public static void LogWithLevel(this ILogger logger, LogLevel logLevel, string message, Exception exception)
{
switch (logLevel)
{
case LogLevel.Critical:
logger.LogCritical(exception, message);
break;
case LogLevel.Error:
logger.LogError(exception, message);
break;
case LogLevel.Warning:
logger.LogWarning(exception, message);
break;
case LogLevel.Information:
logger.LogInformation(exception, message);
break;
case LogLevel.Trace:
logger.LogTrace(exception, message);
break;
default: // LogLevel.Debug || LogLevel.None
logger.LogDebug(message);
break;
}
}
public static void LogException(this ILogger logger, Exception ex)
{
var logLevel = (ex as IHasLogLevel)?.LogLevel ?? LogLevel.Error;
logger.LogWithLevel(logLevel, ex.Message, ex);
LogValidationErrors(logger, ex);
}
private static void LogValidationErrors(ILogger logger, Exception exception)
{
//Try to find inner validation exception
if (exception is AggregateException && exception.InnerException != null)
{
var aggException = exception as AggregateException;
if (aggException.InnerException is AbpValidationException)
{
exception = aggException.InnerException;
}
}
if (!(exception is AbpValidationException))
{
return;
}
var validationException = exception as AbpValidationException;
if (validationException.ValidationErrors.IsNullOrEmpty())
{
return;
}
logger.LogWithLevel(validationException.LogLevel, "There are " + validationException.ValidationErrors.Count + " validation errors:");
foreach (var validationResult in validationException.ValidationErrors)
{
var memberNames = "";
if (validationResult.MemberNames != null && validationResult.MemberNames.Any())
{
memberNames = " (" + string.Join(", ", validationResult.MemberNames) + ")";
}
logger.LogWithLevel(validationException.LogLevel, validationResult.ErrorMessage + memberNames);
}
}
}
}

@ -1,4 +1,5 @@
using System;
using System.Runtime.Serialization;
namespace Volo.Abp
{
@ -35,5 +36,14 @@ namespace Volo.Abp
{
}
/// <summary>
/// Constructor for serializing.
/// </summary>
public AbpException(SerializationInfo serializationInfo, StreamingContext context)
: base(serializationInfo, context)
{
}
}
}

@ -0,0 +1,58 @@
using System;
using System.Runtime.Serialization;
using Microsoft.Extensions.Logging;
using Volo.Abp.Logging;
namespace Volo.Abp.Authorization
{
/// <summary>
/// This exception is thrown on an unauthorized request.
/// </summary>
[Serializable]
public class AbpAuthorizationException : AbpException, IHasLogLevel
{
/// <summary>
/// Severity of the exception.
/// Default: Warn.
/// </summary>
public LogLevel LogLevel { get; set; }
/// <summary>
/// Creates a new <see cref="AbpAuthorizationException"/> object.
/// </summary>
public AbpAuthorizationException()
{
LogLevel = LogLevel.Warning;
}
/// <summary>
/// Creates a new <see cref="AbpAuthorizationException"/> object.
/// </summary>
public AbpAuthorizationException(SerializationInfo serializationInfo, StreamingContext context)
: base(serializationInfo, context)
{
}
/// <summary>
/// Creates a new <see cref="AbpAuthorizationException"/> object.
/// </summary>
/// <param name="message">Exception message</param>
public AbpAuthorizationException(string message)
: base(message)
{
LogLevel = LogLevel.Warning;
}
/// <summary>
/// Creates a new <see cref="AbpAuthorizationException"/> object.
/// </summary>
/// <param name="message">Exception message</param>
/// <param name="innerException">Inner exception</param>
public AbpAuthorizationException(string message, Exception innerException)
: base(message, innerException)
{
LogLevel = LogLevel.Warning;
}
}
}

@ -0,0 +1,7 @@
namespace Volo.Abp
{
public interface IHasErrorCode
{
int Code { get; set; }
}
}

@ -0,0 +1,15 @@
using Microsoft.Extensions.Logging;
namespace Volo.Abp.Logging
{
/// <summary>
/// Interface to define a <see cref="LogLevel"/> property (see <see cref="LogLevel"/>).
/// </summary>
public interface IHasLogLevel
{
/// <summary>
/// Log severity.
/// </summary>
LogLevel LogLevel { get; set; }
}
}

@ -92,6 +92,22 @@ namespace Volo.Abp.Reflection
return defaultValue;
}
/// <summary>
/// Tries to gets an of attribute defined for a class member and it's declaring type including inherited attributes.
/// Returns default value if it's not declared at all.
/// </summary>
/// <typeparam name="TAttribute">Type of the attribute</typeparam>
/// <param name="memberInfo">MemberInfo</param>
/// <param name="defaultValue">Default value (null as default)</param>
/// <param name="inherit">Inherit attribute from base classes</param>
public static TAttribute GetSingleAttributeOfMemberOrDeclaringTypeOrDefault<TAttribute>(MemberInfo memberInfo, TAttribute defaultValue = default(TAttribute), bool inherit = true)
where TAttribute : class
{
return memberInfo.GetCustomAttributes(true).OfType<TAttribute>().FirstOrDefault()
?? memberInfo.DeclaringType?.GetTypeInfo().GetCustomAttributes(true).OfType<TAttribute>().FirstOrDefault()
?? defaultValue;
}
/// <summary>
/// Gets value of a property by it's full path from given object
/// </summary>

@ -0,0 +1,125 @@
using System;
using System.Runtime.Serialization;
using Microsoft.Extensions.Logging;
using Volo.Abp.Logging;
namespace Volo.Abp.Ui
{
/// <summary>
/// This exception type is directly shown to the user.
/// </summary>
[Serializable]
public class UserFriendlyException : AbpException, IHasLogLevel, IHasErrorCode
{
/// <summary>
/// Additional information about the exception.
/// </summary>
public string Details { get; private set; }
/// <summary>
/// An arbitrary error code.
/// </summary>
public int Code { get; set; }
/// <summary>
/// Severity of the exception.
/// Default: Warn.
/// </summary>
public LogLevel LogLevel { get; set; }
/// <summary>
/// Constructor.
/// </summary>
public UserFriendlyException()
{
LogLevel = LogLevel.Warning;
}
/// <summary>
/// Constructor for serializing.
/// </summary>
public UserFriendlyException(SerializationInfo serializationInfo, StreamingContext context)
: base(serializationInfo, context)
{
}
/// <summary>
/// Constructor.
/// </summary>
/// <param name="message">Exception message</param>
public UserFriendlyException(string message)
: base(message)
{
LogLevel = LogLevel.Warning;
}
/// <summary>
/// Constructor.
/// </summary>
/// <param name="message">Exception message</param>
/// <param name="severity">Exception severity</param>
public UserFriendlyException(string message, LogLevel logLevel)
: base(message)
{
LogLevel = logLevel;
}
/// <summary>
/// Constructor.
/// </summary>
/// <param name="code">Error code</param>
/// <param name="message">Exception message</param>
public UserFriendlyException(int code, string message)
: this(message)
{
Code = code;
}
/// <summary>
/// Constructor.
/// </summary>
/// <param name="message">Exception message</param>
/// <param name="details">Additional information about the exception</param>
public UserFriendlyException(string message, string details)
: this(message)
{
Details = details;
}
/// <summary>
/// Constructor.
/// </summary>
/// <param name="code">Error code</param>
/// <param name="message">Exception message</param>
/// <param name="details">Additional information about the exception</param>
public UserFriendlyException(int code, string message, string details)
: this(message, details)
{
Code = code;
}
/// <summary>
/// Constructor.
/// </summary>
/// <param name="message">Exception message</param>
/// <param name="innerException">Inner exception</param>
public UserFriendlyException(string message, Exception innerException)
: base(message, innerException)
{
LogLevel = LogLevel.Warning;
}
/// <summary>
/// Constructor.
/// </summary>
/// <param name="message">Exception message</param>
/// <param name="details">Additional information about the exception</param>
/// <param name="innerException">Inner exception</param>
public UserFriendlyException(string message, string details, Exception innerException)
: this(message, innerException)
{
Details = details;
}
}
}

@ -0,0 +1,81 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Runtime.Serialization;
using Microsoft.Extensions.Logging;
using Volo.Abp.Logging;
namespace Volo.Abp.Validation
{
/// <summary>
/// This exception type is used to throws validation exceptions.
/// </summary>
[Serializable]
public class AbpValidationException : AbpException, IHasLogLevel
{
/// <summary>
/// Detailed list of validation errors for this exception.
/// </summary>
public IList<ValidationResult> ValidationErrors { get; set; }
/// <summary>
/// Exception severity.
/// Default: Warn.
/// </summary>
public LogLevel LogLevel { get; set; }
/// <summary>
/// Constructor.
/// </summary>
public AbpValidationException()
{
ValidationErrors = new List<ValidationResult>();
LogLevel = LogLevel.Warning;
}
/// <summary>
/// Constructor for serializing.
/// </summary>
public AbpValidationException(SerializationInfo serializationInfo, StreamingContext context)
: base(serializationInfo, context)
{
ValidationErrors = new List<ValidationResult>();
LogLevel = LogLevel.Warning;
}
/// <summary>
/// Constructor.
/// </summary>
/// <param name="message">Exception message</param>
public AbpValidationException(string message)
: base(message)
{
ValidationErrors = new List<ValidationResult>();
LogLevel = LogLevel.Warning;
}
/// <summary>
/// Constructor.
/// </summary>
/// <param name="message">Exception message</param>
/// <param name="validationErrors">Validation errors</param>
public AbpValidationException(string message, IList<ValidationResult> validationErrors)
: base(message)
{
ValidationErrors = validationErrors;
LogLevel = LogLevel.Warning;
}
/// <summary>
/// Constructor.
/// </summary>
/// <param name="message">Exception message</param>
/// <param name="innerException">Inner exception</param>
public AbpValidationException(string message, Exception innerException)
: base(message, innerException)
{
ValidationErrors = new List<ValidationResult>();
LogLevel = LogLevel.Warning;
}
}
}

@ -0,0 +1,24 @@
using Microsoft.AspNetCore.Mvc;
using Volo.Abp.AspNetCore.Mvc;
using Volo.Abp.Ui;
namespace Volo.Abp.AspNetCore.App
{
[Route("api/exception-test")]
public class ExceptionTestController : AbpController
{
[HttpGet]
[Route("UserFriendlyException1")]
public void UserFriendlyException1()
{
throw new UserFriendlyException("This is a sample exception!");
}
[HttpGet]
[Route("UserFriendlyException2")]
public ActionResult UserFriendlyException2()
{
throw new UserFriendlyException("This is a sample exception!");
}
}
}

@ -0,0 +1,30 @@
using System.Net;
using System.Threading.Tasks;
using Shouldly;
using Volo.Abp.Http;
using Volo.Abp.Ui;
using Xunit;
namespace Volo.Abp.AspNetCore.Mvc
{
public class ExceptionTestController_Tests : AspNetCoreMvcTestBase
{
[Fact]
public async Task Should_Return_RemoteServiceErrorResponse_For_UserFriendlyException_For_Void_Return_Value()
{
var result = await GetResponseAsObjectAsync<RemoteServiceErrorResponse>("/api/exception-test/UserFriendlyException1", HttpStatusCode.InternalServerError);
result.Error.ShouldNotBeNull();
result.Error.Message.ShouldBe("This is a sample exception!");
}
[Fact]
public async Task Should_Not_Handle_Exceptions_For_ActionResult_Return_Values()
{
await Assert.ThrowsAsync<UserFriendlyException>(
async () => await GetResponseAsObjectAsync<RemoteServiceErrorResponse>(
"/api/exception-test/UserFriendlyException2"
)
);
}
}
}
Loading…
Cancel
Save