From 2b7b0e57cdd2ef3d1671d9ab28b55fc7950025b1 Mon Sep 17 00:00:00 2001 From: Eddie Date: Fri, 6 Dec 2024 15:31:29 +0330 Subject: [PATCH] Creating GlobalExceptionHandler for handling errors throughout project with custom Exception --- EWallet.Api/EWallet.Api.csproj | 9 ++ EWallet.Api/Program.cs | 13 ++- EWallet.Api/UsingGlobals.cs | 1 - .../ExceptionHandler/AppException.cs | 6 ++ .../ExceptionHandler/ExceptionExtensions.cs | 36 ++++++++ .../GlobalExceptionHandler.cs | 86 +++++++++++++++++++ Wallet.sln | 7 ++ 7 files changed, 154 insertions(+), 4 deletions(-) create mode 100644 EWallet.Infrastructure/ExceptionHandler/AppException.cs create mode 100644 EWallet.Infrastructure/ExceptionHandler/ExceptionExtensions.cs create mode 100644 EWallet.Infrastructure/ExceptionHandler/GlobalExceptionHandler.cs diff --git a/EWallet.Api/EWallet.Api.csproj b/EWallet.Api/EWallet.Api.csproj index 388a0b3..8974227 100644 --- a/EWallet.Api/EWallet.Api.csproj +++ b/EWallet.Api/EWallet.Api.csproj @@ -8,6 +8,7 @@ + @@ -29,4 +30,12 @@ + + + + + + + + diff --git a/EWallet.Api/Program.cs b/EWallet.Api/Program.cs index 4b85319..1dd438a 100644 --- a/EWallet.Api/Program.cs +++ b/EWallet.Api/Program.cs @@ -1,18 +1,25 @@ +using System.Security.Authentication; +using EWallet.Infrastructure.ExceptionHandler; +using Figgle; + var builder = WebApplication.CreateBuilder(args); +Console.WriteLine(FiggleFonts.Standard.Render("Wallet")); + builder.Services .AddCustomAuthentication(builder.Configuration) - .AddCustomAuthorization(); + .AddCustomAuthorization() + .AddExceptionHandler(); +builder.Services.AddProblemDetails(); builder.AddWalletDbContext(); var app = builder.Build(); +app.UseExceptionHandler(); app.UseAuthentication(); app.UseAuthorization(); - - app.MapWalletsEndpoints(); app.MapCurrencyEndpoints(); diff --git a/EWallet.Api/UsingGlobals.cs b/EWallet.Api/UsingGlobals.cs index c3c44f7..93d6799 100644 --- a/EWallet.Api/UsingGlobals.cs +++ b/EWallet.Api/UsingGlobals.cs @@ -13,7 +13,6 @@ #region EWallet global using EWallet.Api.Common.Extensions; -global using EWallet.Api.Common; global using EWallet.Api.Common.Exceptions; global using EWallet.Api.Common.Models; global using DefaultColumnType = EWallet.Api.Common.StaticData.WalletDbContextDefaultColumnType; diff --git a/EWallet.Infrastructure/ExceptionHandler/AppException.cs b/EWallet.Infrastructure/ExceptionHandler/AppException.cs new file mode 100644 index 0000000..329e271 --- /dev/null +++ b/EWallet.Infrastructure/ExceptionHandler/AppException.cs @@ -0,0 +1,6 @@ +namespace EWallet.Infrastructure.ExceptionHandler; + +public class AppException : Exception +{ + // TODO : complete +} \ No newline at end of file diff --git a/EWallet.Infrastructure/ExceptionHandler/ExceptionExtensions.cs b/EWallet.Infrastructure/ExceptionHandler/ExceptionExtensions.cs new file mode 100644 index 0000000..e666a90 --- /dev/null +++ b/EWallet.Infrastructure/ExceptionHandler/ExceptionExtensions.cs @@ -0,0 +1,36 @@ +namespace EWallet.Infrastructure.ExceptionHandler; + +public static class ExceptionExtensions +{ + private const string ErrorCodeKey = "ErrorCode"; + + /// + /// Adds or updates the error code for the exception. + /// + /// The exception to enrich. + /// The error code to add. Defaults to "GenericError". + public static void AddErrorCode(this Exception exception, string errorCode = "GenericError") + { + if (exception == null) + { + throw new ArgumentNullException(nameof(exception), "Exception cannot be null."); + } + + exception.Data[ErrorCodeKey] = errorCode; + } + + /// + /// Retrieves the error code from the exception, if it exists. + /// + /// The exception to retrieve the error code from. + /// The error code, or null if none exists. + public static string? GetErrorCode(this Exception exception) + { + if (exception == null) + { + throw new ArgumentNullException(nameof(exception), "Exception cannot be null."); + } + + return exception.Data.Contains(ErrorCodeKey) ? exception.Data[ErrorCodeKey]?.ToString() : null; + } +} \ No newline at end of file diff --git a/EWallet.Infrastructure/ExceptionHandler/GlobalExceptionHandler.cs b/EWallet.Infrastructure/ExceptionHandler/GlobalExceptionHandler.cs new file mode 100644 index 0000000..5fc65e7 --- /dev/null +++ b/EWallet.Infrastructure/ExceptionHandler/GlobalExceptionHandler.cs @@ -0,0 +1,86 @@ +using System.Diagnostics; +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.AspNetCore.Diagnostics; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.WebUtilities; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace EWallet.Infrastructure.ExceptionHandler; + +public class GlobalExceptionHandler(IHostEnvironment env, ILogger logger) : IExceptionHandler +{ + private const string UnhandledExceptionMsg = "An unhandled exception has occurred while executing the request."; + + private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web) + { + Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) } + }; + + + public async ValueTask TryHandleAsync(HttpContext context, Exception exception, + CancellationToken cancellationToken) + { + exception.AddErrorCode(); + logger.LogError(exception, exception is AppException ? exception.Message : UnhandledExceptionMsg); + + var problemDetails = CreateProblemDetails(context, exception); + var json = ToJson(problemDetails); + + const string contentType = "application/problem+json"; + context.Response.ContentType = contentType; + await context.Response.WriteAsync(json, cancellationToken); + + return true; + } + + private ProblemDetails CreateProblemDetails(in HttpContext context, in Exception exception) + { + var errorCode = exception.GetErrorCode(); + var statusCode = context.Response.StatusCode; + var reasonPhrase = ReasonPhrases.GetReasonPhrase(statusCode); + if (string.IsNullOrEmpty(reasonPhrase)) + { + reasonPhrase = UnhandledExceptionMsg; + } + + var problemDetails = new ProblemDetails + { + Status = statusCode, + Title = reasonPhrase, + Extensions = + { + [nameof(errorCode)] = errorCode + } + }; + + if (!env.IsDevelopment()) + { + return problemDetails; + } + + problemDetails.Detail = exception.ToString(); + problemDetails.Extensions["traceId"] = Activity.Current?.Id; + problemDetails.Extensions["requestId"] = context.TraceIdentifier; + problemDetails.Extensions["data"] = exception.Data; + + return problemDetails; + } + + private string ToJson(in ProblemDetails problemDetails) + { + try + { + return JsonSerializer.Serialize(problemDetails, SerializerOptions); + } + catch (Exception ex) + { + const string msg = "An exception has occurred while serializing error to JSON"; + logger.LogError(ex, msg); + } + + return string.Empty; + } +} \ No newline at end of file diff --git a/Wallet.sln b/Wallet.sln index 400e83c..277b951 100644 --- a/Wallet.sln +++ b/Wallet.sln @@ -10,6 +10,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EWallet.Api", "EWallet.Api\ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JwtAuth.Api", "JwtAuth.Api\JwtAuth.Api.csproj", "{1AD22D7C-F858-470C-819C-49ADF31A0F81}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EWallet.Infrastructure", "EWallet.Infrastructure\EWallet.Infrastructure.csproj", "{11280331-D7E1-4855-B31C-EC52A73DAEBA}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -28,10 +30,15 @@ Global {1AD22D7C-F858-470C-819C-49ADF31A0F81}.Debug|Any CPU.Build.0 = Debug|Any CPU {1AD22D7C-F858-470C-819C-49ADF31A0F81}.Release|Any CPU.ActiveCfg = Release|Any CPU {1AD22D7C-F858-470C-819C-49ADF31A0F81}.Release|Any CPU.Build.0 = Release|Any CPU + {11280331-D7E1-4855-B31C-EC52A73DAEBA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {11280331-D7E1-4855-B31C-EC52A73DAEBA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {11280331-D7E1-4855-B31C-EC52A73DAEBA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {11280331-D7E1-4855-B31C-EC52A73DAEBA}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {628581B3-CBF4-47B5-A7BE-D81634999389} = {9B06A700-D91F-43C8-977D-CCA3FBFE244A} {0D30908F-DE5A-4896-B97A-0F1A61D958AA} = {272CA49E-0294-4E32-A94C-635F32D0B74A} {1AD22D7C-F858-470C-819C-49ADF31A0F81} = {272CA49E-0294-4E32-A94C-635F32D0B74A} + {11280331-D7E1-4855-B31C-EC52A73DAEBA} = {272CA49E-0294-4E32-A94C-635F32D0B74A} EndGlobalSection EndGlobal