diff --git a/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpMvcOptionsExtensions.cs b/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpMvcOptionsExtensions.cs index 9b9a85f4cc..6a0d7a1d28 100644 --- a/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpMvcOptionsExtensions.cs +++ b/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpMvcOptionsExtensions.cs @@ -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)); } diff --git a/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ActionResultHelper.cs b/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ActionResultHelper.cs new file mode 100644 index 0000000000..48cc8cc51d --- /dev/null +++ b/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ActionResultHelper.cs @@ -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; + } + } +} \ No newline at end of file diff --git a/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ExceptionHandling/AbpExceptionFilter.cs b/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ExceptionHandling/AbpExceptionFilter.cs new file mode 100644 index 0000000000..7a273e097d --- /dev/null +++ b/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ExceptionHandling/AbpExceptionFilter.cs @@ -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 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.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; + } + } +} diff --git a/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ExceptionHandling/DefaultExceptionToErrorInfoConverter.cs b/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ExceptionHandling/DefaultExceptionToErrorInfoConverter.cs new file mode 100644 index 0000000000..6ef1df1060 --- /dev/null +++ b/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ExceptionHandling/DefaultExceptionToErrorInfoConverter.cs @@ -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(); + + 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; + } + } +} \ No newline at end of file diff --git a/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ExceptionHandling/IExceptionToErrorInfoConverter.cs b/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ExceptionHandling/IExceptionToErrorInfoConverter.cs new file mode 100644 index 0000000000..ad5e5fa1aa --- /dev/null +++ b/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ExceptionHandling/IExceptionToErrorInfoConverter.cs @@ -0,0 +1,19 @@ +using System; +using Volo.Abp.Http; + +namespace Volo.Abp.AspNetCore.Mvc.ExceptionHandling +{ + /// + /// This interface can be implemented to convert an object to an object. + /// Implements Chain Of Responsibility pattern. + /// + public interface IExceptionToErrorInfoConverter + { + /// + /// Converter method. + /// + /// The exception + /// Error info or null + RemoteServiceErrorInfo Convert(Exception exception); + } +} \ No newline at end of file diff --git a/src/Volo.Abp.Http/Volo/Abp/Http/RemoteServiceErrorResponse.cs b/src/Volo.Abp.Http/Volo/Abp/Http/RemoteServiceErrorResponse.cs index f2ecd1f591..b3dd56f962 100644 --- a/src/Volo.Abp.Http/Volo/Abp/Http/RemoteServiceErrorResponse.cs +++ b/src/Volo.Abp.Http/Volo/Abp/Http/RemoteServiceErrorResponse.cs @@ -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. /// public bool __abp { get; } = true; + + public RemoteServiceErrorResponse(RemoteServiceErrorInfo error) + { + Error = error; + } } } \ No newline at end of file diff --git a/src/Volo.Abp/Microsoft/Extensions/Logging/LoggerExtensions.cs b/src/Volo.Abp/Microsoft/Extensions/Logging/LoggerExtensions.cs new file mode 100644 index 0000000000..ff6362b3ae --- /dev/null +++ b/src/Volo.Abp/Microsoft/Extensions/Logging/LoggerExtensions.cs @@ -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); + } + } + } +} diff --git a/src/Volo.Abp/Volo/Abp/AbpException.cs b/src/Volo.Abp/Volo/Abp/AbpException.cs index 10418a4088..39792ecf34 100644 --- a/src/Volo.Abp/Volo/Abp/AbpException.cs +++ b/src/Volo.Abp/Volo/Abp/AbpException.cs @@ -1,4 +1,5 @@ using System; +using System.Runtime.Serialization; namespace Volo.Abp { @@ -35,5 +36,14 @@ namespace Volo.Abp { } + + /// + /// Constructor for serializing. + /// + public AbpException(SerializationInfo serializationInfo, StreamingContext context) + : base(serializationInfo, context) + { + + } } } diff --git a/src/Volo.Abp/Volo/Abp/Authorization/AbpAuthorizationException.cs b/src/Volo.Abp/Volo/Abp/Authorization/AbpAuthorizationException.cs new file mode 100644 index 0000000000..ad5b0b65a3 --- /dev/null +++ b/src/Volo.Abp/Volo/Abp/Authorization/AbpAuthorizationException.cs @@ -0,0 +1,58 @@ +using System; +using System.Runtime.Serialization; +using Microsoft.Extensions.Logging; +using Volo.Abp.Logging; + +namespace Volo.Abp.Authorization +{ + /// + /// This exception is thrown on an unauthorized request. + /// + [Serializable] + public class AbpAuthorizationException : AbpException, IHasLogLevel + { + /// + /// Severity of the exception. + /// Default: Warn. + /// + public LogLevel LogLevel { get; set; } + + /// + /// Creates a new object. + /// + public AbpAuthorizationException() + { + LogLevel = LogLevel.Warning; + } + + /// + /// Creates a new object. + /// + public AbpAuthorizationException(SerializationInfo serializationInfo, StreamingContext context) + : base(serializationInfo, context) + { + + } + + /// + /// Creates a new object. + /// + /// Exception message + public AbpAuthorizationException(string message) + : base(message) + { + LogLevel = LogLevel.Warning; + } + + /// + /// Creates a new object. + /// + /// Exception message + /// Inner exception + public AbpAuthorizationException(string message, Exception innerException) + : base(message, innerException) + { + LogLevel = LogLevel.Warning; + } + } +} \ No newline at end of file diff --git a/src/Volo.Abp/Volo/Abp/IHasErrorCode.cs b/src/Volo.Abp/Volo/Abp/IHasErrorCode.cs new file mode 100644 index 0000000000..319bc00adc --- /dev/null +++ b/src/Volo.Abp/Volo/Abp/IHasErrorCode.cs @@ -0,0 +1,7 @@ +namespace Volo.Abp +{ + public interface IHasErrorCode + { + int Code { get; set; } + } +} \ No newline at end of file diff --git a/src/Volo.Abp/Volo/Abp/Logging/IHasLogLevel.cs b/src/Volo.Abp/Volo/Abp/Logging/IHasLogLevel.cs new file mode 100644 index 0000000000..9e4ec390ce --- /dev/null +++ b/src/Volo.Abp/Volo/Abp/Logging/IHasLogLevel.cs @@ -0,0 +1,15 @@ +using Microsoft.Extensions.Logging; + +namespace Volo.Abp.Logging +{ + /// + /// Interface to define a property (see ). + /// + public interface IHasLogLevel + { + /// + /// Log severity. + /// + LogLevel LogLevel { get; set; } + } +} \ No newline at end of file diff --git a/src/Volo.Abp/Volo/Abp/Reflection/ReflectionHelper.cs b/src/Volo.Abp/Volo/Abp/Reflection/ReflectionHelper.cs index c4b3f0e450..1cbfba952d 100644 --- a/src/Volo.Abp/Volo/Abp/Reflection/ReflectionHelper.cs +++ b/src/Volo.Abp/Volo/Abp/Reflection/ReflectionHelper.cs @@ -92,6 +92,22 @@ namespace Volo.Abp.Reflection return defaultValue; } + /// + /// 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. + /// + /// Type of the attribute + /// MemberInfo + /// Default value (null as default) + /// Inherit attribute from base classes + public static TAttribute GetSingleAttributeOfMemberOrDeclaringTypeOrDefault(MemberInfo memberInfo, TAttribute defaultValue = default(TAttribute), bool inherit = true) + where TAttribute : class + { + return memberInfo.GetCustomAttributes(true).OfType().FirstOrDefault() + ?? memberInfo.DeclaringType?.GetTypeInfo().GetCustomAttributes(true).OfType().FirstOrDefault() + ?? defaultValue; + } + /// /// Gets value of a property by it's full path from given object /// diff --git a/src/Volo.Abp/Volo/Abp/Ui/UserFriendlyException.cs b/src/Volo.Abp/Volo/Abp/Ui/UserFriendlyException.cs new file mode 100644 index 0000000000..4bd7bc2d0a --- /dev/null +++ b/src/Volo.Abp/Volo/Abp/Ui/UserFriendlyException.cs @@ -0,0 +1,125 @@ +using System; +using System.Runtime.Serialization; +using Microsoft.Extensions.Logging; +using Volo.Abp.Logging; + +namespace Volo.Abp.Ui +{ + /// + /// This exception type is directly shown to the user. + /// + [Serializable] + public class UserFriendlyException : AbpException, IHasLogLevel, IHasErrorCode + { + /// + /// Additional information about the exception. + /// + public string Details { get; private set; } + + /// + /// An arbitrary error code. + /// + public int Code { get; set; } + + /// + /// Severity of the exception. + /// Default: Warn. + /// + public LogLevel LogLevel { get; set; } + + /// + /// Constructor. + /// + public UserFriendlyException() + { + LogLevel = LogLevel.Warning; + } + + /// + /// Constructor for serializing. + /// + public UserFriendlyException(SerializationInfo serializationInfo, StreamingContext context) + : base(serializationInfo, context) + { + + } + + /// + /// Constructor. + /// + /// Exception message + public UserFriendlyException(string message) + : base(message) + { + LogLevel = LogLevel.Warning; + } + + /// + /// Constructor. + /// + /// Exception message + /// Exception severity + public UserFriendlyException(string message, LogLevel logLevel) + : base(message) + { + LogLevel = logLevel; + } + + /// + /// Constructor. + /// + /// Error code + /// Exception message + public UserFriendlyException(int code, string message) + : this(message) + { + Code = code; + } + + /// + /// Constructor. + /// + /// Exception message + /// Additional information about the exception + public UserFriendlyException(string message, string details) + : this(message) + { + Details = details; + } + + /// + /// Constructor. + /// + /// Error code + /// Exception message + /// Additional information about the exception + public UserFriendlyException(int code, string message, string details) + : this(message, details) + { + Code = code; + } + + /// + /// Constructor. + /// + /// Exception message + /// Inner exception + public UserFriendlyException(string message, Exception innerException) + : base(message, innerException) + { + LogLevel = LogLevel.Warning; + } + + /// + /// Constructor. + /// + /// Exception message + /// Additional information about the exception + /// Inner exception + public UserFriendlyException(string message, string details, Exception innerException) + : this(message, innerException) + { + Details = details; + } + } +} \ No newline at end of file diff --git a/src/Volo.Abp/Volo/Abp/Validation/AbpValidationException.cs b/src/Volo.Abp/Volo/Abp/Validation/AbpValidationException.cs new file mode 100644 index 0000000000..e10a58ee7c --- /dev/null +++ b/src/Volo.Abp/Volo/Abp/Validation/AbpValidationException.cs @@ -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 +{ + /// + /// This exception type is used to throws validation exceptions. + /// + [Serializable] + public class AbpValidationException : AbpException, IHasLogLevel + { + /// + /// Detailed list of validation errors for this exception. + /// + public IList ValidationErrors { get; set; } + + /// + /// Exception severity. + /// Default: Warn. + /// + public LogLevel LogLevel { get; set; } + + /// + /// Constructor. + /// + public AbpValidationException() + { + ValidationErrors = new List(); + LogLevel = LogLevel.Warning; + } + + /// + /// Constructor for serializing. + /// + public AbpValidationException(SerializationInfo serializationInfo, StreamingContext context) + : base(serializationInfo, context) + { + ValidationErrors = new List(); + LogLevel = LogLevel.Warning; + } + + /// + /// Constructor. + /// + /// Exception message + public AbpValidationException(string message) + : base(message) + { + ValidationErrors = new List(); + LogLevel = LogLevel.Warning; + } + + /// + /// Constructor. + /// + /// Exception message + /// Validation errors + public AbpValidationException(string message, IList validationErrors) + : base(message) + { + ValidationErrors = validationErrors; + LogLevel = LogLevel.Warning; + } + + /// + /// Constructor. + /// + /// Exception message + /// Inner exception + public AbpValidationException(string message, Exception innerException) + : base(message, innerException) + { + ValidationErrors = new List(); + LogLevel = LogLevel.Warning; + } + } +} diff --git a/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/App/ExceptionTestController.cs b/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/App/ExceptionTestController.cs new file mode 100644 index 0000000000..dfe2f939d5 --- /dev/null +++ b/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/App/ExceptionTestController.cs @@ -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!"); + } + } +} diff --git a/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ExceptionTestController_Tests.cs b/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ExceptionTestController_Tests.cs new file mode 100644 index 0000000000..2ad1e7b447 --- /dev/null +++ b/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ExceptionTestController_Tests.cs @@ -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("/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( + async () => await GetResponseAsObjectAsync( + "/api/exception-test/UserFriendlyException2" + ) + ); + } + } +}