From e849921f7ec97a3d0642698fa830bcf09bf418e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Tue, 24 Mar 2020 14:11:51 +0300 Subject: [PATCH 1/3] Resolved #3328: Allow to subscribe to exceptions handled by the abp framework --- .../Controllers/ErrorController.cs | 11 ++++- .../ExceptionHandling/AbpExceptionFilter.cs | 18 ++++++-- .../AbpExceptionHandlingMiddleware.cs | 8 ++++ .../BackgroundJobs/BackgroundJobExecuter.cs | 6 +++ .../Abp/BackgroundJobs/RabbitMQ/JobQueue.cs | 6 ++- .../AsyncPeriodicBackgroundWorkerBase.cs | 19 +++++--- .../PeriodicBackgroundWorkerBase.cs | 18 +++++--- .../Volo/Abp/Caching/DistributedCache.cs | 44 ++++++++++++++----- .../ExceptionNotificationContext.cs | 32 ++++++++++++++ .../ExceptionHandling/ExceptionNotifier.cs | 41 +++++++++++++++++ .../ExceptionNotifierExtensions.cs | 27 ++++++++++++ .../ExceptionHandling/IExceptionNotifier.cs | 10 +++++ .../ExceptionHandling/IExceptionSubscriber.cs | 10 +++++ .../Volo/Abp/Ldap/LdapManager.cs | 15 ++++++- .../Abp/RabbitMQ/RabbitMqMessageConsumer.cs | 11 ++++- .../Volo/Abp/Threading/AbpTimer.cs | 20 ++++++--- .../ExceptionTestController_Tests.cs | 23 ++++++++++ 17 files changed, 281 insertions(+), 38 deletions(-) create mode 100644 framework/src/Volo.Abp.Core/Volo/Abp/ExceptionHandling/ExceptionNotificationContext.cs create mode 100644 framework/src/Volo.Abp.Core/Volo/Abp/ExceptionHandling/ExceptionNotifier.cs create mode 100644 framework/src/Volo.Abp.Core/Volo/Abp/ExceptionHandling/ExceptionNotifierExtensions.cs create mode 100644 framework/src/Volo.Abp.Core/Volo/Abp/ExceptionHandling/IExceptionNotifier.cs create mode 100644 framework/src/Volo.Abp.Core/Volo/Abp/ExceptionHandling/IExceptionSubscriber.cs diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Controllers/ErrorController.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Controllers/ErrorController.cs index 1fb4c12cf2..9682ec838e 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Controllers/ErrorController.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Controllers/ErrorController.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Threading.Tasks; using Localization.Resources.AbpUi; using Microsoft.AspNetCore.Diagnostics; using Microsoft.AspNetCore.Mvc; @@ -7,6 +8,7 @@ using Microsoft.Extensions.Localization; using Microsoft.Extensions.Options; using Volo.Abp.AspNetCore.ExceptionHandling; using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Views.Error; +using Volo.Abp.ExceptionHandling; namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Controllers { @@ -16,20 +18,23 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Controllers private readonly IHttpExceptionStatusCodeFinder _statusCodeFinder; private readonly IStringLocalizer _localizer; private readonly AbpErrorPageOptions _abpErrorPageOptions; + private readonly IExceptionNotifier _exceptionNotifier; public ErrorController( IExceptionToErrorInfoConverter exceptionToErrorInfoConverter, IHttpExceptionStatusCodeFinder httpExceptionStatusCodeFinder, IOptions abpErrorPageOptions, - IStringLocalizer localizer) + IStringLocalizer localizer, + IExceptionNotifier exceptionNotifier) { _errorInfoConverter = exceptionToErrorInfoConverter; _statusCodeFinder = httpExceptionStatusCodeFinder; _localizer = localizer; + _exceptionNotifier = exceptionNotifier; _abpErrorPageOptions = abpErrorPageOptions.Value; } - public IActionResult Index(int httpStatusCode) + public async Task Index(int httpStatusCode) { var exHandlerFeature = HttpContext.Features.Get(); @@ -37,6 +42,8 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Controllers ? exHandlerFeature.Error : new Exception(_localizer["UnhandledException"]); + await _exceptionNotifier.NotifyAsync(new ExceptionNotificationContext(exception)); + var errorInfo = _errorInfoConverter.Convert(exception); if (httpStatusCode == 0) diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ExceptionHandling/AbpExceptionFilter.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ExceptionHandling/AbpExceptionFilter.cs index 28c1c18fda..1ce177ba44 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ExceptionHandling/AbpExceptionFilter.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ExceptionHandling/AbpExceptionFilter.cs @@ -1,18 +1,21 @@ using System; +using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Volo.Abp.AspNetCore.ExceptionHandling; using Volo.Abp.DependencyInjection; +using Volo.Abp.ExceptionHandling; using Volo.Abp.Http; using Volo.Abp.Json; namespace Volo.Abp.AspNetCore.Mvc.ExceptionHandling { - public class AbpExceptionFilter : IExceptionFilter, ITransientDependency + public class AbpExceptionFilter : IAsyncExceptionFilter, ITransientDependency { public ILogger Logger { get; set; } @@ -32,14 +35,14 @@ namespace Volo.Abp.AspNetCore.Mvc.ExceptionHandling Logger = NullLogger.Instance; } - public virtual void OnException(ExceptionContext context) + public async Task OnExceptionAsync(ExceptionContext context) { if (!ShouldHandleException(context)) { return; } - HandleAndWrapException(context); + await HandleAndWrapException(context); } protected virtual bool ShouldHandleException(ExceptionContext context) @@ -65,7 +68,7 @@ namespace Volo.Abp.AspNetCore.Mvc.ExceptionHandling return false; } - protected virtual void HandleAndWrapException(ExceptionContext context) + protected virtual async Task HandleAndWrapException(ExceptionContext context) { //TODO: Trigger an AbpExceptionHandled event or something like that. @@ -82,6 +85,13 @@ namespace Volo.Abp.AspNetCore.Mvc.ExceptionHandling Logger.LogWithLevel(logLevel, _jsonSerializer.Serialize(remoteServiceErrorInfo, indented: true)); Logger.LogException(context.Exception, logLevel); + await context.HttpContext + .RequestServices + .GetRequiredService() + .NotifyAsync( + new ExceptionNotificationContext(context.Exception) + ); + context.Exception = null; //Handled! } } diff --git a/framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/ExceptionHandling/AbpExceptionHandlingMiddleware.cs b/framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/ExceptionHandling/AbpExceptionHandlingMiddleware.cs index cdd921dae4..9a7a8f1676 100644 --- a/framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/ExceptionHandling/AbpExceptionHandlingMiddleware.cs +++ b/framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/ExceptionHandling/AbpExceptionHandlingMiddleware.cs @@ -7,6 +7,7 @@ using Microsoft.Net.Http.Headers; using Volo.Abp.AspNetCore.Mvc; using Volo.Abp.AspNetCore.Uow; using Volo.Abp.DependencyInjection; +using Volo.Abp.ExceptionHandling; using Volo.Abp.Http; using Volo.Abp.Json; @@ -73,6 +74,13 @@ namespace Volo.Abp.AspNetCore.ExceptionHandling ) ) ); + + await httpContext + .RequestServices + .GetRequiredService() + .NotifyAsync( + new ExceptionNotificationContext(exception) + ); } private Task ClearCacheHeaders(object state) diff --git a/framework/src/Volo.Abp.BackgroundJobs.Abstractions/Volo/Abp/BackgroundJobs/BackgroundJobExecuter.cs b/framework/src/Volo.Abp.BackgroundJobs.Abstractions/Volo/Abp/BackgroundJobs/BackgroundJobExecuter.cs index d4ce5cd03a..a5027daec6 100644 --- a/framework/src/Volo.Abp.BackgroundJobs.Abstractions/Volo/Abp/BackgroundJobs/BackgroundJobExecuter.cs +++ b/framework/src/Volo.Abp.BackgroundJobs.Abstractions/Volo/Abp/BackgroundJobs/BackgroundJobExecuter.cs @@ -3,7 +3,9 @@ using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using System; using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; using Volo.Abp.DependencyInjection; +using Volo.Abp.ExceptionHandling; namespace Volo.Abp.BackgroundJobs { @@ -51,6 +53,10 @@ namespace Volo.Abp.BackgroundJobs { Logger.LogException(ex); + await context.ServiceProvider + .GetRequiredService() + .NotifyAsync(new ExceptionNotificationContext(ex)); + throw new BackgroundJobExecutionException("A background job execution is failed. See inner exception for details.", ex) { JobType = context.JobType.AssemblyQualifiedName, diff --git a/framework/src/Volo.Abp.BackgroundJobs.RabbitMQ/Volo/Abp/BackgroundJobs/RabbitMQ/JobQueue.cs b/framework/src/Volo.Abp.BackgroundJobs.RabbitMQ/Volo/Abp/BackgroundJobs/RabbitMQ/JobQueue.cs index 4cb4f04a09..879cf4f8ff 100644 --- a/framework/src/Volo.Abp.BackgroundJobs.RabbitMQ/Volo/Abp/BackgroundJobs/RabbitMQ/JobQueue.cs +++ b/framework/src/Volo.Abp.BackgroundJobs.RabbitMQ/Volo/Abp/BackgroundJobs/RabbitMQ/JobQueue.cs @@ -9,6 +9,7 @@ using Microsoft.Extensions.Options; using Nito.AsyncEx; using RabbitMQ.Client; using RabbitMQ.Client.Events; +using Volo.Abp.ExceptionHandling; using Volo.Abp.RabbitMQ; using Volo.Abp.Threading; @@ -31,6 +32,7 @@ namespace Volo.Abp.BackgroundJobs.RabbitMQ protected IRabbitMqSerializer Serializer { get; } protected IBackgroundJobExecuter JobExecuter { get; } protected IServiceScopeFactory ServiceScopeFactory { get; } + protected IExceptionNotifier ExceptionNotifier { get; } protected SemaphoreSlim SyncObj = new SemaphoreSlim(1, 1); protected bool IsDiposed { get; private set; } @@ -41,13 +43,15 @@ namespace Volo.Abp.BackgroundJobs.RabbitMQ IChannelPool channelPool, IRabbitMqSerializer serializer, IBackgroundJobExecuter jobExecuter, - IServiceScopeFactory serviceScopeFactory) + IServiceScopeFactory serviceScopeFactory, + IExceptionNotifier exceptionNotifier) { AbpBackgroundJobOptions = backgroundJobOptions.Value; AbpRabbitMqBackgroundJobOptions = rabbitMqAbpBackgroundJobOptions.Value; Serializer = serializer; JobExecuter = jobExecuter; ServiceScopeFactory = serviceScopeFactory; + ExceptionNotifier = exceptionNotifier; ChannelPool = channelPool; JobConfiguration = AbpBackgroundJobOptions.GetJob(typeof(TArgs)); diff --git a/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/AsyncPeriodicBackgroundWorkerBase.cs b/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/AsyncPeriodicBackgroundWorkerBase.cs index 62da163eb9..cb2f6dbfae 100644 --- a/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/AsyncPeriodicBackgroundWorkerBase.cs +++ b/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/AsyncPeriodicBackgroundWorkerBase.cs @@ -3,6 +3,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Volo.Abp.ExceptionHandling; using Volo.Abp.Threading; namespace Volo.Abp.BackgroundWorkers @@ -35,18 +36,24 @@ namespace Volo.Abp.BackgroundWorkers private void Timer_Elapsed(object sender, System.EventArgs e) { - try + using (var scope = ServiceScopeFactory.CreateScope()) { - using (var scope = ServiceScopeFactory.CreateScope()) + try { AsyncHelper.RunSync( () => DoWorkAsync(new PeriodicBackgroundWorkerContext(scope.ServiceProvider)) ); } - } - catch (Exception ex) - { - Logger.LogException(ex); + catch (Exception ex) + { + AsyncHelper.RunSync( + () => scope.ServiceProvider + .GetRequiredService() + .NotifyAsync(new ExceptionNotificationContext(ex)) + ); + + Logger.LogException(ex); + } } } diff --git a/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/PeriodicBackgroundWorkerBase.cs b/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/PeriodicBackgroundWorkerBase.cs index 091bcc917f..c63adaf798 100644 --- a/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/PeriodicBackgroundWorkerBase.cs +++ b/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/PeriodicBackgroundWorkerBase.cs @@ -3,6 +3,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Volo.Abp.ExceptionHandling; using Volo.Abp.Threading; namespace Volo.Abp.BackgroundWorkers @@ -38,16 +39,21 @@ namespace Volo.Abp.BackgroundWorkers private void Timer_Elapsed(object sender, System.EventArgs e) { - try + using (var scope = ServiceScopeFactory.CreateScope()) { - using (var scope = ServiceScopeFactory.CreateScope()) + try { + DoWork(new PeriodicBackgroundWorkerContext(scope.ServiceProvider)); } - } - catch (Exception ex) - { - Logger.LogException(ex); + catch (Exception ex) + { + scope.ServiceProvider + .GetRequiredService() + .NotifyAsync(new ExceptionNotificationContext(ex)); + + Logger.LogException(ex); + } } } diff --git a/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/DistributedCache.cs b/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/DistributedCache.cs index c23fb05aa7..267ab259f5 100644 --- a/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/DistributedCache.cs +++ b/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/DistributedCache.cs @@ -2,10 +2,13 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using Nito.AsyncEx; +using Volo.Abp.DependencyInjection; +using Volo.Abp.ExceptionHandling; using Volo.Abp.MultiTenancy; using Volo.Abp.Threading; @@ -23,12 +26,14 @@ namespace Volo.Abp.Caching IDistributedCache cache, ICancellationTokenProvider cancellationTokenProvider, IDistributedCacheSerializer serializer, - IDistributedCacheKeyNormalizer keyNormalizer) : base( + IDistributedCacheKeyNormalizer keyNormalizer, + IHybridServiceScopeFactory serviceScopeFactory) : base( distributedCacheOption: distributedCacheOption, cache: cache, cancellationTokenProvider: cancellationTokenProvider, serializer: serializer, - keyNormalizer: keyNormalizer) + keyNormalizer: keyNormalizer, + serviceScopeFactory: serviceScopeFactory) { } @@ -56,6 +61,8 @@ namespace Volo.Abp.Caching protected IDistributedCacheKeyNormalizer KeyNormalizer { get; } + protected IHybridServiceScopeFactory ServiceScopeFactory { get; } + protected SemaphoreSlim SyncSemaphore { get; } protected DistributedCacheEntryOptions DefaultCacheOptions; @@ -67,7 +74,8 @@ namespace Volo.Abp.Caching IDistributedCache cache, ICancellationTokenProvider cancellationTokenProvider, IDistributedCacheSerializer serializer, - IDistributedCacheKeyNormalizer keyNormalizer) + IDistributedCacheKeyNormalizer keyNormalizer, + IHybridServiceScopeFactory serviceScopeFactory) { _distributedCacheOption = distributedCacheOption.Value; Cache = cache; @@ -75,6 +83,7 @@ namespace Volo.Abp.Caching Logger = NullLogger>.Instance; Serializer = serializer; KeyNormalizer = keyNormalizer; + ServiceScopeFactory = serviceScopeFactory; SyncSemaphore = new SemaphoreSlim(1, 1); @@ -139,7 +148,7 @@ namespace Volo.Abp.Caching { if (hideErrors == true) { - Logger.LogException(ex, LogLevel.Warning); + AsyncHelper.RunSync(() => HandleExceptionAsync(ex)); return null; } @@ -181,7 +190,7 @@ namespace Volo.Abp.Caching { if (hideErrors == true) { - Logger.LogException(ex, LogLevel.Warning); + await HandleExceptionAsync(ex); return null; } @@ -298,7 +307,7 @@ namespace Volo.Abp.Caching { if (hideErrors == true) { - Logger.LogException(ex, LogLevel.Warning); + AsyncHelper.RunSync(() => HandleExceptionAsync(ex)); return; } @@ -337,7 +346,7 @@ namespace Volo.Abp.Caching { if (hideErrors == true) { - Logger.LogException(ex, LogLevel.Warning); + await HandleExceptionAsync(ex); return; } @@ -364,7 +373,7 @@ namespace Volo.Abp.Caching { if (hideErrors == true) { - Logger.LogException(ex, LogLevel.Warning); + AsyncHelper.RunSync(() => HandleExceptionAsync(ex)); return; } @@ -393,7 +402,7 @@ namespace Volo.Abp.Caching { if (hideErrors == true) { - Logger.LogException(ex, LogLevel.Warning); + await HandleExceptionAsync(ex); return; } @@ -420,7 +429,8 @@ namespace Volo.Abp.Caching { if (hideErrors == true) { - Logger.LogException(ex, LogLevel.Warning); + AsyncHelper.RunSync(() => HandleExceptionAsync(ex)); + return; } throw; @@ -449,12 +459,24 @@ namespace Volo.Abp.Caching { if (hideErrors == true) { - Logger.LogException(ex, LogLevel.Warning); + await HandleExceptionAsync(ex); return; } throw; } } + + protected virtual async Task HandleExceptionAsync(Exception ex) + { + Logger.LogException(ex, LogLevel.Warning); + + using (var scope = ServiceScopeFactory.CreateScope()) + { + await scope.ServiceProvider + .GetRequiredService() + .NotifyAsync(new ExceptionNotificationContext(ex, LogLevel.Warning)); + } + } } } \ No newline at end of file diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/ExceptionHandling/ExceptionNotificationContext.cs b/framework/src/Volo.Abp.Core/Volo/Abp/ExceptionHandling/ExceptionNotificationContext.cs new file mode 100644 index 0000000000..6f7fbda0af --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/ExceptionHandling/ExceptionNotificationContext.cs @@ -0,0 +1,32 @@ +using System; +using JetBrains.Annotations; +using Microsoft.Extensions.Logging; + +namespace Volo.Abp.ExceptionHandling +{ + public class ExceptionNotificationContext + { + /// + /// The exception object. + /// + [NotNull] + public Exception Exception { get; } + + public LogLevel LogLevel { get; } + + /// + /// True, if it is handled. + /// + public bool Handled { get; } + + public ExceptionNotificationContext( + [NotNull] Exception exception, + LogLevel? logLevel = null, + bool handled = true) + { + Exception = Check.NotNull(exception, nameof(exception)); + LogLevel = logLevel ?? exception.GetLogLevel(); + Handled = handled; + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/ExceptionHandling/ExceptionNotifier.cs b/framework/src/Volo.Abp.Core/Volo/Abp/ExceptionHandling/ExceptionNotifier.cs new file mode 100644 index 0000000000..b9076c471b --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/ExceptionHandling/ExceptionNotifier.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.ExceptionHandling +{ + public class ExceptionNotifier : IExceptionNotifier, ITransientDependency + { + public ILogger Logger { get; set; } + + protected IEnumerable ExceptionSubscribers { get; } + + public ExceptionNotifier(IEnumerable exceptionSubscribers) + { + ExceptionSubscribers = exceptionSubscribers; + Logger = NullLogger.Instance; + } + + public virtual async Task NotifyAsync([NotNull] ExceptionNotificationContext context) + { + Check.NotNull(context, nameof(context)); + + foreach (var exceptionSubscriber in ExceptionSubscribers) + { + try + { + await exceptionSubscriber.HandleAsync(context); + } + catch (Exception e) + { + Logger.LogWarning($"Exception subscriber of type {exceptionSubscriber.GetType().AssemblyQualifiedName} has thrown an exception!"); + Logger.LogException(e, LogLevel.Warning); + } + } + } + } +} diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/ExceptionHandling/ExceptionNotifierExtensions.cs b/framework/src/Volo.Abp.Core/Volo/Abp/ExceptionHandling/ExceptionNotifierExtensions.cs new file mode 100644 index 0000000000..493c0cf1f3 --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/ExceptionHandling/ExceptionNotifierExtensions.cs @@ -0,0 +1,27 @@ +using System; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Microsoft.Extensions.Logging; + +namespace Volo.Abp.ExceptionHandling +{ + public static class ExceptionNotifierExtensions + { + public static Task NotifyAsync( + [NotNull] this IExceptionNotifier exceptionNotifier, + [NotNull] Exception exception, + LogLevel? logLevel = null, + bool handled = true) + { + Check.NotNull(exceptionNotifier, nameof(exceptionNotifier)); + + return exceptionNotifier.NotifyAsync( + new ExceptionNotificationContext( + exception, + logLevel, + handled + ) + ); + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/ExceptionHandling/IExceptionNotifier.cs b/framework/src/Volo.Abp.Core/Volo/Abp/ExceptionHandling/IExceptionNotifier.cs new file mode 100644 index 0000000000..5646c28e5e --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/ExceptionHandling/IExceptionNotifier.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; +using JetBrains.Annotations; + +namespace Volo.Abp.ExceptionHandling +{ + public interface IExceptionNotifier + { + Task NotifyAsync([NotNull] ExceptionNotificationContext context); + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/ExceptionHandling/IExceptionSubscriber.cs b/framework/src/Volo.Abp.Core/Volo/Abp/ExceptionHandling/IExceptionSubscriber.cs new file mode 100644 index 0000000000..dc7f336970 --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/ExceptionHandling/IExceptionSubscriber.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; +using JetBrains.Annotations; + +namespace Volo.Abp.ExceptionHandling +{ + public interface IExceptionSubscriber + { + Task HandleAsync([NotNull] ExceptionNotificationContext context); + } +} diff --git a/framework/src/Volo.Abp.Ldap/Volo/Abp/Ldap/LdapManager.cs b/framework/src/Volo.Abp.Ldap/Volo/Abp/Ldap/LdapManager.cs index d54e7d82d0..9e0b8f04a7 100644 --- a/framework/src/Volo.Abp.Ldap/Volo/Abp/Ldap/LdapManager.cs +++ b/framework/src/Volo.Abp.Ldap/Volo/Abp/Ldap/LdapManager.cs @@ -3,7 +3,9 @@ using Microsoft.Extensions.Options; using Novell.Directory.Ldap; using System.Collections.Generic; using System.Text; +using Microsoft.Extensions.DependencyInjection; using Volo.Abp.DependencyInjection; +using Volo.Abp.ExceptionHandling; using Volo.Abp.Ldap.Exceptions; using Volo.Abp.Ldap.Modeling; @@ -13,6 +15,7 @@ namespace Volo.Abp.Ldap { private readonly string _searchBase; private readonly AbpLdapOptions _ldapOptions; + private readonly IHybridServiceScopeFactory _hybridServiceScopeFactory; private readonly string[] _attributes = { @@ -21,8 +24,9 @@ namespace Volo.Abp.Ldap "sAMAccountName", "userPrincipalName", "telephoneNumber", "mail" }; - public LdapManager(IOptions ldapSettingsOptions) + public LdapManager(IOptions ldapSettingsOptions, IHybridServiceScopeFactory hybridServiceScopeFactory) { + _hybridServiceScopeFactory = hybridServiceScopeFactory; _ldapOptions = ldapSettingsOptions.Value; _searchBase = _ldapOptions.SearchBase; } @@ -231,8 +235,15 @@ namespace Volo.Abp.Ldap return true; } } - catch (Exception ) + catch (Exception ex) { + using (var scope = _hybridServiceScopeFactory.CreateScope()) + { + scope.ServiceProvider + .GetRequiredService() + .NotifyAsync(ex); + } + return false; } } diff --git a/framework/src/Volo.Abp.RabbitMQ/Volo/Abp/RabbitMQ/RabbitMqMessageConsumer.cs b/framework/src/Volo.Abp.RabbitMQ/Volo/Abp/RabbitMQ/RabbitMqMessageConsumer.cs index 1214a82a99..135156f7a8 100644 --- a/framework/src/Volo.Abp.RabbitMQ/Volo/Abp/RabbitMQ/RabbitMqMessageConsumer.cs +++ b/framework/src/Volo.Abp.RabbitMQ/Volo/Abp/RabbitMQ/RabbitMqMessageConsumer.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Concurrent; using System.Threading.Tasks; using Volo.Abp.DependencyInjection; +using Volo.Abp.ExceptionHandling; using Volo.Abp.Threading; namespace Volo.Abp.RabbitMQ @@ -17,6 +18,8 @@ namespace Volo.Abp.RabbitMQ protected IConnectionPool ConnectionPool { get; } + protected IExceptionNotifier ExceptionNotifier { get; } + protected AbpTimer Timer { get; } protected ExchangeDeclareConfiguration Exchange { get; private set; } @@ -35,10 +38,12 @@ namespace Volo.Abp.RabbitMQ public RabbitMqMessageConsumer( IConnectionPool connectionPool, - AbpTimer timer) + AbpTimer timer, + IExceptionNotifier exceptionNotifier) { ConnectionPool = connectionPool; Timer = timer; + ExceptionNotifier = exceptionNotifier; Logger = NullLogger.Instance; QueueBindCommands = new ConcurrentQueue(); @@ -114,6 +119,7 @@ namespace Volo.Abp.RabbitMQ catch (Exception ex) { Logger.LogException(ex, LogLevel.Warning); + AsyncHelper.RunSync(() => ExceptionNotifier.NotifyAsync(ex, logLevel: LogLevel.Warning)); } } @@ -180,6 +186,7 @@ namespace Volo.Abp.RabbitMQ catch (Exception ex) { Logger.LogException(ex, LogLevel.Warning); + AsyncHelper.RunSync(() => ExceptionNotifier.NotifyAsync(ex, logLevel: LogLevel.Warning)); } } @@ -197,6 +204,7 @@ namespace Volo.Abp.RabbitMQ catch (Exception ex) { Logger.LogException(ex); + await ExceptionNotifier.NotifyAsync(ex); } } @@ -214,6 +222,7 @@ namespace Volo.Abp.RabbitMQ catch (Exception ex) { Logger.LogException(ex, LogLevel.Warning); + AsyncHelper.RunSync(() => ExceptionNotifier.NotifyAsync(ex, logLevel: LogLevel.Warning)); } } diff --git a/framework/src/Volo.Abp.Threading/Volo/Abp/Threading/AbpTimer.cs b/framework/src/Volo.Abp.Threading/Volo/Abp/Threading/AbpTimer.cs index cae8ac984f..a68d15c7a2 100644 --- a/framework/src/Volo.Abp.Threading/Volo/Abp/Threading/AbpTimer.cs +++ b/framework/src/Volo.Abp.Threading/Volo/Abp/Threading/AbpTimer.cs @@ -3,11 +3,12 @@ using System.Threading; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Volo.Abp.DependencyInjection; +using Volo.Abp.ExceptionHandling; namespace Volo.Abp.Threading { /// - /// A roboust timer implementation that ensures no overlapping occurs. It waits exactly specified between ticks. + /// A robust timer implementation that ensures no overlapping occurs. It waits exactly specified between ticks. /// public class AbpTimer : ITransientDependency { @@ -29,15 +30,23 @@ namespace Volo.Abp.Threading public ILogger Logger { get; set; } + protected IExceptionNotifier ExceptionNotifier { get; } + private readonly Timer _taskTimer; private volatile bool _performingTasks; private volatile bool _isRunning; - public AbpTimer() + public AbpTimer(IExceptionNotifier exceptionNotifier) { + ExceptionNotifier = exceptionNotifier; Logger = NullLogger.Instance; - _taskTimer = new Timer(TimerCallBack, null, Timeout.Infinite, Timeout.Infinite); + _taskTimer = new Timer( + TimerCallBack, + null, + Timeout.Infinite, + Timeout.Infinite + ); } public void Start(CancellationToken cancellationToken = default) @@ -89,9 +98,10 @@ namespace Volo.Abp.Threading { Elapsed.InvokeSafely(this, new EventArgs()); } - catch + catch(Exception ex) { - + Logger.LogException(ex); + AsyncHelper.RunSync(() => ExceptionNotifier.NotifyAsync(ex)); } finally { diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ExceptionHandling/ExceptionTestController_Tests.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ExceptionHandling/ExceptionTestController_Tests.cs index 5243307116..0de46fbe55 100644 --- a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ExceptionHandling/ExceptionTestController_Tests.cs +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ExceptionHandling/ExceptionTestController_Tests.cs @@ -1,6 +1,10 @@ using System.Net; using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using NSubstitute; using Shouldly; +using Volo.Abp.ExceptionHandling; using Volo.Abp.Http; using Xunit; @@ -8,12 +12,27 @@ namespace Volo.Abp.AspNetCore.Mvc.ExceptionHandling { public class ExceptionTestController_Tests : AspNetCoreMvcTestBase { + private IExceptionSubscriber _fakeExceptionSubscriber; + + protected override void ConfigureServices(HostBuilderContext context, IServiceCollection services) + { + base.ConfigureServices(context, services); + + _fakeExceptionSubscriber = Substitute.For(); + + services.AddSingleton(_fakeExceptionSubscriber); + } + [Fact] public async Task Should_Return_RemoteServiceErrorResponse_For_UserFriendlyException_For_Void_Return_Value() { var result = await GetResponseAsObjectAsync("/api/exception-test/UserFriendlyException1", HttpStatusCode.Forbidden); result.Error.ShouldNotBeNull(); result.Error.Message.ShouldBe("This is a sample exception!"); + + _fakeExceptionSubscriber + .Received() + .HandleAsync(Arg.Any()); } [Fact] @@ -24,6 +43,10 @@ namespace Volo.Abp.AspNetCore.Mvc.ExceptionHandling "/api/exception-test/UserFriendlyException2" ) ); + + _fakeExceptionSubscriber + .DidNotReceive() + .HandleAsync(Arg.Any()); } } } From 91226ece1b9203529d509149eecdb26956b8b752 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Tue, 24 Mar 2020 14:36:30 +0300 Subject: [PATCH 2/3] Created ExceptionSubscriber. --- .../Abp/DependencyInjection/ExposedServiceExplorer.cs | 4 ++-- .../Volo/Abp/ExceptionHandling/ExceptionSubscriber.cs | 11 +++++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) create mode 100644 framework/src/Volo.Abp.Core/Volo/Abp/ExceptionHandling/ExceptionSubscriber.cs diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/DependencyInjection/ExposedServiceExplorer.cs b/framework/src/Volo.Abp.Core/Volo/Abp/DependencyInjection/ExposedServiceExplorer.cs index cb0d727795..cf4befae5b 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/DependencyInjection/ExposedServiceExplorer.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/DependencyInjection/ExposedServiceExplorer.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Reflection; namespace Volo.Abp.DependencyInjection { @@ -17,10 +16,11 @@ namespace Volo.Abp.DependencyInjection public static List GetExposedServices(Type type) { return type - .GetCustomAttributes() + .GetCustomAttributes(true) .OfType() .DefaultIfEmpty(DefaultExposeServicesAttribute) .SelectMany(p => p.GetExposedServiceTypes(type)) + .Distinct() .ToList(); } } diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/ExceptionHandling/ExceptionSubscriber.cs b/framework/src/Volo.Abp.Core/Volo/Abp/ExceptionHandling/ExceptionSubscriber.cs new file mode 100644 index 0000000000..5f2971c8c4 --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/ExceptionHandling/ExceptionSubscriber.cs @@ -0,0 +1,11 @@ +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.ExceptionHandling +{ + [ExposeServices(typeof(IExceptionSubscriber))] + public abstract class ExceptionSubscriber : IExceptionSubscriber, ITransientDependency + { + public abstract Task HandleAsync(ExceptionNotificationContext context); + } +} \ No newline at end of file From 4511062b30704376026b6dfb3d360b447e4a7f23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Tue, 24 Mar 2020 14:36:58 +0300 Subject: [PATCH 3/3] #3328 Documented "Subscribing to the Exceptions" --- docs/en/Exception-Handling.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/docs/en/Exception-Handling.md b/docs/en/Exception-Handling.md index d31a2703aa..6dc4ccbd9f 100644 --- a/docs/en/Exception-Handling.md +++ b/docs/en/Exception-Handling.md @@ -291,6 +291,26 @@ services.Configure(options => }); ```` +## Subscribing to the Exceptions + +It is possible to be informed when the ABP Framework **handles an exception**. It automatically **logs** all the exceptions to the standard [logger](Logging.md), but you may want to do more. + +In this case, create a class derived from the `ExceptionSubscriber` class in your application: + +````csharp +public class MyExceptionSubscriber : ExceptionSubscriber +{ + public override async Task HandleAsync(ExceptionNotificationContext context) + { + //TODO... + } +} +```` + +The `context` object contains necessary information about the exception occurred. + +> You can have multiple subscribers, each gets a copy of the exception. Exceptions thrown by your subscriber is ignored (but still logged). + ## Built-In Exceptions Some exception types are automatically thrown by the framework: