< Summary

Information
Class: DotNetApiDiff.ExitCodes.GlobalExceptionHandler
Assembly: DotNetApiDiff
File(s): /home/runner/work/dotnet-api-diff/dotnet-api-diff/src/DotNetApiDiff/ExitCodes/GlobalExceptionHandler.cs
Line coverage
73%
Covered lines: 76
Uncovered lines: 28
Coverable lines: 104
Total lines: 192
Line coverage: 73%
Branch coverage
78%
Covered branches: 30
Total branches: 38
Branch coverage: 78.9%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)50%44100%
HandleException(...)100%44100%
LogExceptionDetails(...)85.71%161478.94%
LogReflectionTypeLoadException(...)80%101085.71%
LogAggregateException(...)75%4476.92%
SetupGlobalExceptionHandling()50%3230.76%

File(s)

/home/runner/work/dotnet-api-diff/dotnet-api-diff/src/DotNetApiDiff/ExitCodes/GlobalExceptionHandler.cs

#LineLine coverage
 1// Copyright DotNet API Diff Project Contributors - SPDX Identifier: MIT
 2using System.Reflection;
 3using System.Security;
 4using DotNetApiDiff.Interfaces;
 5using Microsoft.Extensions.Logging;
 6
 7namespace DotNetApiDiff.ExitCodes
 8{
 9    /// <summary>
 10    /// Provides centralized exception handling for the application.
 11    /// </summary>
 12    public class GlobalExceptionHandler : IGlobalExceptionHandler
 13    {
 14        private readonly ILogger<GlobalExceptionHandler> _logger;
 15        private readonly IExitCodeManager _exitCodeManager;
 16
 17        /// <summary>
 18        /// Initializes a new instance of the <see cref="GlobalExceptionHandler"/> class.
 19        /// </summary>
 20        /// <param name="logger">The logger to use for logging exceptions.</param>
 21        /// <param name="exitCodeManager">The exit code manager to determine appropriate exit codes.</param>
 1822        public GlobalExceptionHandler(ILogger<GlobalExceptionHandler> logger, IExitCodeManager exitCodeManager)
 1823        {
 1824            _logger = logger ?? throw new ArgumentNullException(nameof(logger));
 1825            _exitCodeManager = exitCodeManager ?? throw new ArgumentNullException(nameof(exitCodeManager));
 1826        }
 27
 28        /// <summary>
 29        /// Handles an exception by logging it and determining the appropriate exit code.
 30        /// </summary>
 31        /// <param name="exception">The exception to handle.</param>
 32        /// <param name="context">Optional context information about where the exception occurred.</param>
 33        /// <returns>The appropriate exit code for the exception.</returns>
 34        public int HandleException(Exception exception, string? context = null)
 835        {
 836            if (exception == null)
 137            {
 138                _logger.LogError("HandleException called with null exception");
 139                return _exitCodeManager.GetExitCodeForException(new ArgumentNullException(nameof(exception)));
 40            }
 41
 42            // Log the exception with context if provided
 743            if (!string.IsNullOrEmpty(context))
 444            {
 445                _logger.LogError(exception, "Error in {Context}: {Message}", context, exception.Message);
 446            }
 47            else
 348            {
 349                _logger.LogError(exception, "Error: {Message}", exception.Message);
 350            }
 51
 52            // Log additional details for specific exception types
 753            LogExceptionDetails(exception);
 54
 55            // Determine the appropriate exit code
 756            int exitCode = _exitCodeManager.GetExitCodeForException(exception);
 57
 758            _logger.LogInformation(
 759                "Exiting with code {ExitCode}: {Description}",
 760                exitCode,
 761                _exitCodeManager.GetExitCodeDescription(exitCode));
 62
 763            return exitCode;
 864        }
 65
 66        /// <summary>
 67        /// Logs additional details for specific exception types.
 68        /// </summary>
 69        /// <param name="exception">The exception to log details for.</param>
 70        private void LogExceptionDetails(Exception exception)
 771        {
 772            switch (exception)
 73            {
 74                case ReflectionTypeLoadException typeLoadEx:
 175                    LogReflectionTypeLoadException(typeLoadEx);
 176                    break;
 77                case AggregateException aggregateEx:
 178                    LogAggregateException(aggregateEx);
 179                    break;
 80                case FileNotFoundException fileNotFoundEx:
 181                    _logger.LogError("File not found: {FileName}", fileNotFoundEx.FileName);
 182                    break;
 83                case BadImageFormatException badImageEx:
 084                    _logger.LogError("Bad image format: {FileName}", badImageEx.FileName);
 085                    break;
 86                case SecurityException securityEx:
 087                    _logger.LogError("Security exception: {PermissionType}", securityEx.PermissionType);
 088                    break;
 89                case InvalidOperationException:
 90                    // Log the stack trace for InvalidOperationException to help diagnose the issue
 191                    _logger.LogDebug("Stack trace: {StackTrace}", exception.StackTrace);
 192                    break;
 93            }
 94
 95            // Log inner exception if present
 796            if (exception.InnerException != null)
 197            {
 198                _logger.LogDebug("Inner exception: {Message}", exception.InnerException.Message);
 199            }
 7100        }
 101
 102        /// <summary>
 103        /// Logs details for a ReflectionTypeLoadException.
 104        /// </summary>
 105        /// <param name="exception">The ReflectionTypeLoadException to log details for.</param>
 106        private void LogReflectionTypeLoadException(ReflectionTypeLoadException exception)
 1107        {
 1108            _logger.LogError("ReflectionTypeLoadException: Failed to load {Count} types", exception.Types?.Length ?? 0);
 109
 1110            if (exception.LoaderExceptions != null)
 1111            {
 1112                int loaderExceptionCount = exception.LoaderExceptions.Length;
 1113                _logger.LogError("Loader exceptions count: {Count}", loaderExceptionCount);
 114
 115                // Log up to 5 loader exceptions to avoid excessive logging
 1116                int logCount = Math.Min(loaderExceptionCount, 5);
 6117                for (int i = 0; i < logCount; i++)
 2118                {
 2119                    var loaderEx = exception.LoaderExceptions[i];
 2120                    if (loaderEx != null)
 2121                    {
 2122                        _logger.LogError(loaderEx, "Loader exception {Index}: {Message}", i + 1, loaderEx.Message);
 2123                    }
 2124                }
 125
 1126                if (loaderExceptionCount > logCount)
 0127                {
 0128                    _logger.LogError("... and {Count} more loader exceptions", loaderExceptionCount - logCount);
 0129                }
 1130            }
 1131        }
 132
 133        /// <summary>
 134        /// Logs details for an AggregateException.
 135        /// </summary>
 136        /// <param name="exception">The AggregateException to log details for.</param>
 137        private void LogAggregateException(AggregateException exception)
 1138        {
 1139            _logger.LogError("AggregateException with {Count} inner exceptions", exception.InnerExceptions.Count);
 140
 141            // Log up to 5 inner exceptions to avoid excessive logging
 1142            int logCount = Math.Min(exception.InnerExceptions.Count, 5);
 6143            for (int i = 0; i < logCount; i++)
 2144            {
 2145                var innerEx = exception.InnerExceptions[i];
 2146                _logger.LogError(innerEx, "Inner exception {Index}: {Message}", i + 1, innerEx.Message);
 2147            }
 148
 1149            if (exception.InnerExceptions.Count > logCount)
 0150            {
 0151                _logger.LogError("... and {Count} more inner exceptions", exception.InnerExceptions.Count - logCount);
 0152            }
 1153        }
 154
 155        /// <summary>
 156        /// Sets up global unhandled exception handling.
 157        /// </summary>
 158        public void SetupGlobalExceptionHandling()
 9159        {
 160            // Handle unhandled exceptions in the current AppDomain
 9161            AppDomain.CurrentDomain.UnhandledException += (sender, e) =>
 0162            {
 0163                if (e.ExceptionObject is Exception ex)
 0164                {
 0165                    _logger.LogCritical(ex, "Unhandled exception in AppDomain: {Message}", ex.Message);
 0166                }
 9167                else
 0168                {
 0169                    _logger.LogCritical("Unhandled non-exception object in AppDomain: {Object}", e.ExceptionObject);
 0170                }
 9171            };
 172
 173            // Handle unhandled exceptions in tasks
 9174            TaskScheduler.UnobservedTaskException += (sender, e) =>
 0175            {
 0176                _logger.LogCritical(e.Exception, "Unobserved task exception: {Message}", e.Exception.Message);
 0177                e.SetObserved(); // Mark as observed to prevent process termination
 9178            };
 179
 180            // Handle first-chance exceptions (useful for debugging)
 9181            if (_logger.IsEnabled(LogLevel.Debug))
 0182            {
 183                // Register for FirstChanceException events only when debug logging is enabled
 0184                AppDomain.CurrentDomain.FirstChanceException += (sender, e) =>
 0185                {
 0186                    // Only log first-chance exceptions at debug level to avoid noise
 0187                    _logger.LogDebug(e.Exception, "First chance exception: {Message}", e.Exception.Message);
 0188                };
 0189            }
 9190        }
 191    }
 192}