From 4682aeaa59859bb3527f68a46e7e38cd441707c8 Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Wed, 25 Mar 2026 08:42:13 -0500 Subject: [PATCH 1/6] refactor(platforms): modernize aspnetcore tfms and deps chore: modernize platform integrations and test stack chore: audit sample deps and dogfood apps --- build/common.props | 8 +- .../Exceptionless.SampleAspNetCore.csproj | 4 +- ...tionless.SampleBlazorWebAssemblyApp.csproj | 6 +- .../Exceptionless.SampleConsole.csproj | 8 +- .../Exceptionless.SampleHosting.csproj | 4 +- .../Exceptionless.SampleHosting/Program.cs | 6 +- .../Exceptionless.SampleLambda.csproj | 8 +- ...xceptionless.SampleLambdaAspNetCore.csproj | 8 +- .../Exceptionless.SampleMvc.csproj | 18 +-- .../Exceptionless.SampleMvc/packages.config | 22 ++-- .../Exceptionless.SampleWebApi.csproj | 10 +- .../Exceptionless.SampleWindows.csproj | 6 +- .../Exceptionless.SampleWpf.csproj | 6 +- src/Exceptionless/Exceptionless.csproj | 8 +- .../Exceptionless.AspNetCore.csproj | 16 +-- .../ExceptionlessDiagnosticListener.cs | 69 ++++++----- .../ExceptionlessExceptionHandler.cs | 29 +++++ .../ExceptionlessExtensions.cs | 15 ++- .../ExceptionlessMiddleware.cs | 4 +- .../RequestInfoCollector.cs | 6 +- .../Exceptionless.Extensions.Hosting.csproj | 14 ++- .../ExceptionlessExtensions.cs | 45 ++++++- .../ExceptionlessLifetimeService.cs | 33 +++++- .../Exceptionless.Extensions.Logging.csproj | 14 ++- .../Exceptionless.Log4net.csproj | 4 +- .../DataDictionaryFormatter.cs | 6 +- .../Exceptionless.MessagePack.csproj | 2 +- .../PersistedDictionaryFormatter.cs | 2 + .../Exceptionless.NLog.csproj | 4 +- .../Exceptionless.NLog/ExceptionlessField.cs | 4 +- .../Exceptionless.NLog/ExceptionlessTarget.cs | 11 ++ .../Exceptionless.Web/RequestInfoCollector.cs | 8 +- .../RequestInfoCollector.cs | 4 +- .../Exceptionless.MessagePack.Tests.csproj | 22 ++-- .../Exceptionless.TestHarness.csproj | 17 ++- .../Configuration/ConfigurationTests.cs | 1 - test/Exceptionless.Tests/EventBuilderTests.cs | 3 +- .../Exceptionless.Tests.csproj | 24 ++-- .../ExceptionlessClientTests.cs | 1 - .../AspNetCoreExceptionCaptureTests.cs | 112 ++++++++++++++++++ .../Platforms/AspNetCoreRequestInfoTests.cs | 2 + .../Platforms/HostingExtensionsTests.cs | 35 ++++++ ...05_HandleAggregateExceptionsPluginTests.cs | 3 +- .../Plugins/010_EventExclusionPluginTests.cs | 3 +- .../015_ConfigurationDefaultsPluginTests.cs | 3 +- .../016_SetEnvironmentUserPluginTests.cs | 3 +- .../Plugins/020_ErrorPluginTests.cs | 3 +- .../Plugins/040_ReferenceIdPluginTests.cs | 3 +- .../Plugins/050_EnvironmentInfoPluginTests.cs | 3 +- .../Plugins/110_IgnoreUserAgentPluginTests.cs | 3 +- ...900_CancelSessionsWithNoUserPluginTests.cs | 3 +- .../910_DuplicateCheckerPluginTests.cs | 3 +- .../Plugins/PluginTestBase.cs | 4 +- .../Plugins/PluginTests.cs | 3 +- .../Serializer/JsonSerializerTests.cs | 1 - .../Storage/FileStorageTestsBase.cs | 1 - .../Storage/FolderFileStorageTests.cs | 1 - .../Storage/InMemoryFileStorageTests.cs | 1 - .../IsolatedStorageFileStorageTests.cs | 4 +- .../Utility/TestOutputWriter.cs | 4 +- 60 files changed, 478 insertions(+), 200 deletions(-) create mode 100644 src/Platforms/Exceptionless.AspNetCore/ExceptionlessExceptionHandler.cs create mode 100644 test/Exceptionless.Tests/Platforms/AspNetCoreExceptionCaptureTests.cs create mode 100644 test/Exceptionless.Tests/Platforms/HostingExtensionsTests.cs diff --git a/build/common.props b/build/common.props index 76e6fddb..eea9d0dd 100644 --- a/build/common.props +++ b/build/common.props @@ -17,7 +17,7 @@ $(SolutionDir)artifacts exceptionless-icon.png Exceptionless;Error;Error-Handling;Error-Handler;Error-Reporting;Error-Management;Error-Monitoring;Handling;Management;Monitoring;Report;Reporting;Crash-Reporting;Exception;Exception-Handling;Exception-Handler;Exception-Reporting;Exceptions;Log;Logs;Logging;Unhandled;Unhandled-Exceptions;Feature;Configuration;Debug;FeatureToggle;Metrics;ELMAH - APACHE-2.0 + Apache-2.0 $(PackageProjectUrl) true true @@ -41,9 +41,9 @@ - - - + + + diff --git a/samples/Exceptionless.SampleAspNetCore/Exceptionless.SampleAspNetCore.csproj b/samples/Exceptionless.SampleAspNetCore/Exceptionless.SampleAspNetCore.csproj index 89d90666..3b47adbc 100644 --- a/samples/Exceptionless.SampleAspNetCore/Exceptionless.SampleAspNetCore.csproj +++ b/samples/Exceptionless.SampleAspNetCore/Exceptionless.SampleAspNetCore.csproj @@ -1,10 +1,10 @@  - net8.0 + net10.0 - \ No newline at end of file + diff --git a/samples/Exceptionless.SampleBlazorWebAssemblyApp/Exceptionless.SampleBlazorWebAssemblyApp.csproj b/samples/Exceptionless.SampleBlazorWebAssemblyApp/Exceptionless.SampleBlazorWebAssemblyApp.csproj index 13a6ec13..b675cb51 100644 --- a/samples/Exceptionless.SampleBlazorWebAssemblyApp/Exceptionless.SampleBlazorWebAssemblyApp.csproj +++ b/samples/Exceptionless.SampleBlazorWebAssemblyApp/Exceptionless.SampleBlazorWebAssemblyApp.csproj @@ -1,14 +1,14 @@  - net8.0 + net10.0 enable enable - - + + diff --git a/samples/Exceptionless.SampleConsole/Exceptionless.SampleConsole.csproj b/samples/Exceptionless.SampleConsole/Exceptionless.SampleConsole.csproj index 579f3535..5b7c7b38 100644 --- a/samples/Exceptionless.SampleConsole/Exceptionless.SampleConsole.csproj +++ b/samples/Exceptionless.SampleConsole/Exceptionless.SampleConsole.csproj @@ -1,7 +1,7 @@ - net8.0 + net10.0 Exceptionless.SampleConsole Exe $(DefineConstants);NETSTANDARD;NETSTANDARD2_0 @@ -20,11 +20,11 @@ - - + + - \ No newline at end of file + diff --git a/samples/Exceptionless.SampleHosting/Exceptionless.SampleHosting.csproj b/samples/Exceptionless.SampleHosting/Exceptionless.SampleHosting.csproj index a0088dca..92f35cf6 100644 --- a/samples/Exceptionless.SampleHosting/Exceptionless.SampleHosting.csproj +++ b/samples/Exceptionless.SampleHosting/Exceptionless.SampleHosting.csproj @@ -1,10 +1,10 @@  - net8.0 + net10.0 - \ No newline at end of file + diff --git a/samples/Exceptionless.SampleHosting/Program.cs b/samples/Exceptionless.SampleHosting/Program.cs index 157eaca2..137aa66e 100644 --- a/samples/Exceptionless.SampleHosting/Program.cs +++ b/samples/Exceptionless.SampleHosting/Program.cs @@ -18,7 +18,7 @@ public static IHostBuilder CreateHostBuilder(string[] args) => // Log levels can be controlled remotely per log source from the Exceptionless app in near real-time. builder.AddExceptionless(); }) - .UseExceptionless() // listens for host shutdown and + .UseExceptionless() // initializes the client and flushes the queue during host shutdown .ConfigureServices(services => { // Reads settings from IConfiguration then adds additional configuration from this lambda. // This also configures ExceptionlessClient.Default @@ -64,11 +64,11 @@ public static IHostBuilder CreateHostBuilder(string[] args) => handledException.ToExceptionless().Submit(); } - // Unhandled exceptions will get reported since called UseExceptionless in the Startup.cs which registers a listener for unhandled exceptions. + // Unhandled exceptions will get reported because host-level Exceptionless integration is enabled in Program.cs. throw new Exception($"Unhandled Exception: {Guid.NewGuid()}"); }); }); }); }); } -} \ No newline at end of file +} diff --git a/samples/Exceptionless.SampleLambda/Exceptionless.SampleLambda.csproj b/samples/Exceptionless.SampleLambda/Exceptionless.SampleLambda.csproj index 74381c7a..d4c37ad0 100644 --- a/samples/Exceptionless.SampleLambda/Exceptionless.SampleLambda.csproj +++ b/samples/Exceptionless.SampleLambda/Exceptionless.SampleLambda.csproj @@ -1,6 +1,6 @@  - net8.0 + net10.0 true Lambda @@ -8,11 +8,11 @@ true - - + + - \ No newline at end of file + diff --git a/samples/Exceptionless.SampleLambdaAspNetCore/Exceptionless.SampleLambdaAspNetCore.csproj b/samples/Exceptionless.SampleLambdaAspNetCore/Exceptionless.SampleLambdaAspNetCore.csproj index eb67a1e9..152843da 100644 --- a/samples/Exceptionless.SampleLambdaAspNetCore/Exceptionless.SampleLambdaAspNetCore.csproj +++ b/samples/Exceptionless.SampleLambdaAspNetCore/Exceptionless.SampleLambdaAspNetCore.csproj @@ -1,15 +1,15 @@ - net8.0 + net10.0 true Lambda - - + + - \ No newline at end of file + diff --git a/samples/Exceptionless.SampleMvc/Exceptionless.SampleMvc.csproj b/samples/Exceptionless.SampleMvc/Exceptionless.SampleMvc.csproj index 77d8f63f..a9b6446d 100644 --- a/samples/Exceptionless.SampleMvc/Exceptionless.SampleMvc.csproj +++ b/samples/Exceptionless.SampleMvc/Exceptionless.SampleMvc.csproj @@ -49,35 +49,35 @@ - ..\..\packages\Microsoft.Web.Infrastructure.2.0.0\lib\net40\Microsoft.Web.Infrastructure.dll + ..\..\packages\Microsoft.Web.Infrastructure.2.0.1\lib\net40\Microsoft.Web.Infrastructure.dll - ..\..\packages\Newtonsoft.Json.13.0.3\lib\net45\Newtonsoft.Json.dll + ..\..\packages\Newtonsoft.Json.13.0.4\lib\net45\Newtonsoft.Json.dll - ..\..\packages\Newtonsoft.Json.Bson.1.0.2\lib\net45\Newtonsoft.Json.Bson.dll + ..\..\packages\Newtonsoft.Json.Bson.1.0.3\lib\net45\Newtonsoft.Json.Bson.dll - ..\..\packages\System.Buffers.4.5.1\lib\net461\System.Buffers.dll + ..\..\packages\System.Buffers.4.6.1\lib\net462\System.Buffers.dll - ..\..\packages\System.Memory.4.5.5\lib\net461\System.Memory.dll + ..\..\packages\System.Memory.4.6.3\lib\net462\System.Memory.dll ..\..\packages\Microsoft.AspNet.WebApi.Client.6.0.0\lib\net45\System.Net.Http.Formatting.dll - ..\..\packages\System.Numerics.Vectors.4.5.0\lib\net46\System.Numerics.Vectors.dll + ..\..\packages\System.Numerics.Vectors.4.6.1\lib\net462\System.Numerics.Vectors.dll - ..\..\packages\System.Runtime.CompilerServices.Unsafe.6.0.0\lib\net461\System.Runtime.CompilerServices.Unsafe.dll + ..\..\packages\System.Runtime.CompilerServices.Unsafe.6.1.2\lib\net462\System.Runtime.CompilerServices.Unsafe.dll - ..\..\packages\System.Threading.Tasks.Extensions.4.5.4\lib\net461\System.Threading.Tasks.Extensions.dll + ..\..\packages\System.Threading.Tasks.Extensions.4.6.3\lib\net462\System.Threading.Tasks.Extensions.dll @@ -284,4 +284,4 @@ --> - \ No newline at end of file + diff --git a/samples/Exceptionless.SampleMvc/packages.config b/samples/Exceptionless.SampleMvc/packages.config index 50c1e96d..70a3f3fb 100644 --- a/samples/Exceptionless.SampleMvc/packages.config +++ b/samples/Exceptionless.SampleMvc/packages.config @@ -1,9 +1,9 @@  - + - + @@ -15,14 +15,14 @@ - + - - - - - - - + + + + + + + - \ No newline at end of file + diff --git a/samples/Exceptionless.SampleWebApi/Exceptionless.SampleWebApi.csproj b/samples/Exceptionless.SampleWebApi/Exceptionless.SampleWebApi.csproj index b8120945..57aa935b 100644 --- a/samples/Exceptionless.SampleWebApi/Exceptionless.SampleWebApi.csproj +++ b/samples/Exceptionless.SampleWebApi/Exceptionless.SampleWebApi.csproj @@ -24,11 +24,11 @@ - - - - - + + + + + diff --git a/samples/Exceptionless.SampleWindows/Exceptionless.SampleWindows.csproj b/samples/Exceptionless.SampleWindows/Exceptionless.SampleWindows.csproj index 309f1e57..f08db0b7 100644 --- a/samples/Exceptionless.SampleWindows/Exceptionless.SampleWindows.csproj +++ b/samples/Exceptionless.SampleWindows/Exceptionless.SampleWindows.csproj @@ -1,6 +1,6 @@  -net8.0-windows;net462 +net10.0-windows;net462 @@ -10,7 +10,7 @@ true - + $(DefineConstants);NETSTANDARD;NETSTANDARD2_0 @@ -26,4 +26,4 @@ - \ No newline at end of file + diff --git a/samples/Exceptionless.SampleWpf/Exceptionless.SampleWpf.csproj b/samples/Exceptionless.SampleWpf/Exceptionless.SampleWpf.csproj index b31bac97..745b6475 100644 --- a/samples/Exceptionless.SampleWpf/Exceptionless.SampleWpf.csproj +++ b/samples/Exceptionless.SampleWpf/Exceptionless.SampleWpf.csproj @@ -1,7 +1,7 @@  - net8.0-windows;net462 + net10.0-windows;net462 @@ -10,7 +10,7 @@ Exceptionless - + $(DefineConstants);NETSTANDARD;NETSTANDARD2_0 @@ -22,4 +22,4 @@ - \ No newline at end of file + diff --git a/src/Exceptionless/Exceptionless.csproj b/src/Exceptionless/Exceptionless.csproj index 6e6ffa24..ec945334 100644 --- a/src/Exceptionless/Exceptionless.csproj +++ b/src/Exceptionless/Exceptionless.csproj @@ -34,13 +34,13 @@ - + - - + + @@ -60,4 +60,4 @@ - \ No newline at end of file + diff --git a/src/Platforms/Exceptionless.AspNetCore/Exceptionless.AspNetCore.csproj b/src/Platforms/Exceptionless.AspNetCore/Exceptionless.AspNetCore.csproj index 7d9fe1bd..2e1510a0 100644 --- a/src/Platforms/Exceptionless.AspNetCore/Exceptionless.AspNetCore.csproj +++ b/src/Platforms/Exceptionless.AspNetCore/Exceptionless.AspNetCore.csproj @@ -2,7 +2,7 @@ - netstandard2.0 + net8.0;net9.0;net10.0 @@ -22,15 +22,7 @@ - - - - - - + + - - - $(DefineConstants);NETSTANDARD2_0 - - \ No newline at end of file + diff --git a/src/Platforms/Exceptionless.AspNetCore/ExceptionlessDiagnosticListener.cs b/src/Platforms/Exceptionless.AspNetCore/ExceptionlessDiagnosticListener.cs index 5108b513..a0e5026e 100644 --- a/src/Platforms/Exceptionless.AspNetCore/ExceptionlessDiagnosticListener.cs +++ b/src/Platforms/Exceptionless.AspNetCore/ExceptionlessDiagnosticListener.cs @@ -1,49 +1,62 @@ -using System; +using System; +using System.Collections.Generic; +using System.Reflection; using Exceptionless.Plugins; using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DiagnosticAdapter; namespace Exceptionless.AspNetCore { - public sealed class ExceptionlessDiagnosticListener { + public sealed class ExceptionlessDiagnosticListener : IObserver> { + private const string HandledExceptionEvent = "Microsoft.AspNetCore.Diagnostics.HandledException"; + private const string DiagnosticsUnhandledExceptionEvent = "Microsoft.AspNetCore.Diagnostics.UnhandledException"; + private const string HostingUnhandledExceptionEvent = "Microsoft.AspNetCore.Hosting.UnhandledException"; + private const string MiddlewareExceptionEvent = "Microsoft.AspNetCore.MiddlewareAnalysis.MiddlewareException"; private readonly ExceptionlessClient _client; public ExceptionlessDiagnosticListener(ExceptionlessClient client) { _client = client; } - [DiagnosticName("Microsoft.AspNetCore.Diagnostics.HandledException")] - public void OnDiagnosticHandledException(HttpContext httpContext, Exception exception) { - var contextData = new ContextData(); - contextData.SetSubmissionMethod("Microsoft.AspNetCore.Diagnostics.HandledException"); - - exception.ToExceptionless(contextData, _client).SetHttpContext(httpContext).Submit(); + public void OnCompleted() { } + + public void OnError(Exception error) { } + + public void OnNext(KeyValuePair diagnosticEvent) { + switch (diagnosticEvent.Key) { + case HandledExceptionEvent: + SubmitException(diagnosticEvent.Value, diagnosticEvent.Key, false); + break; + case DiagnosticsUnhandledExceptionEvent: + case HostingUnhandledExceptionEvent: + SubmitException(diagnosticEvent.Value, diagnosticEvent.Key, true); + break; + case MiddlewareExceptionEvent: + string middlewareName = GetPropertyValue(diagnosticEvent.Value, "name") as string; + SubmitException(diagnosticEvent.Value, middlewareName ?? diagnosticEvent.Key, true); + break; + } } - [DiagnosticName("Microsoft.AspNetCore.Diagnostics.UnhandledException")] - public void OnDiagnosticUnhandledException(HttpContext httpContext, Exception exception) { - var contextData = new ContextData(); - contextData.MarkAsUnhandledError(); - contextData.SetSubmissionMethod("Microsoft.AspNetCore.Diagnostics.UnhandledException"); + private void SubmitException(object payload, string submissionMethod, bool isUnhandledError) { + if (payload == null) + return; - exception.ToExceptionless(contextData, _client).SetHttpContext(httpContext).Submit(); - } + var httpContext = GetPropertyValue(payload, "httpContext") as HttpContext; + var exception = GetPropertyValue(payload, "exception") as Exception; + if (httpContext == null || exception == null) + return; - [DiagnosticName("Microsoft.AspNetCore.Hosting.UnhandledException")] - public void OnHostingUnhandledException(HttpContext httpContext, Exception exception) { var contextData = new ContextData(); - contextData.MarkAsUnhandledError(); - contextData.SetSubmissionMethod("Microsoft.AspNetCore.Hosting.UnhandledException"); + if (isUnhandledError) + contextData.MarkAsUnhandledError(); + contextData.SetSubmissionMethod(submissionMethod); exception.ToExceptionless(contextData, _client).SetHttpContext(httpContext).Submit(); } - [DiagnosticName("Microsoft.AspNetCore.MiddlewareAnalysis.MiddlewareException")] - public void OnMiddlewareException(HttpContext httpContext, Exception exception, string name) { - var contextData = new ContextData(); - contextData.MarkAsUnhandledError(); - contextData.SetSubmissionMethod(name ?? "Microsoft.AspNetCore.MiddlewareAnalysis.MiddlewareException"); - - exception.ToExceptionless(contextData, _client).SetHttpContext(httpContext).Submit(); + private static object GetPropertyValue(object payload, string propertyName) { + return payload.GetType() + .GetProperty(propertyName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.IgnoreCase)? + .GetValue(payload); } } -} \ No newline at end of file +} diff --git a/src/Platforms/Exceptionless.AspNetCore/ExceptionlessExceptionHandler.cs b/src/Platforms/Exceptionless.AspNetCore/ExceptionlessExceptionHandler.cs new file mode 100644 index 00000000..660e5ac9 --- /dev/null +++ b/src/Platforms/Exceptionless.AspNetCore/ExceptionlessExceptionHandler.cs @@ -0,0 +1,29 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Exceptionless.Plugins; +using Microsoft.AspNetCore.Diagnostics; +using Microsoft.AspNetCore.Http; + +namespace Exceptionless.AspNetCore { + internal sealed class ExceptionlessExceptionHandler : IExceptionHandler { + private readonly ExceptionlessClient _client; + + public ExceptionlessExceptionHandler(ExceptionlessClient client) { + _client = client ?? ExceptionlessClient.Default; + } + + public ValueTask TryHandleAsync(HttpContext httpContext, Exception exception, CancellationToken cancellationToken) { + if (httpContext.RequestAborted.IsCancellationRequested) + return ValueTask.FromResult(false); + + var contextData = new ContextData(); + contextData.MarkAsUnhandledError(); + contextData.SetSubmissionMethod(nameof(ExceptionlessExceptionHandler)); + + exception.ToExceptionless(contextData, _client).SetHttpContext(httpContext).Submit(); + + return ValueTask.FromResult(false); + } + } +} diff --git a/src/Platforms/Exceptionless.AspNetCore/ExceptionlessExtensions.cs b/src/Platforms/Exceptionless.AspNetCore/ExceptionlessExtensions.cs index cbe6f0c9..d736643b 100644 --- a/src/Platforms/Exceptionless.AspNetCore/ExceptionlessExtensions.cs +++ b/src/Platforms/Exceptionless.AspNetCore/ExceptionlessExtensions.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics; using Microsoft.AspNetCore.Builder; @@ -7,12 +7,23 @@ using Exceptionless.Models; using Exceptionless.Models.Data; using Exceptionless.Plugins.Default; +using Microsoft.AspNetCore.Diagnostics; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; namespace Exceptionless { public static class ExceptionlessExtensions { + /// + /// Registers the Exceptionless for capturing unhandled exceptions + /// in apps that use UseExceptionHandler(). + /// + public static IServiceCollection AddExceptionlessExceptionHandler(this IServiceCollection services) { + services.TryAddEnumerable(ServiceDescriptor.Singleton()); + return services; + } + /// /// Adds the Exceptionless middleware for capturing unhandled exceptions and ensures that the Exceptionless pending queue is processed before the host shuts down. /// @@ -34,7 +45,7 @@ public static IApplicationBuilder UseExceptionless(this IApplicationBuilder app, //client.Configuration.Resolver.Register(); var diagnosticListener = app.ApplicationServices.GetRequiredService(); - diagnosticListener?.SubscribeWithAdapter(new ExceptionlessDiagnosticListener(client)); + diagnosticListener?.Subscribe(new ExceptionlessDiagnosticListener(client)); var lifetime = app.ApplicationServices.GetRequiredService(); lifetime.ApplicationStopping.Register(() => client.ProcessQueueAsync().ConfigureAwait(false).GetAwaiter().GetResult()); diff --git a/src/Platforms/Exceptionless.AspNetCore/ExceptionlessMiddleware.cs b/src/Platforms/Exceptionless.AspNetCore/ExceptionlessMiddleware.cs index 79f72170..514622e6 100644 --- a/src/Platforms/Exceptionless.AspNetCore/ExceptionlessMiddleware.cs +++ b/src/Platforms/Exceptionless.AspNetCore/ExceptionlessMiddleware.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Exceptionless.Plugins; @@ -40,4 +40,4 @@ public async Task Invoke(HttpContext context) { } } } -} \ No newline at end of file +} diff --git a/src/Platforms/Exceptionless.AspNetCore/RequestInfoCollector.cs b/src/Platforms/Exceptionless.AspNetCore/RequestInfoCollector.cs index 146ec89a..9973ac09 100644 --- a/src/Platforms/Exceptionless.AspNetCore/RequestInfoCollector.cs +++ b/src/Platforms/Exceptionless.AspNetCore/RequestInfoCollector.cs @@ -133,8 +133,6 @@ private static object GetPostData(HttpContext context, ExceptionlessConfiguratio HeaderNames.Authorization, HeaderNames.Cookie, HeaderNames.Host, - HeaderNames.Method, - HeaderNames.Path, HeaderNames.ProxyAuthorization, HeaderNames.Referer, HeaderNames.UserAgent @@ -176,7 +174,7 @@ private static Dictionary ToDictionary(this IRequestCookieCollec if (kvp.Value == null || kvp.Value.Length >= MAX_DATA_ITEM_LENGTH) continue; - + d[kvp.Key] = kvp.Value; } @@ -194,7 +192,7 @@ private static Dictionary ToDictionary(this IEnumerable= MAX_DATA_ITEM_LENGTH) continue; - + d[kvp.Key] = value; } catch (Exception ex) { d[kvp.Key] = $"EXCEPTION: {ex.Message}"; diff --git a/src/Platforms/Exceptionless.Extensions.Hosting/Exceptionless.Extensions.Hosting.csproj b/src/Platforms/Exceptionless.Extensions.Hosting/Exceptionless.Extensions.Hosting.csproj index 007d21b0..471209f8 100644 --- a/src/Platforms/Exceptionless.Extensions.Hosting/Exceptionless.Extensions.Hosting.csproj +++ b/src/Platforms/Exceptionless.Extensions.Hosting/Exceptionless.Extensions.Hosting.csproj @@ -7,15 +7,23 @@ Exceptionless provider for Microsoft.Extensions.Hosting Exceptionless provider for Microsoft.Extensions.Hosting. $(Description) $(PackageTags);Microsoft.Extensions.Hosting;Hosting - netstandard2.0 + net8.0;net9.0;net10.0 - - + + + + + + + + + + diff --git a/src/Platforms/Exceptionless.Extensions.Hosting/ExceptionlessExtensions.cs b/src/Platforms/Exceptionless.Extensions.Hosting/ExceptionlessExtensions.cs index 0f400ca2..f218bb5c 100644 --- a/src/Platforms/Exceptionless.Extensions.Hosting/ExceptionlessExtensions.cs +++ b/src/Platforms/Exceptionless.Extensions.Hosting/ExceptionlessExtensions.cs @@ -2,6 +2,7 @@ using Exceptionless.Extensions.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; namespace Exceptionless { @@ -13,10 +14,45 @@ public static class ExceptionlessExtensions { /// public static IHostBuilder UseExceptionless(this IHostBuilder hostBuilder) { return hostBuilder.ConfigureServices(delegate (HostBuilderContext context, IServiceCollection collection) { - collection.AddSingleton(); + collection.AddExceptionlessLifetimeService(); }); } + /// + /// Ensures that the Exceptionless pending queue is processed before the host shuts down using the .NET 8+ builder API. + /// + public static IHostApplicationBuilder UseExceptionless(this IHostApplicationBuilder builder) { + builder.Services.AddExceptionlessLifetimeService(); + return builder; + } + + /// + /// Adds the given pre-configured to the host builder and registers lifecycle hooks. + /// + public static IHostApplicationBuilder AddExceptionless(this IHostApplicationBuilder builder, ExceptionlessClient client) { + builder.Services.AddExceptionless(client); + builder.Services.AddExceptionlessLifetimeService(); + return builder; + } + + /// + /// Adds an to the host builder and registers lifecycle hooks. + /// + public static IHostApplicationBuilder AddExceptionless(this IHostApplicationBuilder builder, string apiKey) { + builder.Services.AddExceptionless(apiKey); + builder.Services.AddExceptionlessLifetimeService(); + return builder; + } + + /// + /// Adds an to the host builder using the builder configuration and registers lifecycle hooks. + /// + public static IHostApplicationBuilder AddExceptionless(this IHostApplicationBuilder builder, Action configure = null) { + builder.Services.AddExceptionless(builder.Configuration, configure); + builder.Services.AddExceptionlessLifetimeService(); + return builder; + } + /// /// Adds the given pre-configured to the services collection as a singleton. /// @@ -78,5 +114,10 @@ public static IServiceCollection AddExceptionless(this IServiceCollection servic return client; }); } + + private static IServiceCollection AddExceptionlessLifetimeService(this IServiceCollection services) { + services.TryAddEnumerable(ServiceDescriptor.Singleton()); + return services; + } } -} \ No newline at end of file +} diff --git a/src/Platforms/Exceptionless.Extensions.Hosting/ExceptionlessLifetimeService.cs b/src/Platforms/Exceptionless.Extensions.Hosting/ExceptionlessLifetimeService.cs index f7ea3a7d..717ebfa9 100644 --- a/src/Platforms/Exceptionless.Extensions.Hosting/ExceptionlessLifetimeService.cs +++ b/src/Platforms/Exceptionless.Extensions.Hosting/ExceptionlessLifetimeService.cs @@ -3,23 +3,46 @@ using Microsoft.Extensions.Hosting; namespace Exceptionless.Extensions.Hosting { - public class ExceptionlessLifetimeService : IHostedService { + public sealed class ExceptionlessLifetimeService : IHostedLifecycleService { private readonly ExceptionlessClient _exceptionlessClient; + private int _started; - public ExceptionlessLifetimeService(ExceptionlessClient client, IHostApplicationLifetime appLifetime) { + public ExceptionlessLifetimeService(ExceptionlessClient client) { _exceptionlessClient = client; - + } + + public Task StartingAsync(CancellationToken cancellationToken) { + if (Interlocked.Exchange(ref _started, 1) == 1) + return Task.CompletedTask; + _exceptionlessClient.RegisterAppDomainUnhandledExceptionHandler(); _exceptionlessClient.RegisterTaskSchedulerUnobservedTaskExceptionHandler(); - - appLifetime.ApplicationStopping.Register(() => _exceptionlessClient.ProcessQueueAsync().ConfigureAwait(false).GetAwaiter().GetResult()); + return Task.CompletedTask; } public Task StartAsync(CancellationToken cancellationToken) { return Task.CompletedTask; } + public Task StartedAsync(CancellationToken cancellationToken) { + return Task.CompletedTask; + } + + public Task StoppingAsync(CancellationToken cancellationToken) { + if (_started == 0) + return Task.CompletedTask; + + return _exceptionlessClient.ProcessQueueAsync(); + } + public Task StopAsync(CancellationToken cancellationToken) { + if (Interlocked.Exchange(ref _started, 0) == 0) + return Task.CompletedTask; + + return _exceptionlessClient.ShutdownAsync(); + } + + public Task StoppedAsync(CancellationToken cancellationToken) { return Task.CompletedTask; } } diff --git a/src/Platforms/Exceptionless.Extensions.Logging/Exceptionless.Extensions.Logging.csproj b/src/Platforms/Exceptionless.Extensions.Logging/Exceptionless.Extensions.Logging.csproj index bc48b3f6..71c28a68 100644 --- a/src/Platforms/Exceptionless.Extensions.Logging/Exceptionless.Extensions.Logging.csproj +++ b/src/Platforms/Exceptionless.Extensions.Logging/Exceptionless.Extensions.Logging.csproj @@ -7,15 +7,23 @@ Exceptionless provider for Microsoft.Extensions.Logging Exceptionless provider for Microsoft.Extensions.Logging. $(Description) $(PackageTags);Microsoft.Extensions.Logging - netstandard2.0 + net8.0;net9.0;net10.0 - - + + + + + + + + + + diff --git a/src/Platforms/Exceptionless.Log4net/Exceptionless.Log4net.csproj b/src/Platforms/Exceptionless.Log4net/Exceptionless.Log4net.csproj index 0619fe5b..36852aa6 100644 --- a/src/Platforms/Exceptionless.Log4net/Exceptionless.Log4net.csproj +++ b/src/Platforms/Exceptionless.Log4net/Exceptionless.Log4net.csproj @@ -25,7 +25,7 @@ - + @@ -41,4 +41,4 @@ - \ No newline at end of file + diff --git a/src/Platforms/Exceptionless.MessagePack/DataDictionaryFormatter.cs b/src/Platforms/Exceptionless.MessagePack/DataDictionaryFormatter.cs index ee064046..e31fd03f 100644 --- a/src/Platforms/Exceptionless.MessagePack/DataDictionaryFormatter.cs +++ b/src/Platforms/Exceptionless.MessagePack/DataDictionaryFormatter.cs @@ -5,8 +5,8 @@ using MessagePack.Formatters; namespace Exceptionless.MessagePack { - internal class DataDictionaryFormatter : IMessagePackFormatter { - public void Serialize(ref MessagePackWriter writer, DataDictionary value, MessagePackSerializerOptions options) { + internal class DataDictionaryFormatter : IMessagePackFormatter { + public void Serialize(ref MessagePackWriter writer, DataDictionary? value, MessagePackSerializerOptions options) { if (value == null) { writer.WriteNil(); return; @@ -65,7 +65,7 @@ public void Serialize(ref MessagePackWriter writer, DataDictionary value, Messag } } - public DataDictionary Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options) { + public DataDictionary? Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options) { if (reader.IsNil) return null; diff --git a/src/Platforms/Exceptionless.MessagePack/Exceptionless.MessagePack.csproj b/src/Platforms/Exceptionless.MessagePack/Exceptionless.MessagePack.csproj index 0fb0c46f..4d1572db 100644 --- a/src/Platforms/Exceptionless.MessagePack/Exceptionless.MessagePack.csproj +++ b/src/Platforms/Exceptionless.MessagePack/Exceptionless.MessagePack.csproj @@ -25,6 +25,6 @@ - + diff --git a/src/Platforms/Exceptionless.MessagePack/PersistedDictionaryFormatter.cs b/src/Platforms/Exceptionless.MessagePack/PersistedDictionaryFormatter.cs index faa096cf..38fae764 100644 --- a/src/Platforms/Exceptionless.MessagePack/PersistedDictionaryFormatter.cs +++ b/src/Platforms/Exceptionless.MessagePack/PersistedDictionaryFormatter.cs @@ -7,6 +7,8 @@ namespace Exceptionless.MessagePack { internal class PersistedDictionaryFormatter : DictionaryFormatterBase { private readonly IDependencyResolver _resolver; + internal PersistedDictionaryFormatter() : this(DependencyResolver.Default) { } + public PersistedDictionaryFormatter(IDependencyResolver resolver) { _resolver = resolver; } diff --git a/src/Platforms/Exceptionless.NLog/Exceptionless.NLog.csproj b/src/Platforms/Exceptionless.NLog/Exceptionless.NLog.csproj index 40c52d5a..33727c35 100644 --- a/src/Platforms/Exceptionless.NLog/Exceptionless.NLog.csproj +++ b/src/Platforms/Exceptionless.NLog/Exceptionless.NLog.csproj @@ -25,7 +25,7 @@ - + @@ -40,4 +40,4 @@ - \ No newline at end of file + diff --git a/src/Platforms/Exceptionless.NLog/ExceptionlessField.cs b/src/Platforms/Exceptionless.NLog/ExceptionlessField.cs index a5eecdd9..3ac848e4 100644 --- a/src/Platforms/Exceptionless.NLog/ExceptionlessField.cs +++ b/src/Platforms/Exceptionless.NLog/ExceptionlessField.cs @@ -4,10 +4,8 @@ namespace Exceptionless.NLog { [NLogConfigurationItem] public class ExceptionlessField { - [RequiredParameter] public string Name { get; set; } - [RequiredParameter] public Layout Layout { get; set; } } -} \ No newline at end of file +} diff --git a/src/Platforms/Exceptionless.NLog/ExceptionlessTarget.cs b/src/Platforms/Exceptionless.NLog/ExceptionlessTarget.cs index 83c7128c..8df056e9 100644 --- a/src/Platforms/Exceptionless.NLog/ExceptionlessTarget.cs +++ b/src/Platforms/Exceptionless.NLog/ExceptionlessTarget.cs @@ -28,6 +28,17 @@ public ExceptionlessTarget() { protected override void InitializeTarget() { base.InitializeTarget(); + foreach (var field in Fields) { + if (field == null) + throw new NLogConfigurationException("Exceptionless field configuration cannot be null."); + + if (String.IsNullOrWhiteSpace(field.Name)) + throw new NLogConfigurationException("Exceptionless field name is required."); + + if (field.Layout == null) + throw new NLogConfigurationException($"Exceptionless field '{field.Name}' must define a layout."); + } + string apiKey = RenderLogEvent(ApiKey, LogEventInfo.CreateNullEvent()); string serverUrl = RenderLogEvent(ServerUrl, LogEventInfo.CreateNullEvent()); diff --git a/src/Platforms/Exceptionless.Web/RequestInfoCollector.cs b/src/Platforms/Exceptionless.Web/RequestInfoCollector.cs index dd864dd8..575447dc 100644 --- a/src/Platforms/Exceptionless.Web/RequestInfoCollector.cs +++ b/src/Platforms/Exceptionless.Web/RequestInfoCollector.cs @@ -54,7 +54,7 @@ public static RequestInfo Collect(HttpContextBase context, ExceptionlessConfigur if (config.IncludeCookies) info.Cookies = context.Request.Cookies.ToDictionary(exclusionList); - + if (config.IncludeQueryString) { try { info.QueryString = context.Request.QueryString.ToDictionary(exclusionList); @@ -124,7 +124,7 @@ private static object GetPostData(HttpContextBase context, ExceptionlessConfigur int numRead; int bufferSize = Math.Min(1024, maxDataToRead); - + char[] buffer = new char[bufferSize]; while ((numRead = inputStream.ReadBlock(buffer, 0, bufferSize)) > 0 && (sb.Length + numRead) < maxDataToRead) { sb.Append(buffer, 0, numRead); @@ -210,7 +210,7 @@ private static Dictionary ToDictionary(this HttpCookieCollection private static Dictionary ToDictionary(this NameValueCollection values, string[] exclusions) { var d = new Dictionary(); - + foreach (string key in values.AllKeys) { if (String.IsNullOrEmpty(key) || key.AnyWildcardMatches(_ignoredFormFields) || key.AnyWildcardMatches(exclusions)) continue; @@ -237,4 +237,4 @@ private static string GetUserIpAddress(HttpContextBase context) { return clientIp; } } -} +} \ No newline at end of file diff --git a/src/Platforms/Exceptionless.WebApi/RequestInfoCollector.cs b/src/Platforms/Exceptionless.WebApi/RequestInfoCollector.cs index b3f5b521..324a31a9 100644 --- a/src/Platforms/Exceptionless.WebApi/RequestInfoCollector.cs +++ b/src/Platforms/Exceptionless.WebApi/RequestInfoCollector.cs @@ -78,7 +78,7 @@ public static RequestInfo Collect(HttpActionContext context, ExceptionlessConfig private static readonly List _ignoredFormFields = new List { "__*" }; - + private static Dictionary ToDictionary(this HttpRequestHeaders headers, string[] exclusions) { var d = new Dictionary(); @@ -119,7 +119,7 @@ private static Dictionary ToDictionary(this IEnumerable ToDictionary(this NameValueCollection values, string[] exclusions) { var d = new Dictionary(); - + foreach (string key in values.AllKeys) { if (String.IsNullOrEmpty(key) || key.AnyWildcardMatches(_ignoredFormFields) || key.AnyWildcardMatches(exclusions)) continue; diff --git a/test/Exceptionless.MessagePack.Tests/Exceptionless.MessagePack.Tests.csproj b/test/Exceptionless.MessagePack.Tests/Exceptionless.MessagePack.Tests.csproj index e5f125f4..19d5566e 100644 --- a/test/Exceptionless.MessagePack.Tests/Exceptionless.MessagePack.Tests.csproj +++ b/test/Exceptionless.MessagePack.Tests/Exceptionless.MessagePack.Tests.csproj @@ -2,22 +2,24 @@ -net8.0 +net10.0 -net8.0;net462 +net10.0;net472 false true + Exe true Exceptionless.MessagePack.Tests - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -29,21 +31,21 @@ - + $(DefineConstants);NETSTANDARD;NETSTANDARD2_0 - + $(DefineConstants);NET45 - + - - + + - \ No newline at end of file + diff --git a/test/Exceptionless.TestHarness/Exceptionless.TestHarness.csproj b/test/Exceptionless.TestHarness/Exceptionless.TestHarness.csproj index 0c752883..e54a4a95 100644 --- a/test/Exceptionless.TestHarness/Exceptionless.TestHarness.csproj +++ b/test/Exceptionless.TestHarness/Exceptionless.TestHarness.csproj @@ -2,10 +2,10 @@ - netstandard2.0 + net10.0 - netstandard2.0;net462 + net10.0;net472 @@ -16,15 +16,22 @@ + + + + + + + + - - + $(DefineConstants);NETSTANDARD;NETSTANDARD2_0 - + $(DefineConstants);NET45 diff --git a/test/Exceptionless.Tests/Configuration/ConfigurationTests.cs b/test/Exceptionless.Tests/Configuration/ConfigurationTests.cs index 3cd35cb4..8a338502 100644 --- a/test/Exceptionless.Tests/Configuration/ConfigurationTests.cs +++ b/test/Exceptionless.Tests/Configuration/ConfigurationTests.cs @@ -13,7 +13,6 @@ using Exceptionless.Tests.Utility; using Moq; using Xunit; -using Xunit.Abstractions; [assembly: Exceptionless("LhhP1C9gijpSKCslHHCvwdSIz298twx271nTest", ServerUrl = "http://localhost:5200")] [assembly: ExceptionlessSetting("testing", "configuration")] diff --git a/test/Exceptionless.Tests/EventBuilderTests.cs b/test/Exceptionless.Tests/EventBuilderTests.cs index dcda2119..713b938e 100644 --- a/test/Exceptionless.Tests/EventBuilderTests.cs +++ b/test/Exceptionless.Tests/EventBuilderTests.cs @@ -2,7 +2,6 @@ using Exceptionless.Tests.Log; using Exceptionless.Tests.Utility; using Xunit; -using Xunit.Abstractions; using LogLevel = Exceptionless.Logging.LogLevel; namespace Exceptionless.Tests { @@ -38,4 +37,4 @@ public void CanCreateEventWithNoDuplicateTags() { Assert.Equal(2, builder.Target.Tags.Count); } } -} \ No newline at end of file +} diff --git a/test/Exceptionless.Tests/Exceptionless.Tests.csproj b/test/Exceptionless.Tests/Exceptionless.Tests.csproj index f4f37a35..d89c6884 100644 --- a/test/Exceptionless.Tests/Exceptionless.Tests.csproj +++ b/test/Exceptionless.Tests/Exceptionless.Tests.csproj @@ -1,16 +1,17 @@ - + net10.0 -net10.0;net462 +net10.0;net472 false true + Exe true Exceptionless.Tests @@ -23,28 +24,31 @@ $(DefineConstants);NETSTANDARD;NETSTANDARD2_0 - + $(DefineConstants);NET45 - + + + + - - - + + - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + @@ -56,4 +60,4 @@ PreserveNewest - + \ No newline at end of file diff --git a/test/Exceptionless.Tests/ExceptionlessClientTests.cs b/test/Exceptionless.Tests/ExceptionlessClientTests.cs index 8d03536d..d88a1b6d 100644 --- a/test/Exceptionless.Tests/ExceptionlessClientTests.cs +++ b/test/Exceptionless.Tests/ExceptionlessClientTests.cs @@ -12,7 +12,6 @@ using Exceptionless.Tests.Log; using Exceptionless.Tests.Utility; using Xunit; -using Xunit.Abstractions; namespace Exceptionless.Tests { public class ExceptionlessClientTests { diff --git a/test/Exceptionless.Tests/Platforms/AspNetCoreExceptionCaptureTests.cs b/test/Exceptionless.Tests/Platforms/AspNetCoreExceptionCaptureTests.cs new file mode 100644 index 00000000..ab90f95e --- /dev/null +++ b/test/Exceptionless.Tests/Platforms/AspNetCoreExceptionCaptureTests.cs @@ -0,0 +1,112 @@ +#if NET10_0_OR_GREATER +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using Exceptionless.AspNetCore; +using Exceptionless.Models; +using Exceptionless.Models.Data; +using Microsoft.AspNetCore.Diagnostics; +using Microsoft.AspNetCore.Http; +using Xunit; + +namespace Exceptionless.Tests.Platforms { + public class AspNetCoreExceptionCaptureTests { + [Fact] + public async Task Invoke_CapturesHandledExceptionsFromExceptionHandlerFeature() { + var submittingEvents = new List(); + var client = CreateClient(submittingEvents); + var context = CreateHttpContext(); + var exception = new InvalidOperationException("handled"); + var middleware = new ExceptionlessMiddleware(currentContext => { + currentContext.Features.Set(new ExceptionHandlerFeature { + Error = exception + }); + + return Task.CompletedTask; + }, client); + + await middleware.Invoke(context); + + var submission = Assert.Single(submittingEvents); + Assert.False(submission.IsUnhandledError); + Assert.Equal(nameof(IExceptionHandlerFeature), submission.Event.Data[Event.KnownDataKeys.SubmissionMethod]); + + var requestInfo = Assert.IsType(submission.Event.Data[Event.KnownDataKeys.RequestInfo]); + Assert.Null(requestInfo.PostData); + } + + [Fact] + public async Task Invoke_DoesNotDuplicateHandledExceptionsCapturedByDiagnostics() { + var submittingEvents = new List(); + var client = CreateClient(submittingEvents); + var context = CreateHttpContext(); + var exception = new InvalidOperationException("handled"); + var listener = new ExceptionlessDiagnosticListener(client); + var middleware = new ExceptionlessMiddleware(currentContext => { + currentContext.Features.Set(new ExceptionHandlerFeature { + Error = exception + }); + + return Task.CompletedTask; + }, client); + + listener.OnNext(new KeyValuePair("Microsoft.AspNetCore.Diagnostics.HandledException", new { + httpContext = context, + exception + })); + + await middleware.Invoke(context); + + Assert.Single(submittingEvents); + } + + [Fact] + public async Task Invoke_DoesNotDuplicateUnhandledExceptionsCapturedByMiddleware() { + var submittingEvents = new List(); + var client = CreateClient(submittingEvents); + var context = CreateHttpContext(); + var exception = new InvalidOperationException("unhandled"); + var listener = new ExceptionlessDiagnosticListener(client); + var middleware = new ExceptionlessMiddleware(_ => throw exception, client); + + await Assert.ThrowsAsync(() => middleware.Invoke(context)); + + listener.OnNext(new KeyValuePair("Microsoft.AspNetCore.Hosting.UnhandledException", new { + httpContext = context, + exception + })); + + var submission = Assert.Single(submittingEvents); + Assert.True(submission.IsUnhandledError); + Assert.Equal(nameof(ExceptionlessMiddleware), submission.Event.Data[Event.KnownDataKeys.SubmissionMethod]); + } + + private static ExceptionlessClient CreateClient(ICollection submittingEvents) { + var client = new ExceptionlessClient(configuration => { + configuration.ApiKey = "test-api-key"; + configuration.ServerUrl = "http://localhost:5200"; + configuration.UpdateSettingsWhenIdleInterval = TimeSpan.Zero; + configuration.UseInMemoryStorage(); + }); + + client.Configuration.AddPlugin(new ExceptionlessAspNetCorePlugin(null)); + client.SubmittingEvent += (_, args) => submittingEvents.Add(args); + + return client; + } + + private static DefaultHttpContext CreateHttpContext() { + const string body = "{\"hello\":\"world\"}"; + var bodyBytes = System.Text.Encoding.UTF8.GetBytes(body); + var context = new DefaultHttpContext(); + context.Request.Method = HttpMethods.Post; + context.Request.ContentType = "application/json"; + context.Request.ContentLength = bodyBytes.Length; + context.Request.Body = new MemoryStream(bodyBytes); + context.Response.Body = new MemoryStream(); + return context; + } + } +} +#endif diff --git a/test/Exceptionless.Tests/Platforms/AspNetCoreRequestInfoTests.cs b/test/Exceptionless.Tests/Platforms/AspNetCoreRequestInfoTests.cs index 599c87db..e6d6f631 100644 --- a/test/Exceptionless.Tests/Platforms/AspNetCoreRequestInfoTests.cs +++ b/test/Exceptionless.Tests/Platforms/AspNetCoreRequestInfoTests.cs @@ -1,3 +1,4 @@ +#if NET10_0_OR_GREATER using System.Collections.Generic; using System.IO; using System.Text; @@ -82,3 +83,4 @@ private static DefaultHttpContext CreateFormHttpContext() { } } } +#endif diff --git a/test/Exceptionless.Tests/Platforms/HostingExtensionsTests.cs b/test/Exceptionless.Tests/Platforms/HostingExtensionsTests.cs new file mode 100644 index 00000000..e009dcbf --- /dev/null +++ b/test/Exceptionless.Tests/Platforms/HostingExtensionsTests.cs @@ -0,0 +1,35 @@ +#if NET10_0_OR_GREATER +using System.Linq; +using Exceptionless.Extensions.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Xunit; + +namespace Exceptionless.Tests.Platforms { + public class HostingExtensionsTests { + [Fact] + public void AddExceptionless_RegistersClientAndLifetimeService_OnHostApplicationBuilder() { + var builder = Host.CreateApplicationBuilder(); + + builder.AddExceptionless(configuration => configuration.ApiKey = "test-api-key"); + + Assert.Contains(builder.Services, descriptor => descriptor.ServiceType == typeof(ExceptionlessClient)); + Assert.Contains(builder.Services, descriptor => + descriptor.ServiceType == typeof(IHostedService) && + descriptor.ImplementationType == typeof(ExceptionlessLifetimeService)); + } + + [Fact] + public void UseExceptionless_DoesNotRegisterDuplicateLifetimeServices_OnHostApplicationBuilder() { + var builder = Host.CreateApplicationBuilder(); + + builder.UseExceptionless(); + builder.UseExceptionless(); + + Assert.Single(builder.Services, descriptor => + descriptor.ServiceType == typeof(IHostedService) && + descriptor.ImplementationType == typeof(ExceptionlessLifetimeService)); + } + } +} +#endif diff --git a/test/Exceptionless.Tests/Plugins/005_HandleAggregateExceptionsPluginTests.cs b/test/Exceptionless.Tests/Plugins/005_HandleAggregateExceptionsPluginTests.cs index 05f9d670..0f05689c 100644 --- a/test/Exceptionless.Tests/Plugins/005_HandleAggregateExceptionsPluginTests.cs +++ b/test/Exceptionless.Tests/Plugins/005_HandleAggregateExceptionsPluginTests.cs @@ -7,7 +7,6 @@ using Exceptionless.Submission; using Exceptionless.Tests.Utility; using Xunit; -using Xunit.Abstractions; namespace Exceptionless.Tests.Plugins { public class HandleAggregateExceptionsPluginTests : PluginTestBase { @@ -57,4 +56,4 @@ public async Task MultipleInnerException() { Assert.Equal(2, submissionClient.Events.Count); } } -} \ No newline at end of file +} diff --git a/test/Exceptionless.Tests/Plugins/010_EventExclusionPluginTests.cs b/test/Exceptionless.Tests/Plugins/010_EventExclusionPluginTests.cs index 5701f501..8c23072e 100644 --- a/test/Exceptionless.Tests/Plugins/010_EventExclusionPluginTests.cs +++ b/test/Exceptionless.Tests/Plugins/010_EventExclusionPluginTests.cs @@ -3,7 +3,6 @@ using Exceptionless.Plugins.Default; using Exceptionless.Models; using Xunit; -using Xunit.Abstractions; namespace Exceptionless.Tests.Plugins { public class EventExclusionPluginTests : PluginTestBase { @@ -138,4 +137,4 @@ public void ExceptionType(string? settingKey, bool cancelled) { Assert.Equal(cancelled, context.Cancel); } } -} \ No newline at end of file +} diff --git a/test/Exceptionless.Tests/Plugins/015_ConfigurationDefaultsPluginTests.cs b/test/Exceptionless.Tests/Plugins/015_ConfigurationDefaultsPluginTests.cs index 890d61b8..5d6da120 100644 --- a/test/Exceptionless.Tests/Plugins/015_ConfigurationDefaultsPluginTests.cs +++ b/test/Exceptionless.Tests/Plugins/015_ConfigurationDefaultsPluginTests.cs @@ -5,7 +5,6 @@ using Exceptionless.Models; using Exceptionless.Models.Data; using Xunit; -using Xunit.Abstractions; namespace Exceptionless.Tests.Plugins { public class ConfigurationDefaultsPluginTests : PluginTestBase { @@ -100,4 +99,4 @@ public void SerializedProperties() { Assert.Equal("blake", context.Event.GetUserIdentity(serializer).Identity); } } -} \ No newline at end of file +} diff --git a/test/Exceptionless.Tests/Plugins/016_SetEnvironmentUserPluginTests.cs b/test/Exceptionless.Tests/Plugins/016_SetEnvironmentUserPluginTests.cs index 2f497196..d97ec1b7 100644 --- a/test/Exceptionless.Tests/Plugins/016_SetEnvironmentUserPluginTests.cs +++ b/test/Exceptionless.Tests/Plugins/016_SetEnvironmentUserPluginTests.cs @@ -3,7 +3,6 @@ using Exceptionless.Plugins.Default; using Exceptionless.Models; using Xunit; -using Xunit.Abstractions; namespace Exceptionless.Tests.Plugins { public class SetEnvironmentUserPluginTests : PluginTestBase { @@ -47,4 +46,4 @@ public void WillNotUpdateIdentity() { Assert.Equal("Blake", user.Name); } } -} \ No newline at end of file +} diff --git a/test/Exceptionless.Tests/Plugins/020_ErrorPluginTests.cs b/test/Exceptionless.Tests/Plugins/020_ErrorPluginTests.cs index a453e75a..e0610a50 100644 --- a/test/Exceptionless.Tests/Plugins/020_ErrorPluginTests.cs +++ b/test/Exceptionless.Tests/Plugins/020_ErrorPluginTests.cs @@ -6,7 +6,6 @@ using Exceptionless.Models; using Exceptionless.Models.Data; using Xunit; -using Xunit.Abstractions; namespace Exceptionless.Tests.Plugins { public class ErrorPluginTests : PluginTestBase { @@ -307,4 +306,4 @@ public void VerifyExceptionHResultIsMappedToErrorCode() { Assert.Equal(unchecked((int)0x8007000E).ToString(), error.Code); } } -} \ No newline at end of file +} diff --git a/test/Exceptionless.Tests/Plugins/040_ReferenceIdPluginTests.cs b/test/Exceptionless.Tests/Plugins/040_ReferenceIdPluginTests.cs index 3af6401a..207ed1c3 100644 --- a/test/Exceptionless.Tests/Plugins/040_ReferenceIdPluginTests.cs +++ b/test/Exceptionless.Tests/Plugins/040_ReferenceIdPluginTests.cs @@ -1,7 +1,6 @@ using Exceptionless.Plugins; using Exceptionless.Models; using Xunit; -using Xunit.Abstractions; namespace Exceptionless.Tests.Plugins { public class ReferenceIdPluginTests : PluginTestBase { @@ -23,4 +22,4 @@ public void ShouldUseReferenceIds() { Assert.NotNull(context.Event.ReferenceId); } } -} \ No newline at end of file +} diff --git a/test/Exceptionless.Tests/Plugins/050_EnvironmentInfoPluginTests.cs b/test/Exceptionless.Tests/Plugins/050_EnvironmentInfoPluginTests.cs index 61f9f8b3..e2cf4197 100644 --- a/test/Exceptionless.Tests/Plugins/050_EnvironmentInfoPluginTests.cs +++ b/test/Exceptionless.Tests/Plugins/050_EnvironmentInfoPluginTests.cs @@ -3,7 +3,6 @@ using Exceptionless.Plugins.Default; using Exceptionless.Models; using Xunit; -using Xunit.Abstractions; namespace Exceptionless.Tests.Plugins { public class EnvironmentInfoPluginTests : PluginTestBase { @@ -34,4 +33,4 @@ public void ShouldAddSessionStart() { Assert.NotNull(context.Event.Data[Event.KnownDataKeys.EnvironmentInfo]); } } -} \ No newline at end of file +} diff --git a/test/Exceptionless.Tests/Plugins/110_IgnoreUserAgentPluginTests.cs b/test/Exceptionless.Tests/Plugins/110_IgnoreUserAgentPluginTests.cs index 2986cf2e..e5e35587 100644 --- a/test/Exceptionless.Tests/Plugins/110_IgnoreUserAgentPluginTests.cs +++ b/test/Exceptionless.Tests/Plugins/110_IgnoreUserAgentPluginTests.cs @@ -3,7 +3,6 @@ using Exceptionless.Models; using Exceptionless.Models.Data; using Xunit; -using Xunit.Abstractions; namespace Exceptionless.Tests.Plugins { public class IgnoreUserAgentPluginTests : PluginTestBase { @@ -31,4 +30,4 @@ public void DiscardBot() { Assert.True(context.Cancel); } } -} \ No newline at end of file +} diff --git a/test/Exceptionless.Tests/Plugins/900_CancelSessionsWithNoUserPluginTests.cs b/test/Exceptionless.Tests/Plugins/900_CancelSessionsWithNoUserPluginTests.cs index a7800582..eff4c446 100644 --- a/test/Exceptionless.Tests/Plugins/900_CancelSessionsWithNoUserPluginTests.cs +++ b/test/Exceptionless.Tests/Plugins/900_CancelSessionsWithNoUserPluginTests.cs @@ -2,7 +2,6 @@ using Exceptionless.Plugins.Default; using Exceptionless.Models; using Xunit; -using Xunit.Abstractions; namespace Exceptionless.Tests.Plugins { public class CancelSessionsWithNoUserPluginTests : PluginTestBase { @@ -25,4 +24,4 @@ public void CancelSessionsWithNoUserTest(string eventType, string? identity, boo Assert.Equal(cancelled, context.Cancel); } } -} \ No newline at end of file +} diff --git a/test/Exceptionless.Tests/Plugins/910_DuplicateCheckerPluginTests.cs b/test/Exceptionless.Tests/Plugins/910_DuplicateCheckerPluginTests.cs index 04245e54..ef479fc0 100644 --- a/test/Exceptionless.Tests/Plugins/910_DuplicateCheckerPluginTests.cs +++ b/test/Exceptionless.Tests/Plugins/910_DuplicateCheckerPluginTests.cs @@ -8,7 +8,6 @@ using Exceptionless.Models; using Exceptionless.Tests.Utility; using Xunit; -using Xunit.Abstractions; using Exceptionless.Json; using Exceptionless.Extensions; @@ -137,4 +136,4 @@ public void VerifyDeduplicationFromFiles() { } } } -} \ No newline at end of file +} diff --git a/test/Exceptionless.Tests/Plugins/PluginTestBase.cs b/test/Exceptionless.Tests/Plugins/PluginTestBase.cs index 1e2de303..5f8f48f8 100644 --- a/test/Exceptionless.Tests/Plugins/PluginTestBase.cs +++ b/test/Exceptionless.Tests/Plugins/PluginTestBase.cs @@ -3,7 +3,7 @@ using Exceptionless.Logging; using Exceptionless.Tests.Log; using Exceptionless.Tests.Utility; -using Xunit.Abstractions; +using Xunit; namespace Exceptionless.Tests.Plugins { @@ -91,4 +91,4 @@ public enum ErrorCategory { SecondErrorBucket, } } -} \ No newline at end of file +} diff --git a/test/Exceptionless.Tests/Plugins/PluginTests.cs b/test/Exceptionless.Tests/Plugins/PluginTests.cs index aa3a2dd2..d9f00ccc 100644 --- a/test/Exceptionless.Tests/Plugins/PluginTests.cs +++ b/test/Exceptionless.Tests/Plugins/PluginTests.cs @@ -7,7 +7,6 @@ using Exceptionless.Plugins.Default; using Exceptionless.Models; using Xunit; -using Xunit.Abstractions; namespace Exceptionless.Tests.Plugins { public class PluginTests : PluginTestBase { @@ -163,4 +162,4 @@ private class PluginWithPriority11 : IEventPlugin { public void Run(EventPluginContext context) {} } } -} \ No newline at end of file +} diff --git a/test/Exceptionless.Tests/Serializer/JsonSerializerTests.cs b/test/Exceptionless.Tests/Serializer/JsonSerializerTests.cs index e33a81fa..7994b7bf 100644 --- a/test/Exceptionless.Tests/Serializer/JsonSerializerTests.cs +++ b/test/Exceptionless.Tests/Serializer/JsonSerializerTests.cs @@ -10,7 +10,6 @@ using Exceptionless.Serializer; using Exceptionless.Tests.Log; using Exceptionless.Tests.Utility; -using Xunit.Abstractions; using LogLevel = Exceptionless.Logging.LogLevel; using Module = Exceptionless.Models.Data.Module; diff --git a/test/Exceptionless.Tests/Storage/FileStorageTestsBase.cs b/test/Exceptionless.Tests/Storage/FileStorageTestsBase.cs index 4fbf88fa..a4b6bbc1 100644 --- a/test/Exceptionless.Tests/Storage/FileStorageTestsBase.cs +++ b/test/Exceptionless.Tests/Storage/FileStorageTestsBase.cs @@ -15,7 +15,6 @@ using Exceptionless.Tests.Log; using Exceptionless.Tests.Utility; using Xunit; -using Xunit.Abstractions; namespace Exceptionless.Tests.Storage { public abstract class FileStorageTestsBase { diff --git a/test/Exceptionless.Tests/Storage/FolderFileStorageTests.cs b/test/Exceptionless.Tests/Storage/FolderFileStorageTests.cs index 95d10821..a2dfcb2d 100644 --- a/test/Exceptionless.Tests/Storage/FolderFileStorageTests.cs +++ b/test/Exceptionless.Tests/Storage/FolderFileStorageTests.cs @@ -5,7 +5,6 @@ using Exceptionless.Logging; using Exceptionless.Serializer; using Exceptionless.Storage; -using Xunit.Abstractions; namespace Exceptionless.Tests.Storage { public class FolderFileStorageTests : FileStorageTestsBase { diff --git a/test/Exceptionless.Tests/Storage/InMemoryFileStorageTests.cs b/test/Exceptionless.Tests/Storage/InMemoryFileStorageTests.cs index 8030ef29..2ef1d90c 100644 --- a/test/Exceptionless.Tests/Storage/InMemoryFileStorageTests.cs +++ b/test/Exceptionless.Tests/Storage/InMemoryFileStorageTests.cs @@ -1,6 +1,5 @@ using Exceptionless.Storage; using Xunit; -using Xunit.Abstractions; namespace Exceptionless.Tests.Storage { public class InMemoryFileStorageTests : FileStorageTestsBase { diff --git a/test/Exceptionless.Tests/Storage/IsolatedStorageFileStorageTests.cs b/test/Exceptionless.Tests/Storage/IsolatedStorageFileStorageTests.cs index c5699e6c..2430269f 100644 --- a/test/Exceptionless.Tests/Storage/IsolatedStorageFileStorageTests.cs +++ b/test/Exceptionless.Tests/Storage/IsolatedStorageFileStorageTests.cs @@ -3,7 +3,7 @@ using Exceptionless.Dependency; using Exceptionless.Serializer; using Exceptionless.Storage; -using Xunit.Abstractions; +using Xunit; namespace Exceptionless.Tests.Storage { public class IsolatedStorageFileStorageTests : FileStorageTestsBase { @@ -17,4 +17,4 @@ protected override IObjectStorage GetStorage() { } } } -#endif \ No newline at end of file +#endif diff --git a/test/Exceptionless.Tests/Utility/TestOutputWriter.cs b/test/Exceptionless.Tests/Utility/TestOutputWriter.cs index 08d77134..6066b62c 100644 --- a/test/Exceptionless.Tests/Utility/TestOutputWriter.cs +++ b/test/Exceptionless.Tests/Utility/TestOutputWriter.cs @@ -2,7 +2,7 @@ using System.Diagnostics; using System.IO; using System.Text; -using Xunit.Abstractions; +using Xunit; namespace Exceptionless.Tests.Utility { public class TestOutputWriter : TextWriter { @@ -28,4 +28,4 @@ public override void WriteLine() { WriteLine(String.Empty); } } -} \ No newline at end of file +} From 5c7d5b25e9e98fe2d73e96361e42b85e6c3b9646 Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Fri, 27 Mar 2026 07:51:06 -0500 Subject: [PATCH 2/6] PR Feedback --- .../ExceptionlessExceptionHandler.cs | 4 +- .../ExceptionlessExtensions.cs | 36 ++----- .../ExceptionlessMiddleware.cs | 16 +-- .../ExceptionlessLifetimeService.cs | 2 +- .../ExceptionlessLoggerExtensions.cs | 49 +--------- .../AspNetCoreExceptionCaptureTests.cs | 97 +++++++++++-------- .../Platforms/AspNetCoreRequestInfoTests.cs | 6 +- .../Platforms/HostingExtensionsTests.cs | 10 +- 8 files changed, 81 insertions(+), 139 deletions(-) diff --git a/src/Platforms/Exceptionless.AspNetCore/ExceptionlessExceptionHandler.cs b/src/Platforms/Exceptionless.AspNetCore/ExceptionlessExceptionHandler.cs index 660e5ac9..716d8d95 100644 --- a/src/Platforms/Exceptionless.AspNetCore/ExceptionlessExceptionHandler.cs +++ b/src/Platforms/Exceptionless.AspNetCore/ExceptionlessExceptionHandler.cs @@ -6,7 +6,7 @@ using Microsoft.AspNetCore.Http; namespace Exceptionless.AspNetCore { - internal sealed class ExceptionlessExceptionHandler : IExceptionHandler { + public sealed class ExceptionlessExceptionHandler : IExceptionHandler { private readonly ExceptionlessClient _client; public ExceptionlessExceptionHandler(ExceptionlessClient client) { @@ -14,7 +14,7 @@ public ExceptionlessExceptionHandler(ExceptionlessClient client) { } public ValueTask TryHandleAsync(HttpContext httpContext, Exception exception, CancellationToken cancellationToken) { - if (httpContext.RequestAborted.IsCancellationRequested) + if (cancellationToken.IsCancellationRequested) return ValueTask.FromResult(false); var contextData = new ContextData(); diff --git a/src/Platforms/Exceptionless.AspNetCore/ExceptionlessExtensions.cs b/src/Platforms/Exceptionless.AspNetCore/ExceptionlessExtensions.cs index d736643b..b348e17c 100644 --- a/src/Platforms/Exceptionless.AspNetCore/ExceptionlessExtensions.cs +++ b/src/Platforms/Exceptionless.AspNetCore/ExceptionlessExtensions.cs @@ -8,7 +8,6 @@ using Exceptionless.Models.Data; using Exceptionless.Plugins.Default; using Microsoft.AspNetCore.Diagnostics; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; @@ -16,21 +15,19 @@ namespace Exceptionless { public static class ExceptionlessExtensions { /// - /// Registers the Exceptionless for capturing unhandled exceptions - /// in apps that use UseExceptionHandler(). + /// Registers the Exceptionless for capturing unhandled exceptions. + /// Call this in your service configuration alongside app.UseExceptionHandler(). /// - public static IServiceCollection AddExceptionlessExceptionHandler(this IServiceCollection services) { + public static IServiceCollection AddExceptionlessAspNetCore(this IServiceCollection services) { + services.AddHttpContextAccessor(); services.TryAddEnumerable(ServiceDescriptor.Singleton()); return services; } /// - /// Adds the Exceptionless middleware for capturing unhandled exceptions and ensures that the Exceptionless pending queue is processed before the host shuts down. + /// Adds the Exceptionless middleware for 404 tracking and queue processing, + /// subscribes to diagnostic events, and configures ASP.NET Core plugins. /// - /// The target to add Exceptionless to. - /// Optional pre-configured instance to use. If not specified (recommended), the - /// instance registered in the services collection will be used. - /// public static IApplicationBuilder UseExceptionless(this IApplicationBuilder app, ExceptionlessClient client = null) { if (client == null) client = app.ApplicationServices.GetService() ?? ExceptionlessClient.Default; @@ -53,27 +50,6 @@ public static IApplicationBuilder UseExceptionless(this IApplicationBuilder app, return app.UseMiddleware(client); } - [Obsolete("UseExceptionless should be called without an overload, ExceptionlessClient should be configured when adding to services collection using AddExceptionless")] - public static IApplicationBuilder UseExceptionless(this IApplicationBuilder app, Action configure) { - var client = app.ApplicationServices.GetService() ?? ExceptionlessClient.Default; - configure?.Invoke(client.Configuration); - return app.UseExceptionless(client); - } - - [Obsolete("UseExceptionless should be called without an overload, ExceptionlessClient should be configured when adding to services collection using AddExceptionless")] - public static IApplicationBuilder UseExceptionless(this IApplicationBuilder app, IConfiguration configuration) { - var client = app.ApplicationServices.GetService() ?? ExceptionlessClient.Default; - client.Configuration.ReadFromConfiguration(configuration); - return app.UseExceptionless(client); - } - - [Obsolete("UseExceptionless should be called without an overload, ExceptionlessClient should be configured when adding to services collection using AddExceptionless")] - public static IApplicationBuilder UseExceptionless(this IApplicationBuilder app, string apiKey) { - var client = app.ApplicationServices.GetService() ?? ExceptionlessClient.Default; - client.Configuration.ApiKey = apiKey; - return app.UseExceptionless(client); - } - /// /// Adds the current request info. /// diff --git a/src/Platforms/Exceptionless.AspNetCore/ExceptionlessMiddleware.cs b/src/Platforms/Exceptionless.AspNetCore/ExceptionlessMiddleware.cs index 514622e6..7a74ccc3 100644 --- a/src/Platforms/Exceptionless.AspNetCore/ExceptionlessMiddleware.cs +++ b/src/Platforms/Exceptionless.AspNetCore/ExceptionlessMiddleware.cs @@ -1,7 +1,5 @@ -using System; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; -using Exceptionless.Plugins; namespace Exceptionless.AspNetCore { public class ExceptionlessMiddleware { @@ -20,19 +18,7 @@ public async Task Invoke(HttpContext context) { }); } - try { - await _next(context); - } catch (Exception ex) { - if (context.RequestAborted.IsCancellationRequested) - throw; - - var contextData = new ContextData(); - contextData.MarkAsUnhandledError(); - contextData.SetSubmissionMethod(nameof(ExceptionlessMiddleware)); - - ex.ToExceptionless(contextData, _client).SetHttpContext(context).Submit(); - throw; - } + await _next(context); if (context.Response?.StatusCode == 404) { string path = context.Request.Path.HasValue ? context.Request.Path.Value : "/"; diff --git a/src/Platforms/Exceptionless.Extensions.Hosting/ExceptionlessLifetimeService.cs b/src/Platforms/Exceptionless.Extensions.Hosting/ExceptionlessLifetimeService.cs index 717ebfa9..5eaeffbc 100644 --- a/src/Platforms/Exceptionless.Extensions.Hosting/ExceptionlessLifetimeService.cs +++ b/src/Platforms/Exceptionless.Extensions.Hosting/ExceptionlessLifetimeService.cs @@ -29,7 +29,7 @@ public Task StartedAsync(CancellationToken cancellationToken) { } public Task StoppingAsync(CancellationToken cancellationToken) { - if (_started == 0) + if (Volatile.Read(ref _started) == 0) return Task.CompletedTask; return _exceptionlessClient.ProcessQueueAsync(); diff --git a/src/Platforms/Exceptionless.Extensions.Logging/ExceptionlessLoggerExtensions.cs b/src/Platforms/Exceptionless.Extensions.Logging/ExceptionlessLoggerExtensions.cs index e7212ae5..2446ffd1 100644 --- a/src/Platforms/Exceptionless.Extensions.Logging/ExceptionlessLoggerExtensions.cs +++ b/src/Platforms/Exceptionless.Extensions.Logging/ExceptionlessLoggerExtensions.cs @@ -1,8 +1,8 @@ using System; using Exceptionless.Extensions.Logging; using ExceptionlessLogLevel = Exceptionless.Logging.LogLevel; -using Microsoft.Extensions.Logging; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; namespace Exceptionless { public static class ExceptionlessLoggerExtensions { @@ -83,52 +83,5 @@ public static ILoggingBuilder AddExceptionless(this ILoggingBuilder builder, Act return builder; } - /// - /// Adds Exceptionless to the logging pipeline using the . - /// - /// The . - /// If a client is not specified then the will be used. - /// The . - [Obsolete("Use ExceptionlessLoggerExtensions.AddExceptionless(ILoggingBuilder, ExceptionlessClient) instead.")] - public static ILoggerFactory AddExceptionless(this ILoggerFactory factory, ExceptionlessClient client = null) { - factory.AddProvider(new ExceptionlessLoggerProvider(client ?? ExceptionlessClient.Default)); - return factory; - } - - /// - /// Adds Exceptionless to the logging pipeline using a new client with the provided api key. - /// - /// The . - /// The project api key. - /// The Server Url - /// The . - [Obsolete("Use ExceptionlessLoggerExtensions.AddExceptionless(ILoggingBuilder, string, string) instead.")] - public static ILoggerFactory AddExceptionless(this ILoggerFactory factory, string apiKey, string serverUrl = null) { - if (String.IsNullOrEmpty(apiKey) && String.IsNullOrEmpty(serverUrl)) - return factory.AddExceptionless(); - - factory.AddProvider(new ExceptionlessLoggerProvider(config => { - if (!String.IsNullOrEmpty(apiKey) && apiKey != "API_KEY_HERE") - config.ApiKey = apiKey; - if (!String.IsNullOrEmpty(serverUrl)) - config.ServerUrl = serverUrl; - - config.UseInMemoryStorage(); - })); - - return factory; - } - - /// - /// Adds Exceptionless to the logging pipeline using a new client configured with the provided action. - /// - /// The . - /// An that applies additional settings and plugins. The project api key must be specified. - /// The . - [Obsolete("Use ExceptionlessLoggerExtensions.AddExceptionless(ILoggingBuilder, Action) instead.")] - public static ILoggerFactory AddExceptionless(this ILoggerFactory factory, Action configure) { - factory.AddProvider(new ExceptionlessLoggerProvider(configure)); - return factory; - } } } \ No newline at end of file diff --git a/test/Exceptionless.Tests/Platforms/AspNetCoreExceptionCaptureTests.cs b/test/Exceptionless.Tests/Platforms/AspNetCoreExceptionCaptureTests.cs index ab90f95e..9c71014c 100644 --- a/test/Exceptionless.Tests/Platforms/AspNetCoreExceptionCaptureTests.cs +++ b/test/Exceptionless.Tests/Platforms/AspNetCoreExceptionCaptureTests.cs @@ -2,84 +2,105 @@ using System; using System.Collections.Generic; using System.IO; +using System.Threading; using System.Threading.Tasks; using Exceptionless.AspNetCore; using Exceptionless.Models; using Exceptionless.Models.Data; -using Microsoft.AspNetCore.Diagnostics; using Microsoft.AspNetCore.Http; using Xunit; namespace Exceptionless.Tests.Platforms { public class AspNetCoreExceptionCaptureTests { [Fact] - public async Task Invoke_CapturesHandledExceptionsFromExceptionHandlerFeature() { + public async Task TryHandleAsync_WhenRequestIsActive_ReturnsFalseAndCapturesUnhandledException() { + // Arrange var submittingEvents = new List(); var client = CreateClient(submittingEvents); var context = CreateHttpContext(); - var exception = new InvalidOperationException("handled"); - var middleware = new ExceptionlessMiddleware(currentContext => { - currentContext.Features.Set(new ExceptionHandlerFeature { - Error = exception - }); - - return Task.CompletedTask; - }, client); + var exception = new InvalidOperationException("unhandled"); + var handler = new ExceptionlessExceptionHandler(client); - await middleware.Invoke(context); + // Act + var result = await handler.TryHandleAsync(context, exception, CancellationToken.None); + // Assert + Assert.False(result); var submission = Assert.Single(submittingEvents); - Assert.False(submission.IsUnhandledError); - Assert.Equal(nameof(IExceptionHandlerFeature), submission.Event.Data[Event.KnownDataKeys.SubmissionMethod]); - - var requestInfo = Assert.IsType(submission.Event.Data[Event.KnownDataKeys.RequestInfo]); - Assert.Null(requestInfo.PostData); + Assert.True(submission.IsUnhandledError); + Assert.Equal(nameof(ExceptionlessExceptionHandler), submission.Event.Data[Event.KnownDataKeys.SubmissionMethod]); } [Fact] - public async Task Invoke_DoesNotDuplicateHandledExceptionsCapturedByDiagnostics() { + public async Task TryHandleAsync_WhenCancellationIsRequested_ReturnsFalseWithoutCapturingException() { + // Arrange var submittingEvents = new List(); var client = CreateClient(submittingEvents); + var cts = new CancellationTokenSource(); var context = CreateHttpContext(); - var exception = new InvalidOperationException("handled"); - var listener = new ExceptionlessDiagnosticListener(client); - var middleware = new ExceptionlessMiddleware(currentContext => { - currentContext.Features.Set(new ExceptionHandlerFeature { - Error = exception - }); + cts.Cancel(); + var handler = new ExceptionlessExceptionHandler(client); - return Task.CompletedTask; - }, client); - - listener.OnNext(new KeyValuePair("Microsoft.AspNetCore.Diagnostics.HandledException", new { - httpContext = context, - exception - })); + // Act + var result = await handler.TryHandleAsync(context, new InvalidOperationException(), cts.Token); - await middleware.Invoke(context); - - Assert.Single(submittingEvents); + // Assert + Assert.False(result); + Assert.Empty(submittingEvents); } [Fact] - public async Task Invoke_DoesNotDuplicateUnhandledExceptionsCapturedByMiddleware() { + public void OnNext_WhenUnhandledExceptionEventIsPublished_CapturesUnhandledException() { + // Arrange var submittingEvents = new List(); var client = CreateClient(submittingEvents); var context = CreateHttpContext(); var exception = new InvalidOperationException("unhandled"); var listener = new ExceptionlessDiagnosticListener(client); - var middleware = new ExceptionlessMiddleware(_ => throw exception, client); - - await Assert.ThrowsAsync(() => middleware.Invoke(context)); + // Act listener.OnNext(new KeyValuePair("Microsoft.AspNetCore.Hosting.UnhandledException", new { httpContext = context, exception })); + // Assert var submission = Assert.Single(submittingEvents); Assert.True(submission.IsUnhandledError); - Assert.Equal(nameof(ExceptionlessMiddleware), submission.Event.Data[Event.KnownDataKeys.SubmissionMethod]); + } + + [Fact] + public async Task Invoke_WhenResponseStatusIsNotFound_SubmitsNotFoundEvent() { + // Arrange + var submittingEvents = new List(); + var client = CreateClient(submittingEvents); + var context = CreateHttpContext(); + var middleware = new ExceptionlessMiddleware(currentContext => { + currentContext.Response.StatusCode = 404; + return Task.CompletedTask; + }, client); + + // Act + await middleware.Invoke(context); + + // Assert + var submission = Assert.Single(submittingEvents); + Assert.Equal(Event.KnownTypes.NotFound, submission.Event.Type); + } + + [Fact] + public async Task Invoke_WhenNextDelegateThrows_RethrowsExceptionWithoutSubmittingEvent() { + // Arrange + var submittingEvents = new List(); + var client = CreateClient(submittingEvents); + var context = CreateHttpContext(); + var middleware = new ExceptionlessMiddleware(_ => throw new InvalidOperationException("boom"), client); + + // Act + await Assert.ThrowsAsync(() => middleware.Invoke(context)); + + // Assert + Assert.Empty(submittingEvents); } private static ExceptionlessClient CreateClient(ICollection submittingEvents) { diff --git a/test/Exceptionless.Tests/Platforms/AspNetCoreRequestInfoTests.cs b/test/Exceptionless.Tests/Platforms/AspNetCoreRequestInfoTests.cs index e6d6f631..40ada7f3 100644 --- a/test/Exceptionless.Tests/Platforms/AspNetCoreRequestInfoTests.cs +++ b/test/Exceptionless.Tests/Platforms/AspNetCoreRequestInfoTests.cs @@ -11,7 +11,7 @@ namespace Exceptionless.Tests.Platforms { public class AspNetCoreRequestInfoTests { [Fact] - public void GetRequestInfo_DoesNotReadPostData_ForHandledErrors() { + public void GetRequestInfo_WhenErrorIsHandled_DoesNotReadPostData() { // Arrange var context = CreateHttpContext("hello=world"); var config = new ExceptionlessConfiguration(DependencyResolver.CreateDefault()); @@ -26,7 +26,7 @@ public void GetRequestInfo_DoesNotReadPostData_ForHandledErrors() { } [Fact] - public void GetRequestInfo_ReadsAndRestoresPostData_ForUnhandledErrors() { + public void GetRequestInfo_WhenErrorIsUnhandled_ReadsAndRestoresPostData() { // Arrange const string body = "{\"hello\":\"world\"}"; var context = CreateHttpContext(body); @@ -44,7 +44,7 @@ public void GetRequestInfo_ReadsAndRestoresPostData_ForUnhandledErrors() { } [Fact] - public void GetRequestInfo_ReadsFormData_ForUnhandledErrors() { + public void GetRequestInfo_WhenUnhandledRequestContainsFormData_ReadsFormData() { // Arrange var context = CreateFormHttpContext(); var config = new ExceptionlessConfiguration(DependencyResolver.CreateDefault()); diff --git a/test/Exceptionless.Tests/Platforms/HostingExtensionsTests.cs b/test/Exceptionless.Tests/Platforms/HostingExtensionsTests.cs index e009dcbf..48afbbe3 100644 --- a/test/Exceptionless.Tests/Platforms/HostingExtensionsTests.cs +++ b/test/Exceptionless.Tests/Platforms/HostingExtensionsTests.cs @@ -8,11 +8,14 @@ namespace Exceptionless.Tests.Platforms { public class HostingExtensionsTests { [Fact] - public void AddExceptionless_RegistersClientAndLifetimeService_OnHostApplicationBuilder() { + public void AddExceptionless_WhenCalled_RegistersClientAndLifetimeService() { + // Arrange var builder = Host.CreateApplicationBuilder(); + // Act builder.AddExceptionless(configuration => configuration.ApiKey = "test-api-key"); + // Assert Assert.Contains(builder.Services, descriptor => descriptor.ServiceType == typeof(ExceptionlessClient)); Assert.Contains(builder.Services, descriptor => descriptor.ServiceType == typeof(IHostedService) && @@ -20,12 +23,15 @@ public void AddExceptionless_RegistersClientAndLifetimeService_OnHostApplication } [Fact] - public void UseExceptionless_DoesNotRegisterDuplicateLifetimeServices_OnHostApplicationBuilder() { + public void UseExceptionless_WhenCalledTwice_DoesNotRegisterDuplicateLifetimeServices() { + // Arrange var builder = Host.CreateApplicationBuilder(); + // Act builder.UseExceptionless(); builder.UseExceptionless(); + // Assert Assert.Single(builder.Services, descriptor => descriptor.ServiceType == typeof(IHostedService) && descriptor.ImplementationType == typeof(ExceptionlessLifetimeService)); From d18ff664a3a55635387cbff523ca5fcea6ceffb1 Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Fri, 27 Mar 2026 08:24:17 -0500 Subject: [PATCH 3/6] PR feedback --- README.md | 12 ++-- .../Controllers/ValuesController.cs | 7 ++- .../Exceptionless.SampleAspNetCore/Program.cs | 58 ++++++++++++------- .../Exceptionless.SampleAspNetCore/Startup.cs | 41 ------------- .../Exceptionless.SampleHosting/Program.cs | 4 +- .../ExceptionlessDiagnosticListener.cs | 18 +++++- .../ExceptionlessExtensions.cs | 13 +++-- .../Exceptionless.AspNetCore/readme.txt | 9 ++- .../readme.txt | 16 ++--- .../readme.txt | 15 +++-- .../Platforms/AspNetCoreExtensionsTests.cs | 43 ++++++++++++++ 11 files changed, 141 insertions(+), 95 deletions(-) delete mode 100644 samples/Exceptionless.SampleAspNetCore/Startup.cs create mode 100644 test/Exceptionless.Tests/Platforms/AspNetCoreExtensionsTests.cs diff --git a/README.md b/README.md index 7b2ca778..63d5f844 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # Exceptionless .NET Clients -[![Build Windows](https://github.com/exceptionless/Exceptionless.Net/workflows/Build%20Windows/badge.svg?branch=master)](https://github.com/Exceptionless/Exceptionless.Net/actions) -[![Build OSX](https://github.com/exceptionless/Exceptionless.Net/workflows/Build%20OSX/badge.svg)](https://github.com/Exceptionless/Exceptionless.Net/actions) -[![Build Linux](https://github.com/exceptionless/Exceptionless.Net/workflows/Build%20Linux/badge.svg)](https://github.com/Exceptionless/Exceptionless.Net/actions) +[![Build Windows](https://github.com/Exceptionless/Exceptionless.Net/actions/workflows/build-windows.yml/badge.svg?branch=main)](https://github.com/Exceptionless/Exceptionless.Net/actions/workflows/build-windows.yml) +[![Build OSX](https://github.com/Exceptionless/Exceptionless.Net/actions/workflows/build-osx.yml/badge.svg?branch=main)](https://github.com/Exceptionless/Exceptionless.Net/actions/workflows/build-osx.yml) +[![Build Linux](https://github.com/Exceptionless/Exceptionless.Net/actions/workflows/build-linux.yml/badge.svg?branch=main)](https://github.com/Exceptionless/Exceptionless.Net/actions/workflows/build-linux.yml) [![NuGet Version](http://img.shields.io/nuget/v/Exceptionless.svg?style=flat)](https://www.nuget.org/packages/Exceptionless/) [![Discord](https://img.shields.io/discord/715744504891703319)](https://discord.gg/6HxgFCx) @@ -29,8 +29,8 @@ editor design surfaces are available. 1. You will need to install: 1. [Visual Studio 2022](https://visualstudio.microsoft.com/vs/community/) - 2. [.NET Core 6.x & 8.x SDK with VS Tooling](https://dotnet.microsoft.com/download) - 3. [.NET Framework 4.6.2 Developer Pack](https://dotnet.microsoft.com/download/dotnet-framework/net462) + 2. [.NET 10 SDK with Visual Studio tooling](https://dotnet.microsoft.com/download) + 3. [.NET Framework 4.7.2 Developer Pack](https://dotnet.microsoft.com/download/dotnet-framework/net472) 2. Open the `Exceptionless.Net.slnx` Visual Studio solution file. 3. Select `Exceptionless.SampleConsole` as the startup project. 4. Run the project by pressing `F5` to start the console. @@ -43,7 +43,7 @@ build windows specific packages. 1. You will need to install: 1. [Visual Studio Code](https://code.visualstudio.com) - 2. [.NET Core 6.x & 8.x SDK with VS Tooling](https://dotnet.microsoft.com/download) + 2. [.NET 10 SDK](https://dotnet.microsoft.com/download) 2. Open the cloned Exceptionless.Net folder. 3. Run the `Exceptionless.SampleConsole` project by pressing `F5` to start the console. diff --git a/samples/Exceptionless.SampleAspNetCore/Controllers/ValuesController.cs b/samples/Exceptionless.SampleAspNetCore/Controllers/ValuesController.cs index 964450dc..3ed9dab9 100644 --- a/samples/Exceptionless.SampleAspNetCore/Controllers/ValuesController.cs +++ b/samples/Exceptionless.SampleAspNetCore/Controllers/ValuesController.cs @@ -10,7 +10,7 @@ public class ValuesController : Controller { private readonly ILogger _logger; public ValuesController(ExceptionlessClient exceptionlessClient, ILogger logger) { - // ExceptionlessClient instance from DI that was registered with the AddExceptionless call in Startup.ConfigureServices + // ExceptionlessClient instance from DI that was registered with the builder.AddExceptionless call in Program.cs. _exceptionlessClient = exceptionlessClient; _logger = logger; } @@ -21,7 +21,7 @@ public Dictionary Get() { // Submit a feature usage event directly using the client instance that is injected from the DI container. _exceptionlessClient.SubmitFeatureUsage("ValuesController_Get"); - // This log message will get sent to Exceptionless since Exceptionless has be added to the logging system in Program.cs. + // This log message will get sent to Exceptionless since Exceptionless has been added to the logging system in Program.cs. _logger.LogWarning("Test warning message"); try { @@ -42,7 +42,8 @@ public Dictionary Get() { handledException.ToExceptionless().Submit(); } - // Unhandled exceptions will get reported since called UseExceptionless in the Startup.cs which registers a listener for unhandled exceptions. + // Unhandled exceptions will get reported because Program.cs enables the built-in exception handler pipeline + // and wires Exceptionless into both ASP.NET Core diagnostics and middleware hooks. throw new Exception($"Unhandled Exception: {Guid.NewGuid()}"); } } diff --git a/samples/Exceptionless.SampleAspNetCore/Program.cs b/samples/Exceptionless.SampleAspNetCore/Program.cs index 2b82e8cb..629d1a3f 100644 --- a/samples/Exceptionless.SampleAspNetCore/Program.cs +++ b/samples/Exceptionless.SampleAspNetCore/Program.cs @@ -1,21 +1,37 @@ -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Hosting; - -namespace Exceptionless.SampleAspNetCore { - public class Program { - public static void Main(string[] args) { - CreateHostBuilder(args).Build().Run(); - } - - public static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureLogging(b => { - // By default sends warning and error log messages to Exceptionless. - // Log levels can be controlled remotely per log source from the Exceptionless app in near real-time. - b.AddExceptionless(); - }) - .ConfigureWebHostDefaults(webBuilder => { - webBuilder.UseStartup(); - }); - } -} \ No newline at end of file +using Exceptionless; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +var builder = WebApplication.CreateBuilder(args); + +// By default sends warning and error log messages to Exceptionless. +// Log levels can be controlled remotely per log source from the Exceptionless app in near real-time. +builder.Logging.AddExceptionless(); + +// Reads settings from IConfiguration then adds additional configuration from this lambda. +// This also configures ExceptionlessClient.Default and host shutdown queue flushing. +builder.AddExceptionless(c => c.DefaultData["Startup"] = "heyyy"); +// OR +// builder.AddExceptionless(); +// OR +// builder.AddExceptionless("API_KEY_HERE"); + +// Adds ASP.NET Core request/unhandled exception hooks and standard exception handling services. +builder.Services.AddExceptionless(); +builder.Services.AddProblemDetails(); + +// This is normal ASP.NET Core code. +builder.Services.AddControllers(); + +var app = builder.Build(); + +// Uses the built-in exception handler pipeline, with Exceptionless capturing via IExceptionHandler. +app.UseExceptionHandler(); + +// Adds Exceptionless middleware for diagnostics, 404 tracking, and queue processing. +app.UseExceptionless(); + +app.MapControllers(); + +app.Run(); diff --git a/samples/Exceptionless.SampleAspNetCore/Startup.cs b/samples/Exceptionless.SampleAspNetCore/Startup.cs deleted file mode 100644 index 2f17c47a..00000000 --- a/samples/Exceptionless.SampleAspNetCore/Startup.cs +++ /dev/null @@ -1,41 +0,0 @@ -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; - -namespace Exceptionless.SampleAspNetCore { - public class Startup { - public Startup(IConfiguration configuration) { - Configuration = configuration; - } - - public IConfiguration Configuration { get; } - - public void ConfigureServices(IServiceCollection services) { - // Reads settings from IConfiguration then adds additional configuration from this lambda. - // This also configures ExceptionlessClient.Default - services.AddExceptionless(c => c.DefaultData["Startup"] = "heyyy"); - // OR - // services.AddExceptionless(); - // OR - // services.AddExceptionless("API_KEY_HERE"); - - // This enables Exceptionless to gather more detailed information about unhandled exceptions and other events - services.AddHttpContextAccessor(); - - // This is normal ASP.NET code - services.AddControllers(); - } - - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { - // Adds Exceptionless middleware to listen for unhandled exceptions - app.UseExceptionless(); - - // This is normal ASP.NET code - app.UseRouting(); - app.UseEndpoints(endpoints => { - endpoints.MapControllers(); - }); - } - } -} \ No newline at end of file diff --git a/samples/Exceptionless.SampleHosting/Program.cs b/samples/Exceptionless.SampleHosting/Program.cs index 137aa66e..03a668d7 100644 --- a/samples/Exceptionless.SampleHosting/Program.cs +++ b/samples/Exceptionless.SampleHosting/Program.cs @@ -64,7 +64,9 @@ public static IHostBuilder CreateHostBuilder(string[] args) => handledException.ToExceptionless().Submit(); } - // Unhandled exceptions will get reported because host-level Exceptionless integration is enabled in Program.cs. + // This simulates an unhandled exception. Host-level Exceptionless integration reports + // host/AppDomain-level unhandled exceptions; ASP.NET Core request-pipeline exceptions + // require the ASP.NET Core integration and UseExceptionHandler. throw new Exception($"Unhandled Exception: {Guid.NewGuid()}"); }); }); diff --git a/src/Platforms/Exceptionless.AspNetCore/ExceptionlessDiagnosticListener.cs b/src/Platforms/Exceptionless.AspNetCore/ExceptionlessDiagnosticListener.cs index a0e5026e..88ed5688 100644 --- a/src/Platforms/Exceptionless.AspNetCore/ExceptionlessDiagnosticListener.cs +++ b/src/Platforms/Exceptionless.AspNetCore/ExceptionlessDiagnosticListener.cs @@ -9,6 +9,7 @@ public sealed class ExceptionlessDiagnosticListener : IObserver diagnosticEvent) { switch (diagnosticEvent.Key) { case HandledExceptionEvent: @@ -27,9 +36,13 @@ public void OnNext(KeyValuePair diagnosticEvent) { break; case DiagnosticsUnhandledExceptionEvent: case HostingUnhandledExceptionEvent: + case HostingDiagnosticsUnhandledExceptionEvent: SubmitException(diagnosticEvent.Value, diagnosticEvent.Key, true); break; case MiddlewareExceptionEvent: + if (diagnosticEvent.Value is null) + break; + string middlewareName = GetPropertyValue(diagnosticEvent.Value, "name") as string; SubmitException(diagnosticEvent.Value, middlewareName ?? diagnosticEvent.Key, true); break; @@ -37,7 +50,7 @@ public void OnNext(KeyValuePair diagnosticEvent) { } private void SubmitException(object payload, string submissionMethod, bool isUnhandledError) { - if (payload == null) + if (payload is null) return; var httpContext = GetPropertyValue(payload, "httpContext") as HttpContext; @@ -54,6 +67,9 @@ private void SubmitException(object payload, string submissionMethod, bool isUnh } private static object GetPropertyValue(object payload, string propertyName) { + if (payload is null) + return null; + return payload.GetType() .GetProperty(propertyName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.IgnoreCase)? .GetValue(payload); diff --git a/src/Platforms/Exceptionless.AspNetCore/ExceptionlessExtensions.cs b/src/Platforms/Exceptionless.AspNetCore/ExceptionlessExtensions.cs index b348e17c..a3b6bb3a 100644 --- a/src/Platforms/Exceptionless.AspNetCore/ExceptionlessExtensions.cs +++ b/src/Platforms/Exceptionless.AspNetCore/ExceptionlessExtensions.cs @@ -9,18 +9,17 @@ using Exceptionless.Plugins.Default; using Microsoft.AspNetCore.Diagnostics; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; namespace Exceptionless { public static class ExceptionlessExtensions { /// - /// Registers the Exceptionless for capturing unhandled exceptions. - /// Call this in your service configuration alongside app.UseExceptionHandler(). + /// Registers the Exceptionless and required ASP.NET Core services + /// for capturing unhandled exceptions. Call this in your service configuration alongside app.UseExceptionHandler(). /// - public static IServiceCollection AddExceptionlessAspNetCore(this IServiceCollection services) { + public static IServiceCollection AddExceptionless(this IServiceCollection services) { services.AddHttpContextAccessor(); - services.TryAddEnumerable(ServiceDescriptor.Singleton()); + services.AddExceptionHandler(); return services; } @@ -42,7 +41,9 @@ public static IApplicationBuilder UseExceptionless(this IApplicationBuilder app, //client.Configuration.Resolver.Register(); var diagnosticListener = app.ApplicationServices.GetRequiredService(); - diagnosticListener?.Subscribe(new ExceptionlessDiagnosticListener(client)); + diagnosticListener?.Subscribe( + new ExceptionlessDiagnosticListener(client), + eventName => ExceptionlessDiagnosticListener.IsRelevantEvent(eventName)); var lifetime = app.ApplicationServices.GetRequiredService(); lifetime.ApplicationStopping.Register(() => client.ProcessQueueAsync().ConfigureAwait(false).GetAwaiter().GetResult()); diff --git a/src/Platforms/Exceptionless.AspNetCore/readme.txt b/src/Platforms/Exceptionless.AspNetCore/readme.txt index 6364a53d..40961fc4 100644 --- a/src/Platforms/Exceptionless.AspNetCore/readme.txt +++ b/src/Platforms/Exceptionless.AspNetCore/readme.txt @@ -1,4 +1,4 @@ -------------------------------------- +------------------------------------- Exceptionless Readme ------------------------------------- Exceptionless provides real-time error reporting for your apps. It organizes the @@ -31,15 +31,18 @@ You must import the "Exceptionless" namespace and add the following code to regi using Exceptionless; var builder = WebApplication.CreateBuilder(args); -builder.Services.AddExceptionless("API_KEY_HERE"); +builder.AddExceptionless(c => c.ApiKey = "API_KEY_HERE"); +builder.Services.AddExceptionless(); +builder.Services.AddProblemDetails(); In order to start gathering unhandled exceptions, you will need to register the Exceptionless middleware in your application like this after building your application: var app = builder.Build(); +app.UseExceptionHandler(); app.UseExceptionless(); -Alternatively, you can use different overloads of the AddExceptionless method for other configuration options. +Alternatively, you can use different overloads of the host builder AddExceptionless method for other configuration options. Please visit the documentation at https://exceptionless.com/docs/clients/dotnet/sending-events/ for additional examples and guidance on sending events to Exceptionless. diff --git a/src/Platforms/Exceptionless.Extensions.Hosting/readme.txt b/src/Platforms/Exceptionless.Extensions.Hosting/readme.txt index 30d508a3..93c4b150 100644 --- a/src/Platforms/Exceptionless.Extensions.Hosting/readme.txt +++ b/src/Platforms/Exceptionless.Extensions.Hosting/readme.txt @@ -24,15 +24,17 @@ Please visit the documentation https://exceptionless.com/docs/clients/dotnet/pri for detailed information on how to configure the client to meet your requirements. ------------------------------------- -Microsoft.Extensions.Logging Integration +Microsoft.Extensions.Hosting Integration ------------------------------------- -You must import the "Exceptionless" namespace and call the following line -of code to start reporting log messages. +You must import the "Exceptionless" namespace and register Exceptionless on the +host builder. -loggerFactory.AddExceptionless("API_KEY_HERE"); +var builder = Host.CreateApplicationBuilder(args); +builder.AddExceptionless(c => c.ApiKey = "API_KEY_HERE"); +builder.UseExceptionless(); -Alternatively, you can also use the different overloads of the AddExceptionless method -for different configuration options. +`AddExceptionless(...)` configures the client, and `UseExceptionless()` ensures +the pending queue is flushed during host shutdown. Please visit the documentation https://exceptionless.com/docs/clients/dotnet/sending-events/ for examples on sending events to Exceptionless. @@ -40,4 +42,4 @@ for examples on sending events to Exceptionless. ------------------------------------- Documentation and Support ------------------------------------- -Please visit http://exceptionless.io for documentation and support. \ No newline at end of file +Please visit http://exceptionless.io for documentation and support. diff --git a/src/Platforms/Exceptionless.Extensions.Logging/readme.txt b/src/Platforms/Exceptionless.Extensions.Logging/readme.txt index 30d508a3..b695b6ab 100644 --- a/src/Platforms/Exceptionless.Extensions.Logging/readme.txt +++ b/src/Platforms/Exceptionless.Extensions.Logging/readme.txt @@ -26,13 +26,16 @@ for detailed information on how to configure the client to meet your requirement ------------------------------------- Microsoft.Extensions.Logging Integration ------------------------------------- -You must import the "Exceptionless" namespace and call the following line -of code to start reporting log messages. +You must import the "Exceptionless" namespace and add Exceptionless to the +logging builder. -loggerFactory.AddExceptionless("API_KEY_HERE"); +var builder = Host.CreateApplicationBuilder(args); +builder.Logging.AddExceptionless(); -Alternatively, you can also use the different overloads of the AddExceptionless method -for different configuration options. +If you want to configure the client in the same app, pair this with one of the +Exceptionless hosting registration overloads such as: + +builder.AddExceptionless(c => c.ApiKey = "API_KEY_HERE"); Please visit the documentation https://exceptionless.com/docs/clients/dotnet/sending-events/ for examples on sending events to Exceptionless. @@ -40,4 +43,4 @@ for examples on sending events to Exceptionless. ------------------------------------- Documentation and Support ------------------------------------- -Please visit http://exceptionless.io for documentation and support. \ No newline at end of file +Please visit http://exceptionless.io for documentation and support. diff --git a/test/Exceptionless.Tests/Platforms/AspNetCoreExtensionsTests.cs b/test/Exceptionless.Tests/Platforms/AspNetCoreExtensionsTests.cs new file mode 100644 index 00000000..b3a064f3 --- /dev/null +++ b/test/Exceptionless.Tests/Platforms/AspNetCoreExtensionsTests.cs @@ -0,0 +1,43 @@ +#if NET10_0_OR_GREATER +using Exceptionless; +using Exceptionless.AspNetCore; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Exceptionless.Tests.Platforms { + public class AspNetCoreExtensionsTests { + [Fact] + public void AddExceptionless_WhenCalled_RegistersAspNetCoreServices() { + // Arrange + var builder = WebApplication.CreateBuilder(); + + // Act + builder.Services.AddExceptionless(); + + // Assert + Assert.Contains(builder.Services, descriptor => descriptor.ServiceType == typeof(IHttpContextAccessor)); + Assert.Contains(builder.Services, descriptor => + descriptor.ServiceType == typeof(IExceptionHandler) && + descriptor.ImplementationType == typeof(ExceptionlessExceptionHandler)); + } + + [Fact] + public void AddExceptionless_WhenCalledTwice_DoesNotRegisterDuplicateExceptionHandlers() { + // Arrange + var builder = WebApplication.CreateBuilder(); + + // Act + builder.Services.AddExceptionless(); + builder.Services.AddExceptionless(); + + // Assert + Assert.Single(builder.Services, descriptor => + descriptor.ServiceType == typeof(IExceptionHandler) && + descriptor.ImplementationType == typeof(ExceptionlessExceptionHandler)); + } + } +} +#endif From ad72853c3c1e5743bd7f17917b217af68b57e5b2 Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Fri, 27 Mar 2026 08:25:30 -0500 Subject: [PATCH 4/6] added test --- .../ExceptionlessExtensions.cs | 6 +++- .../AspNetCoreExceptionCaptureTests.cs | 34 +++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/src/Platforms/Exceptionless.AspNetCore/ExceptionlessExtensions.cs b/src/Platforms/Exceptionless.AspNetCore/ExceptionlessExtensions.cs index a3b6bb3a..e59bae4e 100644 --- a/src/Platforms/Exceptionless.AspNetCore/ExceptionlessExtensions.cs +++ b/src/Platforms/Exceptionless.AspNetCore/ExceptionlessExtensions.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Linq; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Exceptionless.AspNetCore; @@ -19,7 +20,10 @@ public static class ExceptionlessExtensions { /// public static IServiceCollection AddExceptionless(this IServiceCollection services) { services.AddHttpContextAccessor(); - services.AddExceptionHandler(); + if (!services.Any(descriptor => + descriptor.ServiceType == typeof(IExceptionHandler) && + descriptor.ImplementationType == typeof(ExceptionlessExceptionHandler))) + services.AddExceptionHandler(); return services; } diff --git a/test/Exceptionless.Tests/Platforms/AspNetCoreExceptionCaptureTests.cs b/test/Exceptionless.Tests/Platforms/AspNetCoreExceptionCaptureTests.cs index 9c71014c..c8fb21f6 100644 --- a/test/Exceptionless.Tests/Platforms/AspNetCoreExceptionCaptureTests.cs +++ b/test/Exceptionless.Tests/Platforms/AspNetCoreExceptionCaptureTests.cs @@ -69,6 +69,40 @@ public void OnNext_WhenUnhandledExceptionEventIsPublished_CapturesUnhandledExcep Assert.True(submission.IsUnhandledError); } + [Fact] + public void OnNext_WhenHostingDiagnosticsUnhandledExceptionEventIsPublished_CapturesUnhandledException() { + // Arrange + var submittingEvents = new List(); + var client = CreateClient(submittingEvents); + var context = CreateHttpContext(); + var exception = new InvalidOperationException("unhandled"); + var listener = new ExceptionlessDiagnosticListener(client); + + // Act + listener.OnNext(new KeyValuePair("Microsoft.AspNetCore.Hosting.Diagnostics.UnhandledException", new { + httpContext = context, + exception + })); + + // Assert + var submission = Assert.Single(submittingEvents); + Assert.True(submission.IsUnhandledError); + } + + [Fact] + public void OnNext_WhenMiddlewareExceptionPayloadIsNull_DoesNotThrowOrCaptureException() { + // Arrange + var submittingEvents = new List(); + var client = CreateClient(submittingEvents); + var listener = new ExceptionlessDiagnosticListener(client); + + // Act + listener.OnNext(new KeyValuePair("Microsoft.AspNetCore.MiddlewareAnalysis.MiddlewareException", null)); + + // Assert + Assert.Empty(submittingEvents); + } + [Fact] public async Task Invoke_WhenResponseStatusIsNotFound_SubmitsNotFoundEvent() { // Arrange From da0f688fcb498d53ff4142f7c58b00e14e04f7fa Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Fri, 27 Mar 2026 08:36:42 -0500 Subject: [PATCH 5/6] fix(platforms): address remaining review feedback --- .../ExceptionlessExtensions.cs | 9 ++++++- .../Exceptionless.NLog/ExceptionlessTarget.cs | 26 +++++++++++-------- .../Platforms/HostingExtensionsTests.cs | 20 +++++++++----- 3 files changed, 37 insertions(+), 18 deletions(-) diff --git a/src/Platforms/Exceptionless.Extensions.Hosting/ExceptionlessExtensions.cs b/src/Platforms/Exceptionless.Extensions.Hosting/ExceptionlessExtensions.cs index f218bb5c..e18408f7 100644 --- a/src/Platforms/Exceptionless.Extensions.Hosting/ExceptionlessExtensions.cs +++ b/src/Platforms/Exceptionless.Extensions.Hosting/ExceptionlessExtensions.cs @@ -4,6 +4,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; +using System.Linq; namespace Exceptionless { public static class ExceptionlessExtensions { @@ -116,7 +117,13 @@ public static IServiceCollection AddExceptionless(this IServiceCollection servic } private static IServiceCollection AddExceptionlessLifetimeService(this IServiceCollection services) { - services.TryAddEnumerable(ServiceDescriptor.Singleton()); + if (services.Any(descriptor => descriptor.ServiceType == typeof(ExceptionlessLifetimeService))) + return services; + + services.AddSingleton(); + services.AddSingleton(sp => sp.GetRequiredService()); + services.AddSingleton(sp => sp.GetRequiredService()); + return services; } } diff --git a/src/Platforms/Exceptionless.NLog/ExceptionlessTarget.cs b/src/Platforms/Exceptionless.NLog/ExceptionlessTarget.cs index 8df056e9..e0a085de 100644 --- a/src/Platforms/Exceptionless.NLog/ExceptionlessTarget.cs +++ b/src/Platforms/Exceptionless.NLog/ExceptionlessTarget.cs @@ -28,15 +28,17 @@ public ExceptionlessTarget() { protected override void InitializeTarget() { base.InitializeTarget(); - foreach (var field in Fields) { - if (field == null) - throw new NLogConfigurationException("Exceptionless field configuration cannot be null."); + if (Fields != null) { + foreach (var field in Fields) { + if (field == null) + throw new NLogConfigurationException("Exceptionless field configuration cannot be null."); - if (String.IsNullOrWhiteSpace(field.Name)) - throw new NLogConfigurationException("Exceptionless field name is required."); + if (String.IsNullOrWhiteSpace(field.Name)) + throw new NLogConfigurationException("Exceptionless field name is required."); - if (field.Layout == null) - throw new NLogConfigurationException($"Exceptionless field '{field.Name}' must define a layout."); + if (field.Layout == null) + throw new NLogConfigurationException($"Exceptionless field '{field.Name}' must define a layout."); + } } string apiKey = RenderLogEvent(ApiKey, LogEventInfo.CreateNullEvent()); @@ -80,10 +82,12 @@ protected override void Write(LogEventInfo logEvent) { string userIdentityName = RenderLogEvent(UserIdentityName, logEvent); builder.Target.SetUserIdentity(userIdentity, userIdentityName); - foreach (var field in Fields) { - string renderedField = RenderLogEvent(field.Layout, logEvent); - if (!String.IsNullOrWhiteSpace(renderedField)) - builder.AddObject(renderedField, field.Name); + if (Fields != null) { + foreach (var field in Fields) { + string renderedField = RenderLogEvent(field.Layout, logEvent); + if (!String.IsNullOrWhiteSpace(renderedField)) + builder.AddObject(renderedField, field.Name); + } } builder.Submit(); diff --git a/test/Exceptionless.Tests/Platforms/HostingExtensionsTests.cs b/test/Exceptionless.Tests/Platforms/HostingExtensionsTests.cs index 48afbbe3..6a783446 100644 --- a/test/Exceptionless.Tests/Platforms/HostingExtensionsTests.cs +++ b/test/Exceptionless.Tests/Platforms/HostingExtensionsTests.cs @@ -17,9 +17,17 @@ public void AddExceptionless_WhenCalled_RegistersClientAndLifetimeService() { // Assert Assert.Contains(builder.Services, descriptor => descriptor.ServiceType == typeof(ExceptionlessClient)); - Assert.Contains(builder.Services, descriptor => - descriptor.ServiceType == typeof(IHostedService) && - descriptor.ImplementationType == typeof(ExceptionlessLifetimeService)); + Assert.Single(builder.Services, descriptor => descriptor.ServiceType == typeof(ExceptionlessLifetimeService)); + Assert.Single(builder.Services, descriptor => descriptor.ServiceType == typeof(IHostedService)); + Assert.Single(builder.Services, descriptor => descriptor.ServiceType == typeof(IHostedLifecycleService)); + + using var serviceProvider = builder.Services.BuildServiceProvider(); + Assert.Same( + serviceProvider.GetRequiredService(), + serviceProvider.GetRequiredService()); + Assert.Same( + serviceProvider.GetRequiredService(), + serviceProvider.GetRequiredService()); } [Fact] @@ -32,9 +40,9 @@ public void UseExceptionless_WhenCalledTwice_DoesNotRegisterDuplicateLifetimeSer builder.UseExceptionless(); // Assert - Assert.Single(builder.Services, descriptor => - descriptor.ServiceType == typeof(IHostedService) && - descriptor.ImplementationType == typeof(ExceptionlessLifetimeService)); + Assert.Single(builder.Services, descriptor => descriptor.ServiceType == typeof(ExceptionlessLifetimeService)); + Assert.Single(builder.Services, descriptor => descriptor.ServiceType == typeof(IHostedService)); + Assert.Single(builder.Services, descriptor => descriptor.ServiceType == typeof(IHostedLifecycleService)); } } } From afe27631895fa9713e60412dce870dc8d6942388 Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Fri, 27 Mar 2026 08:48:37 -0500 Subject: [PATCH 6/6] Update src/Platforms/Exceptionless.Extensions.Hosting/ExceptionlessExtensions.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../Exceptionless.Extensions.Hosting/ExceptionlessExtensions.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Platforms/Exceptionless.Extensions.Hosting/ExceptionlessExtensions.cs b/src/Platforms/Exceptionless.Extensions.Hosting/ExceptionlessExtensions.cs index e18408f7..b224111f 100644 --- a/src/Platforms/Exceptionless.Extensions.Hosting/ExceptionlessExtensions.cs +++ b/src/Platforms/Exceptionless.Extensions.Hosting/ExceptionlessExtensions.cs @@ -2,7 +2,6 @@ using Exceptionless.Extensions.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; using System.Linq;